Compare commits
190 Commits
Author | SHA1 | Date | |
---|---|---|---|
0b1dd4f4fc | |||
46bd27e126 | |||
b718d19450 | |||
2969e83afe | |||
a732656746 | |||
1daed940b6 | |||
f29cb00aec | |||
693f07a49c | |||
8c899776f2 | |||
f9aa226bf9 | |||
c9bb4197be | |||
9ae8d587d8 | |||
158058dcfb | |||
0bc9947234 | |||
8c58a9083a | |||
f45663754c | |||
cda9e4e3c6 | |||
ee5b447c23 | |||
25bfcf4aa4 | |||
5956d2cd4c | |||
833285d924 | |||
dee4f5e83f | |||
f0d1cae32d | |||
5dc71697b3 | |||
1bb1e03c08 | |||
914ff92e0f | |||
8a1cf463b1 | |||
d4cf224d6b | |||
8d412ec00a | |||
2986c518ce | |||
f1351243a6 | |||
969e8c76a6 | |||
10f5e75752 | |||
169b3c292a | |||
3eb3aef2f2 | |||
6c455a615c | |||
4f3339bf68 | |||
b5aa7e923f | |||
359c335662 | |||
c11ae23885 | |||
e083b11394 | |||
167990fc4c | |||
d5c1be3d80 | |||
f6567794e0 | |||
ded85d88f7 | |||
6d780e9296 | |||
86637267d3 | |||
71e97721aa | |||
5815608288 | |||
5f75d93c6a | |||
118c4b458e | |||
3f7e4c0dda | |||
eed4c923f6 | |||
09dd5ae541 | |||
172a12b134 | |||
f3c558f48a | |||
988757d30e | |||
50cea90d1b | |||
71dfb6f003 | |||
be628b4aa1 | |||
5d444adec4 | |||
014d67f7b8 | |||
26b4d4388f | |||
6e2e92d0fb | |||
b1f87cb707 | |||
928dea8312 | |||
b8cec8a2af | |||
b6ae946f32 | |||
a9acc18a6f | |||
5468010c87 | |||
b505e380df | |||
412da170e1 | |||
fa237795cf | |||
e2f854b5cc | |||
f8895e3bff | |||
84ad065782 | |||
260e3cbe82 | |||
cd039520b6 | |||
05e652aa62 | |||
5c9bf0638c | |||
bbc78433fd | |||
7bcc2b28b2 | |||
c1d9ca284c | |||
165e28441a | |||
621020b0f0 | |||
6ad36cfaa3 | |||
20b0412091 | |||
3ca246d3e0 | |||
85d1b13ccd | |||
3bada28b8f | |||
8f2cef8d81 | |||
e62316c477 | |||
24ddb0c278 | |||
536f3390aa | |||
fadd8e73b6 | |||
12ccf658bf | |||
e30d1257e5 | |||
404b902d88 | |||
a560ff175a | |||
4be1ead6b5 | |||
700e4f822a | |||
c21ed59dfe | |||
c4a8326bfc | |||
371c80f668 | |||
40be3fb664 | |||
1e56403b35 | |||
650c26036a | |||
b19a6e5ffe | |||
1224d6f83e | |||
3a8618f7c3 | |||
5d87205659 | |||
04de4f5c5e | |||
f8ea863b80 | |||
5ae0d03b32 | |||
a9a3ad5871 | |||
5edc95afce | |||
943ace6fc7 | |||
a63bc977e9 | |||
dabe6ddbca | |||
f47e9b3150 | |||
bb5383febe | |||
87f9063ceb | |||
51f0185bcf | |||
7ca08d6cc8 | |||
c8e9e562be | |||
ba43bd7e90 | |||
4e550413ba | |||
59a3cbb472 | |||
d1b64d069e | |||
d823d3254f | |||
5e9a2fb0c3 | |||
3f2e659ba5 | |||
9f7bb6b9de | |||
6857164702 | |||
6bac76be64 | |||
370d2668e5 | |||
5e3e695e62 | |||
510d369e9c | |||
b65cae9252 | |||
285c12406b | |||
df240472a4 | |||
1218b224fc | |||
79689ac0e5 | |||
1660e66766 | |||
12d00c9c7d | |||
428018e4a9 | |||
a8f318b0bb | |||
a3507494e5 | |||
3aa6c8d6f6 | |||
052b62cdd4 | |||
3728a4037d | |||
6eee17d44f | |||
e5cc2b5a2f | |||
ac3b5523b1 | |||
5af6fd9619 | |||
71a20cba29 | |||
4a4cf1ea40 | |||
e9824808ec | |||
c984d2d596 | |||
720e77c814 | |||
0f0412827d | |||
3a0e978f76 | |||
8c10d42d7b | |||
04ec51afbe | |||
fe7a8842ce | |||
66daa5c42c | |||
27fb44937d | |||
7026ed3a65 | |||
fdd3e93778 | |||
def7559457 | |||
7905820d68 | |||
7ae332c975 | |||
86c5b91697 | |||
9168840e64 | |||
21b9cfa8b8 | |||
b0b3b3acb1 | |||
cb1d254cf0 | |||
eb9ad57e72 | |||
ec26f8ef4d | |||
7ed29115ed | |||
95955197ac | |||
d5a0f79e4b | |||
6aa655aa64 | |||
6e532af26e | |||
fa1818d124 | |||
f21ecc2aa9 | |||
5ae1ab95ae | |||
7a5b3b78fc | |||
7df4051452 | |||
85084c68fd |
40
.readthedocs.yaml
Normal file
40
.readthedocs.yaml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/5
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the version of Python and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.11"
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
|
||||||
|
# If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||||
|
formats: all
|
||||||
|
|
||||||
|
# Optionally declare the Python requirements required to build your docs
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- method: pip
|
||||||
|
path: .
|
21
MANIFEST.in
21
MANIFEST.in
@ -15,17 +15,14 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
recursive-include src/accounting/static *
|
||||||
exclude src/accounting/static/js/dummy.js
|
exclude src/accounting/static/js/dummy.js
|
||||||
include src/accounting/translations/*
|
recursive-include src/accounting/templates *
|
||||||
include src/accounting/translations/*/LC_MESSAGES/*
|
recursive-include src/accounting/translations *
|
||||||
include docs/*
|
recursive-include src/accounting/data *
|
||||||
include docs/source/*
|
recursive-include docs *
|
||||||
include docs/source/_static/*
|
recursive-exclude docs/build *
|
||||||
include docs/source/_templates/*
|
recursive-include tests *
|
||||||
include tests/*
|
|
||||||
exclude tests/test_temp.py
|
exclude tests/test_temp.py
|
||||||
include tests/test_site/*
|
recursive-exclude tests *.pyc
|
||||||
include tests/test_site/static/*
|
recursive-exclude tests/instance *
|
||||||
include tests/test_site/templates/*
|
|
||||||
include tests/test_site/translations/*
|
|
||||||
include tests/test_site/translations/*/LC_MESSAGES/*
|
|
||||||
|
60
README.rst
60
README.rst
@ -6,28 +6,54 @@ Mia! Accounting
|
|||||||
Description
|
Description
|
||||||
===========
|
===========
|
||||||
|
|
||||||
This is the Mia! Accounting project. It is an accounting
|
*Mia! Accounting* is an accounting module for Flask_ applications.
|
||||||
module for the Flask_ applications.
|
It is designed both for mobile and desktop environments. It
|
||||||
|
implements `double-entry bookkeeping`_. It generates the following
|
||||||
|
accounting reports:
|
||||||
|
|
||||||
|
* Trial balance
|
||||||
|
* Income statement
|
||||||
|
* Balance sheet
|
||||||
|
|
||||||
|
In addition, *Mia! Accounting* tracks offsets for unpaid payables and
|
||||||
|
receivables.
|
||||||
|
|
||||||
|
|
||||||
Install
|
Live Demonstration and Test Site
|
||||||
=======
|
================================
|
||||||
|
|
||||||
Install ``mia-accounting`` with ``pip``.
|
There is a `live demonstration`_ for *Mia! Accounting*. It runs the
|
||||||
|
same code as the `test site`_ in the `source distribution`_. It is
|
||||||
|
the simplest website that works with *Mia! Accounting*. It is also
|
||||||
|
used in the automatic tests.
|
||||||
|
|
||||||
|
If you do not have a running Flask application or do not know how to
|
||||||
|
start one, you may start with the test site.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Install *Mia! Accounting* with ``pip``:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
pip install mia-accounting
|
pip install mia-accounting
|
||||||
|
|
||||||
|
You may also download from the `PyPI project page`_ or the
|
||||||
|
`release page`_ on the `Git repository`_.
|
||||||
|
|
||||||
Usage
|
|
||||||
=====
|
|
||||||
|
|
||||||
This needs to be done. Currently, you can refer to the test site
|
Documentation
|
||||||
located in the test directory on the `Mia! Accounting repository`_.
|
=============
|
||||||
|
|
||||||
The test site is running as the
|
Refer to the `documentation on Read the Docs`_.
|
||||||
`live demonstration for Mia! Accounting`_.
|
|
||||||
|
|
||||||
|
Change Log
|
||||||
|
==========
|
||||||
|
|
||||||
|
Refer to the `change log`_.
|
||||||
|
|
||||||
|
|
||||||
Copyright
|
Copyright
|
||||||
@ -55,6 +81,14 @@ Authors
|
|||||||
| imacat@mail.imacat.idv.tw
|
| imacat@mail.imacat.idv.tw
|
||||||
| 2023/1/27
|
| 2023/1/27
|
||||||
|
|
||||||
|
|
||||||
.. _Flask: https://flask.palletsprojects.com
|
.. _Flask: https://flask.palletsprojects.com
|
||||||
.. _Mia! Accounting repository: https://github.com/imacat/mia-accounting
|
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
|
||||||
.. _live demonstration for Mia! Accounting: https://accounting.imacat.idv.tw
|
.. _live demonstration: https://accounting.imacat.idv.tw
|
||||||
|
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
|
||||||
|
.. _source distribution: https://pypi.org/project/mia-accounting/#files
|
||||||
|
.. _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
|
||||||
|
.. _documentation on Read the Docs: https://mia-accounting.readthedocs.io
|
||||||
|
.. _change log: https://mia-accounting.readthedocs.io/en/latest/changelog.html
|
||||||
|
@ -20,14 +20,6 @@ accounting.journal\_entry.utils.description\_editor module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.journal\_entry.utils.offset\_alias module
|
|
||||||
----------------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: accounting.journal_entry.utils.offset_alias
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
accounting.journal\_entry.utils.operators module
|
accounting.journal\_entry.utils.operators module
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
|
@ -60,6 +60,38 @@ accounting.report.reports.trial\_balance module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.reports.unapplied module
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.reports.unapplied
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.reports.unapplied\_accounts module
|
||||||
|
----------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.reports.unapplied_accounts
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.reports.unmatched module
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.reports.unmatched
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.reports.unmatched\_accounts module
|
||||||
|
----------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.reports.unmatched_accounts
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
@ -28,6 +28,14 @@ accounting.report.utils.csv\_export module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.utils.offset\_matcher module
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.utils.offset_matcher
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.report.utils.option\_link module
|
accounting.report.utils.option\_link module
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
|
||||||
@ -52,6 +60,22 @@ accounting.report.utils.report\_type module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.utils.unapplied module
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.utils.unapplied
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.report.utils.unmatched module
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.report.utils.unmatched
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.report.utils.urls module
|
accounting.report.utils.urls module
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
@ -18,6 +18,14 @@ Subpackages
|
|||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
accounting.commands module
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.commands
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.forms module
|
accounting.forms module
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
@ -44,6 +44,14 @@ accounting.utils.next\_uri module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.utils.offset\_alias module
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.utils.offset_alias
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.utils.options module
|
accounting.utils.options module
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
|
339
docs/source/changelog.rst
Normal file
339
docs/source/changelog.rst
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
Change Log
|
||||||
|
==========
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/30
|
||||||
|
|
||||||
|
* Fixed the error of the net balance in the unmatched offset list.
|
||||||
|
* Revised the original line item editor not to override the existing
|
||||||
|
amount when the existing amount is less or equal to the net
|
||||||
|
balance.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/30
|
||||||
|
|
||||||
|
* Fixed the error of the net balance in the unmatched offset list.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/30
|
||||||
|
|
||||||
|
* Fixed the error calling the old ``setEnableDescriptionAccount``
|
||||||
|
method in the ``saveOriginalLineItem`` method of the JavaScript
|
||||||
|
``JournalEntryLineItemEditor`` class.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/23
|
||||||
|
|
||||||
|
* Updated to require ``SQLAlchemy >= 2``.
|
||||||
|
* Added the change log.
|
||||||
|
* Added the ``VERSION`` constant to the ``accounting`` module for
|
||||||
|
the package version, and revised ``pyproject.toml`` and ``conf.py``
|
||||||
|
to read the version from it.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.4.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/22
|
||||||
|
|
||||||
|
* Updated to allow editing the description of the journal entry line
|
||||||
|
item with offsets or are offsetting to original line items.
|
||||||
|
* Updated not to override the existing description of a journal entry
|
||||||
|
line item after choosing the original line item to offset to.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.4.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/18
|
||||||
|
|
||||||
|
* Rewrote the unapplied original line items and unmatched offsets.
|
||||||
|
|
||||||
|
* The unapplied original line items and unmatched offsets are both
|
||||||
|
in the report submodule. They can be filtered with currency and
|
||||||
|
period now.
|
||||||
|
* Show the unapplied original line items and unmatched offsets
|
||||||
|
together, and added the accumulated balance in the unmatched
|
||||||
|
offset list, for ease of reference.
|
||||||
|
|
||||||
|
* Removed the account code from the journal entry detail and journal
|
||||||
|
entry form for mobile devices.
|
||||||
|
* Made the account options in the reports to be scrollable.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.3.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/13
|
||||||
|
|
||||||
|
Changed the sample data generation in the test site live demonstration
|
||||||
|
from pre-recorded data to real-time generation, to avoid the problem
|
||||||
|
with the start of months and weeks changed with the date of the
|
||||||
|
import.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.3.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/12
|
||||||
|
|
||||||
|
Added the sample data generation and database reset on the test site
|
||||||
|
for live demonstration.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.3.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/11
|
||||||
|
|
||||||
|
* Fixed the permission of the navigation menu of the unmatched offsets.
|
||||||
|
* Revised the test site to be more accessible as the live demonstration.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.3.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/11
|
||||||
|
|
||||||
|
Added the ``accounting-init-db`` console command to replace all the
|
||||||
|
other console commands to initialize the accounting database. The
|
||||||
|
test site does not work with previous versions (<1.3.0).
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.2.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/9
|
||||||
|
|
||||||
|
Fixed the search result to allow full ``year/month/day``
|
||||||
|
specification.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.2.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/9
|
||||||
|
|
||||||
|
* Simplified the URL of the default reports.
|
||||||
|
* Fixed the crash with malformed Chinese translation.
|
||||||
|
* Fixed the crash when downloading CSV data with non-US-ASCII
|
||||||
|
filenames.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.1.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/9
|
||||||
|
|
||||||
|
* Added the unapplied original line item list, to track unpaid
|
||||||
|
payables, unreceived receivables, assets, prepaids, refundable
|
||||||
|
deposits, etc.
|
||||||
|
* Added the offset matcher to match unapplied original line items
|
||||||
|
with unmatched offsets.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.0.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/6
|
||||||
|
|
||||||
|
Documentation fixes.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.0.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/6
|
||||||
|
|
||||||
|
The first formal release in Flask.
|
||||||
|
|
||||||
|
Added the documentation.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.11.1 (Pre-release)
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Released 2023/4/5
|
||||||
|
|
||||||
|
Removed the zero balances from the trial balance, the income
|
||||||
|
statement, and the balance sheet.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.11.0 (Pre-release)
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Released 2023/4/5
|
||||||
|
|
||||||
|
* Renamed the project from ``mia-accounting-flask`` to
|
||||||
|
``mia-accounting``.
|
||||||
|
* Updated the URL of the reports, as the default views of the
|
||||||
|
accounting application.
|
||||||
|
* Updated ``README``.
|
||||||
|
* Various fixes.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.10.0 (Pre-release)
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Released 2023/4/3
|
||||||
|
|
||||||
|
* Added the unauthorized method to the ``UserUtilityInterface``
|
||||||
|
interface to allow fine control to how to handle the case when the
|
||||||
|
user has not logged in.
|
||||||
|
* Revised the JavaScript description editor to respect the account
|
||||||
|
that the user has confirmed or specifically selected.
|
||||||
|
* Various fixes.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.9.1 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/3/24
|
||||||
|
|
||||||
|
* A distinguishable look in the option detail than the option form.
|
||||||
|
* A better look in the new journal entry forms when there is no line
|
||||||
|
item yet.
|
||||||
|
* Fixed the search in the original entry selector in the journal
|
||||||
|
entry form to always do a partial match, to fix the problem that
|
||||||
|
there is no match when typing is not finished yet.
|
||||||
|
* Fixed the search in the original entry selector to search the net
|
||||||
|
balance correctly.
|
||||||
|
* Replaced the ``editor`` and ``editor2`` accounts with the ``admin``
|
||||||
|
and ``editor`` accounts.
|
||||||
|
* Various fixes.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.9.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/3/23
|
||||||
|
|
||||||
|
Moved the settings from the ``.env`` file to the option table in the
|
||||||
|
database that can be set and updated on the web interface. Added the
|
||||||
|
settings page to show and update the settings.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.8.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/3/22
|
||||||
|
|
||||||
|
* Added the recurring transactions to the description editor.
|
||||||
|
* Added prevention to delete database objects that are essential or
|
||||||
|
referenced by others with foreign keys.
|
||||||
|
* Various fixes on the visual layout.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.7.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/3/21
|
||||||
|
|
||||||
|
* Renamed "transaction" to "journal entry", and "journal entry" to
|
||||||
|
"journal entry line item".
|
||||||
|
* Renamed ``summary`` to ``description``.
|
||||||
|
* Updated tempus-dominus from version 6.2.10 to 6.4.3.
|
||||||
|
* Fixed titles and capitalization.
|
||||||
|
* Fixed to search case-insensitively.
|
||||||
|
* Added favicon to the test site.
|
||||||
|
* Fixed the navigation menu when there is no matching endpoint.
|
||||||
|
* Various fixes.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.6.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/3/18
|
||||||
|
|
||||||
|
* Added offset tracking to the journal entries in the payable and
|
||||||
|
receivable accounts.
|
||||||
|
* Renamed the ``is_offset_needed`` column to ``is_need_offset`` in
|
||||||
|
the ``Account`` data model.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.5.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/3/10
|
||||||
|
|
||||||
|
Added the accounting reports.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.4.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/3/1
|
||||||
|
|
||||||
|
Added the transaction summary helper.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.3.1 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/2/28
|
||||||
|
|
||||||
|
* Fixed the error that cannot select any account when adding new
|
||||||
|
transactions.
|
||||||
|
* Fixed the database error when adding new transactions.
|
||||||
|
* Added the button to convert a cash income or cash expense
|
||||||
|
transaction to a transfer transaction.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.3.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/2/27
|
||||||
|
|
||||||
|
Added the transaction management.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.2.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/2/7
|
||||||
|
|
||||||
|
* Added the currency management.
|
||||||
|
* Changed the ``can_edit`` permission to at least require the user to
|
||||||
|
log in first.
|
||||||
|
* Changed the type hint of the ``current_user`` pseudo property of
|
||||||
|
the ``AbstractUserUtils`` class to return ``None`` when the user
|
||||||
|
has not logged in.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.1.1 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/2/3
|
||||||
|
|
||||||
|
Finalized the account management, with tests and reordering.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.1.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/2/3
|
||||||
|
|
||||||
|
Added the account management, and updated the API to initialize the
|
||||||
|
accounting application.
|
||||||
|
|
||||||
|
|
||||||
|
Version 0.0.0 (Pre-release)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Released 2023/2/3
|
||||||
|
|
||||||
|
Initial release with main account list, localization, pagination,
|
||||||
|
query, permission, Sphinx documentation, and a test case based on a
|
||||||
|
test demonstration site.
|
@ -6,6 +6,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../../src/'))
|
sys.path.insert(0, os.path.abspath('../../src/'))
|
||||||
|
import accounting
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
@ -13,7 +14,7 @@ sys.path.insert(0, os.path.abspath('../../src/'))
|
|||||||
project = 'Mia! Accounting'
|
project = 'Mia! Accounting'
|
||||||
copyright = '2023, imacat'
|
copyright = '2023, imacat'
|
||||||
author = 'imacat'
|
author = 'imacat'
|
||||||
release = '0.11.0'
|
release = accounting.VERSION
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
61
docs/source/examples.rst
Normal file
61
docs/source/examples.rst
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
Examples
|
||||||
|
========
|
||||||
|
|
||||||
|
|
||||||
|
.. _example-userutils:
|
||||||
|
|
||||||
|
An Example Configuration
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
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
|
57
docs/source/history.rst
Normal file
57
docs/source/history.rst
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
History
|
||||||
|
=======
|
||||||
|
|
||||||
|
I created my own private accounting application in Perl_/mod_perl_ in
|
||||||
|
2007, as part of my personal website. The first revision was made
|
||||||
|
using Perl/Mojolicious_ in 2019, with the aim of making it
|
||||||
|
mobile-friendly using Bootstrap_, and with modern back-end and
|
||||||
|
front-end technologies such as jQuery.
|
||||||
|
|
||||||
|
The second revision was done in Python_/Django_ in 2020, as I was
|
||||||
|
looking to change my career from PHP_/Laravel_ to Python, but lacked
|
||||||
|
experience with large Python projects. I needed something in my
|
||||||
|
portfolio and decided to work on the somewhat outdated Mojolicious
|
||||||
|
project.
|
||||||
|
|
||||||
|
Despite having no prior experience with Django, I spent two months
|
||||||
|
working late nights to create the `Mia! Accounting Django`_
|
||||||
|
application. It took me another 1.5 months to make it an independent
|
||||||
|
module, which I later released as an open source project on PyPI.
|
||||||
|
|
||||||
|
The application worked nicely for my household bookkeeping for two
|
||||||
|
years. However, new demands arose over time, especially with tracking
|
||||||
|
payables and receivables. This was critical `during the pandemic`_ as
|
||||||
|
more payments were made online with credit cards.
|
||||||
|
|
||||||
|
The biggest issue I encountered was with
|
||||||
|
`Django's MTV architectural pattern`_. Django takes over the control
|
||||||
|
flow. I had to override several parts of the `class-based views`_ for
|
||||||
|
different but yet simple control flow logic. In the end, it became
|
||||||
|
very difficult to track whether things went wrong because I overrode
|
||||||
|
something or because it just wouldn't work with the basic assumption
|
||||||
|
of the class-based views. By the time I realized it, it was too late
|
||||||
|
for me to drop Django's MTV and rewrite everything from class-based
|
||||||
|
views to function-based views.
|
||||||
|
|
||||||
|
Therefore, I decided to turn to microframeworks_ like Flask_. After
|
||||||
|
working with modularized Flask and FastAPI_ applications for two
|
||||||
|
years, I returned to the project and wrote its third revision using
|
||||||
|
Flask in 2023.
|
||||||
|
|
||||||
|
|
||||||
|
.. _Perl: https://www.perl.org
|
||||||
|
.. _mod_perl: https://perl.apache.org
|
||||||
|
.. _Mojolicious: https://mojolicious.org
|
||||||
|
.. _Bootstrap: https://getbootstrap.com
|
||||||
|
.. _jQuery: https://jquery.com
|
||||||
|
.. _Python: https://www.python.org
|
||||||
|
.. _Django: https://www.djangoproject.com
|
||||||
|
.. _PHP: https://www.php.net
|
||||||
|
.. _Laravel: https://laravel.com
|
||||||
|
.. _Mia! Accounting Django: https://github.com/imacat/mia-accounting-django
|
||||||
|
.. _during the pandemic: https://en.wikipedia.org/wiki/COVID-19_pandemic
|
||||||
|
.. _FastAPI: https://fastapi.tiangolo.com
|
||||||
|
.. _Django's MTV architectural pattern: https://docs.djangoproject.com/en/dev/faq/general/#django-appears-to-be-a-mvc-framework-but-you-call-the-controller-the-view-and-the-view-the-template-how-come-you-don-t-use-the-standard-names
|
||||||
|
.. _class-based views: https://docs.djangoproject.com/en/4.2/topics/class-based-views/
|
||||||
|
.. _microframeworks: https://en.wikipedia.org/wiki/Microframework
|
||||||
|
.. _Flask: https://flask.palletsprojects.com
|
@ -10,6 +10,12 @@ Welcome to Mia! Accounting's documentation!
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
|
intro
|
||||||
|
accounting
|
||||||
|
examples
|
||||||
|
history
|
||||||
|
changelog
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
119
docs/source/intro.rst
Normal file
119
docs/source/intro.rst
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
*Mia! Accounting* is an accounting module for Flask_ applications.
|
||||||
|
It is designed both for mobile and desktop environments. It
|
||||||
|
implements `double-entry bookkeeping`_. It generates the following
|
||||||
|
accounting reports:
|
||||||
|
|
||||||
|
* Trial balance
|
||||||
|
* Income statement
|
||||||
|
* Balance sheet
|
||||||
|
|
||||||
|
In addition, *Mia! Accounting* tracks offsets for unpaid payables and
|
||||||
|
receivables.
|
||||||
|
|
||||||
|
|
||||||
|
Live Demonstration and Test Site
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
There is a `live demonstration`_ for *Mia! Accounting*. It runs the
|
||||||
|
same code as the `test site`_ in the `source distribution`_. It is
|
||||||
|
the simplest website that works with *Mia! Accounting*. It is also
|
||||||
|
used in the automatic tests.
|
||||||
|
|
||||||
|
If you do not have a running Flask application or do not know how to
|
||||||
|
start one, you may start with the test site.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Install *Mia! Accounting* with ``pip``:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pip install mia-accounting
|
||||||
|
|
||||||
|
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
|
||||||
|
:py:class:`accounting.utils.user.UserUtilityInterface` to the
|
||||||
|
:py:func:`accounting.init_app` function. ``UserUtilityInterface``
|
||||||
|
contains everything *Mia! Accounting* needs.
|
||||||
|
|
||||||
|
See an example in :ref:`example-userutils`.
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
.. _Flask: https://flask.palletsprojects.com
|
||||||
|
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
|
||||||
|
.. _live demonstration: https://accounting.imacat.idv.tw
|
||||||
|
.. _test site: https://github.com/imacat/mia-accounting/tree/main/tests/test_site
|
||||||
|
.. _source distribution: https://pypi.org/project/mia-accounting/#files
|
||||||
|
.. _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
|
||||||
|
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
|
||||||
|
|
||||||
# Copyright (c) 2022 imacat.
|
# Copyright (c) 2022-2023 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +15,56 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "mia-accounting"
|
||||||
|
dynamic = ["version"]
|
||||||
|
description = "A Flask accounting module."
|
||||||
|
readme = "README.rst"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
authors = [
|
||||||
|
{name = "imacat", email = "imacat@mail.imacat.idv.tw"},
|
||||||
|
]
|
||||||
|
keywords = ["mia", "accounting", "flask"]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Framework :: Flask",
|
||||||
|
"Topic :: Office/Business :: Financial :: Accounting",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"flask",
|
||||||
|
"SQLAlchemy >= 2",
|
||||||
|
"Flask-SQLAlchemy",
|
||||||
|
"Flask-WTF",
|
||||||
|
"Flask-Babel >= 3",
|
||||||
|
"Flask-Babel-JS",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = [
|
||||||
|
"unittest",
|
||||||
|
"httpx",
|
||||||
|
"OpenCC",
|
||||||
|
]
|
||||||
|
|
||||||
|
[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"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=42"]
|
requires = ["setuptools>=42"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "accounting.VERSION"}
|
||||||
|
|
||||||
|
[tool.setuptools.exclude-package-data]
|
||||||
|
"*" = [
|
||||||
|
"babel.cfg",
|
||||||
|
"*.pot",
|
||||||
|
"*.po",
|
||||||
|
]
|
||||||
|
56
setup.cfg
56
setup.cfg
@ -1,56 +0,0 @@
|
|||||||
# The Mia! Accounting Project.
|
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
|
|
||||||
|
|
||||||
# Copyright (c) 2022-2023 imacat.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
name = mia-accounting
|
|
||||||
version = 0.11.0
|
|
||||||
author = imacat
|
|
||||||
author_email = imacat@mail.imacat.idv.tw
|
|
||||||
description = The Mia! Accounting project.
|
|
||||||
long_description = file: README.rst
|
|
||||||
long_description_content_type = text/x-rst
|
|
||||||
url = https://github.com/imacat/mia-accounting
|
|
||||||
project_urls =
|
|
||||||
Bug Tracker = https://github.com/imacat/mia-accounting/issues
|
|
||||||
classifiers =
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
License :: OSI Approved :: Apache Software License
|
|
||||||
Operating System :: OS Independent
|
|
||||||
Framework :: Flask
|
|
||||||
Topic :: Office/Business :: Financial :: Accounting
|
|
||||||
|
|
||||||
[options]
|
|
||||||
package_dir =
|
|
||||||
= src
|
|
||||||
python_requires = >=3.11
|
|
||||||
install_requires =
|
|
||||||
flask
|
|
||||||
Flask-SQLAlchemy
|
|
||||||
Flask-WTF
|
|
||||||
Flask-Babel >= 3
|
|
||||||
Flask-Babel-JS
|
|
||||||
tests_require =
|
|
||||||
unittest
|
|
||||||
httpx
|
|
||||||
OpenCC
|
|
||||||
|
|
||||||
[options.package_data]
|
|
||||||
accounting =
|
|
||||||
static/**
|
|
||||||
templates/**
|
|
||||||
translations/*/LC_MESSAGES/*.mo
|
|
||||||
data/**
|
|
@ -24,6 +24,8 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
|
|
||||||
from accounting.utils.user import UserUtilityInterface
|
from accounting.utils.user import UserUtilityInterface
|
||||||
|
|
||||||
|
VERSION: str = "1.5.3"
|
||||||
|
"""The package version."""
|
||||||
db: SQLAlchemy = SQLAlchemy()
|
db: SQLAlchemy = SQLAlchemy()
|
||||||
"""The database instance."""
|
"""The database instance."""
|
||||||
data_dir: Path = Path(__file__).parent / "data"
|
data_dir: Path = Path(__file__).parent / "data"
|
||||||
@ -61,6 +63,9 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
|
|||||||
bp.add_app_template_global(default_currency_code,
|
bp.add_app_template_global(default_currency_code,
|
||||||
"accounting_default_currency_code")
|
"accounting_default_currency_code")
|
||||||
|
|
||||||
|
from .commands import init_db_command
|
||||||
|
app.cli.add_command(init_db_command)
|
||||||
|
|
||||||
from . import locale
|
from . import locale
|
||||||
locale.init_app(app, bp)
|
locale.init_app(app, bp)
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Flask, Blueprint
|
from flask import Flask, Blueprint
|
||||||
|
|
||||||
|
from .commands import init_accounts_command
|
||||||
|
|
||||||
|
|
||||||
def init_app(app: Flask, bp: Blueprint) -> None:
|
def init_app(app: Flask, bp: Blueprint) -> None:
|
||||||
"""Initialize the application.
|
"""Initialize the application.
|
||||||
@ -32,6 +34,3 @@ def init_app(app: Flask, bp: Blueprint) -> None:
|
|||||||
|
|
||||||
from .views import bp as account_bp
|
from .views import bp as account_bp
|
||||||
bp.register_blueprint(account_bp, url_prefix="/accounts")
|
bp.register_blueprint(account_bp, url_prefix="/accounts")
|
||||||
|
|
||||||
from .commands import init_accounts_command
|
|
||||||
app.cli.add_command(init_accounts_command)
|
|
||||||
|
@ -17,44 +17,21 @@
|
|||||||
"""The console commands for the account management.
|
"""The console commands for the account management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
from secrets import randbelow
|
from secrets import randbelow
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from flask.cli import with_appcontext
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import BaseAccount, Account, AccountL10n
|
from accounting.models import BaseAccount, Account, AccountL10n
|
||||||
from accounting.utils.user import has_user, get_user_pk
|
from accounting.utils.user import get_user_pk
|
||||||
|
|
||||||
AccountData = tuple[int, str, int, str, str, str, bool]
|
AccountData = tuple[int, str, int, str, str, str, bool]
|
||||||
"""The format of the account data, as a list of (ID, base account code, number,
|
"""The format of the account data, as a list of (ID, base account code, number,
|
||||||
English, Traditional Chinese, Simplified Chinese, is-need-offset) tuples."""
|
English, Traditional Chinese, Simplified Chinese, is-need-offset) tuples."""
|
||||||
|
|
||||||
|
|
||||||
def __validate_username(ctx: click.core.Context, param: click.core.Option,
|
|
||||||
value: str) -> str:
|
|
||||||
"""Validates the username for the click console command.
|
|
||||||
|
|
||||||
:param ctx: The console command context.
|
|
||||||
:param param: The console command option.
|
|
||||||
:param value: The username.
|
|
||||||
:raise click.BadParameter: When validation fails.
|
|
||||||
:return: The username.
|
|
||||||
"""
|
|
||||||
value = value.strip()
|
|
||||||
if value == "":
|
|
||||||
raise click.BadParameter("Username empty.")
|
|
||||||
if not has_user(value):
|
|
||||||
raise click.BadParameter(f"User {value} does not exist.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@click.command("accounting-init-accounts")
|
|
||||||
@click.option("-u", "--username", metavar="USERNAME", prompt=True,
|
|
||||||
help="The username.", callback=__validate_username,
|
|
||||||
default=lambda: os.getlogin())
|
|
||||||
@with_appcontext
|
|
||||||
def init_accounts_command(username: str) -> None:
|
def init_accounts_command(username: str) -> None:
|
||||||
"""Initializes the accounts."""
|
"""Initializes the accounts."""
|
||||||
creator_pk: int = get_user_pk(username)
|
creator_pk: int = get_user_pk(username)
|
||||||
@ -63,8 +40,6 @@ def init_accounts_command(username: str) -> None:
|
|||||||
.filter(db.func.length(BaseAccount.code) == 4)\
|
.filter(db.func.length(BaseAccount.code) == 4)\
|
||||||
.order_by(BaseAccount.code).all()
|
.order_by(BaseAccount.code).all()
|
||||||
if len(bases) == 0:
|
if len(bases) == 0:
|
||||||
click.echo("Please initialize the base accounts with "
|
|
||||||
"\"flask accounting-init-base\" first.")
|
|
||||||
raise click.Abort
|
raise click.Abort
|
||||||
|
|
||||||
existing: list[Account] = Account.query.all()
|
existing: list[Account] = Account.query.all()
|
||||||
@ -73,7 +48,6 @@ def init_accounts_command(username: str) -> None:
|
|||||||
bases_to_add: list[BaseAccount] = [x for x in bases
|
bases_to_add: list[BaseAccount] = [x for x in bases
|
||||||
if x.code not in existing_base_code]
|
if x.code not in existing_base_code]
|
||||||
if len(bases_to_add) == 0:
|
if len(bases_to_add) == 0:
|
||||||
click.echo("No more account to import.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_id: set[int] = {x.id for x in existing}
|
existing_id: set[int] = {x.id for x in existing}
|
||||||
@ -89,14 +63,24 @@ def init_accounts_command(username: str) -> None:
|
|||||||
existing_id.add(new_id)
|
existing_id.add(new_id)
|
||||||
return new_id
|
return new_id
|
||||||
|
|
||||||
data: list[AccountData] = []
|
data: list[dict[str, Any]] = []
|
||||||
|
l10n_data: list[dict[str, Any]] = []
|
||||||
for base in bases_to_add:
|
for base in bases_to_add:
|
||||||
l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
|
l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
|
||||||
is_need_offset: bool = __is_need_offset(base.code)
|
account_id: int = get_new_id()
|
||||||
data.append((get_new_id(), base.code, 1, base.title_l10n,
|
data.append({"id": account_id,
|
||||||
l10n["zh_Hant"], l10n["zh_Hans"], is_need_offset))
|
"base_code": base.code,
|
||||||
__add_accounting_accounts(data, creator_pk)
|
"no": 1,
|
||||||
click.echo(F"{len(data)} added. Accounting accounts initialized.")
|
"title_l10n": base.title_l10n,
|
||||||
|
"is_need_offset": __is_need_offset(base.code),
|
||||||
|
"created_by_id": creator_pk,
|
||||||
|
"updated_by_id": creator_pk})
|
||||||
|
for locale in {"zh_Hant", "zh_Hans"}:
|
||||||
|
l10n_data.append({"account_id": account_id,
|
||||||
|
"locale": locale,
|
||||||
|
"title": l10n[locale]})
|
||||||
|
db.session.execute(sa.insert(Account), data)
|
||||||
|
db.session.execute(sa.insert(AccountL10n), l10n_data)
|
||||||
|
|
||||||
|
|
||||||
def __is_need_offset(base_code: str) -> bool:
|
def __is_need_offset(base_code: str) -> bool:
|
||||||
@ -108,42 +92,16 @@ def __is_need_offset(base_code: str) -> bool:
|
|||||||
"""
|
"""
|
||||||
# Assets
|
# Assets
|
||||||
if base_code[0] == "1":
|
if base_code[0] == "1":
|
||||||
if base_code[:3] in {"113", "114", "118", "184"}:
|
if base_code[:3] in {"113", "114", "118", "184", "186"}:
|
||||||
return True
|
return True
|
||||||
if base_code in {"1411", "1421", "1431", "1441", "1511", "1521",
|
if base_code in {"1286", "1411", "1421", "1431", "1441", "1511",
|
||||||
"1581", "1611", "1851", ""}:
|
"1521", "1581", "1611", "1851"}:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
# Liabilities
|
# Liabilities
|
||||||
if base_code[0] == "2":
|
if base_code[0] == "2":
|
||||||
if base_code in {"2111", "2114", "2284", "2293"}:
|
if base_code in {"2111", "2114", "2284", "2293", "2861"}:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
# Only assets and liabilities need offset
|
# Only assets and liabilities need offset
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def __add_accounting_accounts(data: list[AccountData], creator_pk: int)\
|
|
||||||
-> None:
|
|
||||||
"""Adds the accounts.
|
|
||||||
|
|
||||||
:param data: A list of (base code, number, title) tuples.
|
|
||||||
:param creator_pk: The primary key of the creator.
|
|
||||||
:return: None.
|
|
||||||
"""
|
|
||||||
accounts: list[Account] = [Account(id=x[0],
|
|
||||||
base_code=x[1],
|
|
||||||
no=x[2],
|
|
||||||
title_l10n=x[3],
|
|
||||||
is_need_offset=x[6],
|
|
||||||
created_by_id=creator_pk,
|
|
||||||
updated_by_id=creator_pk)
|
|
||||||
for x in data]
|
|
||||||
l10n: list[AccountL10n] = [AccountL10n(account_id=x[0],
|
|
||||||
locale=y[0],
|
|
||||||
title=y[1])
|
|
||||||
for x in data
|
|
||||||
for y in (("zh_Hant", x[4]), ("zh_Hans", x[5]))]
|
|
||||||
db.session.bulk_save_objects(accounts)
|
|
||||||
db.session.bulk_save_objects(l10n)
|
|
||||||
db.session.commit()
|
|
||||||
|
@ -53,7 +53,7 @@ def list_accounts() -> str:
|
|||||||
list=pagination.list, pagination=pagination)
|
list=pagination.list, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/create", endpoint="create")
|
@bp.get("create", endpoint="create")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_add_account_form() -> str:
|
def show_add_account_form() -> str:
|
||||||
"""Shows the form to add an account.
|
"""Shows the form to add an account.
|
||||||
@ -70,7 +70,7 @@ def show_add_account_form() -> str:
|
|||||||
form=form)
|
form=form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/store", endpoint="store")
|
@bp.post("store", endpoint="store")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def add_account() -> redirect:
|
def add_account() -> redirect:
|
||||||
"""Adds an account.
|
"""Adds an account.
|
||||||
@ -91,7 +91,7 @@ def add_account() -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(account)))
|
return redirect(inherit_next(__get_detail_uri(account)))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<account:account>", endpoint="detail")
|
@bp.get("<account:account>", endpoint="detail")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_account_detail(account: Account) -> str:
|
def show_account_detail(account: Account) -> str:
|
||||||
"""Shows the account detail.
|
"""Shows the account detail.
|
||||||
@ -102,7 +102,7 @@ def show_account_detail(account: Account) -> str:
|
|||||||
return render_template("accounting/account/detail.html", obj=account)
|
return render_template("accounting/account/detail.html", obj=account)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<account:account>/edit", endpoint="edit")
|
@bp.get("<account:account>/edit", endpoint="edit")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_account_edit_form(account: Account) -> str:
|
def show_account_edit_form(account: Account) -> str:
|
||||||
"""Shows the form to edit an account.
|
"""Shows the form to edit an account.
|
||||||
@ -121,7 +121,7 @@ def show_account_edit_form(account: Account) -> str:
|
|||||||
account=account, form=form)
|
account=account, form=form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<account:account>/update", endpoint="update")
|
@bp.post("<account:account>/update", endpoint="update")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def update_account(account: Account) -> redirect:
|
def update_account(account: Account) -> redirect:
|
||||||
"""Updates an account.
|
"""Updates an account.
|
||||||
@ -148,7 +148,7 @@ def update_account(account: Account) -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(account)))
|
return redirect(inherit_next(__get_detail_uri(account)))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<account:account>/delete", endpoint="delete")
|
@bp.post("<account:account>/delete", endpoint="delete")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def delete_account(account: Account) -> redirect:
|
def delete_account(account: Account) -> redirect:
|
||||||
"""Deletes an account.
|
"""Deletes an account.
|
||||||
@ -167,7 +167,7 @@ def delete_account(account: Account) -> redirect:
|
|||||||
return redirect(or_next(__get_list_uri()))
|
return redirect(or_next(__get_list_uri()))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/bases/<baseAccount:base>", endpoint="order")
|
@bp.get("bases/<baseAccount:base>", endpoint="order")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_account_order(base: BaseAccount) -> str:
|
def show_account_order(base: BaseAccount) -> str:
|
||||||
"""Shows the order of the accounts under a same base account.
|
"""Shows the order of the accounts under a same base account.
|
||||||
@ -178,7 +178,7 @@ def show_account_order(base: BaseAccount) -> str:
|
|||||||
return render_template("accounting/account/order.html", base=base)
|
return render_template("accounting/account/order.html", base=base)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/bases/<baseAccount:base>", endpoint="sort")
|
@bp.post("bases/<baseAccount:base>", endpoint="sort")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def sort_accounts(base: BaseAccount) -> redirect:
|
def sort_accounts(base: BaseAccount) -> redirect:
|
||||||
"""Reorders the accounts under a base account.
|
"""Reorders the accounts under a base account.
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Flask, Blueprint
|
from flask import Flask, Blueprint
|
||||||
|
|
||||||
|
from .commands import init_base_accounts_command
|
||||||
|
|
||||||
|
|
||||||
def init_app(app: Flask, bp: Blueprint) -> None:
|
def init_app(app: Flask, bp: Blueprint) -> None:
|
||||||
"""Initialize the application.
|
"""Initialize the application.
|
||||||
@ -32,6 +34,3 @@ def init_app(app: Flask, bp: Blueprint) -> None:
|
|||||||
|
|
||||||
from .views import bp as base_account_bp
|
from .views import bp as base_account_bp
|
||||||
bp.register_blueprint(base_account_bp, url_prefix="/base-accounts")
|
bp.register_blueprint(base_account_bp, url_prefix="/base-accounts")
|
||||||
|
|
||||||
from .commands import init_base_accounts_command
|
|
||||||
app.cli.add_command(init_base_accounts_command)
|
|
||||||
|
@ -19,21 +19,17 @@
|
|||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
import click
|
import sqlalchemy as sa
|
||||||
from flask.cli import with_appcontext
|
|
||||||
|
|
||||||
from accounting import data_dir
|
from accounting import data_dir
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import BaseAccount, BaseAccountL10n
|
from accounting.models import BaseAccount, BaseAccountL10n
|
||||||
|
|
||||||
|
|
||||||
@click.command("accounting-init-base")
|
|
||||||
@with_appcontext
|
|
||||||
def init_base_accounts_command() -> None:
|
def init_base_accounts_command() -> None:
|
||||||
"""Initializes the base accounts."""
|
"""Initializes the base accounts."""
|
||||||
if BaseAccount.query.first() is not None:
|
if BaseAccount.query.first() is not None:
|
||||||
click.echo("Base accounts already exist.")
|
return
|
||||||
raise click.Abort
|
|
||||||
|
|
||||||
with open(data_dir / "base_accounts.csv") as fp:
|
with open(data_dir / "base_accounts.csv") as fp:
|
||||||
data: list[dict[str, str]] = [x for x in csv.DictReader(fp)]
|
data: list[dict[str, str]] = [x for x in csv.DictReader(fp)]
|
||||||
@ -45,7 +41,5 @@ def init_base_accounts_command() -> None:
|
|||||||
"locale": y,
|
"locale": y,
|
||||||
"title": x[f"l10n-{y}"]}
|
"title": x[f"l10n-{y}"]}
|
||||||
for x in data for y in locales]
|
for x in data for y in locales]
|
||||||
db.session.bulk_insert_mappings(BaseAccount, account_data)
|
db.session.execute(sa.insert(BaseAccount), account_data)
|
||||||
db.session.bulk_insert_mappings(BaseAccountL10n, l10n_data)
|
db.session.execute(sa.insert(BaseAccountL10n), l10n_data)
|
||||||
db.session.commit()
|
|
||||||
click.echo("Base accounts initialized.")
|
|
||||||
|
@ -22,6 +22,7 @@ from flask import Blueprint, render_template
|
|||||||
from accounting.models import BaseAccount
|
from accounting.models import BaseAccount
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
from accounting.utils.permission import has_permission, can_view
|
from accounting.utils.permission import has_permission, can_view
|
||||||
|
from .queries import get_base_account_query
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("base-account", __name__)
|
bp: Blueprint = Blueprint("base-account", __name__)
|
||||||
"""The view blueprint for the base account management."""
|
"""The view blueprint for the base account management."""
|
||||||
@ -34,14 +35,13 @@ def list_accounts() -> str:
|
|||||||
|
|
||||||
:return: The account list.
|
:return: The account list.
|
||||||
"""
|
"""
|
||||||
from .queries import get_base_account_query
|
|
||||||
accounts: list[BaseAccount] = get_base_account_query()
|
accounts: list[BaseAccount] = get_base_account_query()
|
||||||
pagination: Pagination = Pagination[BaseAccount](accounts)
|
pagination: Pagination = Pagination[BaseAccount](accounts)
|
||||||
return render_template("accounting/base-account/list.html",
|
return render_template("accounting/base-account/list.html",
|
||||||
list=pagination.list, pagination=pagination)
|
list=pagination.list, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<baseAccount:account>", endpoint="detail")
|
@bp.get("<baseAccount:account>", endpoint="detail")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_account_detail(account: BaseAccount) -> str:
|
def show_account_detail(account: BaseAccount) -> str:
|
||||||
"""Shows the account detail.
|
"""Shows the account detail.
|
||||||
|
62
src/accounting/commands.py
Normal file
62
src/accounting/commands.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/10
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The console commands.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
import click
|
||||||
|
from flask.cli import with_appcontext
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
|
from accounting.account import init_accounts_command
|
||||||
|
from accounting.base_account import init_base_accounts_command
|
||||||
|
from accounting.currency import init_currencies_command
|
||||||
|
from accounting.utils.user import has_user
|
||||||
|
|
||||||
|
|
||||||
|
def __validate_username(ctx: click.core.Context, param: click.core.Option,
|
||||||
|
value: str) -> str:
|
||||||
|
"""Validates the username for the click console command.
|
||||||
|
|
||||||
|
:param ctx: The console command context.
|
||||||
|
:param param: The console command option.
|
||||||
|
:param value: The username.
|
||||||
|
:raise click.BadParameter: When validation fails.
|
||||||
|
:return: The username.
|
||||||
|
"""
|
||||||
|
value = value.strip()
|
||||||
|
if value == "":
|
||||||
|
raise click.BadParameter("Username empty.")
|
||||||
|
if not has_user(value):
|
||||||
|
raise click.BadParameter(f"User {value} does not exist.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("accounting-init-db")
|
||||||
|
@click.option("-u", "--username", metavar="USERNAME", prompt=True,
|
||||||
|
help="The username.", callback=__validate_username,
|
||||||
|
default=lambda: os.getlogin())
|
||||||
|
@with_appcontext
|
||||||
|
def init_db_command(username: str) -> None:
|
||||||
|
"""Initializes the accounting database."""
|
||||||
|
db.create_all()
|
||||||
|
init_base_accounts_command()
|
||||||
|
init_accounts_command(username)
|
||||||
|
init_currencies_command(username)
|
||||||
|
db.session.commit()
|
||||||
|
click.echo("Accounting database initialized.")
|
@ -19,6 +19,8 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Flask, Blueprint
|
from flask import Flask, Blueprint
|
||||||
|
|
||||||
|
from .commands import init_currencies_command
|
||||||
|
|
||||||
|
|
||||||
def init_app(app: Flask, bp: Blueprint) -> None:
|
def init_app(app: Flask, bp: Blueprint) -> None:
|
||||||
"""Initialize the application.
|
"""Initialize the application.
|
||||||
@ -33,6 +35,3 @@ def init_app(app: Flask, bp: Blueprint) -> None:
|
|||||||
from .views import bp as currency_bp, api_bp as currency_api_bp
|
from .views import bp as currency_bp, api_bp as currency_api_bp
|
||||||
bp.register_blueprint(currency_bp, url_prefix="/currencies")
|
bp.register_blueprint(currency_bp, url_prefix="/currencies")
|
||||||
bp.register_blueprint(currency_api_bp, url_prefix="/api/currencies")
|
bp.register_blueprint(currency_api_bp, url_prefix="/api/currencies")
|
||||||
|
|
||||||
from .commands import init_currencies_command
|
|
||||||
app.cli.add_command(init_currencies_command)
|
|
||||||
|
@ -18,42 +18,15 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
import os
|
from typing import Any
|
||||||
import typing as t
|
|
||||||
|
|
||||||
import click
|
import sqlalchemy as sa
|
||||||
from flask.cli import with_appcontext
|
|
||||||
|
|
||||||
from accounting import db, data_dir
|
from accounting import db, data_dir
|
||||||
from accounting.models import Currency, CurrencyL10n
|
from accounting.models import Currency, CurrencyL10n
|
||||||
from accounting.utils.user import has_user, get_user_pk
|
from accounting.utils.user import get_user_pk
|
||||||
|
|
||||||
CurrencyData = tuple[str, str, str, str]
|
|
||||||
|
|
||||||
|
|
||||||
def __validate_username(ctx: click.core.Context, param: click.core.Option,
|
|
||||||
value: str) -> str:
|
|
||||||
"""Validates the username for the click console command.
|
|
||||||
|
|
||||||
:param ctx: The console command context.
|
|
||||||
:param param: The console command option.
|
|
||||||
:param value: The username.
|
|
||||||
:raise click.BadParameter: When validation fails.
|
|
||||||
:return: The username.
|
|
||||||
"""
|
|
||||||
value = value.strip()
|
|
||||||
if value == "":
|
|
||||||
raise click.BadParameter("Username empty.")
|
|
||||||
if not has_user(value):
|
|
||||||
raise click.BadParameter(f"User {value} does not exist.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@click.command("accounting-init-currencies")
|
|
||||||
@click.option("-u", "--username", metavar="USERNAME", prompt=True,
|
|
||||||
help="The username.", callback=__validate_username,
|
|
||||||
default=lambda: os.getlogin())
|
|
||||||
@with_appcontext
|
|
||||||
def init_currencies_command(username: str) -> None:
|
def init_currencies_command(username: str) -> None:
|
||||||
"""Initializes the currencies."""
|
"""Initializes the currencies."""
|
||||||
existing_codes: set[str] = {x.code for x in Currency.query.all()}
|
existing_codes: set[str] = {x.code for x in Currency.query.all()}
|
||||||
@ -63,22 +36,18 @@ def init_currencies_command(username: str) -> None:
|
|||||||
to_add: list[dict[str, str]] = [x for x in data
|
to_add: list[dict[str, str]] = [x for x in data
|
||||||
if x["code"] not in existing_codes]
|
if x["code"] not in existing_codes]
|
||||||
if len(to_add) == 0:
|
if len(to_add) == 0:
|
||||||
click.echo("No more currency to add.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
creator_pk: int = get_user_pk(username)
|
creator_pk: int = get_user_pk(username)
|
||||||
currency_data: list[dict[str, t.Any]] = [{"code": x["code"],
|
currency_data: list[dict[str, Any]] = [{"code": x["code"],
|
||||||
"name_l10n": x["name"],
|
"name_l10n": x["name"],
|
||||||
"created_by_id": creator_pk,
|
"created_by_id": creator_pk,
|
||||||
"updated_by_id": creator_pk}
|
"updated_by_id": creator_pk}
|
||||||
for x in to_add]
|
for x in to_add]
|
||||||
locales: list[str] = [x[5:] for x in to_add[0] if x.startswith("l10n-")]
|
locales: list[str] = [x[5:] for x in to_add[0] if x.startswith("l10n-")]
|
||||||
l10n_data: list[dict[str, str]] = [{"currency_code": x["code"],
|
l10n_data: list[dict[str, str]] = [{"currency_code": x["code"],
|
||||||
"locale": y,
|
"locale": y,
|
||||||
"name": x[f"l10n-{y}"]}
|
"name": x[f"l10n-{y}"]}
|
||||||
for x in to_add for y in locales]
|
for x in to_add for y in locales]
|
||||||
db.session.bulk_insert_mappings(Currency, currency_data)
|
db.session.execute(sa.insert(Currency), currency_data)
|
||||||
db.session.bulk_insert_mappings(CurrencyL10n, l10n_data)
|
db.session.execute(sa.insert(CurrencyL10n), l10n_data)
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
click.echo(F"{len(to_add)} added. Currencies initialized.")
|
|
||||||
|
@ -34,6 +34,7 @@ from accounting.utils.pagination import Pagination
|
|||||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
from accounting.utils.permission import has_permission, can_view, can_edit
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
from .forms import CurrencyForm
|
from .forms import CurrencyForm
|
||||||
|
from .queries import get_currency_query
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("currency", __name__)
|
bp: Blueprint = Blueprint("currency", __name__)
|
||||||
"""The view blueprint for the currency management."""
|
"""The view blueprint for the currency management."""
|
||||||
@ -48,14 +49,13 @@ def list_currencies() -> str:
|
|||||||
|
|
||||||
:return: The currency list.
|
:return: The currency list.
|
||||||
"""
|
"""
|
||||||
from .queries import get_currency_query
|
|
||||||
currencies: list[Currency] = get_currency_query()
|
currencies: list[Currency] = get_currency_query()
|
||||||
pagination: Pagination = Pagination[Currency](currencies)
|
pagination: Pagination = Pagination[Currency](currencies)
|
||||||
return render_template("accounting/currency/list.html",
|
return render_template("accounting/currency/list.html",
|
||||||
list=pagination.list, pagination=pagination)
|
list=pagination.list, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/create", endpoint="create")
|
@bp.get("create", endpoint="create")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_add_currency_form() -> str:
|
def show_add_currency_form() -> str:
|
||||||
"""Shows the form to add a currency.
|
"""Shows the form to add a currency.
|
||||||
@ -72,7 +72,7 @@ def show_add_currency_form() -> str:
|
|||||||
form=form)
|
form=form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/store", endpoint="store")
|
@bp.post("store", endpoint="store")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def add_currency() -> redirect:
|
def add_currency() -> redirect:
|
||||||
"""Adds a currency.
|
"""Adds a currency.
|
||||||
@ -93,7 +93,7 @@ def add_currency() -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(currency)))
|
return redirect(inherit_next(__get_detail_uri(currency)))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<currency:currency>", endpoint="detail")
|
@bp.get("<currency:currency>", endpoint="detail")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_currency_detail(currency: Currency) -> str:
|
def show_currency_detail(currency: Currency) -> str:
|
||||||
"""Shows the currency detail.
|
"""Shows the currency detail.
|
||||||
@ -104,7 +104,7 @@ def show_currency_detail(currency: Currency) -> str:
|
|||||||
return render_template("accounting/currency/detail.html", obj=currency)
|
return render_template("accounting/currency/detail.html", obj=currency)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<currency:currency>/edit", endpoint="edit")
|
@bp.get("<currency:currency>/edit", endpoint="edit")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_currency_edit_form(currency: Currency) -> str:
|
def show_currency_edit_form(currency: Currency) -> str:
|
||||||
"""Shows the form to edit a currency.
|
"""Shows the form to edit a currency.
|
||||||
@ -123,7 +123,7 @@ def show_currency_edit_form(currency: Currency) -> str:
|
|||||||
currency=currency, form=form)
|
currency=currency, form=form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<currency:currency>/update", endpoint="update")
|
@bp.post("<currency:currency>/update", endpoint="update")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def update_currency(currency: Currency) -> redirect:
|
def update_currency(currency: Currency) -> redirect:
|
||||||
"""Updates a currency.
|
"""Updates a currency.
|
||||||
@ -151,7 +151,7 @@ def update_currency(currency: Currency) -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(currency)))
|
return redirect(inherit_next(__get_detail_uri(currency)))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<currency:currency>/delete", endpoint="delete")
|
@bp.post("<currency:currency>/delete", endpoint="delete")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def delete_currency(currency: Currency) -> redirect:
|
def delete_currency(currency: Currency) -> redirect:
|
||||||
"""Deletes a currency.
|
"""Deletes a currency.
|
||||||
@ -169,7 +169,7 @@ def delete_currency(currency: Currency) -> redirect:
|
|||||||
return redirect(or_next(url_for("accounting.currency.list")))
|
return redirect(or_next(url_for("accounting.currency.list")))
|
||||||
|
|
||||||
|
|
||||||
@api_bp.get("/exists-code", endpoint="exists")
|
@api_bp.get("exists-code", endpoint="exists")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def exists_code() -> dict[str, bool]:
|
def exists_code() -> dict[str, bool]:
|
||||||
"""Validates whether a currency code exists.
|
"""Validates whether a currency code exists.
|
||||||
|
@ -17,13 +17,13 @@
|
|||||||
"""The path converters for the journal entry management.
|
"""The path converters for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
|
|
||||||
from flask import abort
|
from flask import abort
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from accounting.models import JournalEntry, JournalEntryLineItem
|
from accounting import db
|
||||||
|
from accounting.models import JournalEntry
|
||||||
from accounting.utils.journal_entry_types import JournalEntryType
|
from accounting.utils.journal_entry_types import JournalEntryType
|
||||||
|
|
||||||
|
|
||||||
@ -37,13 +37,8 @@ class JournalEntryConverter(BaseConverter):
|
|||||||
:param value: The journal entry ID.
|
:param value: The journal entry ID.
|
||||||
:return: The corresponding journal entry.
|
:return: The corresponding journal entry.
|
||||||
"""
|
"""
|
||||||
journal_entry: JournalEntry | None = JournalEntry.query\
|
journal_entry: JournalEntry | None \
|
||||||
.join(JournalEntryLineItem)\
|
= db.session.get(JournalEntry, value)
|
||||||
.filter(JournalEntry.id == value)\
|
|
||||||
.options(selectinload(JournalEntry.line_items)
|
|
||||||
.selectinload(JournalEntryLineItem.offsets)
|
|
||||||
.selectinload(JournalEntryLineItem.journal_entry))\
|
|
||||||
.first()
|
|
||||||
if journal_entry is None:
|
if journal_entry is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
return journal_entry
|
return journal_entry
|
||||||
@ -87,18 +82,18 @@ class DateConverter(BaseConverter):
|
|||||||
"""The date converter to convert the ISO date from and to the
|
"""The date converter to convert the ISO date from and to the
|
||||||
corresponding date in the routes."""
|
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.
|
"""Converts an ISO date to a date.
|
||||||
|
|
||||||
:param value: The ISO date.
|
:param value: The ISO date.
|
||||||
:return: The corresponding date.
|
:return: The corresponding date.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return date.fromisoformat(value)
|
return dt.date.fromisoformat(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def to_url(self, value: date) -> str:
|
def to_url(self, value: dt.date) -> str:
|
||||||
"""Converts a date to its ISO date.
|
"""Converts a date to its ISO date.
|
||||||
|
|
||||||
:param value: The date.
|
:param value: The date.
|
||||||
|
@ -28,14 +28,12 @@ from wtforms.validators import DataRequired
|
|||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.forms import CurrencyExists
|
from accounting.forms import CurrencyExists
|
||||||
from accounting.journal_entry.utils.offset_alias import offset_alias
|
|
||||||
from accounting.locale import lazy_gettext
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import JournalEntryLineItem
|
from accounting.models import JournalEntryLineItem
|
||||||
from accounting.utils.cast import be
|
from accounting.utils.offset_alias import offset_alias
|
||||||
from accounting.utils.strip_text import strip_text
|
from accounting.utils.strip_text import strip_text
|
||||||
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
|
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
|
||||||
|
|
||||||
|
|
||||||
CURRENCY_REQUIRED: DataRequired = DataRequired(
|
CURRENCY_REQUIRED: DataRequired = DataRequired(
|
||||||
lazy_gettext("Please select the currency."))
|
lazy_gettext("Please select the currency."))
|
||||||
"""The validator to check if the currency code is empty."""
|
"""The validator to check if the currency code is empty."""
|
||||||
@ -76,8 +74,8 @@ class KeepCurrencyWhenHavingOffset:
|
|||||||
offset: sa.Alias = offset_alias()
|
offset: sa.Alias = offset_alias()
|
||||||
original_line_items: list[JournalEntryLineItem]\
|
original_line_items: list[JournalEntryLineItem]\
|
||||||
= JournalEntryLineItem.query\
|
= JournalEntryLineItem.query\
|
||||||
.join(offset, be(JournalEntryLineItem.id
|
.join(offset,
|
||||||
== offset.c.original_line_item_id),
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True)\
|
isouter=True)\
|
||||||
.filter(JournalEntryLineItem.id
|
.filter(JournalEntryLineItem.id
|
||||||
.in_({x.id.data for x in form.line_items
|
.in_({x.id.data for x in form.line_items
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import typing as t
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TypeVar, Generic, Type
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask_babel import LazyString
|
from flask_babel import LazyString
|
||||||
@ -29,13 +29,13 @@ from wtforms import DateField, FieldList, FormField, TextAreaField, \
|
|||||||
from wtforms.validators import DataRequired, ValidationError
|
from wtforms.validators import DataRequired, ValidationError
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
|
from accounting.journal_entry.utils.account_option import AccountOption
|
||||||
|
from accounting.journal_entry.utils.description_editor import DescriptionEditor
|
||||||
|
from accounting.journal_entry.utils.original_line_items import \
|
||||||
|
get_selectable_original_line_items
|
||||||
from accounting.locale import lazy_gettext
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import JournalEntry, Account, JournalEntryLineItem, \
|
from accounting.models import JournalEntry, Account, JournalEntryLineItem, \
|
||||||
JournalEntryCurrency
|
JournalEntryCurrency
|
||||||
from accounting.journal_entry.utils.account_option import AccountOption
|
|
||||||
from accounting.journal_entry.utils.original_line_items import \
|
|
||||||
get_selectable_original_line_items
|
|
||||||
from accounting.journal_entry.utils.description_editor import DescriptionEditor
|
|
||||||
from accounting.utils.random_id import new_id
|
from accounting.utils.random_id import new_id
|
||||||
from accounting.utils.strip_text import strip_multiline_text
|
from accounting.utils.strip_text import strip_multiline_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
@ -123,7 +123,7 @@ class JournalEntryForm(FlaskForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.is_modified: bool = False
|
self.is_modified: bool = False
|
||||||
"""Whether the journal entry is modified during populate_obj()."""
|
"""Whether the journal entry is modified during populate_obj()."""
|
||||||
self.collector: t.Type[LineItemCollector] = LineItemCollector
|
self.collector: Type[LineItemCollector] = LineItemCollector
|
||||||
"""The line item collector. The default is the base abstract
|
"""The line item collector. The default is the base abstract
|
||||||
collector only to provide the correct type. The subclass forms should
|
collector only to provide the correct type. The subclass forms should
|
||||||
provide their own collectors."""
|
provide their own collectors."""
|
||||||
@ -155,7 +155,7 @@ class JournalEntryForm(FlaskForm):
|
|||||||
self.__set_date(obj, self.date.data)
|
self.__set_date(obj, self.date.data)
|
||||||
obj.note = self.note.data
|
obj.note = self.note.data
|
||||||
|
|
||||||
collector_cls: t.Type[LineItemCollector] = self.collector
|
collector_cls: Type[LineItemCollector] = self.collector
|
||||||
collector: collector_cls = collector_cls(self, obj)
|
collector: collector_cls = collector_cls(self, obj)
|
||||||
collector.collect()
|
collector.collect()
|
||||||
|
|
||||||
@ -309,11 +309,11 @@ class JournalEntryForm(FlaskForm):
|
|||||||
return db.session.scalar(select)
|
return db.session.scalar(select)
|
||||||
|
|
||||||
|
|
||||||
T = t.TypeVar("T", bound=JournalEntryForm)
|
T = TypeVar("T", bound=JournalEntryForm)
|
||||||
"""A journal entry form variant."""
|
"""A journal entry form variant."""
|
||||||
|
|
||||||
|
|
||||||
class LineItemCollector(t.Generic[T], ABC):
|
class LineItemCollector(Generic[T], ABC):
|
||||||
"""The line item collector."""
|
"""The line item collector."""
|
||||||
|
|
||||||
def __init__(self, form: T, obj: JournalEntry):
|
def __init__(self, form: T, obj: JournalEntry):
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The line item sub-forms for the journal entry management.
|
"""The line item sub-forms for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -31,9 +31,8 @@ from accounting import db
|
|||||||
from accounting.forms import ACCOUNT_REQUIRED, AccountExists, IsDebitAccount, \
|
from accounting.forms import ACCOUNT_REQUIRED, AccountExists, IsDebitAccount, \
|
||||||
IsCreditAccount
|
IsCreditAccount
|
||||||
from accounting.locale import lazy_gettext
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import Account, JournalEntryLineItem
|
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.template_filters import format_amount
|
from accounting.template_filters import format_amount
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.random_id import new_id
|
from accounting.utils.random_id import new_id
|
||||||
from accounting.utils.strip_text import strip_text
|
from accounting.utils.strip_text import strip_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
@ -127,10 +126,8 @@ class KeepAccountWhenHavingOffset:
|
|||||||
assert isinstance(form, LineItemForm)
|
assert isinstance(form, LineItemForm)
|
||||||
if field.data is None or form.id.data is None:
|
if field.data is None or form.id.data is None:
|
||||||
return
|
return
|
||||||
line_item: JournalEntryLineItem | None = db.session\
|
line_item: JournalEntryLineItem | None \
|
||||||
.query(JournalEntryLineItem)\
|
= db.session.get(JournalEntryLineItem, form.id.data)
|
||||||
.filter(JournalEntryLineItem.id == form.id.data)\
|
|
||||||
.options(selectinload(JournalEntryLineItem.offsets)).first()
|
|
||||||
if line_item is None or len(line_item.offsets) == 0:
|
if line_item is None or len(line_item.offsets) == 0:
|
||||||
return
|
return
|
||||||
if field.data != line_item.account_code:
|
if field.data != line_item.account_code:
|
||||||
@ -200,13 +197,13 @@ class NotExceedingOriginalLineItemNetBalance:
|
|||||||
existing_line_item_id \
|
existing_line_item_id \
|
||||||
= {x.id for x in form.journal_entry_form.obj.line_items}
|
= {x.id for x in form.journal_entry_form.obj.line_items}
|
||||||
offset_total_func: sa.Function = sa.func.sum(sa.case(
|
offset_total_func: sa.Function = sa.func.sum(sa.case(
|
||||||
(be(JournalEntryLineItem.is_debit == is_debit),
|
(JournalEntryLineItem.is_debit == is_debit,
|
||||||
JournalEntryLineItem.amount),
|
JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
offset_total_but_form: Decimal | None = db.session.scalar(
|
offset_total_but_form: Decimal | None = db.session.scalar(
|
||||||
sa.select(offset_total_func)
|
sa.select(offset_total_func)
|
||||||
.filter(be(JournalEntryLineItem.original_line_item_id
|
.filter(JournalEntryLineItem.original_line_item_id
|
||||||
== original_line_item.id),
|
== original_line_item.id,
|
||||||
JournalEntryLineItem.id.not_in(existing_line_item_id)))
|
JournalEntryLineItem.id.not_in(existing_line_item_id)))
|
||||||
if offset_total_but_form is None:
|
if offset_total_but_form is None:
|
||||||
offset_total_but_form = Decimal("0")
|
offset_total_but_form = Decimal("0")
|
||||||
@ -234,8 +231,7 @@ class NotLessThanOffsetTotal:
|
|||||||
(JournalEntryLineItem.is_debit != is_debit,
|
(JournalEntryLineItem.is_debit != is_debit,
|
||||||
JournalEntryLineItem.amount),
|
JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount)))\
|
else_=-JournalEntryLineItem.amount)))\
|
||||||
.filter(be(JournalEntryLineItem.original_line_item_id
|
.filter(JournalEntryLineItem.original_line_item_id == form.id.data)
|
||||||
== form.id.data))
|
|
||||||
offset_total: Decimal | None = db.session.scalar(select_offset_total)
|
offset_total: Decimal | None = db.session.scalar(select_offset_total)
|
||||||
if offset_total is not None and field.data < offset_total:
|
if offset_total is not None and field.data < offset_total:
|
||||||
raise ValidationError(lazy_gettext(
|
raise ValidationError(lazy_gettext(
|
||||||
@ -269,6 +265,19 @@ class LineItemForm(FlaskForm):
|
|||||||
self.journal_entry_form: JournalEntryForm | None = None
|
self.journal_entry_form: JournalEntryForm | None = None
|
||||||
"""The source journal entry form."""
|
"""The source journal entry form."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_title(self) -> str:
|
||||||
|
"""Returns the title of the account.
|
||||||
|
|
||||||
|
:return: The title of the account.
|
||||||
|
"""
|
||||||
|
if self.account_code.data is None:
|
||||||
|
return ""
|
||||||
|
account: Account | None = Account.find_by_code(self.account_code.data)
|
||||||
|
if account is None:
|
||||||
|
return ""
|
||||||
|
return account.title
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account_text(self) -> str:
|
def account_text(self) -> str:
|
||||||
"""Returns the text representation of the account.
|
"""Returns the text representation of the account.
|
||||||
@ -298,7 +307,7 @@ class LineItemForm(FlaskForm):
|
|||||||
return getattr(self, "____original_line_item")
|
return getattr(self, "____original_line_item")
|
||||||
|
|
||||||
@property
|
@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.
|
"""Returns the text representation of the original line item.
|
||||||
|
|
||||||
:return: The text representation of the original line item.
|
:return: The text representation of the original line item.
|
||||||
@ -344,14 +353,13 @@ class LineItemForm(FlaskForm):
|
|||||||
def get_offsets() -> list[JournalEntryLineItem]:
|
def get_offsets() -> list[JournalEntryLineItem]:
|
||||||
if not self.is_need_offset or self.id.data is None:
|
if not self.is_need_offset or self.id.data is None:
|
||||||
return []
|
return []
|
||||||
return JournalEntryLineItem.query\
|
return JournalEntryLineItem.query.join(JournalEntry)\
|
||||||
.filter(JournalEntryLineItem.original_line_item_id
|
.filter(JournalEntryLineItem.original_line_item_id
|
||||||
== self.id.data)\
|
== self.id.data)\
|
||||||
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
|
JournalEntryLineItem.no)\
|
||||||
.options(selectinload(JournalEntryLineItem.journal_entry),
|
.options(selectinload(JournalEntryLineItem.journal_entry),
|
||||||
selectinload(JournalEntryLineItem.account),
|
selectinload(JournalEntryLineItem.account)).all()
|
||||||
selectinload(JournalEntryLineItem.offsets)
|
|
||||||
.selectinload(
|
|
||||||
JournalEntryLineItem.journal_entry)).all()
|
|
||||||
setattr(self, "__offsets", get_offsets())
|
setattr(self, "__offsets", get_offsets())
|
||||||
return getattr(self, "__offsets")
|
return getattr(self, "__offsets")
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The reorder forms for the journal entry management.
|
"""The reorder forms for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import request
|
from flask import request
|
||||||
@ -26,17 +26,15 @@ from accounting import db
|
|||||||
from accounting.models import JournalEntry
|
from accounting.models import JournalEntry
|
||||||
|
|
||||||
|
|
||||||
def sort_journal_entries_in(journal_entry_date: date,
|
def sort_journal_entries_in(date: dt.date, exclude: int | None = None) -> None:
|
||||||
exclude: int | None = None) -> None:
|
|
||||||
"""Sorts the journal entries under a date after changing the date or
|
"""Sorts the journal entries under a date after changing the date or
|
||||||
deleting a journal entry.
|
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.
|
:param exclude: The journal entry ID to exclude.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.BinaryExpression] = [JournalEntry.date == date]
|
||||||
= [JournalEntry.date == journal_entry_date]
|
|
||||||
if exclude is not None:
|
if exclude is not None:
|
||||||
conditions.append(JournalEntry.id != exclude)
|
conditions.append(JournalEntry.id != exclude)
|
||||||
journal_entries: list[JournalEntry] = JournalEntry.query\
|
journal_entries: list[JournalEntry] = JournalEntry.query\
|
||||||
@ -50,12 +48,12 @@ def sort_journal_entries_in(journal_entry_date: date,
|
|||||||
class JournalEntryReorderForm:
|
class JournalEntryReorderForm:
|
||||||
"""The form to reorder the journal entries."""
|
"""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.
|
"""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
|
self.is_modified: bool = False
|
||||||
|
|
||||||
def save_order(self) -> None:
|
def save_order(self) -> None:
|
||||||
|
@ -32,6 +32,8 @@ class AccountOption:
|
|||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
self.code: str = account.code
|
self.code: str = account.code
|
||||||
"""The account code."""
|
"""The account code."""
|
||||||
|
self.title: str = account.title
|
||||||
|
"""The account title."""
|
||||||
self.query_values: list[str] = account.query_values
|
self.query_values: list[str] = account.query_values
|
||||||
"""The values to be queried."""
|
"""The values to be queried."""
|
||||||
self.__str: str = str(account)
|
self.__str: str = str(account)
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import typing as t
|
from typing import Literal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
@ -54,6 +54,14 @@ class DescriptionAccount:
|
|||||||
"""
|
"""
|
||||||
return str(self.__account)
|
return str(self.__account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def title(self) -> str:
|
||||||
|
"""Returns the account title.
|
||||||
|
|
||||||
|
:return: The account title.
|
||||||
|
"""
|
||||||
|
return self.__account.title
|
||||||
|
|
||||||
def add_freq(self, freq: int) -> None:
|
def add_freq(self, freq: int) -> None:
|
||||||
"""Adds the frequency of an account.
|
"""Adds the frequency of an account.
|
||||||
|
|
||||||
@ -116,12 +124,12 @@ class DescriptionTag:
|
|||||||
class DescriptionType:
|
class DescriptionType:
|
||||||
"""A description type"""
|
"""A description type"""
|
||||||
|
|
||||||
def __init__(self, type_id: t.Literal["general", "travel", "bus"]):
|
def __init__(self, type_id: Literal["general", "travel", "bus"]):
|
||||||
"""Constructs a description type.
|
"""Constructs a description type.
|
||||||
|
|
||||||
:param type_id: The type ID, either "general", "travel", or "bus".
|
:param type_id: The type ID, either "general", "travel", or "bus".
|
||||||
"""
|
"""
|
||||||
self.id: t.Literal["general", "travel", "bus"] = type_id
|
self.id: Literal["general", "travel", "bus"] = type_id
|
||||||
"""The type ID."""
|
"""The type ID."""
|
||||||
self.__tag_dict: dict[str, DescriptionTag] = {}
|
self.__tag_dict: dict[str, DescriptionTag] = {}
|
||||||
"""A dictionary from the tag name to their corresponding tag."""
|
"""A dictionary from the tag name to their corresponding tag."""
|
||||||
@ -173,12 +181,12 @@ class DescriptionRecurring:
|
|||||||
class DescriptionDebitCredit:
|
class DescriptionDebitCredit:
|
||||||
"""The description on debit or credit."""
|
"""The description on debit or credit."""
|
||||||
|
|
||||||
def __init__(self, debit_credit: t.Literal["debit", "credit"]):
|
def __init__(self, debit_credit: Literal["debit", "credit"]):
|
||||||
"""Constructs the description on debit or credit.
|
"""Constructs the description on debit or credit.
|
||||||
|
|
||||||
:param debit_credit: Either "debit" or "credit".
|
:param debit_credit: Either "debit" or "credit".
|
||||||
"""
|
"""
|
||||||
self.debit_credit: t.Literal["debit", "credit"] = debit_credit
|
self.debit_credit: Literal["debit", "credit"] = debit_credit
|
||||||
"""Either debit or credit."""
|
"""Either debit or credit."""
|
||||||
self.general: DescriptionType = DescriptionType("general")
|
self.general: DescriptionType = DescriptionType("general")
|
||||||
"""The general tags."""
|
"""The general tags."""
|
||||||
@ -186,14 +194,14 @@ class DescriptionDebitCredit:
|
|||||||
"""The travel tags."""
|
"""The travel tags."""
|
||||||
self.bus: DescriptionType = DescriptionType("bus")
|
self.bus: DescriptionType = DescriptionType("bus")
|
||||||
"""The bus tags."""
|
"""The bus tags."""
|
||||||
self.__type_dict: dict[t.Literal["general", "travel", "bus"],
|
self.__type_dict: dict[Literal["general", "travel", "bus"],
|
||||||
DescriptionType] \
|
DescriptionType] \
|
||||||
= {x.id: x for x in {self.general, self.travel, self.bus}}
|
= {x.id: x for x in {self.general, self.travel, self.bus}}
|
||||||
"""A dictionary from the type ID to the corresponding tags."""
|
"""A dictionary from the type ID to the corresponding tags."""
|
||||||
self.recurring: list[DescriptionRecurring] = []
|
self.recurring: list[DescriptionRecurring] = []
|
||||||
"""The recurring transactions."""
|
"""The recurring transactions."""
|
||||||
|
|
||||||
def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
|
def add_tag(self, tag_type: Literal["general", "travel", "bus"],
|
||||||
name: str, account: Account, freq: int) -> None:
|
name: str, account: Account, freq: int) -> None:
|
||||||
"""Adds a tag.
|
"""Adds a tag.
|
||||||
|
|
||||||
@ -270,7 +278,7 @@ class DescriptionEditor:
|
|||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
= {x.id: x for x in Account.query
|
= {x.id: x for x in Account.query
|
||||||
.filter(Account.id.in_({x.account_id for x in result})).all()}
|
.filter(Account.id.in_({x.account_id for x in result})).all()}
|
||||||
debit_credit_dict: dict[t.Literal["debit", "credit"],
|
debit_credit_dict: dict[Literal["debit", "credit"],
|
||||||
DescriptionDebitCredit] \
|
DescriptionDebitCredit] \
|
||||||
= {x.debit_credit: x for x in {self.debit, self.credit}}
|
= {x.debit_credit: x for x in {self.debit, self.credit}}
|
||||||
for row in result:
|
for row in result:
|
||||||
|
@ -17,19 +17,19 @@
|
|||||||
"""The operators for different journal entry types.
|
"""The operators for different journal entry types.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from flask import render_template, request, abort
|
from flask import render_template, request, abort
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
|
|
||||||
from accounting.models import JournalEntry
|
|
||||||
from accounting.template_globals import default_currency_code
|
|
||||||
from accounting.utils.journal_entry_types import JournalEntryType
|
|
||||||
from accounting.journal_entry.forms import JournalEntryForm, \
|
from accounting.journal_entry.forms import JournalEntryForm, \
|
||||||
CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
|
CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
|
||||||
TransferJournalEntryForm
|
TransferJournalEntryForm
|
||||||
from accounting.journal_entry.forms.line_item import LineItemForm
|
from accounting.journal_entry.forms.line_item import LineItemForm
|
||||||
|
from accounting.models import JournalEntry
|
||||||
|
from accounting.template_globals import default_currency_code
|
||||||
|
from accounting.utils.journal_entry_types import JournalEntryType
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryOperator(ABC):
|
class JournalEntryOperator(ABC):
|
||||||
@ -39,7 +39,7 @@ class JournalEntryOperator(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def form(self) -> t.Type[JournalEntryForm]:
|
def form(self) -> Type[JournalEntryForm]:
|
||||||
"""Returns the form class.
|
"""Returns the form class.
|
||||||
|
|
||||||
:return: The form class.
|
:return: The form class.
|
||||||
@ -100,7 +100,7 @@ class CashReceiptJournalEntry(JournalEntryOperator):
|
|||||||
"""The order when checking the journal entry operator."""
|
"""The order when checking the journal entry operator."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> t.Type[JournalEntryForm]:
|
def form(self) -> Type[JournalEntryForm]:
|
||||||
"""Returns the form class.
|
"""Returns the form class.
|
||||||
|
|
||||||
:return: The form class.
|
:return: The form class.
|
||||||
@ -170,7 +170,7 @@ class CashDisbursementJournalEntry(JournalEntryOperator):
|
|||||||
"""The order when checking the journal entry operator."""
|
"""The order when checking the journal entry operator."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> t.Type[JournalEntryForm]:
|
def form(self) -> Type[JournalEntryForm]:
|
||||||
"""Returns the form class.
|
"""Returns the form class.
|
||||||
|
|
||||||
:return: The form class.
|
:return: The form class.
|
||||||
@ -243,7 +243,7 @@ class TransferJournalEntry(JournalEntryOperator):
|
|||||||
"""The order when checking the journal entry operator."""
|
"""The order when checking the journal entry operator."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> t.Type[JournalEntryForm]:
|
def form(self) -> Type[JournalEntryForm]:
|
||||||
"""Returns the form class.
|
"""Returns the form class.
|
||||||
|
|
||||||
:return: The form class.
|
:return: The form class.
|
||||||
|
@ -24,8 +24,7 @@ from sqlalchemy.orm import selectinload
|
|||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.utils.cast import be
|
from accounting.utils.offset_alias import offset_alias
|
||||||
from .offset_alias import offset_alias
|
|
||||||
|
|
||||||
|
|
||||||
def get_selectable_original_line_items(
|
def get_selectable_original_line_items(
|
||||||
@ -45,8 +44,7 @@ def get_selectable_original_line_items(
|
|||||||
offset: sa.Alias = offset_alias()
|
offset: sa.Alias = offset_alias()
|
||||||
net_balance: sa.Label = (JournalEntryLineItem.amount + sa.func.sum(sa.case(
|
net_balance: sa.Label = (JournalEntryLineItem.amount + sa.func.sum(sa.case(
|
||||||
(offset.c.id.in_(line_item_id_on_form), 0),
|
(offset.c.id.in_(line_item_id_on_form), 0),
|
||||||
(be(offset.c.is_debit == JournalEntryLineItem.is_debit),
|
(offset.c.is_debit == JournalEntryLineItem.is_debit, offset.c.amount),
|
||||||
offset.c.amount),
|
|
||||||
else_=-offset.c.amount))).label("net_balance")
|
else_=-offset.c.amount))).label("net_balance")
|
||||||
conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
|
conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
|
||||||
sub_conditions: list[sa.BinaryExpression] = []
|
sub_conditions: list[sa.BinaryExpression] = []
|
||||||
@ -60,8 +58,8 @@ def get_selectable_original_line_items(
|
|||||||
select_net_balances: sa.Select \
|
select_net_balances: sa.Select \
|
||||||
= sa.select(JournalEntryLineItem.id, net_balance)\
|
= sa.select(JournalEntryLineItem.id, net_balance)\
|
||||||
.join(Account)\
|
.join(Account)\
|
||||||
.join(offset, be(JournalEntryLineItem.id
|
.join(offset,
|
||||||
== offset.c.original_line_item_id),
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True)\
|
isouter=True)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(JournalEntryLineItem.id)\
|
.group_by(JournalEntryLineItem.id)\
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The views for the journal entry management.
|
"""The views for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from urllib.parse import parse_qsl, urlencode
|
from urllib.parse import parse_qsl, urlencode
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -30,9 +30,9 @@ from accounting.locale import lazy_gettext
|
|||||||
from accounting.models import JournalEntry
|
from accounting.models import JournalEntry
|
||||||
from accounting.utils.cast import s
|
from accounting.utils.cast import s
|
||||||
from accounting.utils.flash_errors import flash_form_errors
|
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.next_uri import inherit_next, or_next
|
||||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
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 accounting.utils.user import get_current_user_pk
|
||||||
from .forms import sort_journal_entries_in, JournalEntryReorderForm
|
from .forms import sort_journal_entries_in, JournalEntryReorderForm
|
||||||
from .template_filters import with_type, to_transfer, format_amount_input, \
|
from .template_filters import with_type, to_transfer, format_amount_input, \
|
||||||
@ -49,7 +49,7 @@ bp.add_app_template_filter(format_amount_input,
|
|||||||
bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html")
|
bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html")
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/create/<journalEntryType:journal_entry_type>", endpoint="create")
|
@bp.get("create/<journalEntryType:journal_entry_type>", endpoint="create")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
|
def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
|
||||||
"""Shows the form to add a journal entry.
|
"""Shows the form to add a journal entry.
|
||||||
@ -67,11 +67,11 @@ def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
|
|||||||
form.validate()
|
form.validate()
|
||||||
else:
|
else:
|
||||||
form = journal_entry_op.form()
|
form = journal_entry_op.form()
|
||||||
form.date.data = date.today()
|
form.date.data = dt.date.today()
|
||||||
return journal_entry_op.render_create_template(form)
|
return journal_entry_op.render_create_template(form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/store/<journalEntryType:journal_entry_type>", endpoint="store")
|
@bp.post("store/<journalEntryType:journal_entry_type>", endpoint="store")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
|
def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
|
||||||
"""Adds a journal entry.
|
"""Adds a journal entry.
|
||||||
@ -98,7 +98,7 @@ def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<journalEntry:journal_entry>", endpoint="detail")
|
@bp.get("<journalEntry:journal_entry>", endpoint="detail")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
|
def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
|
||||||
"""Shows the journal entry detail.
|
"""Shows the journal entry detail.
|
||||||
@ -111,7 +111,7 @@ def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
|
|||||||
return journal_entry_op.render_detail_template(journal_entry)
|
return journal_entry_op.render_detail_template(journal_entry)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<journalEntry:journal_entry>/edit", endpoint="edit")
|
@bp.get("<journalEntry:journal_entry>/edit", endpoint="edit")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
|
def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
|
||||||
"""Shows the form to edit a journal entry.
|
"""Shows the form to edit a journal entry.
|
||||||
@ -133,7 +133,7 @@ def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
|
|||||||
return journal_entry_op.render_edit_template(journal_entry, form)
|
return journal_entry_op.render_edit_template(journal_entry, form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<journalEntry:journal_entry>/update", endpoint="update")
|
@bp.post("<journalEntry:journal_entry>/update", endpoint="update")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def update_journal_entry(journal_entry: JournalEntry) -> redirect:
|
def update_journal_entry(journal_entry: JournalEntry) -> redirect:
|
||||||
"""Updates a journal entry.
|
"""Updates a journal entry.
|
||||||
@ -166,7 +166,7 @@ def update_journal_entry(journal_entry: JournalEntry) -> redirect:
|
|||||||
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
return redirect(inherit_next(__get_detail_uri(journal_entry)))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<journalEntry:journal_entry>/delete", endpoint="delete")
|
@bp.post("<journalEntry:journal_entry>/delete", endpoint="delete")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
|
def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
|
||||||
"""Deletes a journal entry.
|
"""Deletes a journal entry.
|
||||||
@ -186,31 +186,31 @@ def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
|
|||||||
return redirect(or_next(__get_default_page_uri()))
|
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)
|
@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.
|
"""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.
|
:return: The order of the journal entries in the date.
|
||||||
"""
|
"""
|
||||||
journal_entries: list[JournalEntry] = JournalEntry.query \
|
journal_entries: list[JournalEntry] = JournalEntry.query \
|
||||||
.filter(JournalEntry.date == journal_entry_date) \
|
.filter(JournalEntry.date == date) \
|
||||||
.order_by(JournalEntry.no).all()
|
.order_by(JournalEntry.no).all()
|
||||||
return render_template("accounting/journal-entry/order.html",
|
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)
|
@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.
|
"""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
|
:return: The redirection to the incoming account or the account list. The
|
||||||
reordering operation does not fail.
|
reordering operation does not fail.
|
||||||
"""
|
"""
|
||||||
form: JournalEntryReorderForm = JournalEntryReorderForm(journal_entry_date)
|
form: JournalEntryReorderForm = JournalEntryReorderForm(date)
|
||||||
form.save_order()
|
form.save_order()
|
||||||
if not form.is_modified:
|
if not form.is_modified:
|
||||||
flash(s(lazy_gettext("The order was not modified.")), "success")
|
flash(s(lazy_gettext("The order was not modified.")), "success")
|
||||||
|
@ -19,14 +19,16 @@
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
import re
|
import re
|
||||||
import typing as t
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Type, Self
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from babel import Locale
|
from babel import Locale
|
||||||
from flask_babel import get_locale, get_babel
|
from flask_babel import get_locale, get_babel
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
@ -37,14 +39,14 @@ class BaseAccount(db.Model):
|
|||||||
"""A base account."""
|
"""A base account."""
|
||||||
__tablename__ = "accounting_base_accounts"
|
__tablename__ = "accounting_base_accounts"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
code = db.Column(db.String, nullable=False, primary_key=True)
|
code: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The code."""
|
"""The code."""
|
||||||
title_l10n = db.Column("title", db.String, nullable=False)
|
title_l10n: Mapped[str] = mapped_column("title")
|
||||||
"""The title."""
|
"""The title."""
|
||||||
l10n = db.relationship("BaseAccountL10n", back_populates="account",
|
l10n: Mapped[list[BaseAccountL10n]] \
|
||||||
lazy=False)
|
= db.relationship(back_populates="account", lazy=False)
|
||||||
"""The localized titles."""
|
"""The localized titles."""
|
||||||
accounts = db.relationship("Account", back_populates="base")
|
accounts: Mapped[list[Account]] = db.relationship(back_populates="base")
|
||||||
"""The descendant accounts under the base account."""
|
"""The descendant accounts under the base account."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -81,17 +83,16 @@ class BaseAccountL10n(db.Model):
|
|||||||
"""A localized base account title."""
|
"""A localized base account title."""
|
||||||
__tablename__ = "accounting_base_accounts_l10n"
|
__tablename__ = "accounting_base_accounts_l10n"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
account_code = db.Column(db.String,
|
account_code: Mapped[str] \
|
||||||
db.ForeignKey(BaseAccount.code,
|
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
||||||
onupdate="CASCADE",
|
ondelete="CASCADE"),
|
||||||
ondelete="CASCADE"),
|
primary_key=True)
|
||||||
nullable=False, primary_key=True)
|
|
||||||
"""The code of the account."""
|
"""The code of the account."""
|
||||||
account = db.relationship(BaseAccount, back_populates="l10n")
|
account: Mapped[BaseAccount] = db.relationship(back_populates="l10n")
|
||||||
"""The account."""
|
"""The account."""
|
||||||
locale = db.Column(db.String, nullable=False, primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The locale."""
|
"""The locale."""
|
||||||
title = db.Column(db.String, nullable=False)
|
title: Mapped[str]
|
||||||
"""The localized title."""
|
"""The localized title."""
|
||||||
|
|
||||||
|
|
||||||
@ -99,47 +100,43 @@ class Account(db.Model):
|
|||||||
"""An account."""
|
"""An account."""
|
||||||
__tablename__ = "accounting_accounts"
|
__tablename__ = "accounting_accounts"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
||||||
autoincrement=False)
|
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
base_code = db.Column(db.String,
|
base_code: Mapped[str] \
|
||||||
db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"))
|
||||||
nullable=False)
|
|
||||||
"""The code of the base account."""
|
"""The code of the base account."""
|
||||||
base = db.relationship(BaseAccount, back_populates="accounts")
|
base: Mapped[BaseAccount] = db.relationship(back_populates="accounts")
|
||||||
"""The base account."""
|
"""The base account."""
|
||||||
no = db.Column(db.Integer, nullable=False, default=text("1"))
|
no: Mapped[int] = mapped_column(default=text("1"))
|
||||||
"""The account number under the base account."""
|
"""The account number under the base account."""
|
||||||
title_l10n = db.Column("title", db.String, nullable=False)
|
title_l10n: Mapped[str] = mapped_column("title")
|
||||||
"""The title."""
|
"""The title."""
|
||||||
is_need_offset = db.Column(db.Boolean, nullable=False, default=False)
|
is_need_offset: Mapped[bool] = mapped_column(default=False)
|
||||||
"""Whether the journal entry line items of this account need offset."""
|
"""Whether the journal entry line items of this account need offset."""
|
||||||
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
created_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
"""The time of creation."""
|
server_default=db.func.now())
|
||||||
created_by_id = db.Column(db.Integer,
|
"""The date and time when this record was created."""
|
||||||
db.ForeignKey(user_pk_column,
|
created_by_id: Mapped[int] \
|
||||||
onupdate="CASCADE"),
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
nullable=False)
|
"""The ID of the user who created the record."""
|
||||||
"""The ID of the creator."""
|
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
||||||
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
|
"""The user who created the record."""
|
||||||
"""The creator."""
|
updated_at: Mapped[dt.datetime] \
|
||||||
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The date and time when this record was last updated."""
|
||||||
updated_by_id = db.Column(db.Integer,
|
updated_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
"""The ID of the last user who updated the record."""
|
||||||
nullable=False)
|
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
|
||||||
"""The ID of the updator."""
|
"""The last user who updated the record."""
|
||||||
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
l10n: Mapped[list[AccountL10n]] \
|
||||||
"""The updator."""
|
= db.relationship(back_populates="account", lazy=False)
|
||||||
l10n = db.relationship("AccountL10n", back_populates="account",
|
|
||||||
lazy=False)
|
|
||||||
"""The localized titles."""
|
"""The localized titles."""
|
||||||
line_items = db.relationship("JournalEntryLineItem",
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
back_populates="account")
|
= db.relationship(back_populates="account")
|
||||||
"""The journal entry line items."""
|
"""The journal entry line items."""
|
||||||
|
|
||||||
CASH_CODE: str = "1111-001"
|
CASH_CODE: str = "1111-001"
|
||||||
@ -185,6 +182,8 @@ class Account(db.Model):
|
|||||||
:param value: The new title.
|
:param value: The new title.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
|
if self.title == value:
|
||||||
|
return
|
||||||
if self.title_l10n is None:
|
if self.title_l10n is None:
|
||||||
self.title_l10n = value
|
self.title_l10n = value
|
||||||
return
|
return
|
||||||
@ -214,6 +213,25 @@ class Account(db.Model):
|
|||||||
"""
|
"""
|
||||||
return not self.is_real
|
return not self.is_real
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count(self) -> int:
|
||||||
|
"""Returns the number of items in the account.
|
||||||
|
|
||||||
|
:return: The number of items in the account.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "__count"):
|
||||||
|
setattr(self, "__count", 0)
|
||||||
|
return getattr(self, "__count")
|
||||||
|
|
||||||
|
@count.setter
|
||||||
|
def count(self, value: int) -> None:
|
||||||
|
"""Sets the number of items in the account.
|
||||||
|
|
||||||
|
:param value: The number of items in the account.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
setattr(self, "__count", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def query_values(self) -> list[str]:
|
def query_values(self) -> list[str]:
|
||||||
"""Returns the values to be queried.
|
"""Returns the values to be queried.
|
||||||
@ -251,11 +269,11 @@ class Account(db.Model):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
AccountL10n.query.filter(AccountL10n.account == self).delete()
|
AccountL10n.query.filter(AccountL10n.account == self).delete()
|
||||||
cls: t.Type[t.Self] = self.__class__
|
cls: Type[Self] = self.__class__
|
||||||
cls.query.filter(cls.id == self.id).delete()
|
cls.query.filter(cls.id == self.id).delete()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_by_code(cls, code: str) -> t.Self | None:
|
def find_by_code(cls, code: str) -> Self | None:
|
||||||
"""Finds an account by its code.
|
"""Finds an account by its code.
|
||||||
|
|
||||||
:param code: The code.
|
:param code: The code.
|
||||||
@ -268,7 +286,7 @@ class Account(db.Model):
|
|||||||
cls.no == int(m.group(2))).first()
|
cls.no == int(m.group(2))).first()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def selectable_debit(cls) -> list[t.Self]:
|
def selectable_debit(cls) -> list[Self]:
|
||||||
"""Returns the selectable debit accounts.
|
"""Returns the selectable debit accounts.
|
||||||
Payable line items can not start from debit.
|
Payable line items can not start from debit.
|
||||||
|
|
||||||
@ -291,7 +309,7 @@ class Account(db.Model):
|
|||||||
.order_by(cls.base_code, cls.no).all()
|
.order_by(cls.base_code, cls.no).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def selectable_credit(cls) -> list[t.Self]:
|
def selectable_credit(cls) -> list[Self]:
|
||||||
"""Returns the selectable debit accounts.
|
"""Returns the selectable debit accounts.
|
||||||
Receivable line items can not start from credit.
|
Receivable line items can not start from credit.
|
||||||
|
|
||||||
@ -313,7 +331,7 @@ class Account(db.Model):
|
|||||||
.order_by(cls.base_code, cls.no).all()
|
.order_by(cls.base_code, cls.no).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cash(cls) -> t.Self:
|
def cash(cls) -> Self:
|
||||||
"""Returns the cash account.
|
"""Returns the cash account.
|
||||||
|
|
||||||
:return: The cash account
|
:return: The cash account
|
||||||
@ -321,7 +339,7 @@ class Account(db.Model):
|
|||||||
return cls.find_by_code(cls.CASH_CODE)
|
return cls.find_by_code(cls.CASH_CODE)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def accumulated_change(cls) -> t.Self:
|
def accumulated_change(cls) -> Self:
|
||||||
"""Returns the accumulated-change account.
|
"""Returns the accumulated-change account.
|
||||||
|
|
||||||
:return: The accumulated-change account
|
:return: The accumulated-change account
|
||||||
@ -333,16 +351,16 @@ class AccountL10n(db.Model):
|
|||||||
"""A localized account title."""
|
"""A localized account title."""
|
||||||
__tablename__ = "accounting_accounts_l10n"
|
__tablename__ = "accounting_accounts_l10n"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
account_id = db.Column(db.Integer,
|
account_id: Mapped[int] \
|
||||||
db.ForeignKey(Account.id, onupdate="CASCADE",
|
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"),
|
||||||
nullable=False, primary_key=True)
|
primary_key=True)
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
account = db.relationship(Account, back_populates="l10n")
|
account: Mapped[Account] = db.relationship(back_populates="l10n")
|
||||||
"""The account."""
|
"""The account."""
|
||||||
locale = db.Column(db.String, nullable=False, primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The locale."""
|
"""The locale."""
|
||||||
title = db.Column(db.String, nullable=False)
|
title: Mapped[str]
|
||||||
"""The localized title."""
|
"""The localized title."""
|
||||||
|
|
||||||
|
|
||||||
@ -350,35 +368,34 @@ class Currency(db.Model):
|
|||||||
"""A currency."""
|
"""A currency."""
|
||||||
__tablename__ = "accounting_currencies"
|
__tablename__ = "accounting_currencies"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
code = db.Column(db.String, nullable=False, primary_key=True)
|
code: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The code."""
|
"""The code."""
|
||||||
name_l10n = db.Column("name", db.String, nullable=False)
|
name_l10n: Mapped[str] = mapped_column("name")
|
||||||
"""The name."""
|
"""The name."""
|
||||||
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
created_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
"""The time of creation."""
|
server_default=db.func.now())
|
||||||
created_by_id = db.Column(db.Integer,
|
"""The date and time when this record was created."""
|
||||||
db.ForeignKey(user_pk_column,
|
created_by_id: Mapped[int] \
|
||||||
onupdate="CASCADE"),
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
nullable=False)
|
"""The ID of the user who created the record."""
|
||||||
"""The ID of the creator."""
|
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
||||||
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
|
"""The user who created the record."""
|
||||||
"""The creator."""
|
updated_at: Mapped[dt.datetime] \
|
||||||
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The date and time when this record was last updated."""
|
||||||
updated_by_id = db.Column(db.Integer,
|
updated_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
"""The ID of the last user who updated the record."""
|
||||||
nullable=False)
|
updated_by: Mapped[user_cls] \
|
||||||
"""The ID of the updator."""
|
= db.relationship(foreign_keys=updated_by_id)
|
||||||
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
"""The last user who updated the record."""
|
||||||
"""The updator."""
|
l10n: Mapped[list[CurrencyL10n]] \
|
||||||
l10n = db.relationship("CurrencyL10n", back_populates="currency",
|
= db.relationship(back_populates="currency", lazy=False)
|
||||||
lazy=False)
|
|
||||||
"""The localized names."""
|
"""The localized names."""
|
||||||
line_items = db.relationship("JournalEntryLineItem",
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
back_populates="currency")
|
= db.relationship(back_populates="currency")
|
||||||
"""The journal entry line items."""
|
"""The journal entry line items."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -409,6 +426,8 @@ class Currency(db.Model):
|
|||||||
:param value: The new name.
|
:param value: The new name.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
|
if self.name == value:
|
||||||
|
return
|
||||||
if self.name_l10n is None:
|
if self.name_l10n is None:
|
||||||
self.name_l10n = value
|
self.name_l10n = value
|
||||||
return
|
return
|
||||||
@ -452,7 +471,7 @@ class Currency(db.Model):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
CurrencyL10n.query.filter(CurrencyL10n.currency == self).delete()
|
CurrencyL10n.query.filter(CurrencyL10n.currency == self).delete()
|
||||||
cls: t.Type[t.Self] = self.__class__
|
cls: Type[Self] = self.__class__
|
||||||
cls.query.filter(cls.code == self.code).delete()
|
cls.query.filter(cls.code == self.code).delete()
|
||||||
|
|
||||||
|
|
||||||
@ -460,16 +479,16 @@ class CurrencyL10n(db.Model):
|
|||||||
"""A localized currency name."""
|
"""A localized currency name."""
|
||||||
__tablename__ = "accounting_currencies_l10n"
|
__tablename__ = "accounting_currencies_l10n"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
currency_code = db.Column(db.String,
|
currency_code: Mapped[str] \
|
||||||
db.ForeignKey(Currency.code, onupdate="CASCADE",
|
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"),
|
||||||
nullable=False, primary_key=True)
|
primary_key=True)
|
||||||
"""The currency code."""
|
"""The currency code."""
|
||||||
currency = db.relationship(Currency, back_populates="l10n")
|
currency: Mapped[Currency] = db.relationship(back_populates="l10n")
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
locale = db.Column(db.String, nullable=False, primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The locale."""
|
"""The locale."""
|
||||||
name = db.Column(db.String, nullable=False)
|
name: Mapped[str]
|
||||||
"""The localized name."""
|
"""The localized name."""
|
||||||
|
|
||||||
|
|
||||||
@ -520,37 +539,34 @@ class JournalEntry(db.Model):
|
|||||||
"""A journal entry."""
|
"""A journal entry."""
|
||||||
__tablename__ = "accounting_journal_entries"
|
__tablename__ = "accounting_journal_entries"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
||||||
autoincrement=False)
|
|
||||||
"""The journal entry ID."""
|
"""The journal entry ID."""
|
||||||
date = db.Column(db.Date, nullable=False)
|
date: Mapped[dt.date]
|
||||||
"""The date."""
|
"""The date."""
|
||||||
no = db.Column(db.Integer, nullable=False, default=text("1"))
|
no: Mapped[int] = mapped_column(default=text("1"))
|
||||||
"""The account number under the date."""
|
"""The account number under the date."""
|
||||||
note = db.Column(db.String)
|
note: Mapped[str | None]
|
||||||
"""The note."""
|
"""The note."""
|
||||||
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
created_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
"""The time of creation."""
|
server_default=db.func.now())
|
||||||
created_by_id = db.Column(db.Integer,
|
"""The date and time when this record was created."""
|
||||||
db.ForeignKey(user_pk_column,
|
created_by_id: Mapped[int] \
|
||||||
onupdate="CASCADE"),
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
nullable=False)
|
"""The ID of the user who created the record."""
|
||||||
"""The ID of the creator."""
|
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
||||||
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
|
"""The user who created the record."""
|
||||||
"""The creator."""
|
updated_at: Mapped[dt.datetime] \
|
||||||
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The date and time when this record was last updated."""
|
||||||
updated_by_id = db.Column(db.Integer,
|
updated_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
"""The ID of the last user who updated the record."""
|
||||||
nullable=False)
|
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
|
||||||
"""The ID of the updator."""
|
"""The last user who updated the record."""
|
||||||
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
"""The updator."""
|
= db.relationship(back_populates="journal_entry")
|
||||||
line_items = db.relationship("JournalEntryLineItem",
|
|
||||||
back_populates="journal_entry")
|
|
||||||
"""The line items."""
|
"""The line items."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -640,48 +656,39 @@ class JournalEntryLineItem(db.Model):
|
|||||||
"""A line item in the journal entry."""
|
"""A line item in the journal entry."""
|
||||||
__tablename__ = "accounting_journal_entry_line_items"
|
__tablename__ = "accounting_journal_entry_line_items"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
||||||
autoincrement=False)
|
|
||||||
"""The line item ID."""
|
"""The line item ID."""
|
||||||
journal_entry_id = db.Column(db.Integer,
|
journal_entry_id: Mapped[int] \
|
||||||
db.ForeignKey(JournalEntry.id,
|
= mapped_column(db.ForeignKey(JournalEntry.id, onupdate="CASCADE",
|
||||||
onupdate="CASCADE",
|
ondelete="CASCADE"))
|
||||||
ondelete="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The journal entry ID."""
|
"""The journal entry ID."""
|
||||||
journal_entry = db.relationship(JournalEntry, back_populates="line_items")
|
journal_entry: Mapped[JournalEntry] \
|
||||||
|
= db.relationship(back_populates="line_items")
|
||||||
"""The journal entry."""
|
"""The journal entry."""
|
||||||
is_debit = db.Column(db.Boolean, nullable=False)
|
is_debit: Mapped[bool]
|
||||||
"""True for a debit line item, or False for a credit line item."""
|
"""True for a debit line item, or False for a credit line item."""
|
||||||
no = db.Column(db.Integer, nullable=False)
|
no: Mapped[int]
|
||||||
"""The line item number under the journal entry and debit or credit."""
|
"""The line item number under the journal entry and debit or credit."""
|
||||||
original_line_item_id = db.Column(db.Integer,
|
original_line_item_id: Mapped[int | None] \
|
||||||
db.ForeignKey(id, onupdate="CASCADE"),
|
= mapped_column(db.ForeignKey(id, onupdate="CASCADE"))
|
||||||
nullable=True)
|
|
||||||
"""The ID of the original line item."""
|
"""The ID of the original line item."""
|
||||||
original_line_item = db.relationship("JournalEntryLineItem",
|
original_line_item: Mapped[JournalEntryLineItem | None] \
|
||||||
back_populates="offsets",
|
= db.relationship(remote_side=id, passive_deletes=True)
|
||||||
remote_side=id, passive_deletes=True)
|
|
||||||
"""The original line item."""
|
"""The original line item."""
|
||||||
offsets = db.relationship("JournalEntryLineItem",
|
currency_code: Mapped[str] \
|
||||||
back_populates="original_line_item")
|
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE"))
|
||||||
"""The offset items."""
|
|
||||||
currency_code = db.Column(db.String,
|
|
||||||
db.ForeignKey(Currency.code, onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The currency code."""
|
"""The currency code."""
|
||||||
currency = db.relationship(Currency, back_populates="line_items")
|
currency: Mapped[Currency] = db.relationship(back_populates="line_items")
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
account_id = db.Column(db.Integer,
|
account_id: Mapped[int] \
|
||||||
db.ForeignKey(Account.id,
|
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
account = db.relationship(Account, back_populates="line_items", lazy=False)
|
account: Mapped[Account] \
|
||||||
|
= db.relationship(back_populates="line_items", lazy=False)
|
||||||
"""The account."""
|
"""The account."""
|
||||||
description = db.Column(db.String, nullable=True)
|
description: Mapped[str | None]
|
||||||
"""The description."""
|
"""The description."""
|
||||||
amount = db.Column(db.Numeric(14, 2), nullable=False)
|
amount: Mapped[Decimal] = mapped_column(db.Numeric(14, 2))
|
||||||
"""The amount."""
|
"""The amount."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -707,14 +714,6 @@ class JournalEntryLineItem(db.Model):
|
|||||||
"""
|
"""
|
||||||
return self.account.code
|
return self.account.code
|
||||||
|
|
||||||
@property
|
|
||||||
def debit(self) -> Decimal | None:
|
|
||||||
"""Returns the debit amount.
|
|
||||||
|
|
||||||
:return: The debit amount, or None if this is not a debit line item.
|
|
||||||
"""
|
|
||||||
return self.amount if self.is_debit else None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_need_offset(self) -> bool:
|
def is_need_offset(self) -> bool:
|
||||||
"""Returns whether the line item needs offset.
|
"""Returns whether the line item needs offset.
|
||||||
@ -729,13 +728,43 @@ class JournalEntryLineItem(db.Model):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def debit(self) -> Decimal | None:
|
||||||
|
"""Returns the debit amount.
|
||||||
|
|
||||||
|
:return: The debit amount, or None if this is not a debit line item.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "__debit"):
|
||||||
|
setattr(self, "__debit", self.amount if self.is_debit else None)
|
||||||
|
return getattr(self, "__debit")
|
||||||
|
|
||||||
|
@debit.setter
|
||||||
|
def debit(self, value: Decimal | None) -> None:
|
||||||
|
"""Sets the debit amount.
|
||||||
|
|
||||||
|
:param value: The debit amount.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
setattr(self, "__debit", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def credit(self) -> Decimal | None:
|
def credit(self) -> Decimal | None:
|
||||||
"""Returns the credit amount.
|
"""Returns the credit amount.
|
||||||
|
|
||||||
:return: The credit amount, or None if this is not a credit line item.
|
:return: The credit amount, or None if this is not a credit line item.
|
||||||
"""
|
"""
|
||||||
return None if self.is_debit else self.amount
|
if not hasattr(self, "__credit"):
|
||||||
|
setattr(self, "__credit", None if self.is_debit else self.amount)
|
||||||
|
return getattr(self, "__credit")
|
||||||
|
|
||||||
|
@credit.setter
|
||||||
|
def credit(self, value: Decimal | None) -> None:
|
||||||
|
"""Sets the credit amount.
|
||||||
|
|
||||||
|
:param value: The credit amount.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
setattr(self, "__credit", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def net_balance(self) -> Decimal:
|
def net_balance(self) -> Decimal:
|
||||||
@ -750,13 +779,85 @@ class JournalEntryLineItem(db.Model):
|
|||||||
return getattr(self, "__net_balance")
|
return getattr(self, "__net_balance")
|
||||||
|
|
||||||
@net_balance.setter
|
@net_balance.setter
|
||||||
def net_balance(self, net_balance: Decimal) -> None:
|
def net_balance(self, value: Decimal) -> None:
|
||||||
"""Sets the net balance.
|
"""Sets the net balance.
|
||||||
|
|
||||||
:param net_balance: The net balance.
|
:param value: The net balance.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
setattr(self, "__net_balance", net_balance)
|
setattr(self, "__net_balance", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def balance(self) -> Decimal:
|
||||||
|
"""Returns the balance.
|
||||||
|
|
||||||
|
:return: The balance.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "__balance"):
|
||||||
|
setattr(self, "__balance", Decimal("0"))
|
||||||
|
return getattr(self, "__balance")
|
||||||
|
|
||||||
|
@balance.setter
|
||||||
|
def balance(self, value: Decimal) -> None:
|
||||||
|
"""Sets the balance.
|
||||||
|
|
||||||
|
:param value: The balance.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
setattr(self, "__balance", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def offsets(self) -> list[Self]:
|
||||||
|
"""Returns the offset items.
|
||||||
|
|
||||||
|
:return: The offset items.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "__offsets"):
|
||||||
|
cls: Type[Self] = self.__class__
|
||||||
|
offsets: list[Self] = cls.query.join(JournalEntry)\
|
||||||
|
.filter(JournalEntryLineItem.original_line_item_id == self.id)\
|
||||||
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
|
cls.is_debit, cls.no).all()
|
||||||
|
setattr(self, "__offsets", offsets)
|
||||||
|
return getattr(self, "__offsets")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_offset(self) -> bool:
|
||||||
|
"""Returns whether the line item is an offset.
|
||||||
|
|
||||||
|
:return: True if the line item is an offset, or False otherwise.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "__is_offset"):
|
||||||
|
setattr(self, "__is_offset", False)
|
||||||
|
return getattr(self, "__is_offset")
|
||||||
|
|
||||||
|
@is_offset.setter
|
||||||
|
def is_offset(self, value: bool) -> None:
|
||||||
|
"""Sets whether the line item is an offset.
|
||||||
|
|
||||||
|
:param value: True if the line item is an offset, or False otherwise.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
setattr(self, "__is_offset", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def match(self) -> Self | None:
|
||||||
|
"""Returns the match of the line item.
|
||||||
|
|
||||||
|
:return: The match of the line item.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "__match"):
|
||||||
|
setattr(self, "__match", None)
|
||||||
|
return getattr(self, "__match")
|
||||||
|
|
||||||
|
@match.setter
|
||||||
|
def match(self, value: Self) -> None:
|
||||||
|
"""Sets the match of the line item.
|
||||||
|
|
||||||
|
:param value: The matcho of the line item.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
setattr(self, "__match", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def query_values(self) -> list[str]:
|
def query_values(self) -> list[str]:
|
||||||
@ -773,6 +874,7 @@ class JournalEntryLineItem(db.Model):
|
|||||||
self.journal_entry.date.month,
|
self.journal_entry.date.month,
|
||||||
self.journal_entry.date.day),
|
self.journal_entry.date.day),
|
||||||
"" if self.description is None else self.description,
|
"" if self.description is None else self.description,
|
||||||
|
str(self.account),
|
||||||
format_amount(self.amount)]
|
format_amount(self.amount)]
|
||||||
|
|
||||||
|
|
||||||
@ -780,27 +882,25 @@ class Option(db.Model):
|
|||||||
"""An option."""
|
"""An option."""
|
||||||
__tablename__ = "accounting_options"
|
__tablename__ = "accounting_options"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
name = db.Column(db.String, nullable=False, primary_key=True)
|
name: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The name."""
|
"""The name."""
|
||||||
value = db.Column(db.Text, nullable=False)
|
value: Mapped[str] = mapped_column(db.Text)
|
||||||
"""The option value."""
|
"""The option value."""
|
||||||
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
created_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
"""The time of creation."""
|
server_default=db.func.now())
|
||||||
created_by_id = db.Column(db.Integer,
|
"""The date and time when this record was created."""
|
||||||
db.ForeignKey(user_pk_column,
|
created_by_id: Mapped[int] \
|
||||||
onupdate="CASCADE"),
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
nullable=False)
|
"""The ID of the user who created the record."""
|
||||||
"""The ID of the creator."""
|
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
||||||
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
|
"""The user who created the record."""
|
||||||
"""The creator."""
|
updated_at: Mapped[dt.datetime] \
|
||||||
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The date and time when this record was last updated."""
|
||||||
updated_by_id = db.Column(db.Integer,
|
updated_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
"""The ID of the last user who updated the record."""
|
||||||
nullable=False)
|
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
|
||||||
"""The ID of the updator."""
|
"""The last user who updated the record."""
|
||||||
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
|
||||||
"""The updator."""
|
|
||||||
|
@ -27,9 +27,11 @@ def init_app(app: Flask, url_prefix: str) -> None:
|
|||||||
:param url_prefix: The URL prefix of the accounting application.
|
:param url_prefix: The URL prefix of the accounting application.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from .converters import PeriodConverter, IncomeExpensesAccountConverter
|
from .converters import PeriodConverter, CurrentAccountConverter, \
|
||||||
|
NeedOffsetAccountConverter
|
||||||
app.url_map.converters["period"] = PeriodConverter
|
app.url_map.converters["period"] = PeriodConverter
|
||||||
app.url_map.converters["ieAccount"] = IncomeExpensesAccountConverter
|
app.url_map.converters["currentAccount"] = CurrentAccountConverter
|
||||||
|
app.url_map.converters["needOffsetAccount"] = NeedOffsetAccountConverter
|
||||||
|
|
||||||
from .views import bp as report_bp
|
from .views import bp as report_bp
|
||||||
app.register_blueprint(report_bp, url_prefix=url_prefix)
|
app.register_blueprint(report_bp, url_prefix=url_prefix)
|
||||||
|
@ -28,8 +28,8 @@ from .period import Period, get_period
|
|||||||
|
|
||||||
|
|
||||||
class PeriodConverter(BaseConverter):
|
class PeriodConverter(BaseConverter):
|
||||||
"""The supplier converter to convert the period specification from and to
|
"""The converter to convert the period specification from and to the
|
||||||
the corresponding period in the routes."""
|
corresponding period in the routes."""
|
||||||
|
|
||||||
def to_python(self, value: str) -> Period:
|
def to_python(self, value: str) -> Period:
|
||||||
"""Converts a period specification to a period.
|
"""Converts a period specification to a period.
|
||||||
@ -51,9 +51,9 @@ class PeriodConverter(BaseConverter):
|
|||||||
return value.spec
|
return value.spec
|
||||||
|
|
||||||
|
|
||||||
class IncomeExpensesAccountConverter(BaseConverter):
|
class CurrentAccountConverter(BaseConverter):
|
||||||
"""The supplier converter to convert the income and expenses log pseudo
|
"""The converter to convert the current account code from and to the
|
||||||
account code from and to the corresponding pseudo account in the routes."""
|
corresponding account in the routes."""
|
||||||
|
|
||||||
def to_python(self, value: str) -> CurrentAccount:
|
def to_python(self, value: str) -> CurrentAccount:
|
||||||
"""Converts an account code to an account.
|
"""Converts an account code to an account.
|
||||||
@ -77,3 +77,29 @@ class IncomeExpensesAccountConverter(BaseConverter):
|
|||||||
:return: Its code.
|
:return: Its code.
|
||||||
"""
|
"""
|
||||||
return value.code
|
return value.code
|
||||||
|
|
||||||
|
|
||||||
|
class NeedOffsetAccountConverter(BaseConverter):
|
||||||
|
"""The converter to convert the unapplied original line item account code
|
||||||
|
from and to the corresponding account in the routes."""
|
||||||
|
|
||||||
|
def to_python(self, value: str) -> Account:
|
||||||
|
"""Converts an account code to an account.
|
||||||
|
|
||||||
|
:param value: The account code.
|
||||||
|
:return: The corresponding account.
|
||||||
|
"""
|
||||||
|
account: Account | None = Account.find_by_code(value)
|
||||||
|
if account is None:
|
||||||
|
abort(404)
|
||||||
|
if not account.is_need_offset:
|
||||||
|
abort(404)
|
||||||
|
return account
|
||||||
|
|
||||||
|
def to_url(self, value: Account) -> str:
|
||||||
|
"""Converts an account to account code.
|
||||||
|
|
||||||
|
:param value: The account.
|
||||||
|
:return: Its code.
|
||||||
|
"""
|
||||||
|
return value.code
|
||||||
|
@ -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).
|
2021/9/16 by imacat (imacat@nanoparma.com).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import datetime as dt
|
||||||
from datetime import date
|
from collections.abc import Callable
|
||||||
|
|
||||||
from accounting.models import JournalEntry
|
from accounting.models import JournalEntry
|
||||||
from .period import Period
|
from .period import Period
|
||||||
@ -32,13 +32,13 @@ from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
|||||||
class PeriodChooser:
|
class PeriodChooser:
|
||||||
"""The period chooser."""
|
"""The period chooser."""
|
||||||
|
|
||||||
def __init__(self, get_url: t.Callable[[Period], str]):
|
def __init__(self, get_url: Callable[[Period], str]):
|
||||||
"""Constructs a period chooser.
|
"""Constructs a period chooser.
|
||||||
|
|
||||||
:param get_url: The callback to return the URL of the current report in
|
:param get_url: The callback to return the URL of the current report in
|
||||||
a period.
|
a period.
|
||||||
"""
|
"""
|
||||||
self.__get_url: t.Callable[[Period], str] = get_url
|
self.__get_url: Callable[[Period], str] = get_url
|
||||||
"""The callback to return the URL of the current report in a period."""
|
"""The callback to return the URL of the current report in a period."""
|
||||||
|
|
||||||
# Shortcut periods
|
# Shortcut periods
|
||||||
@ -63,10 +63,10 @@ class PeriodChooser:
|
|||||||
|
|
||||||
first: JournalEntry | None \
|
first: JournalEntry | None \
|
||||||
= JournalEntry.query.order_by(JournalEntry.date).first()
|
= 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
|
# Attributes
|
||||||
self.data_start: date | None = start
|
self.data_start: dt.date | None = start
|
||||||
"""The start of the data."""
|
"""The start of the data."""
|
||||||
self.has_data: bool = start is not None
|
self.has_data: bool = start is not None
|
||||||
"""Whether there is any data."""
|
"""Whether there is any data."""
|
||||||
@ -80,8 +80,8 @@ class PeriodChooser:
|
|||||||
"""The available years."""
|
"""The available years."""
|
||||||
|
|
||||||
if self.has_data:
|
if self.has_data:
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
self.has_last_month = start < date(today.year, today.month, 1)
|
self.has_last_month = start < dt.date(today.year, today.month, 1)
|
||||||
self.has_last_year = start.year < today.year
|
self.has_last_year = start.year < today.year
|
||||||
self.has_yesterday = start < today
|
self.has_yesterday = start < today
|
||||||
if start.year < today.year - 1:
|
if start.year < today.year - 1:
|
||||||
|
@ -17,12 +17,12 @@
|
|||||||
"""The period description composer.
|
"""The period description composer.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date, timedelta
|
import datetime as dt
|
||||||
|
|
||||||
from accounting.locale import gettext
|
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.
|
"""Returns the period description.
|
||||||
|
|
||||||
:param start: The start of the period.
|
: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)
|
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.
|
"""Returns the description without the end day.
|
||||||
|
|
||||||
:param start: The start of the period.
|
: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())
|
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.
|
"""Returns the description without the start day.
|
||||||
|
|
||||||
:param end: The end of the period.
|
: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:
|
if end.month == 12 and end.day == 31:
|
||||||
return str(end.year)
|
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_month(end)
|
||||||
return __format_day(end)
|
return __format_day(end)
|
||||||
|
|
||||||
return gettext("until %(end)s", end=get_end_desc())
|
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.
|
"""Returns the description as a year range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
: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))
|
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.
|
"""Returns the description as a month range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
: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.
|
:return: The description as a month range.
|
||||||
:raise ValueError: The period is not 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
|
raise ValueError
|
||||||
start_text: str = __format_month(start)
|
start_text: str = __format_month(start)
|
||||||
if start.year == end.year and start.month == end.month:
|
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))
|
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.
|
"""Returns the description as a day range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
: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))
|
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.
|
"""Formats a month.
|
||||||
|
|
||||||
:param month: The month.
|
:param month: The month.
|
||||||
@ -151,7 +151,7 @@ def __format_month(month: date) -> str:
|
|||||||
return f"{month.year}/{month.month}"
|
return f"{month.year}/{month.month}"
|
||||||
|
|
||||||
|
|
||||||
def __format_day(day: date) -> str:
|
def __format_day(day: dt.date) -> str:
|
||||||
"""Formats a day.
|
"""Formats a day.
|
||||||
|
|
||||||
:param day: The day.
|
:param day: The day.
|
||||||
|
@ -18,14 +18,14 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import calendar
|
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.
|
"""Returns the end day of month for a date.
|
||||||
|
|
||||||
:param day: The date.
|
:param day: The date.
|
||||||
:return: The end day of the month of that day.
|
:return: The end day of the month of that day.
|
||||||
"""
|
"""
|
||||||
last_day: int = calendar.monthrange(day.year, day.month)[1]
|
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)
|
||||||
|
@ -18,9 +18,10 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import calendar
|
import calendar
|
||||||
|
import datetime as dt
|
||||||
import re
|
import re
|
||||||
import typing as t
|
from collections.abc import Callable
|
||||||
from datetime import date
|
from typing import Type
|
||||||
|
|
||||||
from .period import Period
|
from .period import Period
|
||||||
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
||||||
@ -39,7 +40,7 @@ def get_period(spec: str | None = None) -> Period:
|
|||||||
"""
|
"""
|
||||||
if spec is None:
|
if spec is None:
|
||||||
return ThisMonth()
|
return ThisMonth()
|
||||||
named_periods: dict[str, t.Type[t.Callable[[], Period]]] = {
|
named_periods: dict[str, Type[Callable[[], Period]]] = {
|
||||||
"this-month": lambda: ThisMonth(),
|
"this-month": lambda: ThisMonth(),
|
||||||
"last-month": lambda: LastMonth(),
|
"last-month": lambda: LastMonth(),
|
||||||
"since-last-month": lambda: SinceLastMonth(),
|
"since-last-month": lambda: SinceLastMonth(),
|
||||||
@ -57,7 +58,7 @@ def get_period(spec: str | None = None) -> Period:
|
|||||||
return Period(start, end)
|
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.
|
"""Parses the period specification.
|
||||||
|
|
||||||
:param text: The period specification.
|
:param text: The period specification.
|
||||||
@ -84,7 +85,7 @@ def __parse_spec(text: str) -> tuple[date | None, date | None]:
|
|||||||
raise ValueError
|
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.
|
"""Returns the start of the period from the date representation.
|
||||||
|
|
||||||
:param year: The year.
|
:param year: The year.
|
||||||
@ -94,13 +95,13 @@ def __get_start(year: str, month: str | None, day: str | None) -> date:
|
|||||||
:raise ValueError: When the date is invalid.
|
:raise ValueError: When the date is invalid.
|
||||||
"""
|
"""
|
||||||
if day is not None:
|
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:
|
if month is not None:
|
||||||
return date(int(year), int(month), 1)
|
return dt.date(int(year), int(month), 1)
|
||||||
return date(int(year), 1, 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.
|
"""Returns the end of the period from the date representation.
|
||||||
|
|
||||||
:param year: The year.
|
:param year: The year.
|
||||||
@ -110,10 +111,10 @@ def __get_end(year: str, month: str | None, day: str | None) -> date:
|
|||||||
:raise ValueError: When the date is invalid.
|
:raise ValueError: When the date is invalid.
|
||||||
"""
|
"""
|
||||||
if day is not None:
|
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:
|
if month is not None:
|
||||||
year_n: int = int(year)
|
year_n: int = int(year)
|
||||||
month_n: int = int(month)
|
month_n: int = int(month)
|
||||||
day_n: int = calendar.monthrange(year_n, month_n)[1]
|
day_n: int = calendar.monthrange(year_n, month_n)[1]
|
||||||
return date(year_n, month_n, day_n)
|
return dt.date(year_n, month_n, day_n)
|
||||||
return date(int(year), 12, 31)
|
return dt.date(int(year), 12, 31)
|
||||||
|
@ -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).
|
2021/9/16 by imacat (imacat@nanoparma.com).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import datetime as dt
|
||||||
from datetime import date, timedelta
|
from typing import Self
|
||||||
|
|
||||||
from .description import get_desc
|
from .description import get_desc
|
||||||
from .month_end import month_end
|
from .month_end import month_end
|
||||||
@ -31,15 +31,15 @@ from .specification import get_spec
|
|||||||
class Period:
|
class Period:
|
||||||
"""A date 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.
|
"""Constructs a new date period.
|
||||||
|
|
||||||
:param start: The start date, or None from the very beginning.
|
:param start: The start date, or None from the very beginning.
|
||||||
:param end: The end date, or None till no end.
|
: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."""
|
"""The start of the period."""
|
||||||
self.end: date | None = end
|
self.end: dt.date | None = end
|
||||||
"""The end of the period."""
|
"""The end of the period."""
|
||||||
self.is_default: bool = False
|
self.is_default: bool = False
|
||||||
"""Whether the is the default period."""
|
"""Whether the is the default period."""
|
||||||
@ -95,8 +95,8 @@ class Period:
|
|||||||
self.is_a_month = self.start.day == 1 \
|
self.is_a_month = self.start.day == 1 \
|
||||||
and self.end == month_end(self.start)
|
and self.end == month_end(self.start)
|
||||||
self.is_type_month = self.is_a_month
|
self.is_type_month = self.is_a_month
|
||||||
self.is_a_year = self.start == date(self.start.year, 1, 1) \
|
self.is_a_year = self.start == dt.date(self.start.year, 1, 1) \
|
||||||
and self.end == date(self.start.year, 12, 31)
|
and self.end == dt.date(self.start.year, 12, 31)
|
||||||
self.is_a_day = self.start == self.end
|
self.is_a_day = self.start == self.end
|
||||||
|
|
||||||
def is_year(self, year: int) -> bool:
|
def is_year(self, year: int) -> bool:
|
||||||
@ -119,11 +119,11 @@ class Period:
|
|||||||
and not self.is_a_day
|
and not self.is_a_day
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def before(self) -> t.Self | None:
|
def before(self) -> Self | None:
|
||||||
"""Returns the period before this period.
|
"""Returns the period before this period.
|
||||||
|
|
||||||
:return: The period before this period.
|
:return: The period before this period.
|
||||||
"""
|
"""
|
||||||
if self.start is None:
|
if self.start is None:
|
||||||
return None
|
return None
|
||||||
return Period(None, self.start - timedelta(days=1))
|
return Period(None, self.start - dt.timedelta(days=1))
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The named shortcut periods.
|
"""The named shortcut periods.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date, timedelta
|
import datetime as dt
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from .month_end import month_end
|
from .month_end import month_end
|
||||||
@ -27,8 +27,8 @@ from .period import Period
|
|||||||
class ThisMonth(Period):
|
class ThisMonth(Period):
|
||||||
"""The period of this month."""
|
"""The period of this month."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
this_month_start: date = date(today.year, today.month, 1)
|
this_month_start: dt.date = dt.date(today.year, today.month, 1)
|
||||||
super().__init__(this_month_start, month_end(today))
|
super().__init__(this_month_start, month_end(today))
|
||||||
self.is_default = True
|
self.is_default = True
|
||||||
self.is_this_month = True
|
self.is_this_month = True
|
||||||
@ -43,13 +43,13 @@ class ThisMonth(Period):
|
|||||||
class LastMonth(Period):
|
class LastMonth(Period):
|
||||||
"""The period of this month."""
|
"""The period of this month."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
year: int = today.year
|
year: int = today.year
|
||||||
month: int = today.month - 1
|
month: int = today.month - 1
|
||||||
if month < 1:
|
if month < 1:
|
||||||
year = year - 1
|
year = year - 1
|
||||||
month = 12
|
month = 12
|
||||||
start: date = date(year, month, 1)
|
start: dt.date = dt.date(year, month, 1)
|
||||||
super().__init__(start, month_end(start))
|
super().__init__(start, month_end(start))
|
||||||
self.is_last_month = True
|
self.is_last_month = True
|
||||||
|
|
||||||
@ -63,13 +63,13 @@ class LastMonth(Period):
|
|||||||
class SinceLastMonth(Period):
|
class SinceLastMonth(Period):
|
||||||
"""The period of this month."""
|
"""The period of this month."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
year: int = today.year
|
year: int = today.year
|
||||||
month: int = today.month - 1
|
month: int = today.month - 1
|
||||||
if month < 1:
|
if month < 1:
|
||||||
year = year - 1
|
year = year - 1
|
||||||
month = 12
|
month = 12
|
||||||
start: date = date(year, month, 1)
|
start: dt.date = dt.date(year, month, 1)
|
||||||
super().__init__(start, None)
|
super().__init__(start, None)
|
||||||
self.is_since_last_month = True
|
self.is_since_last_month = True
|
||||||
|
|
||||||
@ -82,9 +82,9 @@ class SinceLastMonth(Period):
|
|||||||
class ThisYear(Period):
|
class ThisYear(Period):
|
||||||
"""The period of this year."""
|
"""The period of this year."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
year: int = date.today().year
|
year: int = dt.date.today().year
|
||||||
start: date = date(year, 1, 1)
|
start: dt.date = dt.date(year, 1, 1)
|
||||||
end: date = date(year, 12, 31)
|
end: dt.date = dt.date(year, 12, 31)
|
||||||
super().__init__(start, end)
|
super().__init__(start, end)
|
||||||
self.is_this_year = True
|
self.is_this_year = True
|
||||||
|
|
||||||
@ -97,9 +97,9 @@ class ThisYear(Period):
|
|||||||
class LastYear(Period):
|
class LastYear(Period):
|
||||||
"""The period of last year."""
|
"""The period of last year."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
year: int = date.today().year
|
year: int = dt.date.today().year
|
||||||
start: date = date(year - 1, 1, 1)
|
start: dt.date = dt.date(year - 1, 1, 1)
|
||||||
end: date = date(year - 1, 12, 31)
|
end: dt.date = dt.date(year - 1, 12, 31)
|
||||||
super().__init__(start, end)
|
super().__init__(start, end)
|
||||||
self.is_last_year = True
|
self.is_last_year = True
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ class LastYear(Period):
|
|||||||
class Today(Period):
|
class Today(Period):
|
||||||
"""The period of today."""
|
"""The period of today."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
super().__init__(today, today)
|
super().__init__(today, today)
|
||||||
self.is_today = True
|
self.is_today = True
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ class Today(Period):
|
|||||||
class Yesterday(Period):
|
class Yesterday(Period):
|
||||||
"""The period of yesterday."""
|
"""The period of yesterday."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
yesterday: date = date.today() - timedelta(days=1)
|
yesterday: dt.date = dt.date.today() - dt.timedelta(days=1)
|
||||||
super().__init__(yesterday, yesterday)
|
super().__init__(yesterday, yesterday)
|
||||||
self.is_yesterday = True
|
self.is_yesterday = True
|
||||||
|
|
||||||
@ -163,6 +163,6 @@ class YearPeriod(Period):
|
|||||||
|
|
||||||
:param year: The year.
|
:param year: The year.
|
||||||
"""
|
"""
|
||||||
start: date = date(year, 1, 1)
|
start: dt.date = dt.date(year, 1, 1)
|
||||||
end: date = date(year, 12, 31)
|
end: dt.date = dt.date(year, 12, 31)
|
||||||
super().__init__(start, end)
|
super().__init__(start, end)
|
||||||
|
@ -17,10 +17,10 @@
|
|||||||
"""The period specification composer.
|
"""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.
|
"""Returns the period specification.
|
||||||
|
|
||||||
:param start: The start of the period.
|
: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)
|
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.
|
"""Returns the period specification without the end day.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -57,7 +57,7 @@ def __get_since_spec(start: date) -> str:
|
|||||||
return start.strftime("%Y-%m-%d-")
|
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.
|
"""Returns the period specification without the start day.
|
||||||
|
|
||||||
:param end: The end of the period.
|
: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:
|
if end.month == 12 and end.day == 31:
|
||||||
return end.strftime("-%Y")
|
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")
|
||||||
return end.strftime("-%Y-%m-%d")
|
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.
|
"""Returns the period specification as a year range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
: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}"
|
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.
|
"""Returns the period specification as a month range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
: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.
|
:return: The period specification as a month range.
|
||||||
:raise ValueError: The period is not 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
|
raise ValueError
|
||||||
start_spec: str = start.strftime("%Y-%m")
|
start_spec: str = start.strftime("%Y-%m")
|
||||||
if start.year == end.year and start.month == end.month:
|
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}"
|
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.
|
"""Returns the period specification as a day range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
|
@ -137,6 +137,7 @@ class AccountCollector:
|
|||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(Account.id, Account.base_code, Account.no)\
|
.group_by(Account.id, Account.base_code, Account.no)\
|
||||||
|
.having(balance_func != 0)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
account_balances: list[sa.Row] \
|
account_balances: list[sa.Row] \
|
||||||
= db.session.execute(select_balance).all()
|
= db.session.execute(select_balance).all()
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The income and expenses log.
|
"""The income and expenses log.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
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_chooser import ReportChooser
|
||||||
from accounting.report.utils.report_type import ReportType
|
from accounting.report.utils.report_type import ReportType
|
||||||
from accounting.report.utils.urls import income_expenses_url
|
from accounting.report.utils.urls import income_expenses_url
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.current_account import CurrentAccount
|
from accounting.utils.current_account import CurrentAccount
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
|
|
||||||
@ -54,7 +53,7 @@ class ReportLineItem:
|
|||||||
"""Whether this is the brought-forward line item."""
|
"""Whether this is the brought-forward line item."""
|
||||||
self.is_total: bool = False
|
self.is_total: bool = False
|
||||||
"""Whether this is the total line item."""
|
"""Whether this is the total line item."""
|
||||||
self.date: date | None = None
|
self.date: dt.date | None = None
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.account: Account | None = None
|
self.account: Account | None = None
|
||||||
"""The account."""
|
"""The account."""
|
||||||
@ -122,8 +121,7 @@ class LineItemCollector:
|
|||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
select: sa.Select = sa.Select(balance_func)\
|
select: sa.Select = sa.Select(balance_func)\
|
||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(be(JournalEntryLineItem.currency_code
|
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
== self.__currency.code),
|
|
||||||
self.__account_condition,
|
self.__account_condition,
|
||||||
JournalEntry.date < self.__period.start)
|
JournalEntry.date < self.__period.start)
|
||||||
balance: int | None = db.session.scalar(select)
|
balance: int | None = db.session.scalar(select)
|
||||||
@ -215,7 +213,7 @@ class LineItemCollector:
|
|||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""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,
|
account: str | None,
|
||||||
description: str | None,
|
description: str | None,
|
||||||
income: str | Decimal | None,
|
income: str | Decimal | None,
|
||||||
@ -224,7 +222,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
note: str | None):
|
note: str | None):
|
||||||
"""Constructs a row in the CSV.
|
"""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 account: The account.
|
||||||
:param description: The description.
|
:param description: The description.
|
||||||
:param income: The income.
|
:param income: The income.
|
||||||
@ -232,7 +230,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
:param balance: The balance.
|
:param balance: The balance.
|
||||||
:param note: The note.
|
:param note: The note.
|
||||||
"""
|
"""
|
||||||
self.date: date | str | None = journal_entry_date
|
self.date: dt.date | str | None = date
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.account: str | None = account
|
self.account: str | None = account
|
||||||
"""The account."""
|
"""The account."""
|
||||||
@ -347,8 +345,7 @@ class PageParams(BasePageParams):
|
|||||||
self.account.id == 0)]
|
self.account.id == 0)]
|
||||||
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
||||||
.join(Account)\
|
.join(Account)\
|
||||||
.filter(be(JournalEntryLineItem.currency_code
|
.filter(JournalEntryLineItem.currency_code == self.currency.code,
|
||||||
== self.currency.code),
|
|
||||||
CurrentAccount.sql_condition())\
|
CurrentAccount.sql_condition())\
|
||||||
.group_by(JournalEntryLineItem.account_id)
|
.group_by(JournalEntryLineItem.account_id)
|
||||||
options.extend([OptionLink(str(x),
|
options.extend([OptionLink(str(x),
|
||||||
|
@ -269,6 +269,7 @@ class IncomeStatement(BaseReport):
|
|||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(Account.id)\
|
.group_by(Account.id)\
|
||||||
|
.having(balance_func != 0)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The journal.
|
"""The journal.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -67,7 +67,7 @@ class ReportLineItem:
|
|||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV."""
|
||||||
|
|
||||||
def __init__(self, journal_entry_date: str | date,
|
def __init__(self, journal_entry_date: str | dt.date,
|
||||||
currency: str,
|
currency: str,
|
||||||
account: str,
|
account: str,
|
||||||
description: str | None,
|
description: str | None,
|
||||||
@ -77,12 +77,14 @@ class CSVRow(BaseCSVRow):
|
|||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV.
|
||||||
|
|
||||||
:param journal_entry_date: The journal entry date.
|
:param journal_entry_date: The journal entry date.
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
:param description: The description.
|
:param description: The description.
|
||||||
:param debit: The debit amount.
|
:param debit: The debit amount.
|
||||||
:param credit: The credit amount.
|
:param credit: The credit amount.
|
||||||
:param note: The note.
|
:param note: The note.
|
||||||
"""
|
"""
|
||||||
self.date: str | date = journal_entry_date
|
self.date: str | dt.date = journal_entry_date
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.currency: str = currency
|
self.currency: str = currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
@ -116,6 +118,7 @@ class PageParams(BasePageParams):
|
|||||||
"""Constructs the HTML page parameters.
|
"""Constructs the HTML page parameters.
|
||||||
|
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
|
:param pagination: The pagination.
|
||||||
:param line_items: The line items.
|
:param line_items: The line items.
|
||||||
"""
|
"""
|
||||||
self.period: Period = period
|
self.period: Period = period
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The ledger.
|
"""The ledger.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
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_chooser import ReportChooser
|
||||||
from accounting.report.utils.report_type import ReportType
|
from accounting.report.utils.report_type import ReportType
|
||||||
from accounting.report.utils.urls import ledger_url
|
from accounting.report.utils.urls import ledger_url
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
|
|
||||||
|
|
||||||
@ -53,7 +52,7 @@ class ReportLineItem:
|
|||||||
"""Whether this is the brought-forward line item."""
|
"""Whether this is the brought-forward line item."""
|
||||||
self.is_total: bool = False
|
self.is_total: bool = False
|
||||||
"""Whether this is the total line item."""
|
"""Whether this is the total line item."""
|
||||||
self.date: date | None = None
|
self.date: dt.date | None = None
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.description: str | None = None
|
self.description: str | None = None
|
||||||
"""The description."""
|
"""The description."""
|
||||||
@ -118,10 +117,8 @@ class LineItemCollector:
|
|||||||
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
|
select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
|
||||||
.filter(be(JournalEntryLineItem.currency_code
|
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
== self.__currency.code),
|
JournalEntryLineItem.account_id == self.__account.id,
|
||||||
be(JournalEntryLineItem.account_id
|
|
||||||
== self.__account.id),
|
|
||||||
JournalEntry.date < self.__period.start)
|
JournalEntry.date < self.__period.start)
|
||||||
balance: int | None = db.session.scalar(select)
|
balance: int | None = db.session.scalar(select)
|
||||||
if balance is None:
|
if balance is None:
|
||||||
@ -199,7 +196,7 @@ class LineItemCollector:
|
|||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""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,
|
description: str | None,
|
||||||
debit: str | Decimal | None,
|
debit: str | Decimal | None,
|
||||||
credit: str | Decimal | None,
|
credit: str | Decimal | None,
|
||||||
@ -207,14 +204,14 @@ class CSVRow(BaseCSVRow):
|
|||||||
note: str | None):
|
note: str | None):
|
||||||
"""Constructs a row in the CSV.
|
"""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 description: The description.
|
||||||
:param debit: The debit amount.
|
:param debit: The debit amount.
|
||||||
:param credit: The credit amount.
|
:param credit: The credit amount.
|
||||||
:param balance: The balance.
|
:param balance: The balance.
|
||||||
:param note: The note.
|
:param note: The note.
|
||||||
"""
|
"""
|
||||||
self.date: date | str | None = journal_entry_date
|
self.date: dt.date | str | None = date
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.description: str | None = description
|
self.description: str | None = description
|
||||||
"""The description."""
|
"""The description."""
|
||||||
@ -313,8 +310,7 @@ class PageParams(BasePageParams):
|
|||||||
:return: The account options.
|
:return: The account options.
|
||||||
"""
|
"""
|
||||||
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
||||||
.filter(be(JournalEntryLineItem.currency_code
|
.filter(JournalEntryLineItem.currency_code == self.currency.code)\
|
||||||
== self.currency.code))\
|
|
||||||
.group_by(JournalEntryLineItem.account_id)
|
.group_by(JournalEntryLineItem.account_id)
|
||||||
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
|
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
|
||||||
x.id == self.account.id)
|
x.id == self.account.id)
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The search.
|
"""The search.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
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.csv_export import csv_download
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from accounting.report.utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.report_type import ReportType
|
from accounting.report.utils.report_type import ReportType
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
from accounting.utils.query import parse_query_keywords
|
from accounting.utils.query import parse_query_keywords
|
||||||
from .journal import get_csv_rows
|
from .journal import get_csv_rows
|
||||||
@ -125,30 +124,33 @@ class LineItemCollector:
|
|||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.BinaryExpression] \
|
||||||
= [JournalEntry.note.icontains(k)]
|
= [JournalEntry.note.icontains(k)]
|
||||||
journal_entry_date: datetime
|
date: dt.datetime
|
||||||
try:
|
try:
|
||||||
journal_entry_date = datetime.strptime(k, "%Y")
|
date = dt.datetime.strptime(k, "%Y")
|
||||||
conditions.append(
|
conditions.append(
|
||||||
be(sa.extract("year", JournalEntry.date)
|
sa.extract("year", JournalEntry.date) == date.year)
|
||||||
== journal_entry_date.year))
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
journal_entry_date = datetime.strptime(k, "%Y/%m")
|
date = dt.datetime.strptime(k, "%Y/%m")
|
||||||
conditions.append(sa.and_(
|
conditions.append(sa.and_(
|
||||||
sa.extract("year", JournalEntry.date)
|
sa.extract("year", JournalEntry.date) == date.year,
|
||||||
== journal_entry_date.year,
|
sa.extract("month", JournalEntry.date) == date.month))
|
||||||
sa.extract("month", JournalEntry.date)
|
|
||||||
== journal_entry_date.month))
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
try:
|
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_(
|
conditions.append(sa.and_(
|
||||||
sa.extract("month", JournalEntry.date)
|
sa.extract("month", JournalEntry.date) == date.month,
|
||||||
== journal_entry_date.month,
|
sa.extract("day", JournalEntry.date) == date.day))
|
||||||
sa.extract("day", JournalEntry.date)
|
except ValueError:
|
||||||
== journal_entry_date.day))
|
pass
|
||||||
|
try:
|
||||||
|
date = dt.datetime.strptime(k, "%Y/%m/%d")
|
||||||
|
conditions.append(sa.and_(
|
||||||
|
sa.extract("year", JournalEntry.date) == date.year,
|
||||||
|
sa.extract("month", JournalEntry.date) == date.month,
|
||||||
|
sa.extract("day", JournalEntry.date) == date.day))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return sa.select(JournalEntry.id).filter(sa.or_(*conditions))
|
return sa.select(JournalEntry.id).filter(sa.or_(*conditions))
|
||||||
|
@ -191,6 +191,7 @@ class TrialBalance(BaseReport):
|
|||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(Account.id)\
|
.group_by(Account.id)\
|
||||||
|
.having(balance_func != 0)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
|
216
src/accounting/report/reports/unapplied.py
Normal file
216
src/accounting/report/reports/unapplied.py
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The unapplied original line items.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import datetime as dt
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import render_template, Response
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from accounting.locale import gettext
|
||||||
|
from accounting.models import Currency, Account, JournalEntry, \
|
||||||
|
JournalEntryLineItem
|
||||||
|
from accounting.report.utils.base_page_params import BasePageParams
|
||||||
|
from accounting.report.utils.base_report import BaseReport
|
||||||
|
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
||||||
|
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.unapplied import get_accounts_with_unapplied, \
|
||||||
|
get_net_balances
|
||||||
|
from accounting.report.utils.urls import unapplied_url
|
||||||
|
from accounting.utils.pagination import Pagination
|
||||||
|
|
||||||
|
|
||||||
|
class CSVRow(BaseCSVRow):
|
||||||
|
"""A row in the CSV."""
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
:param journal_entry_date: The journal entry date.
|
||||||
|
:param currency: The currency.
|
||||||
|
:param description: The description.
|
||||||
|
:param amount: The amount.
|
||||||
|
:param net_balance: The net balance.
|
||||||
|
"""
|
||||||
|
self.date: str | dt.date = journal_entry_date
|
||||||
|
"""The date."""
|
||||||
|
self.currency: str = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.description: str | None = description
|
||||||
|
"""The description."""
|
||||||
|
self.amount: str | Decimal = amount
|
||||||
|
"""The amount."""
|
||||||
|
self.net_balance: str | Decimal = net_balance
|
||||||
|
"""The net balance."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self) -> list[str | dt.date | Decimal | None]:
|
||||||
|
"""Returns the values of the row.
|
||||||
|
|
||||||
|
:return: The values of the row.
|
||||||
|
"""
|
||||||
|
return [self.date, self.currency, self.description, self.amount,
|
||||||
|
self.net_balance]
|
||||||
|
|
||||||
|
|
||||||
|
class PageParams(BasePageParams):
|
||||||
|
"""The HTML page parameters."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency,
|
||||||
|
account: Account,
|
||||||
|
pagination: Pagination[JournalEntryLineItem],
|
||||||
|
line_items: list[JournalEntryLineItem]):
|
||||||
|
"""Constructs the HTML page parameters.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:param pagination: The pagination.
|
||||||
|
:param line_items: The line items.
|
||||||
|
"""
|
||||||
|
self.currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
self.pagination: Pagination[JournalEntryLineItem] = pagination
|
||||||
|
"""The pagination."""
|
||||||
|
self.line_items: list[JournalEntryLineItem] = line_items
|
||||||
|
"""The line items."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_data(self) -> bool:
|
||||||
|
"""Returns whether there is any data on the page.
|
||||||
|
|
||||||
|
:return: True if there is any data, or False otherwise.
|
||||||
|
"""
|
||||||
|
return len(self.line_items) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_chooser(self) -> ReportChooser:
|
||||||
|
"""Returns the report chooser.
|
||||||
|
|
||||||
|
:return: The report chooser.
|
||||||
|
"""
|
||||||
|
return ReportChooser(ReportType.UNAPPLIED, currency=self.currency,
|
||||||
|
account=self.account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def currency_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the currency options.
|
||||||
|
|
||||||
|
:return: The currency options.
|
||||||
|
"""
|
||||||
|
return self._get_currency_options(
|
||||||
|
lambda x: unapplied_url(x, self.account), self.currency)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the account options.
|
||||||
|
|
||||||
|
:return: The account options.
|
||||||
|
"""
|
||||||
|
options: list[OptionLink] \
|
||||||
|
= [OptionLink(gettext("Accounts"),
|
||||||
|
unapplied_url(self.currency, None),
|
||||||
|
False)]
|
||||||
|
options.extend(
|
||||||
|
[OptionLink(str(x), unapplied_url(self.currency, x),
|
||||||
|
x.id == self.account.id)
|
||||||
|
for x in get_accounts_with_unapplied(self.currency)])
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
|
||||||
|
"""Composes and returns the CSV rows from the line items.
|
||||||
|
|
||||||
|
:param line_items: The line items.
|
||||||
|
:return: The CSV rows.
|
||||||
|
"""
|
||||||
|
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
|
||||||
|
gettext("Description"), gettext("Amount"),
|
||||||
|
gettext("Net Balance"))]
|
||||||
|
rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
|
||||||
|
x.description, x.amount, x.net_balance)
|
||||||
|
for x in line_items])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class UnappliedOriginalLineItems(BaseReport):
|
||||||
|
"""The unapplied original line items."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency, account: Account):
|
||||||
|
"""Constructs the unapplied original line items.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
"""
|
||||||
|
self.__currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.__account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
self.__line_items: list[JournalEntryLineItem] \
|
||||||
|
= self.__query_line_items()
|
||||||
|
"""The line items."""
|
||||||
|
|
||||||
|
def __query_line_items(self) -> list[JournalEntryLineItem]:
|
||||||
|
"""Queries and returns the line items.
|
||||||
|
|
||||||
|
:return: The line items.
|
||||||
|
"""
|
||||||
|
net_balances: dict[int, Decimal | None] \
|
||||||
|
= get_net_balances(self.__currency, self.__account)
|
||||||
|
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query \
|
||||||
|
.join(Account).join(JournalEntry) \
|
||||||
|
.filter(JournalEntryLineItem.id.in_(net_balances)) \
|
||||||
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
|
JournalEntryLineItem.is_debit, JournalEntryLineItem.no) \
|
||||||
|
.options(selectinload(JournalEntryLineItem.currency),
|
||||||
|
selectinload(JournalEntryLineItem.journal_entry)).all()
|
||||||
|
for line_item in line_items:
|
||||||
|
line_item.net_balance = line_item.amount \
|
||||||
|
if net_balances[line_item.id] is None \
|
||||||
|
else net_balances[line_item.id]
|
||||||
|
return line_items
|
||||||
|
|
||||||
|
def csv(self) -> Response:
|
||||||
|
"""Returns the report as CSV for download.
|
||||||
|
|
||||||
|
:return: The response of the report for download.
|
||||||
|
"""
|
||||||
|
filename: str = "unapplied-{currency}-{account}.csv"\
|
||||||
|
.format(currency=self.__currency.code, account=self.__account.code)
|
||||||
|
return csv_download(filename, get_csv_rows(self.__line_items))
|
||||||
|
|
||||||
|
def html(self) -> str:
|
||||||
|
"""Composes and returns the report as HTML.
|
||||||
|
|
||||||
|
:return: The report as HTML.
|
||||||
|
"""
|
||||||
|
pagination: Pagination[JournalEntryLineItem] \
|
||||||
|
= Pagination[JournalEntryLineItem](self.__line_items,
|
||||||
|
is_reversed=True)
|
||||||
|
params: PageParams = PageParams(currency=self.__currency,
|
||||||
|
account=self.__account,
|
||||||
|
pagination=pagination,
|
||||||
|
line_items=pagination.list)
|
||||||
|
return render_template("accounting/report/unapplied.html",
|
||||||
|
report=params)
|
156
src/accounting/report/reports/unapplied_accounts.py
Normal file
156
src/accounting/report/reports/unapplied_accounts.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The accounts with unapplied original line items.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import datetime as dt
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import render_template, Response
|
||||||
|
|
||||||
|
from accounting.locale import gettext
|
||||||
|
from accounting.models import Currency, Account
|
||||||
|
from accounting.report.utils.base_page_params import BasePageParams
|
||||||
|
from accounting.report.utils.base_report import BaseReport
|
||||||
|
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
||||||
|
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.unapplied import get_accounts_with_unapplied
|
||||||
|
from accounting.report.utils.urls import unapplied_url
|
||||||
|
|
||||||
|
|
||||||
|
class CSVRow(BaseCSVRow):
|
||||||
|
"""A row in the CSV."""
|
||||||
|
|
||||||
|
def __init__(self, account: str, count: int | str):
|
||||||
|
"""Constructs a row in the CSV.
|
||||||
|
|
||||||
|
:param account: The account.
|
||||||
|
:param count: The number of unapplied original line items.
|
||||||
|
"""
|
||||||
|
self.account: str = account
|
||||||
|
"""The currency."""
|
||||||
|
self.count: int | str = count
|
||||||
|
"""The number of unapplied original line items."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self) -> list[str | dt.date | Decimal | None]:
|
||||||
|
"""Returns the values of the row.
|
||||||
|
|
||||||
|
:return: The values of the row.
|
||||||
|
"""
|
||||||
|
return [self.account, self.count]
|
||||||
|
|
||||||
|
|
||||||
|
class PageParams(BasePageParams):
|
||||||
|
"""The HTML page parameters."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency, accounts: list[Account]):
|
||||||
|
"""Constructs the HTML page parameters.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param accounts: The accounts.
|
||||||
|
"""
|
||||||
|
self.currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.accounts: list[Account] = accounts
|
||||||
|
"""The accounts."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_data(self) -> bool:
|
||||||
|
"""Returns whether there is any data on the page.
|
||||||
|
|
||||||
|
:return: True if there is any data, or False otherwise.
|
||||||
|
"""
|
||||||
|
return len(self.accounts) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_chooser(self) -> ReportChooser:
|
||||||
|
"""Returns the report chooser.
|
||||||
|
|
||||||
|
:return: The report chooser.
|
||||||
|
"""
|
||||||
|
return ReportChooser(ReportType.UNAPPLIED, currency=self.currency,
|
||||||
|
account=None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def currency_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the currency options.
|
||||||
|
|
||||||
|
:return: The currency options.
|
||||||
|
"""
|
||||||
|
return self._get_currency_options(lambda x: unapplied_url(x, None),
|
||||||
|
self.currency)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the account options.
|
||||||
|
|
||||||
|
:return: The account options.
|
||||||
|
"""
|
||||||
|
options: list[OptionLink] \
|
||||||
|
= [OptionLink(gettext("Accounts"),
|
||||||
|
unapplied_url(self.currency, None),
|
||||||
|
True)]
|
||||||
|
options.extend(
|
||||||
|
[OptionLink(str(x), unapplied_url(self.currency, x), False)
|
||||||
|
for x in self.accounts])
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def get_csv_rows(accounts: list[Account]) -> list[CSVRow]:
|
||||||
|
"""Composes and returns the CSV rows from the line items.
|
||||||
|
|
||||||
|
:param accounts: The accounts.
|
||||||
|
:return: The CSV rows.
|
||||||
|
"""
|
||||||
|
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
|
||||||
|
rows.extend([CSVRow(str(x).title(), x.count)
|
||||||
|
for x in accounts])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsWithUnappliedOriginalLineItems(BaseReport):
|
||||||
|
"""The accounts with unapplied original line items."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency):
|
||||||
|
"""Constructs the outstanding balances.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
"""
|
||||||
|
self.__currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.__accounts: list[Account] = get_accounts_with_unapplied(currency)
|
||||||
|
"""The accounts."""
|
||||||
|
|
||||||
|
def csv(self) -> Response:
|
||||||
|
"""Returns the report as CSV for download.
|
||||||
|
|
||||||
|
:return: The response of the report for download.
|
||||||
|
"""
|
||||||
|
filename: str = "unapplied-accounts.csv"
|
||||||
|
return csv_download(filename, get_csv_rows(self.__accounts))
|
||||||
|
|
||||||
|
def html(self) -> str:
|
||||||
|
"""Composes and returns the report as HTML.
|
||||||
|
|
||||||
|
:return: The report as HTML.
|
||||||
|
"""
|
||||||
|
return render_template("accounting/report/unapplied-accounts.html",
|
||||||
|
report=PageParams(currency=self.__currency,
|
||||||
|
accounts=self.__accounts))
|
214
src/accounting/report/reports/unmatched.py
Normal file
214
src/accounting/report/reports/unmatched.py
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The unmatched offsets.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import datetime as dt
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import render_template, Response
|
||||||
|
from flask_babel import LazyString
|
||||||
|
|
||||||
|
from accounting.locale import gettext
|
||||||
|
from accounting.models import Currency, Account, JournalEntryLineItem
|
||||||
|
from accounting.report.utils.base_page_params import BasePageParams
|
||||||
|
from accounting.report.utils.base_report import BaseReport
|
||||||
|
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
||||||
|
from accounting.report.utils.offset_matcher import OffsetMatcher, OffsetPair
|
||||||
|
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.unmatched import get_accounts_with_unmatched
|
||||||
|
from accounting.report.utils.urls import unmatched_url
|
||||||
|
from accounting.utils.pagination import Pagination
|
||||||
|
|
||||||
|
|
||||||
|
class CSVRow(BaseCSVRow):
|
||||||
|
"""A row in the CSV."""
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
:param journal_entry_date: The journal entry date.
|
||||||
|
:param currency: The currency.
|
||||||
|
:param description: The description.
|
||||||
|
:param debit: The debit amount.
|
||||||
|
:param credit: The credit amount.
|
||||||
|
:param balance: The balance.
|
||||||
|
"""
|
||||||
|
self.date: str | dt.date = journal_entry_date
|
||||||
|
"""The date."""
|
||||||
|
self.currency: str = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.description: str | None = description
|
||||||
|
"""The description."""
|
||||||
|
self.debit: str | Decimal | None = debit
|
||||||
|
"""The debit amount."""
|
||||||
|
self.credit: str | Decimal | None = credit
|
||||||
|
"""The credit amount."""
|
||||||
|
self.balance: str | Decimal = balance
|
||||||
|
"""The balance."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self) -> list[str | dt.date | Decimal | None]:
|
||||||
|
"""Returns the values of the row.
|
||||||
|
|
||||||
|
:return: The values of the row.
|
||||||
|
"""
|
||||||
|
return [self.date, self.currency, self.description, self.debit,
|
||||||
|
self.credit, self.balance]
|
||||||
|
|
||||||
|
|
||||||
|
class PageParams(BasePageParams):
|
||||||
|
"""The HTML page parameters."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency,
|
||||||
|
account: Account,
|
||||||
|
match_status: str | LazyString,
|
||||||
|
matched_pairs: list[OffsetPair],
|
||||||
|
pagination: Pagination[JournalEntryLineItem],
|
||||||
|
line_items: list[JournalEntryLineItem]):
|
||||||
|
"""Constructs the HTML page parameters.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:param match_status: The match status message.
|
||||||
|
:param matched_pairs: A list of matched pairs.
|
||||||
|
:param pagination: The pagination.
|
||||||
|
:param line_items: The line items.
|
||||||
|
"""
|
||||||
|
self.currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
self.match_status: str | LazyString = match_status
|
||||||
|
"""The match status message."""
|
||||||
|
self.matched_pairs: list[OffsetPair] = matched_pairs
|
||||||
|
"""A list of matched pairs."""
|
||||||
|
self.pagination: Pagination[JournalEntryLineItem] = pagination
|
||||||
|
"""The pagination."""
|
||||||
|
self.line_items: list[JournalEntryLineItem] = line_items
|
||||||
|
"""The line items."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_data(self) -> bool:
|
||||||
|
"""Returns whether there is any data on the page.
|
||||||
|
|
||||||
|
:return: True if there is any data, or False otherwise.
|
||||||
|
"""
|
||||||
|
return len(self.line_items) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_chooser(self) -> ReportChooser:
|
||||||
|
"""Returns the report chooser.
|
||||||
|
|
||||||
|
:return: The report chooser.
|
||||||
|
"""
|
||||||
|
return ReportChooser(ReportType.UNMATCHED, currency=self.currency,
|
||||||
|
account=self.account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def currency_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the currency options.
|
||||||
|
|
||||||
|
:return: The currency options.
|
||||||
|
"""
|
||||||
|
return self._get_currency_options(
|
||||||
|
lambda x: unmatched_url(x, self.account), self.currency)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the account options.
|
||||||
|
|
||||||
|
:return: The account options.
|
||||||
|
"""
|
||||||
|
options: list[OptionLink] \
|
||||||
|
= [OptionLink(gettext("Accounts"),
|
||||||
|
unmatched_url(self.currency, None),
|
||||||
|
False)]
|
||||||
|
options.extend(
|
||||||
|
[OptionLink(str(x), unmatched_url(self.currency, x),
|
||||||
|
x.id == self.account.id)
|
||||||
|
for x in get_accounts_with_unmatched(self.currency)])
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
|
||||||
|
"""Composes and returns the CSV rows from the line items.
|
||||||
|
|
||||||
|
:param line_items: The line items.
|
||||||
|
:return: The CSV rows.
|
||||||
|
"""
|
||||||
|
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
|
||||||
|
gettext("Description"), gettext("Debit"),
|
||||||
|
gettext("Credit"), gettext("Balance"))]
|
||||||
|
rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
|
||||||
|
x.description, x.debit, x.credit, x.balance)
|
||||||
|
for x in line_items])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class UnmatchedOffsets(BaseReport):
|
||||||
|
"""The unmatched offsets."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency, account: Account):
|
||||||
|
"""Constructs the unmatched offsets.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
"""
|
||||||
|
self.__currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.__account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
offset_matcher: OffsetMatcher \
|
||||||
|
= OffsetMatcher(self.__currency, self.__account)
|
||||||
|
self.__line_items: list[JournalEntryLineItem] \
|
||||||
|
= offset_matcher.line_items
|
||||||
|
"""The line items."""
|
||||||
|
self.__match_status: str | LazyString = offset_matcher.status
|
||||||
|
"""The match status message."""
|
||||||
|
self.__matched_pairs: list[OffsetPair] = offset_matcher.matched_pairs
|
||||||
|
"""A list of matched pairs."""
|
||||||
|
|
||||||
|
def csv(self) -> Response:
|
||||||
|
"""Returns the report as CSV for download.
|
||||||
|
|
||||||
|
:return: The response of the report for download.
|
||||||
|
"""
|
||||||
|
filename: str = "unmatched-{currency}-{account}.csv"\
|
||||||
|
.format(currency=self.__currency.code, account=self.__account.code)
|
||||||
|
return csv_download(filename, get_csv_rows(self.__line_items))
|
||||||
|
|
||||||
|
def html(self) -> str:
|
||||||
|
"""Composes and returns the report as HTML.
|
||||||
|
|
||||||
|
:return: The report as HTML.
|
||||||
|
"""
|
||||||
|
pagination: Pagination[JournalEntryLineItem] \
|
||||||
|
= Pagination[JournalEntryLineItem](self.__line_items,
|
||||||
|
is_reversed=True)
|
||||||
|
params: PageParams = PageParams(currency=self.__currency,
|
||||||
|
account=self.__account,
|
||||||
|
match_status=self.__match_status,
|
||||||
|
matched_pairs=self.__matched_pairs,
|
||||||
|
pagination=pagination,
|
||||||
|
line_items=pagination.list)
|
||||||
|
return render_template("accounting/report/unmatched.html",
|
||||||
|
report=params)
|
157
src/accounting/report/reports/unmatched_accounts.py
Normal file
157
src/accounting/report/reports/unmatched_accounts.py
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The accounts with unmatched offsets.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import datetime as dt
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import render_template, Response
|
||||||
|
|
||||||
|
from accounting.locale import gettext
|
||||||
|
from accounting.models import Currency, Account
|
||||||
|
from accounting.report.utils.base_page_params import BasePageParams
|
||||||
|
from accounting.report.utils.base_report import BaseReport
|
||||||
|
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
||||||
|
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.unmatched import get_accounts_with_unmatched
|
||||||
|
from accounting.report.utils.urls import unmatched_url
|
||||||
|
|
||||||
|
|
||||||
|
class CSVRow(BaseCSVRow):
|
||||||
|
"""A row in the CSV."""
|
||||||
|
|
||||||
|
def __init__(self, account: str, count: int | str):
|
||||||
|
"""Constructs a row in the CSV.
|
||||||
|
|
||||||
|
:param account: The account.
|
||||||
|
:param count: The number of unapplied original line items.
|
||||||
|
"""
|
||||||
|
self.account: str = account
|
||||||
|
"""The currency."""
|
||||||
|
self.count: int | str = count
|
||||||
|
"""The number of unapplied original line items."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self) -> list[str | dt.date | Decimal | None]:
|
||||||
|
"""Returns the values of the row.
|
||||||
|
|
||||||
|
:return: The values of the row.
|
||||||
|
"""
|
||||||
|
return [self.account, self.count]
|
||||||
|
|
||||||
|
|
||||||
|
class PageParams(BasePageParams):
|
||||||
|
"""The HTML page parameters."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency, accounts: list[Account]):
|
||||||
|
"""Constructs the HTML page parameters.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param accounts: The accounts.
|
||||||
|
"""
|
||||||
|
self.currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.accounts: list[Account] = accounts
|
||||||
|
"""The accounts."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_data(self) -> bool:
|
||||||
|
"""Returns whether there is any data on the page.
|
||||||
|
|
||||||
|
:return: True if there is any data, or False otherwise.
|
||||||
|
"""
|
||||||
|
return len(self.accounts) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_chooser(self) -> ReportChooser:
|
||||||
|
"""Returns the report chooser.
|
||||||
|
|
||||||
|
:return: The report chooser.
|
||||||
|
"""
|
||||||
|
return ReportChooser(ReportType.UNMATCHED, currency=self.currency,
|
||||||
|
account=None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def currency_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the currency options.
|
||||||
|
|
||||||
|
:return: The currency options.
|
||||||
|
"""
|
||||||
|
return self._get_currency_options(lambda x: unmatched_url(x, None),
|
||||||
|
self.currency)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the account options.
|
||||||
|
|
||||||
|
:return: The account options.
|
||||||
|
"""
|
||||||
|
options: list[OptionLink] \
|
||||||
|
= [OptionLink(gettext("Accounts"),
|
||||||
|
unmatched_url(self.currency, None),
|
||||||
|
True)]
|
||||||
|
options.extend(
|
||||||
|
[OptionLink(str(x), unmatched_url(self.currency, x), False)
|
||||||
|
for x in self.accounts])
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def get_csv_rows(accounts: list[Account]) -> list[CSVRow]:
|
||||||
|
"""Composes and returns the CSV rows from the line items.
|
||||||
|
|
||||||
|
:param accounts: The accounts.
|
||||||
|
:return: The CSV rows.
|
||||||
|
"""
|
||||||
|
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
|
||||||
|
rows.extend([CSVRow(str(x).title(), x.count)
|
||||||
|
for x in accounts])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsWithUnmatchedOffsets(BaseReport):
|
||||||
|
"""The accounts with unmatched offsets."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency):
|
||||||
|
"""Constructs the outstanding balances.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
"""
|
||||||
|
self.__currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.__accounts: list[Account] \
|
||||||
|
= get_accounts_with_unmatched(currency)
|
||||||
|
"""The accounts."""
|
||||||
|
|
||||||
|
def csv(self) -> Response:
|
||||||
|
"""Returns the report as CSV for download.
|
||||||
|
|
||||||
|
:return: The response of the report for download.
|
||||||
|
"""
|
||||||
|
filename: str = "unmatched-accounts.csv"
|
||||||
|
return csv_download(filename, get_csv_rows(self.__accounts))
|
||||||
|
|
||||||
|
def html(self) -> str:
|
||||||
|
"""Composes and returns the report as HTML.
|
||||||
|
|
||||||
|
:return: The report as HTML.
|
||||||
|
"""
|
||||||
|
return render_template("accounting/report/unmatched-accounts.html",
|
||||||
|
report=PageParams(currency=self.__currency,
|
||||||
|
accounts=self.__accounts))
|
@ -17,8 +17,9 @@
|
|||||||
"""The page parameters of a report.
|
"""The page parameters of a report.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Type
|
||||||
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
|
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
|
||||||
urlunparse
|
urlunparse
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ class BasePageParams(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def journal_entry_types(self) -> t.Type[JournalEntryType]:
|
def journal_entry_types(self) -> Type[JournalEntryType]:
|
||||||
"""Returns the journal entry types.
|
"""Returns the journal entry types.
|
||||||
|
|
||||||
:return: The journal entry types.
|
:return: The journal entry types.
|
||||||
@ -72,7 +73,7 @@ class BasePageParams(ABC):
|
|||||||
return urlunparse(parts)
|
return urlunparse(parts)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_currency_options(get_url: t.Callable[[Currency], str],
|
def _get_currency_options(get_url: Callable[[Currency], str],
|
||||||
active_currency: Currency) -> list[OptionLink]:
|
active_currency: Currency) -> list[OptionLink]:
|
||||||
"""Returns the currency options.
|
"""Returns the currency options.
|
||||||
|
|
||||||
|
@ -18,10 +18,11 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
|
import datetime as dt
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import timedelta, date
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from flask import Response
|
from flask import Response
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
|
|||||||
fp.seek(0)
|
fp.seek(0)
|
||||||
response: Response = Response(fp.read(), mimetype="text/csv")
|
response: Response = Response(fp.read(), mimetype="text/csv")
|
||||||
response.headers["Content-Disposition"] \
|
response.headers["Content-Disposition"] \
|
||||||
= f"attachment; filename={filename}"
|
= f"attachment; filename={quote(filename)}"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ def period_spec(period: Period) -> str:
|
|||||||
return f"{start}-{end}"
|
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.
|
"""Returns the string representation of the start date.
|
||||||
|
|
||||||
:param start: The start date.
|
:param start: The start date.
|
||||||
@ -92,7 +93,7 @@ def __get_start_str(start: date | None) -> str | None:
|
|||||||
return start.strftime("%Y%m%d")
|
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.
|
"""Returns the string representation of the end date.
|
||||||
|
|
||||||
:param end: The end date.
|
:param end: The end date.
|
||||||
@ -103,6 +104,6 @@ def __get_end_str(end: date | None) -> str | None:
|
|||||||
return None
|
return None
|
||||||
if end.month == 12 and end.day == 31:
|
if end.month == 12 and end.day == 31:
|
||||||
return str(end.year)
|
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")
|
||||||
return end.strftime("%Y%m%d")
|
return end.strftime("%Y%m%d")
|
||||||
|
178
src/accounting/report/utils/offset_matcher.py
Normal file
178
src/accounting/report/utils/offset_matcher.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/8
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The forms for the unmatched offset management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from flask_babel import LazyString
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from accounting.locale import lazy_gettext
|
||||||
|
from accounting.models import Currency, Account, JournalEntry, \
|
||||||
|
JournalEntryLineItem
|
||||||
|
from accounting.report.utils.unapplied import get_net_balances
|
||||||
|
|
||||||
|
|
||||||
|
class OffsetPair:
|
||||||
|
"""A pair of an original line item and its offset."""
|
||||||
|
|
||||||
|
def __init__(self, original_line_item: JournalEntryLineItem,
|
||||||
|
offset: JournalEntryLineItem):
|
||||||
|
"""Constructs a pair of an original line item and its offset.
|
||||||
|
|
||||||
|
:param original_line_item: The original line item.
|
||||||
|
:param offset: The offset.
|
||||||
|
"""
|
||||||
|
self.original_line_item: JournalEntryLineItem = original_line_item
|
||||||
|
"""The original line item."""
|
||||||
|
self.offset: JournalEntryLineItem = offset
|
||||||
|
"""The offset."""
|
||||||
|
|
||||||
|
|
||||||
|
class OffsetMatcher:
|
||||||
|
"""The offset matcher."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency, account: Account):
|
||||||
|
"""Constructs the offset matcher.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
"""
|
||||||
|
self.__currency: Account = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.__account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
self.matched_pairs: list[OffsetPair] = []
|
||||||
|
"""A list of matched pairs."""
|
||||||
|
self.line_items: list[JournalEntryLineItem] = []
|
||||||
|
"""The unapplied debits or credits and unmatched offsets."""
|
||||||
|
self.unapplied: list[JournalEntryLineItem] = []
|
||||||
|
"""The unapplied debits or credits."""
|
||||||
|
self.unmatched: list[JournalEntryLineItem] = []
|
||||||
|
"""The unmatched offsets."""
|
||||||
|
self.__find_matches()
|
||||||
|
|
||||||
|
def __find_matches(self) -> None:
|
||||||
|
"""Finds the matched original line items and their offsets.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
self.__get_line_items()
|
||||||
|
if len(self.unapplied) == 0 or len(self.unmatched) == 0:
|
||||||
|
return
|
||||||
|
remains: list[JournalEntryLineItem] = self.unmatched.copy()
|
||||||
|
for original_item in self.unapplied:
|
||||||
|
offset_candidates: list[JournalEntryLineItem] \
|
||||||
|
= [x for x in remains
|
||||||
|
if (x.journal_entry.date > original_item.journal_entry.date
|
||||||
|
or (x.journal_entry.date
|
||||||
|
== original_item.journal_entry.date
|
||||||
|
and x.journal_entry.no
|
||||||
|
> original_item.journal_entry.no))
|
||||||
|
and x.currency_code == original_item.currency_code
|
||||||
|
and x.description == original_item.description
|
||||||
|
and x.amount == original_item.net_balance]
|
||||||
|
if len(offset_candidates) == 0:
|
||||||
|
continue
|
||||||
|
self.matched_pairs.append(
|
||||||
|
OffsetPair(original_item, offset_candidates[0]))
|
||||||
|
original_item.match = offset_candidates[0]
|
||||||
|
offset_candidates[0].match = original_item
|
||||||
|
remains.remove(offset_candidates[0])
|
||||||
|
|
||||||
|
def __get_line_items(self) -> None:
|
||||||
|
"""Returns the unapplied original line items and unmatched offsets of
|
||||||
|
the account.
|
||||||
|
|
||||||
|
:return: The unapplied original line items and unmatched offsets of the
|
||||||
|
account.
|
||||||
|
"""
|
||||||
|
net_balances: dict[int, Decimal | None] \
|
||||||
|
= get_net_balances(self.__currency, self.__account)
|
||||||
|
unmatched_offset_condition: sa.BinaryExpression \
|
||||||
|
= sa.and_(Account.id == self.__account.id,
|
||||||
|
JournalEntryLineItem.currency_code
|
||||||
|
== self.__currency.code,
|
||||||
|
JournalEntryLineItem.original_line_item_id.is_(None),
|
||||||
|
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
||||||
|
JournalEntryLineItem.is_debit),
|
||||||
|
sa.and_(Account.base_code.startswith("1"),
|
||||||
|
sa.not_(JournalEntryLineItem.is_debit))))
|
||||||
|
self.line_items = JournalEntryLineItem.query \
|
||||||
|
.join(Account).join(JournalEntry) \
|
||||||
|
.filter(sa.or_(JournalEntryLineItem.id.in_(net_balances),
|
||||||
|
unmatched_offset_condition)) \
|
||||||
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
|
JournalEntryLineItem.is_debit, JournalEntryLineItem.no) \
|
||||||
|
.options(selectinload(JournalEntryLineItem.currency),
|
||||||
|
selectinload(JournalEntryLineItem.journal_entry)).all()
|
||||||
|
for line_item in self.line_items:
|
||||||
|
line_item.is_offset = line_item.id not in net_balances
|
||||||
|
self.unapplied = [x for x in self.line_items if not x.is_offset]
|
||||||
|
for line_item in self.unapplied:
|
||||||
|
line_item.net_balance = line_item.amount \
|
||||||
|
if net_balances[line_item.id] is None \
|
||||||
|
else net_balances[line_item.id]
|
||||||
|
self.unmatched = [x for x in self.line_items if x.is_offset]
|
||||||
|
self.__populate_accumulated_balances()
|
||||||
|
|
||||||
|
def __populate_accumulated_balances(self) -> None:
|
||||||
|
"""Populates the accumulated balances of the line items.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
balance: Decimal = Decimal("0")
|
||||||
|
for line_item in self.line_items:
|
||||||
|
amount: Decimal = line_item.amount if line_item.is_offset \
|
||||||
|
else line_item.net_balance
|
||||||
|
if line_item.is_debit:
|
||||||
|
line_item.debit = amount
|
||||||
|
line_item.credit = None
|
||||||
|
balance = balance + amount
|
||||||
|
else:
|
||||||
|
line_item.debit = None
|
||||||
|
line_item.credit = amount
|
||||||
|
balance = balance - amount
|
||||||
|
line_item.balance = balance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> str | LazyString:
|
||||||
|
"""Returns the match status message.
|
||||||
|
|
||||||
|
:return: The match status message.
|
||||||
|
"""
|
||||||
|
if len(self.unmatched) == 0:
|
||||||
|
return lazy_gettext("There is no unmatched offset.")
|
||||||
|
if len(self.matched_pairs) == 0:
|
||||||
|
return lazy_gettext(
|
||||||
|
"%(total)s unmatched offsets without original items.",
|
||||||
|
total=len(self.unmatched))
|
||||||
|
return lazy_gettext(
|
||||||
|
"%(matches)s unmatched offsets out of %(total)s"
|
||||||
|
" can match with their original items.",
|
||||||
|
matches=len(self.matched_pairs),
|
||||||
|
total=len(self.unmatched))
|
||||||
|
|
||||||
|
def match(self) -> None:
|
||||||
|
"""Matches the original line items with offsets.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
for pair in self.matched_pairs:
|
||||||
|
pair.offset.original_line_item_id = pair.original_line_item.id
|
@ -21,7 +21,7 @@ This file is largely taken from the NanoParma ERP project, first written in
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import typing as t
|
from collections.abc import Iterator
|
||||||
|
|
||||||
from flask_babel import LazyString
|
from flask_babel import LazyString
|
||||||
|
|
||||||
@ -31,10 +31,12 @@ from accounting.models import Currency, Account
|
|||||||
from accounting.report.period import Period, get_period
|
from accounting.report.period import Period, get_period
|
||||||
from accounting.template_globals import default_currency_code
|
from accounting.template_globals import default_currency_code
|
||||||
from accounting.utils.current_account import CurrentAccount
|
from accounting.utils.current_account import CurrentAccount
|
||||||
|
from accounting.utils.permission import can_edit
|
||||||
from .option_link import OptionLink
|
from .option_link import OptionLink
|
||||||
from .report_type import ReportType
|
from .report_type import ReportType
|
||||||
from .urls import journal_url, ledger_url, income_expenses_url, \
|
from .urls import journal_url, ledger_url, income_expenses_url, \
|
||||||
trial_balance_url, income_statement_url, balance_sheet_url
|
trial_balance_url, income_statement_url, balance_sheet_url, \
|
||||||
|
unapplied_url, unmatched_url
|
||||||
|
|
||||||
|
|
||||||
class ReportChooser:
|
class ReportChooser:
|
||||||
@ -74,6 +76,9 @@ class ReportChooser:
|
|||||||
self.__reports.append(self.__trial_balance)
|
self.__reports.append(self.__trial_balance)
|
||||||
self.__reports.append(self.__income_statement)
|
self.__reports.append(self.__income_statement)
|
||||||
self.__reports.append(self.__balance_sheet)
|
self.__reports.append(self.__balance_sheet)
|
||||||
|
self.__reports.append(self.__unapplied)
|
||||||
|
if can_edit():
|
||||||
|
self.__reports.append(self.__unmatched)
|
||||||
for report in self.__reports:
|
for report in self.__reports:
|
||||||
if report.is_active:
|
if report.is_active:
|
||||||
self.current_report = report.title
|
self.current_report = report.title
|
||||||
@ -151,7 +156,41 @@ class ReportChooser:
|
|||||||
self.__active_report == ReportType.BALANCE_SHEET,
|
self.__active_report == ReportType.BALANCE_SHEET,
|
||||||
fa_icon="fa-solid fa-scale-balanced")
|
fa_icon="fa-solid fa-scale-balanced")
|
||||||
|
|
||||||
def __iter__(self) -> t.Iterator[OptionLink]:
|
@property
|
||||||
|
def __unapplied(self) -> OptionLink:
|
||||||
|
"""Returns the unapplied original line items.
|
||||||
|
|
||||||
|
:return: The unapplied original line items.
|
||||||
|
"""
|
||||||
|
account: Account = self.__account
|
||||||
|
if not account.is_need_offset:
|
||||||
|
return OptionLink(gettext("Unapplied Items"),
|
||||||
|
unapplied_url(self.__currency, None),
|
||||||
|
self.__active_report == ReportType.UNAPPLIED,
|
||||||
|
fa_icon="fa-solid fa-link-slash")
|
||||||
|
return OptionLink(gettext("Unapplied Items"),
|
||||||
|
unapplied_url(self.__currency, self.__account),
|
||||||
|
self.__active_report == ReportType.UNAPPLIED,
|
||||||
|
fa_icon="fa-solid fa-link-slash")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __unmatched(self) -> OptionLink:
|
||||||
|
"""Returns the unmatched offsets.
|
||||||
|
|
||||||
|
:return: The unmatched offsets.
|
||||||
|
"""
|
||||||
|
account: Account = self.__account
|
||||||
|
if not account.is_need_offset:
|
||||||
|
return OptionLink(gettext("Unmatched Offsets"),
|
||||||
|
unmatched_url(self.__currency, None),
|
||||||
|
self.__active_report == ReportType.UNMATCHED,
|
||||||
|
fa_icon="fa-solid fa-file-circle-question")
|
||||||
|
return OptionLink(gettext("Unmatched Offsets"),
|
||||||
|
unmatched_url(self.__currency, self.__account),
|
||||||
|
self.__active_report == ReportType.UNMATCHED,
|
||||||
|
fa_icon="fa-solid fa-file-circle-question")
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[OptionLink]:
|
||||||
"""Returns the iteration of the reports.
|
"""Returns the iteration of the reports.
|
||||||
|
|
||||||
:return: The iteration of the reports.
|
:return: The iteration of the reports.
|
||||||
|
@ -34,5 +34,9 @@ class ReportType(Enum):
|
|||||||
"""The income statement."""
|
"""The income statement."""
|
||||||
BALANCE_SHEET: str = "balance-sheet"
|
BALANCE_SHEET: str = "balance-sheet"
|
||||||
"""The balance sheet."""
|
"""The balance sheet."""
|
||||||
|
UNAPPLIED: str = "unapplied"
|
||||||
|
"""The unapplied original line items."""
|
||||||
|
UNMATCHED: str = "unmatched"
|
||||||
|
"""The unmatched offsets."""
|
||||||
SEARCH: str = "search"
|
SEARCH: str = "search"
|
||||||
"""The search."""
|
"""The search."""
|
||||||
|
104
src/accounting/report/utils/unapplied.py
Normal file
104
src/accounting/report/utils/unapplied.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The unapplied original line item utilities.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
|
from accounting.models import Currency, Account, JournalEntry, \
|
||||||
|
JournalEntryLineItem
|
||||||
|
from accounting.utils.offset_alias import offset_alias
|
||||||
|
|
||||||
|
|
||||||
|
def get_accounts_with_unapplied(currency: Currency) -> list[Account]:
|
||||||
|
"""Returns the accounts with unapplied original line items.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:return: The accounts with unapplied original line items.
|
||||||
|
"""
|
||||||
|
offset: sa.Alias = offset_alias()
|
||||||
|
net_balance: sa.Label \
|
||||||
|
= (JournalEntryLineItem.amount
|
||||||
|
+ sa.func.sum(sa.case(
|
||||||
|
(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,
|
||||||
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
|
isouter=True)\
|
||||||
|
.filter(Account.is_need_offset,
|
||||||
|
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"),
|
||||||
|
JournalEntryLineItem.is_debit)))\
|
||||||
|
.group_by(JournalEntryLineItem.id)\
|
||||||
|
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
|
||||||
|
|
||||||
|
count_func: sa.Label \
|
||||||
|
= sa.func.count(JournalEntryLineItem.id).label("count")
|
||||||
|
select: sa.Select = sa.select(Account.id, count_func)\
|
||||||
|
.join(JournalEntryLineItem, isouter=True)\
|
||||||
|
.filter(JournalEntryLineItem.id.in_(select_unapplied))\
|
||||||
|
.group_by(Account.id)\
|
||||||
|
.having(count_func > 0)
|
||||||
|
counts: dict[int, int] \
|
||||||
|
= {x.id: x.count for x in db.session.execute(select)}
|
||||||
|
accounts: list[Account] = Account.query.filter(Account.id.in_(counts))\
|
||||||
|
.order_by(Account.base_code, Account.no).all()
|
||||||
|
for account in accounts:
|
||||||
|
account.count = counts[account.id]
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
|
||||||
|
def get_net_balances(currency: Currency, account: Account) \
|
||||||
|
-> dict[int, Decimal | None]:
|
||||||
|
"""Returns the net balances of the unapplied line items of the account.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:return: The net balances of the unapplied line items of the account.
|
||||||
|
"""
|
||||||
|
offset: sa.Alias = offset_alias()
|
||||||
|
net_balance: sa.Label \
|
||||||
|
= (JournalEntryLineItem.amount
|
||||||
|
+ sa.func.sum(sa.case(
|
||||||
|
(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,
|
||||||
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
|
isouter=True) \
|
||||||
|
.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"),
|
||||||
|
JournalEntryLineItem.is_debit))) \
|
||||||
|
.group_by(JournalEntryLineItem.id) \
|
||||||
|
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
|
||||||
|
return {x.id: x.net_balance
|
||||||
|
for x in db.session.execute(select_net_balances).all()}
|
54
src/accounting/report/utils/unmatched.py
Normal file
54
src/accounting/report/utils/unmatched.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/8
|
||||||
|
|
||||||
|
# Copyright (c) 2023 imacat.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""The unmatched offset utilities.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from accounting import db
|
||||||
|
from accounting.models import Currency, Account, JournalEntry, \
|
||||||
|
JournalEntryLineItem
|
||||||
|
|
||||||
|
|
||||||
|
def get_accounts_with_unmatched(currency: Currency) -> list[Account]:
|
||||||
|
"""Returns the accounts with unmatched offsets.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:return: The accounts with unmatched offsets, with the "count" property set
|
||||||
|
to the number of unmatched offsets.
|
||||||
|
"""
|
||||||
|
count_func: sa.Label \
|
||||||
|
= sa.func.count(JournalEntryLineItem.id).label("count")
|
||||||
|
select: sa.Select = sa.select(Account.id, count_func)\
|
||||||
|
.select_from(Account)\
|
||||||
|
.join(JournalEntryLineItem, isouter=True).join(JournalEntry)\
|
||||||
|
.filter(Account.is_need_offset,
|
||||||
|
JournalEntryLineItem.currency_code == currency.code,
|
||||||
|
JournalEntryLineItem.original_line_item_id.is_(None),
|
||||||
|
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
||||||
|
JournalEntryLineItem.is_debit),
|
||||||
|
sa.and_(Account.base_code.startswith("1"),
|
||||||
|
sa.not_(JournalEntryLineItem.is_debit))))\
|
||||||
|
.group_by(Account.id)\
|
||||||
|
.having(count_func > 0)
|
||||||
|
counts: dict[int, int] \
|
||||||
|
= {x.id: x.count for x in db.session.execute(select)}
|
||||||
|
accounts: list[Account] = Account.query.filter(Account.id.in_(counts))\
|
||||||
|
.order_by(Account.base_code, Account.no).all()
|
||||||
|
for account in accounts:
|
||||||
|
account.count = counts[account.id]
|
||||||
|
return accounts
|
@ -47,9 +47,10 @@ def ledger_url(currency: Currency, account: Account, period: Period) \
|
|||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The URL of the ledger.
|
:return: The URL of the ledger.
|
||||||
"""
|
"""
|
||||||
if period.is_default:
|
if currency.code == default_currency_code() \
|
||||||
return url_for("accounting-report.ledger-default",
|
and account.code == Account.CASH_CODE \
|
||||||
currency=currency, account=account)
|
and period.is_default:
|
||||||
|
return url_for("accounting-report.ledger-default")
|
||||||
return url_for("accounting-report.ledger",
|
return url_for("accounting-report.ledger",
|
||||||
currency=currency, account=account,
|
currency=currency, account=account,
|
||||||
period=period)
|
period=period)
|
||||||
@ -68,9 +69,6 @@ def income_expenses_url(currency: Currency, account: CurrentAccount,
|
|||||||
and account.code == options.default_ie_account_code \
|
and account.code == options.default_ie_account_code \
|
||||||
and period.is_default:
|
and period.is_default:
|
||||||
return url_for("accounting-report.default")
|
return url_for("accounting-report.default")
|
||||||
if period.is_default:
|
|
||||||
return url_for("accounting-report.income-expenses-default",
|
|
||||||
currency=currency, account=account)
|
|
||||||
return url_for("accounting-report.income-expenses",
|
return url_for("accounting-report.income-expenses",
|
||||||
currency=currency, account=account,
|
currency=currency, account=account,
|
||||||
period=period)
|
period=period)
|
||||||
@ -83,9 +81,8 @@ def trial_balance_url(currency: Currency, period: Period) -> str:
|
|||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The URL of the trial balance.
|
:return: The URL of the trial balance.
|
||||||
"""
|
"""
|
||||||
if period.is_default:
|
if currency.code == default_currency_code() and period.is_default:
|
||||||
return url_for("accounting-report.trial-balance-default",
|
return url_for("accounting-report.trial-balance-default")
|
||||||
currency=currency)
|
|
||||||
return url_for("accounting-report.trial-balance",
|
return url_for("accounting-report.trial-balance",
|
||||||
currency=currency, period=period)
|
currency=currency, period=period)
|
||||||
|
|
||||||
@ -97,9 +94,8 @@ def income_statement_url(currency: Currency, period: Period) -> str:
|
|||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The URL of the income statement.
|
:return: The URL of the income statement.
|
||||||
"""
|
"""
|
||||||
if period.is_default:
|
if currency.code == default_currency_code() and period.is_default:
|
||||||
return url_for("accounting-report.income-statement-default",
|
return url_for("accounting-report.income-statement-default")
|
||||||
currency=currency)
|
|
||||||
return url_for("accounting-report.income-statement",
|
return url_for("accounting-report.income-statement",
|
||||||
currency=currency, period=period)
|
currency=currency, period=period)
|
||||||
|
|
||||||
@ -111,8 +107,41 @@ def balance_sheet_url(currency: Currency, period: Period) -> str:
|
|||||||
:param period: The period.
|
:param period: The period.
|
||||||
:return: The URL of the balance sheet.
|
:return: The URL of the balance sheet.
|
||||||
"""
|
"""
|
||||||
if period.is_default:
|
if currency.code == default_currency_code() and period.is_default:
|
||||||
return url_for("accounting-report.balance-sheet-default",
|
return url_for("accounting-report.balance-sheet-default")
|
||||||
currency=currency)
|
|
||||||
return url_for("accounting-report.balance-sheet",
|
return url_for("accounting-report.balance-sheet",
|
||||||
currency=currency, period=period)
|
currency=currency, period=period)
|
||||||
|
|
||||||
|
|
||||||
|
def unapplied_url(currency: Currency, account: Account | None) -> str:
|
||||||
|
"""Returns the URL of the unapplied original line items.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account, or None to list the accounts with unapplied
|
||||||
|
original line items.
|
||||||
|
:return: The URL of the unapplied original line items.
|
||||||
|
"""
|
||||||
|
if account is None:
|
||||||
|
if currency.code == default_currency_code():
|
||||||
|
return url_for("accounting-report.unapplied-accounts-default")
|
||||||
|
return url_for("accounting-report.unapplied-accounts",
|
||||||
|
currency=currency)
|
||||||
|
return url_for("accounting-report.unapplied",
|
||||||
|
currency=currency, account=account)
|
||||||
|
|
||||||
|
|
||||||
|
def unmatched_url(currency: Currency, account: Account | None) -> str:
|
||||||
|
"""Returns the URL of the unmatched offset line items.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account, or None to list the accounts with unmatched
|
||||||
|
offset line items.
|
||||||
|
:return: The URL of the unmatched offset line items.
|
||||||
|
"""
|
||||||
|
if account is None:
|
||||||
|
if currency.code == default_currency_code():
|
||||||
|
return url_for("accounting-report.unmatched-accounts-default")
|
||||||
|
return url_for("accounting-report.unmatched-accounts",
|
||||||
|
currency=currency)
|
||||||
|
return url_for("accounting-report.unmatched",
|
||||||
|
currency=currency, account=account)
|
||||||
|
@ -17,18 +17,27 @@
|
|||||||
"""The views for the report management.
|
"""The views for the report management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from flask import Blueprint, request, Response
|
from flask import Blueprint, request, Response, redirect, flash
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import Currency, Account
|
from accounting.models import Currency, Account
|
||||||
from accounting.report.period import Period, get_period
|
|
||||||
from accounting.template_globals import default_currency_code
|
from accounting.template_globals import default_currency_code
|
||||||
|
from accounting.utils.cast import s
|
||||||
from accounting.utils.current_account import CurrentAccount
|
from accounting.utils.current_account import CurrentAccount
|
||||||
|
from accounting.utils.next_uri import or_next
|
||||||
from accounting.utils.options import options
|
from accounting.utils.options import options
|
||||||
from accounting.utils.permission import has_permission, can_view
|
from accounting.utils.permission import has_permission, can_view, can_edit
|
||||||
|
from .period import Period, get_period
|
||||||
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
|
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
|
||||||
IncomeStatement, BalanceSheet, Search
|
IncomeStatement, BalanceSheet, Search
|
||||||
|
from .reports.unapplied import UnappliedOriginalLineItems
|
||||||
|
from .reports.unapplied_accounts import AccountsWithUnappliedOriginalLineItems
|
||||||
|
from .reports.unmatched import UnmatchedOffsets
|
||||||
|
from .reports.unmatched_accounts import AccountsWithUnmatchedOffsets
|
||||||
from .template_filters import format_amount
|
from .template_filters import format_amount
|
||||||
|
from .utils.offset_matcher import OffsetMatcher
|
||||||
|
from .utils.urls import unmatched_url
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("accounting-report", __name__)
|
bp: Blueprint = Blueprint("accounting-report", __name__)
|
||||||
"""The view blueprint for the reports."""
|
"""The view blueprint for the reports."""
|
||||||
@ -42,10 +51,7 @@ def get_default_report() -> str | Response:
|
|||||||
|
|
||||||
:return: The income and expenses log in the default period.
|
:return: The income and expenses log in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_income_expenses(
|
return get_default_income_expenses()
|
||||||
db.session.get(Currency, default_currency_code()),
|
|
||||||
options.default_ie_account,
|
|
||||||
get_period())
|
|
||||||
|
|
||||||
|
|
||||||
@bp.get("journal", endpoint="journal-default")
|
@bp.get("journal", endpoint="journal-default")
|
||||||
@ -81,17 +87,15 @@ def __get_journal(period: Period) -> str | Response:
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("ledger/<currency:currency>/<account:account>",
|
@bp.get("ledger", endpoint="ledger-default")
|
||||||
endpoint="ledger-default")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_ledger(currency: Currency, account: Account) -> str | Response:
|
def get_default_ledger() -> str | Response:
|
||||||
"""Returns the ledger in the default period.
|
"""Returns the ledger in the default currency, cash, and default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
:return: The ledger in the default currency, cash, and default period.
|
||||||
:param account: The account.
|
|
||||||
:return: The ledger in the default period.
|
|
||||||
"""
|
"""
|
||||||
return __get_ledger(currency, account, get_period())
|
return __get_ledger(db.session.get(Currency, default_currency_code()),
|
||||||
|
Account.cash(), get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
|
@bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
|
||||||
@ -124,23 +128,21 @@ def __get_ledger(currency: Currency, account: Account, period: Period) \
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
|
@bp.get("income-expenses", endpoint="income-expenses-default")
|
||||||
endpoint="income-expenses-default")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_income_expenses(currency: Currency, account: CurrentAccount) \
|
def get_default_income_expenses() -> str | Response:
|
||||||
-> str | Response:
|
|
||||||
"""Returns the income and expenses log in the default period.
|
"""Returns the income and expenses log in the default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
|
||||||
:param account: The account.
|
|
||||||
:return: The income and expenses log in the default period.
|
:return: The income and expenses log in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_income_expenses(currency, account, get_period())
|
return __get_income_expenses(
|
||||||
|
db.session.get(Currency, default_currency_code()),
|
||||||
|
options.default_ie_account,
|
||||||
|
get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get(
|
@bp.get("income-expenses/<currency:currency>/<currentAccount:account>/"
|
||||||
"income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
|
"<period:period>", endpoint="income-expenses")
|
||||||
endpoint="income-expenses")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_income_expenses(currency: Currency, account: CurrentAccount,
|
def get_income_expenses(currency: Currency, account: CurrentAccount,
|
||||||
period: Period) -> str | Response:
|
period: Period) -> str | Response:
|
||||||
@ -169,16 +171,15 @@ def __get_income_expenses(currency: Currency, account: CurrentAccount,
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("trial-balance/<currency:currency>",
|
@bp.get("trial-balance", endpoint="trial-balance-default")
|
||||||
endpoint="trial-balance-default")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_trial_balance(currency: Currency) -> str | Response:
|
def get_default_trial_balance() -> str | Response:
|
||||||
"""Returns the trial balance in the default period.
|
"""Returns the trial balance in the default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
|
||||||
:return: The trial balance in the default period.
|
:return: The trial balance in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_trial_balance(currency, get_period())
|
return __get_trial_balance(
|
||||||
|
db.session.get(Currency, default_currency_code()), get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get("trial-balance/<currency:currency>/<period:period>",
|
@bp.get("trial-balance/<currency:currency>/<period:period>",
|
||||||
@ -207,16 +208,15 @@ def __get_trial_balance(currency: Currency, period: Period) -> str | Response:
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("income-statement/<currency:currency>",
|
@bp.get("income-statement", endpoint="income-statement-default")
|
||||||
endpoint="income-statement-default")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_income_statement(currency: Currency) -> str | Response:
|
def get_default_income_statement() -> str | Response:
|
||||||
"""Returns the income statement in the default period.
|
"""Returns the income statement in the default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
|
||||||
:return: The income statement in the default period.
|
:return: The income statement in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_income_statement(currency, get_period())
|
return __get_income_statement(
|
||||||
|
db.session.get(Currency, default_currency_code()), get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get("income-statement/<currency:currency>/<period:period>",
|
@bp.get("income-statement/<currency:currency>/<period:period>",
|
||||||
@ -246,16 +246,15 @@ def __get_income_statement(currency: Currency, period: Period) \
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
@bp.get("balance-sheet/<currency:currency>",
|
@bp.get("balance-sheet", endpoint="balance-sheet-default")
|
||||||
endpoint="balance-sheet-default")
|
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def get_default_balance_sheet(currency: Currency) -> str | Response:
|
def get_default_balance_sheet() -> str | Response:
|
||||||
"""Returns the balance sheet in the default period.
|
"""Returns the balance sheet in the default period.
|
||||||
|
|
||||||
:param currency: The currency.
|
|
||||||
:return: The balance sheet in the default period.
|
:return: The balance sheet in the default period.
|
||||||
"""
|
"""
|
||||||
return __get_balance_sheet(currency, get_period())
|
return __get_balance_sheet(
|
||||||
|
db.session.get(Currency, default_currency_code()), get_period())
|
||||||
|
|
||||||
|
|
||||||
@bp.get("balance-sheet/<currency:currency>/<period:period>",
|
@bp.get("balance-sheet/<currency:currency>/<period:period>",
|
||||||
@ -286,6 +285,130 @@ def __get_balance_sheet(currency: Currency, period: Period) \
|
|||||||
return report.html()
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("unapplied", endpoint="unapplied-accounts-default")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def get_default_unapplied_accounts() -> str | Response:
|
||||||
|
"""Returns the accounts with unapplied original line items.
|
||||||
|
|
||||||
|
:return: The accounts with unapplied original line items.
|
||||||
|
"""
|
||||||
|
return __get_unapplied_accounts(
|
||||||
|
db.session.get(Currency, default_currency_code()))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("unapplied/<currency:currency>", endpoint="unapplied-accounts")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def get_unapplied_accounts(currency: Currency) -> str | Response:
|
||||||
|
"""Returns the accounts with unapplied original line items.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:return: The accounts with unapplied original line items.
|
||||||
|
"""
|
||||||
|
return __get_unapplied_accounts(currency)
|
||||||
|
|
||||||
|
|
||||||
|
def __get_unapplied_accounts(currency: Currency) -> str | Response:
|
||||||
|
"""Returns the accounts with unapplied original line items.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:return: The accounts with unapplied original line items.
|
||||||
|
"""
|
||||||
|
report: AccountsWithUnappliedOriginalLineItems \
|
||||||
|
= AccountsWithUnappliedOriginalLineItems(currency)
|
||||||
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
|
return report.csv()
|
||||||
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("unapplied/<currency:currency>/<needOffsetAccount:account>",
|
||||||
|
endpoint="unapplied")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def get_unapplied(currency: Currency, account: Account) -> str | Response:
|
||||||
|
"""Returns the unapplied original line items.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The Account.
|
||||||
|
:return: The unapplied original line items in the period.
|
||||||
|
"""
|
||||||
|
report: UnappliedOriginalLineItems \
|
||||||
|
= UnappliedOriginalLineItems(currency, account)
|
||||||
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
|
return report.csv()
|
||||||
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("unmatched", endpoint="unmatched-accounts-default")
|
||||||
|
@has_permission(can_edit)
|
||||||
|
def get_default_unmatched_accounts() -> str | Response:
|
||||||
|
"""Returns the accounts with unmatched offsets.
|
||||||
|
|
||||||
|
:return: The accounts with unmatched offsets.
|
||||||
|
"""
|
||||||
|
return __get_unmatched_accounts(
|
||||||
|
db.session.get(Currency, default_currency_code()))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("unmatched/<currency:currency>", endpoint="unmatched-accounts")
|
||||||
|
@has_permission(can_edit)
|
||||||
|
def get_unmatched_accounts(currency: Currency) -> str | Response:
|
||||||
|
"""Returns the accounts with unmatched offsets.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:return: The accounts with unmatched offsets.
|
||||||
|
"""
|
||||||
|
return __get_unmatched_accounts(currency)
|
||||||
|
|
||||||
|
|
||||||
|
def __get_unmatched_accounts(currency: Currency) -> str | Response:
|
||||||
|
"""Returns the accounts with unmatched offsets.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:return: The accounts with unmatched offsets.
|
||||||
|
"""
|
||||||
|
report: AccountsWithUnmatchedOffsets \
|
||||||
|
= AccountsWithUnmatchedOffsets(currency)
|
||||||
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
|
return report.csv()
|
||||||
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("unmatched/<currency:currency>/<needOffsetAccount:account>",
|
||||||
|
endpoint="unmatched")
|
||||||
|
@has_permission(can_edit)
|
||||||
|
def get_unmatched(currency: Currency, account: Account) -> str | Response:
|
||||||
|
"""Returns the unmatched offsets.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The Account.
|
||||||
|
:return: The unmatched offsets in the period.
|
||||||
|
"""
|
||||||
|
report: UnmatchedOffsets = UnmatchedOffsets(currency, account)
|
||||||
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
|
return report.csv()
|
||||||
|
return report.html()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("match-offsets/<currency:currency>/<needOffsetAccount:account>",
|
||||||
|
endpoint="match-offsets")
|
||||||
|
@has_permission(can_edit)
|
||||||
|
def match_offsets(currency: Currency, account: Account) -> redirect:
|
||||||
|
"""Matches the original line items with their offsets.
|
||||||
|
|
||||||
|
:return: Redirection to the view of the unmatched offsets.
|
||||||
|
"""
|
||||||
|
matcher: OffsetMatcher = OffsetMatcher(currency, account)
|
||||||
|
if len(matcher.matched_pairs) == 0:
|
||||||
|
flash(s(lazy_gettext("No more offset to match automatically.")),
|
||||||
|
"success")
|
||||||
|
return redirect(or_next(
|
||||||
|
unmatched_url(currency, account)))
|
||||||
|
matcher.match()
|
||||||
|
db.session.commit()
|
||||||
|
flash(s(lazy_gettext("Matched %(matches)s offsets.",
|
||||||
|
matches=len(matcher.matched_pairs))), "success")
|
||||||
|
return redirect(or_next(unmatched_url(currency, account)))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("search", endpoint="search")
|
@bp.get("search", endpoint="search")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def search() -> str | Response:
|
def search() -> str | Response:
|
||||||
|
@ -39,6 +39,10 @@
|
|||||||
.accounting-toolbar {
|
.accounting-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.accounting-toolbar-accounts {
|
||||||
|
max-height: 20rem;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
.accounting-toolbar .input-group > .input-group-text {
|
.accounting-toolbar .input-group > .input-group-text {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -209,11 +213,23 @@ a.accounting-report-table-row {
|
|||||||
.accounting-report-table-body .accounting-amount {
|
.accounting-report-table-body .accounting-amount {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
.accounting-report-table-body .accounting-report-table-row {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
.accounting-report-table-body .accounting-report-table-row:nth-child(2n+1) {
|
.accounting-report-table-body .accounting-report-table-row:nth-child(2n+1) {
|
||||||
background-color: #f2f2f2;
|
background-color: #ecedee;
|
||||||
}
|
}
|
||||||
.accounting-report-table-body .accounting-report-table-row:hover {
|
.accounting-report-table-body .accounting-report-table-row:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.075);
|
background-color: #e5e6e7;
|
||||||
|
}
|
||||||
|
.accounting-report-table-body .accounting-report-table-row-danger {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
}
|
||||||
|
.accounting-report-table-body .accounting-report-table-row-danger:nth-child(2n+1) {
|
||||||
|
background-color: #eccccf;
|
||||||
|
}
|
||||||
|
.accounting-report-table-body .accounting-report-table-row-danger:hover {
|
||||||
|
background-color: #e5c7ca;
|
||||||
}
|
}
|
||||||
.accounting-journal-table .accounting-report-table-row {
|
.accounting-journal-table .accounting-report-table-row {
|
||||||
grid-template-columns: 1fr 1fr 2fr 4fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 2fr 4fr 1fr 1fr;
|
||||||
@ -309,6 +325,26 @@ a.accounting-report-table-row {
|
|||||||
.accounting-balance-sheet-total .accounting-amount, .accounting-balance-sheet-subtotal, .accounting-amount {
|
.accounting-balance-sheet-total .accounting-amount, .accounting-balance-sheet-subtotal, .accounting-amount {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
.accounting-unapplied-table .accounting-report-table-row {
|
||||||
|
grid-template-columns: 1fr 5fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
.accounting-unapplied-account-table .accounting-report-table-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.accounting-unapplied-account-table .accounting-report-table-header .accounting-report-table-row {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.accounting-unmatched-table .accounting-report-table-row {
|
||||||
|
grid-template-columns: 1fr 5fr 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
.accounting-unmatched-account-table .accounting-report-table-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.accounting-unmatched-account-table .accounting-report-table-header .accounting-report-table-row {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* The accounting report */
|
/* The accounting report */
|
||||||
.accounting-mobile-journal-credit {
|
.accounting-mobile-journal-credit {
|
||||||
@ -343,6 +379,12 @@ a.accounting-report-table-row {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The unmatched offsets */
|
||||||
|
.accounting-unmatched-offset-pair-list {
|
||||||
|
height: 20rem;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
/* The Material Design text field (floating form control in Bootstrap) */
|
/* The Material Design text field (floating form control in Bootstrap) */
|
||||||
.accounting-material-text-field {
|
.accounting-material-text-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -364,12 +364,13 @@ class DescriptionEditorAccount extends JournalEntryAccount {
|
|||||||
*
|
*
|
||||||
* @param editor {DescriptionEditor} the description editor
|
* @param editor {DescriptionEditor} the description editor
|
||||||
* @param code {string} the account code
|
* @param code {string} the account code
|
||||||
|
* @param title {string} the account title
|
||||||
* @param text {string} the account text
|
* @param text {string} the account text
|
||||||
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
|
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
|
||||||
* @param button {HTMLButtonElement} the account button
|
* @param button {HTMLButtonElement} the account button
|
||||||
*/
|
*/
|
||||||
constructor(editor, code, text, isNeedOffset, button) {
|
constructor(editor, code, title, text, isNeedOffset, button) {
|
||||||
super(code, text, isNeedOffset);
|
super(code, title, text, isNeedOffset);
|
||||||
this.#element = button;
|
this.#element = button;
|
||||||
this.#element.onclick = () => editor.selectAccount(this);
|
this.#element.onclick = () => editor.selectAccount(this);
|
||||||
}
|
}
|
||||||
@ -424,7 +425,7 @@ class DescriptionEditorSuggestedAccount extends DescriptionEditorAccount {
|
|||||||
* @param button {HTMLButtonElement} the account button
|
* @param button {HTMLButtonElement} the account button
|
||||||
*/
|
*/
|
||||||
constructor(editor, button) {
|
constructor(editor, button) {
|
||||||
super(editor, button.dataset.code, button.dataset.text, button.classList.contains("accounting-account-is-need-offset"), button);
|
super(editor, button.dataset.code, button.dataset.title, button.dataset.text, button.classList.contains("accounting-account-is-need-offset"), button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,7 +442,7 @@ class DescriptionEditorConfirmedAccount extends DescriptionEditorAccount {
|
|||||||
* @param button {HTMLButtonElement} the account button
|
* @param button {HTMLButtonElement} the account button
|
||||||
*/
|
*/
|
||||||
constructor(editor, button) {
|
constructor(editor, button) {
|
||||||
super(editor, "", "", false, button);
|
super(editor, "", "", "", false, button);
|
||||||
this.isConfirmedAccount = true;
|
this.isConfirmedAccount = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ class JournalEntryAccountSelector {
|
|||||||
option.setShown(false);
|
option.setShown(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isAnyMatched) {
|
if (!isAnyMatched && this.#isShowMore) {
|
||||||
this.#optionList.classList.add("d-none");
|
this.#optionList.classList.add("d-none");
|
||||||
this.#queryNoResult.classList.remove("d-none");
|
this.#queryNoResult.classList.remove("d-none");
|
||||||
} else {
|
} else {
|
||||||
@ -202,6 +202,12 @@ class JournalEntryAccountOption {
|
|||||||
*/
|
*/
|
||||||
code;
|
code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The account title
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
title;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account text
|
* The account text
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -235,6 +241,7 @@ class JournalEntryAccountOption {
|
|||||||
constructor(selector, element) {
|
constructor(selector, element) {
|
||||||
this.#element = element;
|
this.#element = element;
|
||||||
this.code = element.dataset.code;
|
this.code = element.dataset.code;
|
||||||
|
this.title = element.dataset.title;
|
||||||
this.text = element.dataset.text;
|
this.text = element.dataset.text;
|
||||||
this.#isInUse = element.classList.contains("accounting-account-is-in-use");
|
this.#isInUse = element.classList.contains("accounting-account-is-in-use");
|
||||||
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
|
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
|
||||||
|
@ -791,6 +791,12 @@ class JournalEntryAccount {
|
|||||||
*/
|
*/
|
||||||
code;
|
code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The account title
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
title;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account text
|
* The account text
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -807,11 +813,13 @@ class JournalEntryAccount {
|
|||||||
* Constructs a journal entry account.
|
* Constructs a journal entry account.
|
||||||
*
|
*
|
||||||
* @param code {string} the account code
|
* @param code {string} the account code
|
||||||
|
* @param title {string} the account title
|
||||||
* @param text {string} the account text
|
* @param text {string} the account text
|
||||||
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
|
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
|
||||||
*/
|
*/
|
||||||
constructor(code, text, isNeedOffset) {
|
constructor(code, title, text, isNeedOffset) {
|
||||||
this.code = code;
|
this.code = code;
|
||||||
|
this.title = title;
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.isNeedOffset = isNeedOffset;
|
this.isNeedOffset = isNeedOffset;
|
||||||
}
|
}
|
||||||
@ -822,7 +830,7 @@ class JournalEntryAccount {
|
|||||||
* @return {JournalEntryAccount} the copy of the account
|
* @return {JournalEntryAccount} the copy of the account
|
||||||
*/
|
*/
|
||||||
copy() {
|
copy() {
|
||||||
return new JournalEntryAccount(this.code, this.text, this.isNeedOffset);
|
return new JournalEntryAccount(this.code, this.title, this.text, this.isNeedOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -887,10 +895,16 @@ class LineItemSubForm {
|
|||||||
#accountCode;
|
#accountCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The text display of the account
|
* The code part of the text display of the account
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
#accountText;
|
#accountTextCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title part of the text display of the account
|
||||||
|
* @type {HTMLSpanElement}
|
||||||
|
*/
|
||||||
|
#accountTextTitle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The description
|
* The description
|
||||||
@ -957,7 +971,8 @@ class LineItemSubForm {
|
|||||||
this.#error = document.getElementById(`${prefix}-error`);
|
this.#error = document.getElementById(`${prefix}-error`);
|
||||||
this.#no = document.getElementById(`${prefix}-no`);
|
this.#no = document.getElementById(`${prefix}-no`);
|
||||||
this.#accountCode = document.getElementById(`${prefix}-account-code`);
|
this.#accountCode = document.getElementById(`${prefix}-account-code`);
|
||||||
this.#accountText = document.getElementById(`${prefix}-account-text`);
|
this.#accountTextCode = document.getElementById(`${prefix}-account-text-code`);
|
||||||
|
this.#accountTextTitle = document.getElementById(`${prefix}-account-text-title`);
|
||||||
this.#description = document.getElementById(`${prefix}-description`);
|
this.#description = document.getElementById(`${prefix}-description`);
|
||||||
this.#descriptionText = document.getElementById(`${prefix}-description-text`);
|
this.#descriptionText = document.getElementById(`${prefix}-description-text`);
|
||||||
this.#originalLineItemId = document.getElementById(`${prefix}-original-line-item-id`);
|
this.#originalLineItemId = document.getElementById(`${prefix}-original-line-item-id`);
|
||||||
@ -1024,7 +1039,7 @@ class LineItemSubForm {
|
|||||||
* @return {JournalEntryAccount|null} the account
|
* @return {JournalEntryAccount|null} the account
|
||||||
*/
|
*/
|
||||||
get account() {
|
get account() {
|
||||||
return this.#accountCode.value === null? null: new JournalEntryAccount(this.#accountCode.value, this.#accountCode.dataset.text, this.#accountCode.classList.contains("accounting-account-is-need-offset"));
|
return this.#accountCode.value === null? null: new JournalEntryAccount(this.#accountCode.value, this.#accountCode.dataset.title, this.#accountCode.dataset.text, this.#accountCode.classList.contains("accounting-account-is-need-offset"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1092,13 +1107,15 @@ class LineItemSubForm {
|
|||||||
this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText});
|
this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText});
|
||||||
}
|
}
|
||||||
this.#accountCode.value = editor.account.code;
|
this.#accountCode.value = editor.account.code;
|
||||||
|
this.#accountCode.dataset.title = editor.account.title;
|
||||||
this.#accountCode.dataset.text = editor.account.text;
|
this.#accountCode.dataset.text = editor.account.text;
|
||||||
if (editor.account.isNeedOffset) {
|
if (editor.account.isNeedOffset) {
|
||||||
this.#accountCode.classList.add("accounting-account-is-need-offset");
|
this.#accountCode.classList.add("accounting-account-is-need-offset");
|
||||||
} else {
|
} else {
|
||||||
this.#accountCode.classList.remove("accounting-account-is-need-offset");
|
this.#accountCode.classList.remove("accounting-account-is-need-offset");
|
||||||
}
|
}
|
||||||
this.#accountText.innerText = editor.account.text;
|
this.#accountTextCode.innerText = editor.account.code
|
||||||
|
this.#accountTextTitle.innerText = editor.account.title
|
||||||
this.#description.value = editor.description === null? "": editor.description;
|
this.#description.value = editor.description === null? "": editor.description;
|
||||||
this.#descriptionText.innerText = editor.description === null? "": editor.description;
|
this.#descriptionText.innerText = editor.description === null? "": editor.description;
|
||||||
this.#amount.value = editor.amount;
|
this.#amount.value = editor.amount;
|
||||||
|
@ -276,19 +276,23 @@ class JournalEntryLineItemEditor {
|
|||||||
this.originalLineItemDate = originalLineItem.date;
|
this.originalLineItemDate = originalLineItem.date;
|
||||||
this.originalLineItemText = originalLineItem.text;
|
this.originalLineItemText = originalLineItem.text;
|
||||||
this.#originalLineItemText.innerText = originalLineItem.text;
|
this.#originalLineItemText.innerText = originalLineItem.text;
|
||||||
this.#setEnableDescriptionAccount(false);
|
if (this.description === null) {
|
||||||
if (originalLineItem.description === "") {
|
if (originalLineItem.description === "") {
|
||||||
this.#descriptionControl.classList.remove("accounting-not-empty");
|
this.#descriptionControl.classList.remove("accounting-not-empty");
|
||||||
} else {
|
} else {
|
||||||
this.#descriptionControl.classList.add("accounting-not-empty");
|
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.#setEnableAccount(false);
|
||||||
this.#descriptionText.innerText = originalLineItem.description;
|
|
||||||
this.#accountControl.classList.add("accounting-not-empty");
|
this.#accountControl.classList.add("accounting-not-empty");
|
||||||
this.account = originalLineItem.account.copy();
|
this.account = originalLineItem.account.copy();
|
||||||
this.isAccountConfirmed = false;
|
this.isAccountConfirmed = false;
|
||||||
this.#accountText.innerText = this.account.text;
|
this.#accountText.innerText = this.account.text;
|
||||||
this.#amountInput.value = String(originalLineItem.netBalance);
|
if (this.#amountInput.value === "" || new Decimal(this.#amountInput.value).greaterThan(originalLineItem.netBalance)) {
|
||||||
|
this.#amountInput.value = String(originalLineItem.netBalance);
|
||||||
|
}
|
||||||
this.#amountInput.max = String(originalLineItem.netBalance);
|
this.#amountInput.max = String(originalLineItem.netBalance);
|
||||||
this.#amountInput.min = "0";
|
this.#amountInput.min = "0";
|
||||||
this.#validate();
|
this.#validate();
|
||||||
@ -305,7 +309,7 @@ class JournalEntryLineItemEditor {
|
|||||||
this.originalLineItemDate = null;
|
this.originalLineItemDate = null;
|
||||||
this.originalLineItemText = null;
|
this.originalLineItemText = null;
|
||||||
this.#originalLineItemText.innerText = "";
|
this.#originalLineItemText.innerText = "";
|
||||||
this.#setEnableDescriptionAccount(true);
|
this.#setEnableAccount(true);
|
||||||
this.#accountControl.classList.remove("accounting-not-empty");
|
this.#accountControl.classList.remove("accounting-not-empty");
|
||||||
this.account = null;
|
this.account = null;
|
||||||
this.isAccountConfirmed = false;
|
this.isAccountConfirmed = false;
|
||||||
@ -356,7 +360,7 @@ class JournalEntryLineItemEditor {
|
|||||||
*/
|
*/
|
||||||
saveAccount(account) {
|
saveAccount(account) {
|
||||||
this.#accountControl.classList.add("accounting-not-empty");
|
this.#accountControl.classList.add("accounting-not-empty");
|
||||||
this.account = new JournalEntryAccount(account.code, account.text, account.isNeedOffset);
|
this.account = new JournalEntryAccount(account.code, account.title, account.text, account.isNeedOffset);
|
||||||
this.isAccountConfirmed = true;
|
this.isAccountConfirmed = true;
|
||||||
this.#accountText.innerText = account.text;
|
this.#accountText.innerText = account.text;
|
||||||
this.#validateAccount();
|
this.#validateAccount();
|
||||||
@ -472,12 +476,13 @@ class JournalEntryLineItemEditor {
|
|||||||
this.originalLineItemDate = null;
|
this.originalLineItemDate = null;
|
||||||
this.originalLineItemText = null;
|
this.originalLineItemText = null;
|
||||||
this.#originalLineItemText.innerText = "";
|
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("accounting-not-empty");
|
||||||
this.#descriptionControl.classList.remove("is-invalid");
|
this.#descriptionControl.classList.remove("is-invalid");
|
||||||
this.description = null;
|
this.description = null;
|
||||||
this.#descriptionText.innerText = ""
|
this.#descriptionText.innerText = ""
|
||||||
this.#descriptionError.innerText = ""
|
this.#descriptionError.innerText = ""
|
||||||
|
this.#setEnableAccount(true);
|
||||||
this.#accountControl.classList.remove("accounting-not-empty");
|
this.#accountControl.classList.remove("accounting-not-empty");
|
||||||
this.#accountControl.classList.remove("is-invalid");
|
this.#accountControl.classList.remove("is-invalid");
|
||||||
this.account = null;
|
this.account = null;
|
||||||
@ -511,7 +516,7 @@ class JournalEntryLineItemEditor {
|
|||||||
this.#originalLineItemContainer.classList.remove("d-none");
|
this.#originalLineItemContainer.classList.remove("d-none");
|
||||||
this.#originalLineItemControl.classList.add("accounting-not-empty");
|
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;
|
this.description = lineItem.description;
|
||||||
if (this.description === null) {
|
if (this.description === null) {
|
||||||
this.#descriptionControl.classList.remove("accounting-not-empty");
|
this.#descriptionControl.classList.remove("accounting-not-empty");
|
||||||
@ -519,6 +524,7 @@ class JournalEntryLineItemEditor {
|
|||||||
this.#descriptionControl.classList.add("accounting-not-empty");
|
this.#descriptionControl.classList.add("accounting-not-empty");
|
||||||
}
|
}
|
||||||
this.#descriptionText.innerText = this.description === null? "": this.description;
|
this.#descriptionText.innerText = this.description === null? "": this.description;
|
||||||
|
this.#setEnableAccount(!lineItem.isMatched && this.originalLineItemId === null);
|
||||||
this.account = lineItem.account;
|
this.account = lineItem.account;
|
||||||
this.isAccountConfirmed = true;
|
this.isAccountConfirmed = true;
|
||||||
if (this.account === null) {
|
if (this.account === null) {
|
||||||
@ -547,25 +553,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
|
* @param isEnabled {boolean} true to enable, or false otherwise
|
||||||
*/
|
*/
|
||||||
#setEnableDescriptionAccount(isEnabled) {
|
#setEnableAccount(isEnabled) {
|
||||||
if (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.bsToggle = "modal";
|
||||||
this.#accountControl.dataset.bsTarget = `#accounting-account-selector-${this.#debitCreditSubForm.debitCredit}-modal`;
|
this.#accountControl.dataset.bsTarget = `#accounting-account-selector-${this.#debitCreditSubForm.debitCredit}-modal`;
|
||||||
this.#accountControl.classList.remove("accounting-disabled");
|
this.#accountControl.classList.remove("accounting-disabled");
|
||||||
this.#accountControl.classList.add("accounting-clickable");
|
this.#accountControl.classList.add("accounting-clickable");
|
||||||
} else {
|
} 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.bsToggle = "";
|
||||||
this.#accountControl.dataset.bsTarget = "";
|
this.#accountControl.dataset.bsTarget = "";
|
||||||
this.#accountControl.classList.add("accounting-disabled");
|
this.#accountControl.classList.add("accounting-disabled");
|
||||||
|
@ -284,7 +284,7 @@ class OriginalLineItem {
|
|||||||
this.date = element.dataset.date;
|
this.date = element.dataset.date;
|
||||||
this.#debitCredit = element.dataset.debitCredit;
|
this.#debitCredit = element.dataset.debitCredit;
|
||||||
this.#currencyCode = element.dataset.currencyCode;
|
this.#currencyCode = element.dataset.currencyCode;
|
||||||
this.account = new JournalEntryAccount(element.dataset.accountCode, element.dataset.accountText, false);
|
this.account = new JournalEntryAccount(element.dataset.accountCode, element.dataset.accountTitle, element.dataset.accountText, false);
|
||||||
this.description = element.dataset.description;
|
this.description = element.dataset.description;
|
||||||
this.bareNetBalance = new Decimal(element.dataset.netBalance);
|
this.bareNetBalance = new Decimal(element.dataset.netBalance);
|
||||||
this.netBalance = this.bareNetBalance;
|
this.netBalance = this.bareNetBalance;
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
"""The template filters.
|
"""The template filters.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import datetime as dt
|
||||||
from datetime import date, timedelta
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from flask_babel import get_locale
|
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:]
|
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.
|
"""Formats a date to be human-friendly.
|
||||||
|
|
||||||
:param value: The date.
|
:param value: The date.
|
||||||
:return: The human-friendly date text.
|
:return: The human-friendly date text.
|
||||||
"""
|
"""
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
if value == today:
|
if value == today:
|
||||||
return gettext("Today")
|
return gettext("Today")
|
||||||
if value == today - timedelta(days=1):
|
if value == today - dt.timedelta(days=1):
|
||||||
return gettext("Yesterday")
|
return gettext("Yesterday")
|
||||||
if value == today + timedelta(days=1):
|
if value == today + dt.timedelta(days=1):
|
||||||
return gettext("Tomorrow")
|
return gettext("Tomorrow")
|
||||||
locale = str(get_locale())
|
locale = str(get_locale())
|
||||||
if locale == "zh" or locale.startswith("zh_"):
|
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")
|
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")
|
return gettext("The day after tomorrow")
|
||||||
if locale == "zh" or locale.startswith("zh_"):
|
if locale == "zh" or locale.startswith("zh_"):
|
||||||
weekdays = ["一", "二", "三", "四", "五", "六", "日"]
|
weekdays = ["一", "二", "三", "四", "五", "六", "日"]
|
||||||
@ -71,7 +71,7 @@ def format_date(value: date) -> str:
|
|||||||
return "{}/{}({})".format(value.month, value.day, weekday)
|
return "{}/{}({})".format(value.month, value.day, weekday)
|
||||||
|
|
||||||
|
|
||||||
def default(value: t.Any, default_value: t.Any = "") -> t.Any:
|
def default(value: Any, default_value: Any = "") -> Any:
|
||||||
"""Returns the default value if the given value is None.
|
"""Returns the default value if the given value is None.
|
||||||
|
|
||||||
:param value: The value.
|
:param value: The value.
|
||||||
|
@ -21,7 +21,7 @@ from accounting.models import Currency
|
|||||||
from accounting.utils.options import options
|
from accounting.utils.options import options
|
||||||
|
|
||||||
|
|
||||||
def currency_options() -> str:
|
def currency_options() -> list[Currency]:
|
||||||
"""Returns the currency options.
|
"""Returns the currency options.
|
||||||
|
|
||||||
:return: The currency options.
|
:return: The currency options.
|
||||||
|
@ -37,7 +37,7 @@ First written: 2023/2/25
|
|||||||
|
|
||||||
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list">
|
||||||
{% for account in account_options %}
|
{% for account in account_options %}
|
||||||
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-title="{{ account.title }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
{{ account }}
|
{{ account }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -184,7 +184,7 @@ First written: 2023/2/28
|
|||||||
<div class="mt-3 accounting-description-editor-buttons">
|
<div class="mt-3 accounting-description-editor-buttons">
|
||||||
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-account-confirmed" class="btn btn-primary mb-1 d-none" type="button"></button>
|
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-account-confirmed" class="btn btn-primary mb-1 d-none" type="button"></button>
|
||||||
{% for account in description_editor.accounts %}
|
{% for account in description_editor.accounts %}
|
||||||
<button class="btn btn-outline-primary d-none accounting-description-editor-{{ description_editor.debit_credit }}-account {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="button" data-code="{{ account.code }}" data-text="{{ account }}">
|
<button class="btn btn-outline-primary d-none accounting-description-editor-{{ description_editor.debit_credit }}-account {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="button" data-code="{{ account.code }}" data-title="{{ account.title }}" data-text="{{ account }}">
|
||||||
{{ account }}
|
{{ account }}
|
||||||
</button>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -24,7 +24,10 @@ First written: 2023/3/14
|
|||||||
<li class="list-group-item accounting-journal-entry-line-item">
|
<li class="list-group-item accounting-journal-entry-line-item">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="small">{{ line_item.account }}</div>
|
<div class="small">
|
||||||
|
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
||||||
|
{{ line_item.account.title|title }}
|
||||||
|
</div>
|
||||||
{% if line_item.description is not none %}
|
{% if line_item.description is not none %}
|
||||||
<div>{{ line_item.description }}</div>
|
<div>{{ line_item.description }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -50,10 +53,10 @@ First written: 2023/3/14
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% if line_item.balance %}
|
{% if line_item.net_balance %}
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>{{ A_("Net balance") }}</div>
|
<div>{{ A_("Net balance") }}</div>
|
||||||
<div>{{ line_item.balance|accounting_format_amount }}</div>
|
<div>{{ line_item.net_balance|accounting_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
|
@ -36,7 +36,7 @@ First written: 2023/2/26
|
|||||||
{{ A_("Edit") }}
|
{{ A_("Edit") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% 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>
|
<i class="fa-solid fa-bars-staggered"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Order") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Order") }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -26,13 +26,16 @@ First written: 2023/2/25
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" value="{{ line_item_index }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" value="{{ line_item_index }}">
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-id" class="accounting-original-line-item-id" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original_line_item_id" value="{{ form.original_line_item_id.data|accounting_default }}" data-date="{{ form.original_line_item_date|accounting_default }}" data-text="{{ form.original_line_item_text|accounting_default }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-id" class="accounting-original-line-item-id" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original_line_item_id" value="{{ form.original_line_item_id.data|accounting_default }}" data-date="{{ form.original_line_item_date|accounting_default }}" data-text="{{ form.original_line_item_text|accounting_default }}">
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-code" class="{% if form.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account_code" value="{{ form.account_code.data|accounting_default }}" data-text="{{ form.account_text }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-code" class="{% if form.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account_code" value="{{ form.account_code.data|accounting_default }}" data-title="{{ form.account_title }}" data-text="{{ form.account_text }}">
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" value="{{ form.description.data|accounting_default }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" value="{{ form.description.data|accounting_default }}">
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" value="{{ form.amount.data|accounting_journal_entry_format_amount_input }}" data-min="{{ form.offset_total|accounting_default("0") }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" value="{{ form.amount.data|accounting_journal_entry_format_amount_input }}" data-min="{{ form.offset_total|accounting_default("0") }}">
|
||||||
<div class="accounting-line-item-content">
|
<div class="accounting-line-item-content">
|
||||||
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-control" class="form-control clickable d-flex justify-content-between {% if form.all_errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-control" class="form-control clickable d-flex justify-content-between {% if form.all_errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
<div>
|
<div>
|
||||||
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-text" class="small">{{ form.account_text }}</div>
|
<div class="small">
|
||||||
|
<span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-text-code" class="d-none d-md-inline">{{ form.account_code.data|accounting_default }}</span>
|
||||||
|
<span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-text-title">{{ form.account_title }}</span>
|
||||||
|
</div>
|
||||||
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description-text">{{ form.description.data|accounting_default }}</div>
|
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description-text">{{ form.description.data|accounting_default }}</div>
|
||||||
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-text" class="fst-italic small accounting-original-line-item {% if not form.original_line_item_id.data %} d-none {% endif %}">
|
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-text" class="fst-italic small accounting-original-line-item {% if not form.original_line_item_id.data %} d-none {% endif %}">
|
||||||
{% if form.original_line_item_id.data %}{{ A_("Offset %(item)s", item=form.original_line_item_text|accounting_default) }}{% endif %}
|
{% if form.original_line_item_id.data %}{{ A_("Offset %(item)s", item=form.original_line_item_text|accounting_default) }}{% endif %}
|
||||||
|
@ -37,8 +37,15 @@ First written: 2023/2/25
|
|||||||
|
|
||||||
<ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list">
|
||||||
{% for line_item in form.original_line_item_options %}
|
{% for line_item in form.original_line_item_options %}
|
||||||
<li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.journal_entry.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_journal_entry_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.journal_entry.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-title="{{ line_item.account.title }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_journal_entry_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
<div>{{ line_item.journal_entry.date|accounting_format_date }} {{ line_item.description|accounting_default }}</div>
|
<div>
|
||||||
|
<div class="small">
|
||||||
|
{{ line_item.journal_entry.date|accounting_format_date }}
|
||||||
|
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
||||||
|
{{ line_item.account.title|title }}
|
||||||
|
</div>
|
||||||
|
{{ line_item.description|accounting_default }}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="badge bg-primary rounded-pill">
|
<span class="badge bg-primary rounded-pill">
|
||||||
<span id="accounting-original-line-item-selector-option-{{ line_item.id }}-net-balance">{{ line_item.net_balance|accounting_format_amount }}</span>
|
<span id="accounting-original-line-item-selector-option-{{ line_item.id }}-net-balance">{{ line_item.net_balance|accounting_format_amount }}</span>
|
||||||
|
@ -38,7 +38,7 @@ First written: 2023/2/26
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if list|length > 1 and accounting_can_edit() %}
|
{% 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() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
{% if request.args.next %}
|
{% if request.args.next %}
|
||||||
<input type="hidden" name="next" value="{{ request.args.next }}">
|
<input type="hidden" name="next" value="{{ request.args.next }}">
|
||||||
|
@ -42,11 +42,11 @@ First written: 2023/3/22
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ A_("Default Currency") }}</th>
|
<th scope="row">{{ A_("Default Currency") }}</th>
|
||||||
<td>{{ obj.default_currency_text }}</td>
|
<td>{{ obj.default_currency }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ A_("Default Account for the Income and Expenses Log") }}</th>
|
<th scope="row">{{ A_("Default Account for the Income and Expenses Log") }}</th>
|
||||||
<td>{{ obj.default_ie_account_code_text }}</td>
|
<td>{{ obj.default_ie_account }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/3/7
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Balance Sheet %(period)s", period=report.period.desc|title) }}{% else %}{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -46,7 +46,13 @@ First written: 2023/3/7
|
|||||||
{% if report.has_data %}
|
{% if report.has_data %}
|
||||||
<div class="accounting-sheet">
|
<div class="accounting-sheet">
|
||||||
<div class="d-none d-sm-flex justify-content-center mb-3">
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
<h2 class="text-center">{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
|
<h2 class="text-center">
|
||||||
|
{% if report.currency.code == accounting_default_currency_code() %}
|
||||||
|
{{ A_("Balance Sheet %(period)s", period=report.period.desc|title) }}
|
||||||
|
{% else %}
|
||||||
|
{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row accounting-report-table accounting-balance-sheet-table">
|
<div class="row accounting-report-table accounting-balance-sheet-table">
|
||||||
|
@ -89,7 +89,7 @@ First written: 2023/3/8
|
|||||||
<i class="fa-solid fa-clipboard"></i>
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Account") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Account") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="accounting-choose-account">
|
<ul class="dropdown-menu accounting-toolbar-accounts" aria-labelledby="accounting-choose-account">
|
||||||
{% for account in report.account_options %}
|
{% for account in report.account_options %}
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/3/5
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Income and Expenses Log of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Income and Expenses Log of %(account)s %(period)s", account=report.account.title|title, period=report.period.desc|title) }}{% else %}{{ A_("Income and Expenses Log of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/3/7
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Income Statement %(period)s", period=report.period.desc|title) }}{% else %}{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -46,7 +46,13 @@ First written: 2023/3/7
|
|||||||
{% if report.has_data %}
|
{% if report.has_data %}
|
||||||
<div class="accounting-sheet">
|
<div class="accounting-sheet">
|
||||||
<div class="d-none d-sm-flex justify-content-center mb-3">
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
<h2 class="text-center">{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
|
<h2 class="text-center">
|
||||||
|
{% if report.currency.code == accounting_default_currency_code() %}
|
||||||
|
{{ A_("Income Statement %(period)s", period=report.period.desc|title) }}
|
||||||
|
{% else %}
|
||||||
|
{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="accounting-report-table accounting-income-statement-table">
|
<div class="accounting-report-table accounting-income-statement-table">
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/3/5
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Ledger of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Ledger of %(account)s %(period)s", account=report.account.title|title, period=report.period.desc|title) }}{% else %}{{ A_("Ledger of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/3/5
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Trial Balance %(period)s", period=report.period.desc|title) }}{% else %}{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -46,7 +46,13 @@ First written: 2023/3/5
|
|||||||
{% if report.has_data %}
|
{% if report.has_data %}
|
||||||
<div class="accounting-sheet">
|
<div class="accounting-sheet">
|
||||||
<div class="d-none d-sm-flex justify-content-center mb-3">
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
<h2 class="text-center">{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
|
<h2 class="text-center">
|
||||||
|
{% if report.currency.code == accounting_default_currency_code() %}
|
||||||
|
{{ A_("Trial Balance %(period)s", period=report.period.desc|title) }}
|
||||||
|
{% else %}
|
||||||
|
{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="accounting-report-table accounting-trial-balance-table">
|
<div class="accounting-report-table accounting-trial-balance-table">
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Project
|
||||||
|
unapplied-accounts.html: The account list with unapplied original line items
|
||||||
|
|
||||||
|
Copyright (c) 2023 imacat.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
First written: 2023/4/8
|
||||||
|
#}
|
||||||
|
{% extends "accounting/base.html" %}
|
||||||
|
|
||||||
|
{% block accounting_scripts %}
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Accounts with Unapplied Items") }}{% else %}{{ A_("Accounts with Unapplied Items in %(currency)s", currency=report.currency.name|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="mb-3 accounting-toolbar">
|
||||||
|
{% with use_currency_chooser = true,
|
||||||
|
use_account_chooser = true %}
|
||||||
|
{% include "accounting/report/include/toolbar-buttons.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
|
||||||
|
|
||||||
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
|
{% if report.has_data %}
|
||||||
|
<div class="accounting-sheet">
|
||||||
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
|
<h2 class="text-center">
|
||||||
|
{% if report.currency.code == accounting_default_currency_code() %}
|
||||||
|
{{ A_("Accounts with Unapplied Items") }}
|
||||||
|
{% else %}
|
||||||
|
{{ A_("Accounts with Unapplied Items in %(currency)s", currency=report.currency.name|title) }}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accounting-report-table accounting-unapplied-account-table">
|
||||||
|
<div class="accounting-report-table-header">
|
||||||
|
<div class="accounting-report-table-row">
|
||||||
|
<div class="accounting-amount">{{ A_("Count") }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="accounting-report-table-body">
|
||||||
|
{% for account in report.accounts %}
|
||||||
|
<a class="accounting-report-table-row" href="{{ url_for("accounting-report.unapplied", currency=report.currency, account=account, period=report.period) }}">
|
||||||
|
<div>
|
||||||
|
<span class="d-none d-md-inline">{{ account.code }}</span>
|
||||||
|
{{ account.title|title }}
|
||||||
|
</div>
|
||||||
|
<div class="accounting-amount">{{ account.count }}</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ A_("There is no data.") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
93
src/accounting/templates/accounting/report/unapplied.html
Normal file
93
src/accounting/templates/accounting/report/unapplied.html
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Project
|
||||||
|
unapplied.html: The unapplied original line items
|
||||||
|
|
||||||
|
Copyright (c) 2023 imacat.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
First written: 2023/4/7
|
||||||
|
#}
|
||||||
|
{% extends "accounting/base.html" %}
|
||||||
|
|
||||||
|
{% block accounting_scripts %}
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Unapplied Items of %(account)s", account=report.account.title|title) }}{% else %}{{ A_("Unapplied Items of %(account)s in %(currency)s", currency=report.currency.name|title, account=report.account.title|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="mb-3 accounting-toolbar">
|
||||||
|
{% with use_currency_chooser = true,
|
||||||
|
use_account_chooser = true %}
|
||||||
|
{% include "accounting/report/include/toolbar-buttons.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
|
||||||
|
|
||||||
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
|
{% if report.has_data %}
|
||||||
|
{% with pagination = report.pagination %}
|
||||||
|
{% include "accounting/include/pagination.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="d-none d-md-block accounting-report-table accounting-unapplied-table">
|
||||||
|
<div class="accounting-report-table-header">
|
||||||
|
<div class="accounting-report-table-row">
|
||||||
|
<div>{{ A_("Date") }}</div>
|
||||||
|
<div>{{ A_("Description") }}</div>
|
||||||
|
<div class="accounting-amount">{{ A_("Amount") }}</div>
|
||||||
|
<div class="accounting-amount">{{ A_("Net Balance") }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="accounting-report-table-body">
|
||||||
|
{% for line_item in report.line_items %}
|
||||||
|
<a class="accounting-report-table-row" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
|
||||||
|
<div>{{ line_item.journal_entry.date|accounting_format_date }}</div>
|
||||||
|
<div>{{ line_item.description|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ line_item.amount|accounting_format_amount }}</div>
|
||||||
|
<div class="accounting-amount">{{ line_item.net_balance|accounting_format_amount }}</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group d-md-none">
|
||||||
|
{% for line_item in report.line_items %}
|
||||||
|
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
{{ line_item.journal_entry.date|accounting_format_date }}
|
||||||
|
</div>
|
||||||
|
{% if line_item.description is not none %}
|
||||||
|
<div>{{ line_item.description }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="badge rounded-pill bg-info">{{ line_item.amount|accounting_format_amount }}</span>
|
||||||
|
<span class="badge rounded-pill bg-info">{{ line_item.net_balance|accounting_format_amount }}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ A_("There is no data.") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,79 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Project
|
||||||
|
unmatched-accounts.html: The account list with unmatched offsets
|
||||||
|
|
||||||
|
Copyright (c) 2023 imacat.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
First written: 2023/4/17
|
||||||
|
#}
|
||||||
|
{% extends "accounting/base.html" %}
|
||||||
|
|
||||||
|
{% block accounting_scripts %}
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Accounts with Unmatched Offsets") }}{% else %}{{ A_("Accounts with Unmatched Offsets in %(currency)s", currency=report.currency.name|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="mb-3 accounting-toolbar">
|
||||||
|
{% with use_currency_chooser = true,
|
||||||
|
use_account_chooser = true %}
|
||||||
|
{% include "accounting/report/include/toolbar-buttons.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
|
||||||
|
|
||||||
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
|
{% if report.has_data %}
|
||||||
|
<div class="accounting-sheet">
|
||||||
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
|
<h2 class="text-center">
|
||||||
|
{% if report.currency.code == accounting_default_currency_code() %}
|
||||||
|
{{ A_("Accounts with Unmatched Offsets") }}
|
||||||
|
{% else %}
|
||||||
|
{{ A_("Accounts with Unmatched Offsets in %(currency)s", currency=report.currency.name|title) }}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accounting-report-table accounting-unapplied-account-table">
|
||||||
|
<div class="accounting-report-table-header">
|
||||||
|
<div class="accounting-report-table-row">
|
||||||
|
<div class="accounting-amount">{{ A_("Count") }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="accounting-report-table-body">
|
||||||
|
{% for account in report.accounts %}
|
||||||
|
<a class="accounting-report-table-row" href="{{ url_for("accounting-report.unmatched", currency=report.currency, account=account, period=report.period) }}">
|
||||||
|
<div>
|
||||||
|
<span class="d-none d-md-inline">{{ account.code }}</span>
|
||||||
|
{{ account.title|title }}
|
||||||
|
</div>
|
||||||
|
<div class="accounting-amount">{{ account.count }}</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ A_("There is no data.") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
150
src/accounting/templates/accounting/report/unmatched.html
Normal file
150
src/accounting/templates/accounting/report/unmatched.html
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Project
|
||||||
|
unmatched.html: The unmatched offsets
|
||||||
|
|
||||||
|
Copyright (c) 2023 imacat.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
First written: 2023/4/17
|
||||||
|
#}
|
||||||
|
{% extends "accounting/base.html" %}
|
||||||
|
|
||||||
|
{% block accounting_scripts %}
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Unmatched Offsets of %(account)s", account=report.account.title|title) }}{% else %}{{ A_("Unmatched Offsets of %(account)s in %(currency)s", currency=report.currency.name|title, account=report.account.title|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="mb-3 accounting-toolbar">
|
||||||
|
{% with use_currency_chooser = true,
|
||||||
|
use_account_chooser = true %}
|
||||||
|
{% include "accounting/report/include/toolbar-buttons.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
|
||||||
|
|
||||||
|
{% include "accounting/report/include/search-modal.html" %}
|
||||||
|
|
||||||
|
{% if report.matched_pairs %}
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-match-modal">
|
||||||
|
<i class="fa-solid fa-link"></i>
|
||||||
|
{{ A_("Match") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<form action="{{ url_for("accounting-report.match-offsets", currency=report.currency, account=report.account) }}" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="next" value="{{ request.full_path if request.query_string else request.path }}">
|
||||||
|
<div class="modal fade" id="accounting-match-modal" tabindex="-1" aria-labelledby="accounting-match-modal-label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="accounting-match-modal-label">{{ A_("Confirm Match Offsets") }}</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{{ A_("Do you really want to match the following original line items with their offsets? This cannot be undone. Please backup your database first, and review before you confirm.") }}</p>
|
||||||
|
|
||||||
|
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover accounting-unmatched-offset-pair-list">
|
||||||
|
{% for pair in report.matched_pairs %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
{{ pair.offset.description|accounting_default }}
|
||||||
|
<span class="badge bg-info">{{ pair.offset.amount|accounting_format_amount }}</span>
|
||||||
|
{{ pair.original_line_item.journal_entry.date|accounting_format_date }} → {{ pair.offset.journal_entry.date|accounting_format_date }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
|
||||||
|
<button type="submit" class="btn btn-danger">{{ A_("Confirm") }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>{{ report.match_status }}</p>
|
||||||
|
|
||||||
|
{% if report.has_data %}
|
||||||
|
{% with pagination = report.pagination %}
|
||||||
|
{% include "accounting/include/pagination.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="d-none d-md-block accounting-report-table accounting-unmatched-table">
|
||||||
|
<div class="accounting-report-table-header">
|
||||||
|
<div class="accounting-report-table-row">
|
||||||
|
<div>{{ A_("Date") }}</div>
|
||||||
|
<div>{{ A_("Description") }}</div>
|
||||||
|
<div class="accounting-amount">{{ A_("Debit") }}</div>
|
||||||
|
<div class="accounting-amount">{{ A_("Credit") }}</div>
|
||||||
|
<div class="accounting-amount">{{ A_("Balance") }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="accounting-report-table-body">
|
||||||
|
{% for line_item in report.line_items %}
|
||||||
|
<a class="accounting-report-table-row {% if not line_item.match %} accounting-report-table-row-danger {% endif %}" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
|
||||||
|
<div>{{ line_item.journal_entry.date|accounting_format_date }}</div>
|
||||||
|
<div>
|
||||||
|
{{ line_item.description|accounting_default }}
|
||||||
|
{% if line_item.match %}
|
||||||
|
<div class="small">{{ A_("Can match %(item)s", item=line_item.match) }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount">{{ line_item.credit|accounting_format_amount|accounting_default }}</div>
|
||||||
|
<div class="accounting-amount {% if line_item.balance < 0 %} text-danger {% endif %}">{{ line_item.balance|accounting_report_format_amount }}</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group d-md-none">
|
||||||
|
{% for line_item in report.line_items %}
|
||||||
|
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
{{ line_item.journal_entry.date|accounting_format_date }}
|
||||||
|
</div>
|
||||||
|
{% if line_item.description is not none %}
|
||||||
|
<div>{{ line_item.description }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if line_item.debit %}
|
||||||
|
<span class="badge rounded-pill bg-success">+{{ line_item.debit|accounting_format_amount }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if line_item.credit %}
|
||||||
|
<span class="badge rounded-pill bg-warning">-{{ line_item.credit|accounting_format_amount }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if line_item.balance < 0 %}
|
||||||
|
<span class="badge rounded-pill bg-danger">{{ line_item.balance|accounting_format_amount }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge rounded-pill bg-primary">{{ line_item.balance|accounting_format_amount }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ A_("There is no data.") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -1,15 +1,15 @@
|
|||||||
# Chinese (Traditional) translations for the Mia! Accounting project.
|
# Chinese (Traditional) translations for the Mia! Accounting project.
|
||||||
# Copyright (C) 2023 imacat
|
# Copyright (C) 2023 imacat
|
||||||
# This file is distributed under the same license as the Mia! Accounting
|
# This file is distributed under the same license as the Mia! Accounting
|
||||||
# Flask project.
|
# project.
|
||||||
# imacat <imacat@mail.imacat.idv.tw>, 2023.
|
# imacat <imacat@mail.imacat.idv.tw>, 2023.
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Mia! Accounting 0.0.0\n"
|
"Project-Id-Version: mia-accounting 1.4.0\n"
|
||||||
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
|
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
|
||||||
"POT-Creation-Date: 2023-03-23 00:45+0800\n"
|
"POT-Creation-Date: 2023-04-18 09:32+0800\n"
|
||||||
"PO-Revision-Date: 2023-03-23 00:46+0800\n"
|
"PO-Revision-Date: 2023-04-18 09:32+0800\n"
|
||||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
||||||
"Language: zh_Hant\n"
|
"Language: zh_Hant\n"
|
||||||
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
|
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
|
||||||
@ -20,10 +20,10 @@ msgstr ""
|
|||||||
"Generated-By: Babel 2.12.1\n"
|
"Generated-By: Babel 2.12.1\n"
|
||||||
|
|
||||||
#: src/accounting/forms.py:33
|
#: src/accounting/forms.py:33
|
||||||
#: src/accounting/static/js/journal-entry-form.js:980
|
#: src/accounting/static/js/journal-entry-form.js:1080
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:430
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:411
|
||||||
#: src/accounting/static/js/option-form.js:530
|
#: src/accounting/static/js/option-form.js:537
|
||||||
#: src/accounting/static/js/option-form.js:796
|
#: src/accounting/static/js/option-form.js:803
|
||||||
msgid "Please select the account."
|
msgid "Please select the account."
|
||||||
msgstr "請選擇科目。"
|
msgstr "請選擇科目。"
|
||||||
|
|
||||||
@ -35,22 +35,22 @@ msgstr "沒有這個貨幣。"
|
|||||||
msgid "The account does not exist."
|
msgid "The account does not exist."
|
||||||
msgstr "沒有這個科目。"
|
msgstr "沒有這個科目。"
|
||||||
|
|
||||||
#: src/accounting/models.py:563
|
#: src/accounting/models.py:581
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Cash Disbursement Journal Entry#%(id)s"
|
msgid "Cash Disbursement Journal Entry#%(id)s"
|
||||||
msgstr "現金支出傳票#%(id)s"
|
msgstr "現金支出傳票#%(id)s"
|
||||||
|
|
||||||
#: src/accounting/models.py:566
|
#: src/accounting/models.py:584
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Cash Receipt Journal Entry#%(id)s"
|
msgid "Cash Receipt Journal Entry#%(id)s"
|
||||||
msgstr "現金收入傳票#%(id)s"
|
msgstr "現金收入傳票#%(id)s"
|
||||||
|
|
||||||
#: src/accounting/models.py:567
|
#: src/accounting/models.py:585
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Transfer Journal Entry#%(id)s"
|
msgid "Transfer Journal Entry#%(id)s"
|
||||||
msgstr "轉帳傳票#%(id)s"
|
msgstr "轉帳傳票#%(id)s"
|
||||||
|
|
||||||
#: src/accounting/models.py:700
|
#: src/accounting/models.py:714
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(date)s %(description)s %(amount)s"
|
msgid "%(date)s %(description)s %(amount)s"
|
||||||
msgstr "%(date)s %(description)s %(amount)s"
|
msgstr "%(date)s %(description)s %(amount)s"
|
||||||
@ -92,7 +92,7 @@ msgid "A nominal account does not need offset."
|
|||||||
msgstr "虛科目不需抵銷。"
|
msgstr "虛科目不需抵銷。"
|
||||||
|
|
||||||
#: src/accounting/account/forms.py:75
|
#: src/accounting/account/forms.py:75
|
||||||
#: src/accounting/static/js/account-form.js:181
|
#: src/accounting/static/js/account-form.js:189
|
||||||
msgid "Please select the base account."
|
msgid "Please select the base account."
|
||||||
msgstr "請選擇基本科目。"
|
msgstr "請選擇基本科目。"
|
||||||
|
|
||||||
@ -160,8 +160,8 @@ msgstr "不能用這個代碼。"
|
|||||||
#: src/accounting/currency/forms.py:62 src/accounting/option/forms.py:124
|
#: src/accounting/currency/forms.py:62 src/accounting/option/forms.py:124
|
||||||
#: src/accounting/option/forms.py:148
|
#: src/accounting/option/forms.py:148
|
||||||
#: src/accounting/static/js/currency-form.js:153
|
#: src/accounting/static/js/currency-form.js:153
|
||||||
#: src/accounting/static/js/option-form.js:525
|
#: src/accounting/static/js/option-form.js:532
|
||||||
#: src/accounting/static/js/option-form.js:780
|
#: src/accounting/static/js/option-form.js:787
|
||||||
msgid "Please fill in the name."
|
msgid "Please fill in the name."
|
||||||
msgstr "請填上名稱。"
|
msgstr "請填上名稱。"
|
||||||
|
|
||||||
@ -205,36 +205,36 @@ msgstr "傳票不可刪除。"
|
|||||||
msgid "The journal entry is deleted successfully."
|
msgid "The journal entry is deleted successfully."
|
||||||
msgstr "傳票刪掉了"
|
msgstr "傳票刪掉了"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:40
|
#: src/accounting/journal_entry/forms/currency.py:39
|
||||||
msgid "Please select the currency."
|
msgid "Please select the currency."
|
||||||
msgstr "請選擇貨幣。"
|
msgstr "請選擇貨幣。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:63
|
#: src/accounting/journal_entry/forms/currency.py:62
|
||||||
msgid "The currency must be the same as the original line item."
|
msgid "The currency must be the same as the original line item."
|
||||||
msgstr "貨幣需和原始分錄相同。"
|
msgstr "貨幣需和原始分錄相同。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:90
|
#: src/accounting/journal_entry/forms/currency.py:89
|
||||||
msgid "The currency must not be changed when there is offset."
|
msgid "The currency must not be changed when there is offset."
|
||||||
msgstr "抵銷過不可變更貨幣。"
|
msgstr "抵銷過不可變更貨幣。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:99
|
#: src/accounting/journal_entry/forms/currency.py:98
|
||||||
#: src/accounting/static/js/journal-entry-form.js:729
|
#: src/accounting/static/js/journal-entry-form.js:773
|
||||||
msgid "Please add some line items."
|
msgid "Please add some line items."
|
||||||
msgstr "請加上分錄。"
|
msgstr "請加上分錄。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:112
|
#: src/accounting/journal_entry/forms/currency.py:111
|
||||||
#: src/accounting/static/js/journal-entry-form.js:516
|
#: src/accounting/static/js/journal-entry-form.js:522
|
||||||
msgid "The totals of the debit and credit amounts do not match."
|
msgid "The totals of the debit and credit amounts do not match."
|
||||||
msgstr "借方貸方合計不符。 "
|
msgstr "借方貸方合計不符。 "
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/journal_entry.py:48
|
#: src/accounting/journal_entry/forms/journal_entry.py:48
|
||||||
#: src/accounting/static/js/journal-entry-form.js:270
|
#: src/accounting/static/js/journal-entry-form.js:264
|
||||||
#: src/accounting/static/js/period-chooser.js:273
|
#: src/accounting/static/js/period-chooser.js:265
|
||||||
msgid "Please fill in the date."
|
msgid "Please fill in the date."
|
||||||
msgstr "請填上日期。"
|
msgstr "請填上日期。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/journal_entry.py:64
|
#: src/accounting/journal_entry/forms/journal_entry.py:64
|
||||||
#: src/accounting/static/js/journal-entry-form.js:275
|
#: src/accounting/static/js/journal-entry-form.js:269
|
||||||
msgid "The date cannot be earlier than the original line items."
|
msgid "The date cannot be earlier than the original line items."
|
||||||
msgstr "日期不可早於原始分錄。"
|
msgstr "日期不可早於原始分錄。"
|
||||||
|
|
||||||
@ -243,7 +243,7 @@ msgid "The date cannot be later than the offset items."
|
|||||||
msgstr "日期不可晚於抵銷日期。"
|
msgstr "日期不可晚於抵銷日期。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/journal_entry.py:88
|
#: src/accounting/journal_entry/forms/journal_entry.py:88
|
||||||
#: src/accounting/static/js/journal-entry-form.js:305
|
#: src/accounting/static/js/journal-entry-form.js:299
|
||||||
msgid "Please add some currencies."
|
msgid "Please add some currencies."
|
||||||
msgstr "請加上貨幣。"
|
msgstr "請加上貨幣。"
|
||||||
|
|
||||||
@ -271,42 +271,42 @@ msgstr "原始分錄不可以是抵銷分錄。"
|
|||||||
msgid "The account must be the same as the original line item."
|
msgid "The account must be the same as the original line item."
|
||||||
msgstr "科目需和原始分錄相同。"
|
msgstr "科目需和原始分錄相同。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:137
|
#: src/accounting/journal_entry/forms/line_item.py:135
|
||||||
msgid "The account must not be changed when there is offset."
|
msgid "The account must not be changed when there is offset."
|
||||||
msgstr "抵銷過不可變更科目。"
|
msgstr "抵銷過不可變更科目。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:153
|
#: src/accounting/journal_entry/forms/line_item.py:151
|
||||||
msgid "A payable line item cannot start from debit."
|
msgid "A payable line item cannot start from debit."
|
||||||
msgstr "不可由借方新建應付款。"
|
msgstr "不可由借方新建應付款。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:169
|
#: src/accounting/journal_entry/forms/line_item.py:167
|
||||||
msgid "A receivable line item cannot start from credit."
|
msgid "A receivable line item cannot start from credit."
|
||||||
msgstr "不可由貸方新建應收款。"
|
msgstr "不可由貸方新建應收款。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:180
|
#: src/accounting/journal_entry/forms/line_item.py:178
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:455
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:436
|
||||||
msgid "Please fill in a positive amount."
|
msgid "Please fill in a positive amount."
|
||||||
msgstr "金額請填正數。"
|
msgstr "金額請填正數。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:222
|
#: src/accounting/journal_entry/forms/line_item.py:220
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:461
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:442
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The amount must not exceed the net balance %(balance)s of the original "
|
"The amount must not exceed the net balance %(balance)s of the original "
|
||||||
"line item."
|
"line item."
|
||||||
msgstr "金額不可超過原始分錄凈額 %(balance)s 。"
|
msgstr "金額不可超過原始分錄凈額 %(balance)s 。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:243
|
#: src/accounting/journal_entry/forms/line_item.py:241
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:469
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:450
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The amount must not be less than the offset total %(total)s."
|
msgid "The amount must not be less than the offset total %(total)s."
|
||||||
msgstr "金額不可低於抵銷總額 %(total)s 。"
|
msgstr "金額不可低於抵銷總額 %(total)s 。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:416
|
#: src/accounting/journal_entry/forms/line_item.py:426
|
||||||
msgid "This account is not for debit line items."
|
msgid "This account is not for debit line items."
|
||||||
msgstr "科目不是借方科目。"
|
msgstr "科目不是借方科目。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:468
|
#: src/accounting/journal_entry/forms/line_item.py:478
|
||||||
msgid "This account is not for credit line items."
|
msgid "This account is not for credit line items."
|
||||||
msgstr "科目不是貸方科目。"
|
msgstr "科目不是貸方科目。"
|
||||||
|
|
||||||
@ -327,8 +327,8 @@ msgid "This account is not for expense."
|
|||||||
msgstr "科目不是支出科目。"
|
msgstr "科目不是支出科目。"
|
||||||
|
|
||||||
#: src/accounting/option/forms.py:137 src/accounting/option/forms.py:161
|
#: src/accounting/option/forms.py:137 src/accounting/option/forms.py:161
|
||||||
#: src/accounting/static/js/option-form.js:535
|
#: src/accounting/static/js/option-form.js:542
|
||||||
#: src/accounting/static/js/option-form.js:813
|
#: src/accounting/static/js/option-form.js:820
|
||||||
msgid "Please fill in the description template."
|
msgid "Please fill in the description template."
|
||||||
msgstr "請填上摘要範本。"
|
msgstr "請填上摘要範本。"
|
||||||
|
|
||||||
@ -354,6 +354,15 @@ msgstr "設定未異動。"
|
|||||||
msgid "The settings are saved successfully."
|
msgid "The settings are saved successfully."
|
||||||
msgstr "設定存好了。"
|
msgstr "設定存好了。"
|
||||||
|
|
||||||
|
#: src/accounting/report/views.py:401
|
||||||
|
msgid "No more offset to match automatically."
|
||||||
|
msgstr "無法自動配對抵銷。"
|
||||||
|
|
||||||
|
#: src/accounting/report/views.py:408
|
||||||
|
#, python-format
|
||||||
|
msgid "Matched %(matches)s offsets."
|
||||||
|
msgstr "抵銷了 %(matches)s 筆。"
|
||||||
|
|
||||||
#: src/accounting/report/period/description.py:33
|
#: src/accounting/report/period/description.py:33
|
||||||
msgid "for all time"
|
msgid "for all time"
|
||||||
msgstr "全部"
|
msgstr "全部"
|
||||||
@ -408,31 +417,31 @@ msgstr "去年"
|
|||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "全部"
|
msgstr "全部"
|
||||||
|
|
||||||
#: src/accounting/report/reports/balance_sheet.py:422
|
#: src/accounting/report/reports/balance_sheet.py:423
|
||||||
#: src/accounting/report/reports/balance_sheet.py:426
|
#: src/accounting/report/reports/balance_sheet.py:427
|
||||||
#: src/accounting/report/reports/balance_sheet.py:438
|
#: src/accounting/report/reports/balance_sheet.py:439
|
||||||
#: src/accounting/report/reports/balance_sheet.py:440
|
#: src/accounting/report/reports/balance_sheet.py:441
|
||||||
#: src/accounting/report/reports/income_expenses.py:189
|
#: src/accounting/report/reports/income_expenses.py:189
|
||||||
#: src/accounting/report/reports/income_expenses.py:423
|
#: src/accounting/report/reports/income_expenses.py:423
|
||||||
#: src/accounting/report/reports/income_statement.py:299
|
#: src/accounting/report/reports/income_statement.py:300
|
||||||
#: src/accounting/report/reports/ledger.py:171
|
#: src/accounting/report/reports/ledger.py:171
|
||||||
#: src/accounting/report/reports/ledger.py:380
|
#: src/accounting/report/reports/ledger.py:380
|
||||||
#: src/accounting/report/reports/trial_balance.py:228
|
#: src/accounting/report/reports/trial_balance.py:229
|
||||||
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:43
|
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:43
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:37
|
#: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:38
|
||||||
#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:43
|
#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:43
|
||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:39
|
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:39
|
||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:55
|
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:55
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:59
|
#: src/accounting/templates/accounting/report/balance-sheet.html:65
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:71
|
#: src/accounting/templates/accounting/report/balance-sheet.html:77
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:81
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:87
|
#: src/accounting/templates/accounting/report/balance-sheet.html:87
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:96
|
#: src/accounting/templates/accounting/report/balance-sheet.html:93
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:103
|
#: src/accounting/templates/accounting/report/balance-sheet.html:102
|
||||||
|
#: src/accounting/templates/accounting/report/balance-sheet.html:109
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:81
|
#: src/accounting/templates/accounting/report/income-expenses.html:81
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:83
|
#: src/accounting/templates/accounting/report/income-statement.html:89
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:82
|
#: src/accounting/templates/accounting/report/ledger.html:82
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:74
|
#: src/accounting/templates/accounting/report/trial-balance.html:80
|
||||||
msgid "Total"
|
msgid "Total"
|
||||||
msgstr "合計"
|
msgstr "合計"
|
||||||
|
|
||||||
@ -442,39 +451,49 @@ msgid "Brought forward"
|
|||||||
msgstr "前期轉入"
|
msgstr "前期轉入"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:407
|
#: src/accounting/report/reports/income_expenses.py:407
|
||||||
#: src/accounting/report/reports/journal.py:155
|
#: src/accounting/report/reports/journal.py:158
|
||||||
#: src/accounting/report/reports/ledger.py:366
|
#: src/accounting/report/reports/ledger.py:366
|
||||||
|
#: src/accounting/report/reports/unapplied.py:148
|
||||||
|
#: src/accounting/report/reports/unmatched.py:158
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:50
|
#: src/accounting/templates/accounting/journal-entry/include/form.html:50
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:111
|
#: src/accounting/templates/accounting/report/include/period-chooser.html:111
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:55
|
#: src/accounting/templates/accounting/report/income-expenses.html:55
|
||||||
#: src/accounting/templates/accounting/report/journal.html:53
|
#: src/accounting/templates/accounting/report/journal.html:53
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:55
|
#: src/accounting/templates/accounting/report/ledger.html:55
|
||||||
#: src/accounting/templates/accounting/report/search.html:50
|
#: src/accounting/templates/accounting/report/search.html:50
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied.html:52
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:93
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr "日期"
|
msgstr "日期"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:407
|
#: src/accounting/report/reports/income_expenses.py:407
|
||||||
#: src/accounting/report/reports/journal.py:156
|
#: src/accounting/report/reports/journal.py:159
|
||||||
#: src/accounting/report/reports/trial_balance.py:224
|
#: src/accounting/report/reports/trial_balance.py:225
|
||||||
|
#: src/accounting/report/reports/unapplied_accounts.py:122
|
||||||
|
#: src/accounting/report/reports/unmatched_accounts.py:122
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:57
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:57
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:39
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:39
|
||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:90
|
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:90
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:56
|
#: src/accounting/templates/accounting/report/income-expenses.html:56
|
||||||
#: src/accounting/templates/accounting/report/journal.html:55
|
#: src/accounting/templates/accounting/report/journal.html:55
|
||||||
#: src/accounting/templates/accounting/report/search.html:52
|
#: src/accounting/templates/accounting/report/search.html:52
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:55
|
#: src/accounting/templates/accounting/report/trial-balance.html:61
|
||||||
msgid "Account"
|
msgid "Account"
|
||||||
msgstr "科目"
|
msgstr "科目"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:408
|
#: src/accounting/report/reports/income_expenses.py:408
|
||||||
#: src/accounting/report/reports/journal.py:156
|
#: src/accounting/report/reports/journal.py:159
|
||||||
#: src/accounting/report/reports/ledger.py:366
|
#: src/accounting/report/reports/ledger.py:366
|
||||||
|
#: src/accounting/report/reports/unapplied.py:149
|
||||||
|
#: src/accounting/report/reports/unmatched.py:159
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:28
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:28
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:49
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:49
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:57
|
#: src/accounting/templates/accounting/report/income-expenses.html:57
|
||||||
#: src/accounting/templates/accounting/report/journal.html:56
|
#: src/accounting/templates/accounting/report/journal.html:56
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:56
|
#: src/accounting/templates/accounting/report/ledger.html:56
|
||||||
#: src/accounting/templates/accounting/report/search.html:53
|
#: src/accounting/templates/accounting/report/search.html:53
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied.html:53
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:94
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "摘要"
|
msgstr "摘要"
|
||||||
|
|
||||||
@ -490,13 +509,15 @@ msgstr "支出"
|
|||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:409
|
#: src/accounting/report/reports/income_expenses.py:409
|
||||||
#: src/accounting/report/reports/ledger.py:368
|
#: src/accounting/report/reports/ledger.py:368
|
||||||
|
#: src/accounting/report/reports/unmatched.py:160
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:60
|
#: src/accounting/templates/accounting/report/income-expenses.html:60
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:60
|
#: src/accounting/templates/accounting/report/ledger.html:60
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:97
|
||||||
msgid "Balance"
|
msgid "Balance"
|
||||||
msgstr "餘額"
|
msgstr "餘額"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:410
|
#: src/accounting/report/reports/income_expenses.py:410
|
||||||
#: src/accounting/report/reports/journal.py:158
|
#: src/accounting/report/reports/journal.py:161
|
||||||
#: src/accounting/report/reports/ledger.py:368
|
#: src/accounting/report/reports/ledger.py:368
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:178
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:178
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:73
|
#: src/accounting/templates/accounting/journal-entry/include/form.html:73
|
||||||
@ -527,13 +548,17 @@ msgstr "稅後淨利"
|
|||||||
msgid "net income or loss for current period"
|
msgid "net income or loss for current period"
|
||||||
msgstr "本期損益"
|
msgstr "本期損益"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:300
|
#: src/accounting/report/reports/income_statement.py:301
|
||||||
|
#: src/accounting/report/reports/unapplied.py:149
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:65
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:65
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:55
|
#: src/accounting/templates/accounting/report/income-statement.html:61
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied.html:54
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
msgstr "金額"
|
msgstr "金額"
|
||||||
|
|
||||||
#: src/accounting/report/reports/journal.py:155
|
#: src/accounting/report/reports/journal.py:158
|
||||||
|
#: src/accounting/report/reports/unapplied.py:148
|
||||||
|
#: src/accounting/report/reports/unmatched.py:158
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form-currency.html:33
|
#: src/accounting/templates/accounting/journal-entry/include/form-currency.html:33
|
||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:73
|
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:73
|
||||||
#: src/accounting/templates/accounting/report/journal.html:54
|
#: src/accounting/templates/accounting/report/journal.html:54
|
||||||
@ -541,31 +566,71 @@ msgstr "金額"
|
|||||||
msgid "Currency"
|
msgid "Currency"
|
||||||
msgstr "貨幣"
|
msgstr "貨幣"
|
||||||
|
|
||||||
#: src/accounting/report/reports/journal.py:157
|
#: src/accounting/report/reports/journal.py:160
|
||||||
#: src/accounting/report/reports/ledger.py:367
|
#: src/accounting/report/reports/ledger.py:367
|
||||||
#: src/accounting/report/reports/trial_balance.py:224
|
#: src/accounting/report/reports/trial_balance.py:225
|
||||||
|
#: src/accounting/report/reports/unmatched.py:159
|
||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:33
|
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:33
|
||||||
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:30
|
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:30
|
||||||
#: src/accounting/templates/accounting/report/journal.html:57
|
#: src/accounting/templates/accounting/report/journal.html:57
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:57
|
#: src/accounting/templates/accounting/report/ledger.html:57
|
||||||
#: src/accounting/templates/accounting/report/search.html:54
|
#: src/accounting/templates/accounting/report/search.html:54
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:56
|
#: src/accounting/templates/accounting/report/trial-balance.html:62
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:95
|
||||||
msgid "Debit"
|
msgid "Debit"
|
||||||
msgstr "借方"
|
msgstr "借方"
|
||||||
|
|
||||||
#: src/accounting/report/reports/journal.py:157
|
#: src/accounting/report/reports/journal.py:160
|
||||||
#: src/accounting/report/reports/ledger.py:367
|
#: src/accounting/report/reports/ledger.py:367
|
||||||
#: src/accounting/report/reports/trial_balance.py:225
|
#: src/accounting/report/reports/trial_balance.py:226
|
||||||
|
#: src/accounting/report/reports/unmatched.py:160
|
||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:49
|
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:49
|
||||||
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:41
|
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:41
|
||||||
#: src/accounting/templates/accounting/report/journal.html:58
|
#: src/accounting/templates/accounting/report/journal.html:58
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:58
|
#: src/accounting/templates/accounting/report/ledger.html:58
|
||||||
#: src/accounting/templates/accounting/report/search.html:55
|
#: src/accounting/templates/accounting/report/search.html:55
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:57
|
#: src/accounting/templates/accounting/report/trial-balance.html:63
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:96
|
||||||
msgid "Credit"
|
msgid "Credit"
|
||||||
msgstr "貸方"
|
msgstr "貸方"
|
||||||
|
|
||||||
#: src/accounting/report/utils/report_chooser.py:81
|
#: src/accounting/report/reports/unapplied.py:132
|
||||||
|
#: src/accounting/report/reports/unapplied_accounts.py:107
|
||||||
|
#: src/accounting/report/reports/unmatched.py:142
|
||||||
|
#: src/accounting/report/reports/unmatched_accounts.py:107
|
||||||
|
#: src/accounting/templates/accounting/include/nav.html:39
|
||||||
|
msgid "Accounts"
|
||||||
|
msgstr "科目"
|
||||||
|
|
||||||
|
#: src/accounting/report/reports/unapplied.py:150
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied.html:55
|
||||||
|
msgid "Net Balance"
|
||||||
|
msgstr "淨額"
|
||||||
|
|
||||||
|
#: src/accounting/report/reports/unapplied_accounts.py:122
|
||||||
|
#: src/accounting/report/reports/unmatched_accounts.py:122
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:59
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:59
|
||||||
|
msgid "Count"
|
||||||
|
msgstr "數量"
|
||||||
|
|
||||||
|
#: src/accounting/report/utils/offset_matcher.py:163
|
||||||
|
msgid "There is no unmatched offset."
|
||||||
|
msgstr "沒有遺漏的抵銷分錄"
|
||||||
|
|
||||||
|
#: src/accounting/report/utils/offset_matcher.py:167
|
||||||
|
#, python-format
|
||||||
|
msgid "%(total)s unmatched offsets without original items."
|
||||||
|
msgstr "%(total)s 筆遺漏的抵銷分錄無法自動抵銷。"
|
||||||
|
|
||||||
|
#: src/accounting/report/utils/offset_matcher.py:172
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"%(matches)s unmatched offsets out of %(total)s can match with their "
|
||||||
|
"original items."
|
||||||
|
msgstr "%(total)s 筆遺漏的抵銷分錄中,可配對抵銷掉 %(matches)s 筆。"
|
||||||
|
|
||||||
|
#: src/accounting/report/utils/report_chooser.py:86
|
||||||
#: src/accounting/templates/accounting/account/include/form.html:98
|
#: src/accounting/templates/accounting/account/include/form.html:98
|
||||||
#: src/accounting/templates/accounting/account/list.html:40
|
#: src/accounting/templates/accounting/account/list.html:40
|
||||||
#: src/accounting/templates/accounting/base-account/list.html:34
|
#: src/accounting/templates/accounting/base-account/list.html:34
|
||||||
@ -581,134 +646,144 @@ msgstr "貸方"
|
|||||||
msgid "Search"
|
msgid "Search"
|
||||||
msgstr "搜尋"
|
msgstr "搜尋"
|
||||||
|
|
||||||
#: src/accounting/report/utils/report_chooser.py:89
|
#: src/accounting/report/utils/report_chooser.py:97
|
||||||
msgid "Journal"
|
|
||||||
msgstr "日記簿"
|
|
||||||
|
|
||||||
#: src/accounting/report/utils/report_chooser.py:99
|
|
||||||
msgid "Ledger"
|
|
||||||
msgstr "分類帳"
|
|
||||||
|
|
||||||
#: src/accounting/report/utils/report_chooser.py:114
|
|
||||||
msgid "Income and Expenses Log"
|
msgid "Income and Expenses Log"
|
||||||
msgstr "收支帳"
|
msgstr "收支帳"
|
||||||
|
|
||||||
#: src/accounting/report/utils/report_chooser.py:127
|
#: src/accounting/report/utils/report_chooser.py:110
|
||||||
|
msgid "Ledger"
|
||||||
|
msgstr "分類帳"
|
||||||
|
|
||||||
|
#: src/accounting/report/utils/report_chooser.py:122
|
||||||
|
msgid "Journal"
|
||||||
|
msgstr "日記簿"
|
||||||
|
|
||||||
|
#: src/accounting/report/utils/report_chooser.py:132
|
||||||
msgid "Trial Balance"
|
msgid "Trial Balance"
|
||||||
msgstr "試算表"
|
msgstr "試算表"
|
||||||
|
|
||||||
#: src/accounting/report/utils/report_chooser.py:138
|
#: src/accounting/report/utils/report_chooser.py:143
|
||||||
msgid "Income Statement"
|
msgid "Income Statement"
|
||||||
msgstr "損益表"
|
msgstr "損益表"
|
||||||
|
|
||||||
#: src/accounting/report/utils/report_chooser.py:149
|
#: src/accounting/report/utils/report_chooser.py:154
|
||||||
msgid "Balance Sheet"
|
msgid "Balance Sheet"
|
||||||
msgstr "資產負債表"
|
msgstr "資產負債表"
|
||||||
|
|
||||||
#: src/accounting/static/js/account-form.js:198
|
#: src/accounting/report/utils/report_chooser.py:167
|
||||||
|
#: src/accounting/report/utils/report_chooser.py:171
|
||||||
|
msgid "Unapplied Items"
|
||||||
|
msgstr "未抵銷項目"
|
||||||
|
|
||||||
|
#: src/accounting/report/utils/report_chooser.py:184
|
||||||
|
#: src/accounting/report/utils/report_chooser.py:188
|
||||||
|
msgid "Unmatched Offsets"
|
||||||
|
msgstr "遺漏的抵銷項目"
|
||||||
|
|
||||||
|
#: src/accounting/static/js/account-form.js:206
|
||||||
msgid "Please fill in the title."
|
msgid "Please fill in the title."
|
||||||
msgstr "請填上標題。"
|
msgstr "請填上標題。"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:756
|
#: src/accounting/static/js/description-editor.js:952
|
||||||
#: src/accounting/static/js/description-editor.js:934
|
#: src/accounting/static/js/description-editor.js:1130
|
||||||
msgid "Please fill in the tag."
|
msgid "Please fill in the tag."
|
||||||
msgstr "請填上標籤。"
|
msgstr "請填上標籤。"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:766
|
#: src/accounting/static/js/description-editor.js:962
|
||||||
#: src/accounting/static/js/description-editor.js:954
|
#: src/accounting/static/js/description-editor.js:1150
|
||||||
msgid "Please fill in the origin."
|
msgid "Please fill in the origin."
|
||||||
msgstr "請填上起點。"
|
msgstr "請填上起點。"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:776
|
#: src/accounting/static/js/description-editor.js:972
|
||||||
#: src/accounting/static/js/description-editor.js:964
|
#: src/accounting/static/js/description-editor.js:1160
|
||||||
msgid "Please fill in the destination."
|
msgid "Please fill in the destination."
|
||||||
msgstr "請填上終點。"
|
msgstr "請填上終點。"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:944
|
#: src/accounting/static/js/description-editor.js:1140
|
||||||
msgid "Please fill in the route."
|
msgid "Please fill in the route."
|
||||||
msgstr "請填上路線名稱。"
|
msgstr "請填上路線名稱。"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:998
|
#: src/accounting/static/js/description-editor.js:1193
|
||||||
msgid "January"
|
msgid "January"
|
||||||
msgstr "一月"
|
msgstr "一月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:998
|
#: src/accounting/static/js/description-editor.js:1193
|
||||||
msgid "February"
|
msgid "February"
|
||||||
msgstr "二月"
|
msgstr "二月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:998
|
#: src/accounting/static/js/description-editor.js:1193
|
||||||
msgid "March"
|
msgid "March"
|
||||||
msgstr "三月"
|
msgstr "三月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:998
|
#: src/accounting/static/js/description-editor.js:1193
|
||||||
msgid "April"
|
msgid "April"
|
||||||
msgstr "四月"
|
msgstr "四月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:999
|
#: src/accounting/static/js/description-editor.js:1194
|
||||||
msgid "May"
|
msgid "May"
|
||||||
msgstr "五月"
|
msgstr "五月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:999
|
#: src/accounting/static/js/description-editor.js:1194
|
||||||
msgid "June"
|
msgid "June"
|
||||||
msgstr "六月"
|
msgstr "六月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:999
|
#: src/accounting/static/js/description-editor.js:1194
|
||||||
msgid "July"
|
msgid "July"
|
||||||
msgstr "七月"
|
msgstr "七月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:999
|
#: src/accounting/static/js/description-editor.js:1194
|
||||||
msgid "August"
|
msgid "August"
|
||||||
msgstr "八月"
|
msgstr "八月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:1000
|
#: src/accounting/static/js/description-editor.js:1195
|
||||||
msgid "September"
|
msgid "September"
|
||||||
msgstr "九月"
|
msgstr "九月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:1000
|
#: src/accounting/static/js/description-editor.js:1195
|
||||||
msgid "October"
|
msgid "October"
|
||||||
msgstr "十月"
|
msgstr "十月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:1000
|
#: src/accounting/static/js/description-editor.js:1195
|
||||||
msgid "November"
|
msgid "November"
|
||||||
msgstr "十一月"
|
msgstr "十一月"
|
||||||
|
|
||||||
#: src/accounting/static/js/description-editor.js:1000
|
#: src/accounting/static/js/description-editor.js:1195
|
||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr "十二月"
|
msgstr "十二月"
|
||||||
|
|
||||||
#: src/accounting/static/js/journal-entry-form.js:985
|
#: src/accounting/static/js/journal-entry-form.js:1085
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:449
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:430
|
||||||
msgid "Please fill in the amount."
|
msgid "Please fill in the amount."
|
||||||
msgstr "請填上金額。"
|
msgstr "請填上金額。"
|
||||||
|
|
||||||
#: src/accounting/static/js/journal-entry-form.js:1012
|
#: src/accounting/static/js/journal-entry-form.js:1107
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:34
|
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:37
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:38
|
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:41
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Offset %(item)s"
|
msgid "Offset %(item)s"
|
||||||
msgstr "抵銷 %(item)s"
|
msgstr "抵銷 %(item)s"
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:278
|
#: src/accounting/static/js/period-chooser.js:270
|
||||||
msgid "The date is too early."
|
msgid "The date is too early."
|
||||||
msgstr "日期太早。"
|
msgstr "日期太早。"
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:377
|
#: src/accounting/static/js/period-chooser.js:369
|
||||||
msgid "Please fill in the start date."
|
msgid "Please fill in the start date."
|
||||||
msgstr "請填上開始日期。"
|
msgstr "請填上開始日期。"
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:382
|
#: src/accounting/static/js/period-chooser.js:374
|
||||||
msgid "The start date is too early."
|
msgid "The start date is too early."
|
||||||
msgstr "開始日期太早。"
|
msgstr "開始日期太早。"
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:387
|
#: src/accounting/static/js/period-chooser.js:379
|
||||||
msgid "The start date cannot be beyond the end date."
|
msgid "The start date cannot be beyond the end date."
|
||||||
msgstr "開始日期不可晚於結束日期。"
|
msgstr "開始日期不可晚於結束日期。"
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:405
|
#: src/accounting/static/js/period-chooser.js:397
|
||||||
msgid "Please fill in the end date."
|
msgid "Please fill in the end date."
|
||||||
msgstr "請填上結束日期。"
|
msgstr "請填上結束日期。"
|
||||||
|
|
||||||
#: src/accounting/static/js/period-chooser.js:410
|
#: src/accounting/static/js/period-chooser.js:402
|
||||||
msgid "The end date cannot be beyond the start date."
|
msgid "The end date cannot be beyond the start date."
|
||||||
msgstr "結束日期不可早於開始日期。"
|
msgstr "結束日期不可早於開始日期。"
|
||||||
|
|
||||||
@ -766,6 +841,7 @@ msgstr "確認刪除科目"
|
|||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:28
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:28
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:27
|
#: src/accounting/templates/accounting/report/include/period-chooser.html:27
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:28
|
#: src/accounting/templates/accounting/report/include/search-modal.html:28
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:58
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "關閉"
|
msgstr "關閉"
|
||||||
|
|
||||||
@ -777,12 +853,13 @@ msgstr "你確定要刪掉這個科目嗎?"
|
|||||||
#: src/accounting/templates/accounting/account/include/form.html:112
|
#: src/accounting/templates/accounting/account/include/form.html:112
|
||||||
#: src/accounting/templates/accounting/currency/detail.html:79
|
#: src/accounting/templates/accounting/currency/detail.html:79
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:49
|
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:49
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:193
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:194
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:84
|
#: src/accounting/templates/accounting/journal-entry/include/detail.html:84
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:70
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:70
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:48
|
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:48
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:65
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:65
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:37
|
#: src/accounting/templates/accounting/report/include/search-modal.html:37
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:74
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "取消"
|
msgstr "取消"
|
||||||
|
|
||||||
@ -790,6 +867,7 @@ msgstr "取消"
|
|||||||
#: src/accounting/templates/accounting/currency/detail.html:80
|
#: src/accounting/templates/accounting/currency/detail.html:80
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:85
|
#: src/accounting/templates/accounting/journal-entry/include/detail.html:85
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:141
|
#: src/accounting/templates/accounting/report/include/period-chooser.html:141
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:75
|
||||||
msgid "Confirm"
|
msgid "Confirm"
|
||||||
msgstr "確定"
|
msgstr "確定"
|
||||||
|
|
||||||
@ -824,7 +902,7 @@ msgstr "科目管理"
|
|||||||
|
|
||||||
#: src/accounting/templates/accounting/account/list.html:32
|
#: src/accounting/templates/accounting/account/list.html:32
|
||||||
#: src/accounting/templates/accounting/currency/list.html:32
|
#: src/accounting/templates/accounting/currency/list.html:32
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:44
|
#: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:45
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:64
|
#: src/accounting/templates/accounting/journal-entry/include/form.html:64
|
||||||
#: src/accounting/templates/accounting/option/include/form-recurring-expense-income.html:37
|
#: src/accounting/templates/accounting/option/include/form-recurring-expense-income.html:37
|
||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:26
|
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:26
|
||||||
@ -837,16 +915,22 @@ msgstr "新增"
|
|||||||
#: src/accounting/templates/accounting/base-account/list.html:51
|
#: src/accounting/templates/accounting/base-account/list.html:51
|
||||||
#: src/accounting/templates/accounting/currency/list.html:65
|
#: src/accounting/templates/accounting/currency/list.html:65
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:46
|
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:46
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:51
|
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:58
|
||||||
#: src/accounting/templates/accounting/journal-entry/order.html:82
|
#: src/accounting/templates/accounting/journal-entry/order.html:82
|
||||||
|
#: src/accounting/templates/accounting/option/detail.html:67
|
||||||
|
#: src/accounting/templates/accounting/option/detail.html:83
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:45
|
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:45
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:110
|
#: src/accounting/templates/accounting/report/balance-sheet.html:116
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:113
|
#: src/accounting/templates/accounting/report/income-expenses.html:113
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:96
|
#: src/accounting/templates/accounting/report/income-statement.html:102
|
||||||
#: src/accounting/templates/accounting/report/journal.html:103
|
#: src/accounting/templates/accounting/report/journal.html:103
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:116
|
#: src/accounting/templates/accounting/report/ledger.html:116
|
||||||
#: src/accounting/templates/accounting/report/search.html:100
|
#: src/accounting/templates/accounting/report/search.html:100
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:82
|
#: src/accounting/templates/accounting/report/trial-balance.html:88
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:76
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied.html:90
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:76
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:147
|
||||||
msgid "There is no data."
|
msgid "There is no data."
|
||||||
msgstr "沒有資料。"
|
msgstr "沒有資料。"
|
||||||
|
|
||||||
@ -858,7 +942,7 @@ msgstr "%(base)s下的科目"
|
|||||||
#: src/accounting/templates/accounting/account/include/form.html:75
|
#: src/accounting/templates/accounting/account/include/form.html:75
|
||||||
#: src/accounting/templates/accounting/account/order.html:62
|
#: src/accounting/templates/accounting/account/order.html:62
|
||||||
#: src/accounting/templates/accounting/currency/include/form.html:57
|
#: src/accounting/templates/accounting/currency/include/form.html:57
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:194
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:195
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:80
|
#: src/accounting/templates/accounting/journal-entry/include/form.html:80
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:71
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:71
|
||||||
#: src/accounting/templates/accounting/journal-entry/order.html:61
|
#: src/accounting/templates/accounting/journal-entry/order.html:61
|
||||||
@ -936,10 +1020,6 @@ msgstr "記帳"
|
|||||||
msgid "Reports"
|
msgid "Reports"
|
||||||
msgstr "報表"
|
msgstr "報表"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/include/nav.html:39
|
|
||||||
msgid "Accounts"
|
|
||||||
msgstr "科目"
|
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/include/nav.html:45
|
#: src/accounting/templates/accounting/include/nav.html:45
|
||||||
msgid "Base Accounts"
|
msgid "Base Accounts"
|
||||||
msgstr "基本科目"
|
msgstr "基本科目"
|
||||||
@ -950,6 +1030,7 @@ msgstr "貨幣"
|
|||||||
|
|
||||||
#: src/accounting/templates/accounting/include/nav.html:58
|
#: src/accounting/templates/accounting/include/nav.html:58
|
||||||
#: src/accounting/templates/accounting/option/detail.html:24
|
#: src/accounting/templates/accounting/option/detail.html:24
|
||||||
|
#: src/accounting/templates/accounting/option/detail.html:41
|
||||||
#: src/accounting/templates/accounting/option/form.html:29
|
#: src/accounting/templates/accounting/option/form.html:29
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "設定"
|
msgstr "設定"
|
||||||
@ -1046,23 +1127,23 @@ msgstr "路線"
|
|||||||
msgid "The Number of Items"
|
msgid "The Number of Items"
|
||||||
msgstr "數量"
|
msgstr "數量"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:42
|
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:45
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:43
|
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:46
|
||||||
msgid "Offsets"
|
msgid "Offsets"
|
||||||
msgstr "抵銷"
|
msgstr "抵銷"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:55
|
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:58
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:54
|
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:57
|
||||||
msgid "Net balance"
|
msgid "Net balance"
|
||||||
msgstr "淨額"
|
msgstr "淨額"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:60
|
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:63
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:51
|
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:54
|
||||||
msgid "Fully offset"
|
msgid "Fully offset"
|
||||||
msgstr "全部抵銷"
|
msgstr "全部抵銷"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:65
|
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:68
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:59
|
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:62
|
||||||
msgid "Unmatched"
|
msgid "Unmatched"
|
||||||
msgstr "未抵銷"
|
msgstr "未抵銷"
|
||||||
|
|
||||||
@ -1112,7 +1193,7 @@ msgstr "新增現金收入傳票"
|
|||||||
msgid "Add a New Transfer Journal Entry"
|
msgid "Add a New Transfer Journal Entry"
|
||||||
msgstr "新增轉帳傳票"
|
msgstr "新增轉帳傳票"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/detail.html:43
|
#: src/accounting/templates/accounting/option/detail.html:44
|
||||||
#: src/accounting/templates/accounting/option/form.html:51
|
#: src/accounting/templates/accounting/option/form.html:51
|
||||||
msgid "Default Currency"
|
msgid "Default Currency"
|
||||||
msgstr "預設貨幣"
|
msgstr "預設貨幣"
|
||||||
@ -1122,13 +1203,13 @@ msgstr "預設貨幣"
|
|||||||
msgid "Default Account for the Income and Expenses Log"
|
msgid "Default Account for the Income and Expenses Log"
|
||||||
msgstr "收支帳預設科目"
|
msgstr "收支帳預設科目"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/detail.html:52
|
#: src/accounting/templates/accounting/option/detail.html:54
|
||||||
#: src/accounting/templates/accounting/option/form.html:66
|
#: src/accounting/templates/accounting/option/form.html:66
|
||||||
#: src/accounting/templates/accounting/option/form.html:92
|
#: src/accounting/templates/accounting/option/form.html:92
|
||||||
msgid "Recurring Expense"
|
msgid "Recurring Expense"
|
||||||
msgstr "常用支出"
|
msgstr "常用支出"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/detail.html:58
|
#: src/accounting/templates/accounting/option/detail.html:70
|
||||||
#: src/accounting/templates/accounting/option/form.html:72
|
#: src/accounting/templates/accounting/option/form.html:72
|
||||||
#: src/accounting/templates/accounting/option/form.html:96
|
#: src/accounting/templates/accounting/option/form.html:96
|
||||||
msgid "Recurring Income"
|
msgid "Recurring Income"
|
||||||
@ -1175,18 +1256,35 @@ msgid "Water bill for {last_bimonthly_name}"
|
|||||||
msgstr "水費{last_bimonthly_number}月"
|
msgstr "水費{last_bimonthly_number}月"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:29
|
#: src/accounting/templates/accounting/report/balance-sheet.html:29
|
||||||
#: src/accounting/templates/accounting/report/balance-sheet.html:49
|
#: src/accounting/templates/accounting/report/balance-sheet.html:51
|
||||||
|
#, python-format
|
||||||
|
msgid "Balance Sheet %(period)s"
|
||||||
|
msgstr "%(period)s資產負債表"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/balance-sheet.html:29
|
||||||
|
#: src/accounting/templates/accounting/report/balance-sheet.html:53
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Balance Sheet of %(currency)s %(period)s"
|
msgid "Balance Sheet of %(currency)s %(period)s"
|
||||||
msgstr "%(period)s%(currency)s資產負債表"
|
msgstr "%(period)s%(currency)s資產負債表"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/income-expenses.html:29
|
||||||
|
#, python-format
|
||||||
|
msgid "Income and Expenses Log of %(account)s %(period)s"
|
||||||
|
msgstr "%(period)s%(account)s收支帳"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:29
|
#: src/accounting/templates/accounting/report/income-expenses.html:29
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Income and Expenses Log of %(account)s in %(currency)s %(period)s"
|
msgid "Income and Expenses Log of %(account)s in %(currency)s %(period)s"
|
||||||
msgstr "%(period)s%(currency)s%(account)s收支帳"
|
msgstr "%(period)s%(currency)s%(account)s收支帳"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:29
|
#: src/accounting/templates/accounting/report/income-statement.html:29
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:49
|
#: src/accounting/templates/accounting/report/income-statement.html:51
|
||||||
|
#, python-format
|
||||||
|
msgid "Income Statement %(period)s"
|
||||||
|
msgstr "%(period)s損益表"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/income-statement.html:29
|
||||||
|
#: src/accounting/templates/accounting/report/income-statement.html:53
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Income Statement of %(currency)s %(period)s"
|
msgid "Income Statement of %(currency)s %(period)s"
|
||||||
msgstr "%(period)s%(currency)s損益表"
|
msgstr "%(period)s%(currency)s損益表"
|
||||||
@ -1196,17 +1294,90 @@ msgstr "%(period)s%(currency)s損益表"
|
|||||||
msgid "Journal %(period)s"
|
msgid "Journal %(period)s"
|
||||||
msgstr "%(period)s日記簿"
|
msgstr "%(period)s日記簿"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/ledger.html:29
|
||||||
|
#, python-format
|
||||||
|
msgid "Ledger of %(account)s %(period)s"
|
||||||
|
msgstr "%(period)s%(account)s分類帳"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:29
|
#: src/accounting/templates/accounting/report/ledger.html:29
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Ledger of %(account)s in %(currency)s %(period)s"
|
msgid "Ledger of %(account)s in %(currency)s %(period)s"
|
||||||
msgstr "%(period)s%(currency)s%(account)s分類帳"
|
msgstr "%(period)s%(currency)s%(account)s分類帳"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:29
|
#: src/accounting/templates/accounting/report/trial-balance.html:29
|
||||||
#: src/accounting/templates/accounting/report/trial-balance.html:49
|
#: src/accounting/templates/accounting/report/trial-balance.html:51
|
||||||
|
#, python-format
|
||||||
|
msgid "Trial Balance %(period)s"
|
||||||
|
msgstr "%(period)s試算表"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/trial-balance.html:29
|
||||||
|
#: src/accounting/templates/accounting/report/trial-balance.html:53
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Trial Balance of %(currency)s %(period)s"
|
msgid "Trial Balance of %(currency)s %(period)s"
|
||||||
msgstr "%(period)s%(currency)s試算表"
|
msgstr "%(period)s%(currency)s試算表"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:29
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:49
|
||||||
|
msgid "Accounts with Unapplied Items"
|
||||||
|
msgstr "含未抵銷項目的科目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:29
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:51
|
||||||
|
#, python-format
|
||||||
|
msgid "Accounts with Unapplied Items in %(currency)s"
|
||||||
|
msgstr "%(currency)s含未抵銷項目的科目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied.html:29
|
||||||
|
#, python-format
|
||||||
|
msgid "Unapplied Items of %(account)s"
|
||||||
|
msgstr "%(account)s未抵銷項目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unapplied.html:29
|
||||||
|
#, python-format
|
||||||
|
msgid "Unapplied Items of %(account)s in %(currency)s"
|
||||||
|
msgstr "%(currency)s%(account)s未抵銷項目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:29
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:49
|
||||||
|
msgid "Accounts with Unmatched Offsets"
|
||||||
|
msgstr "含遺漏抵銷項目的科目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:29
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:51
|
||||||
|
#, python-format
|
||||||
|
msgid "Accounts with Unmatched Offsets in %(currency)s"
|
||||||
|
msgstr "%(currency)s含遺漏抵銷項目的科目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:29
|
||||||
|
#, python-format
|
||||||
|
msgid "Unmatched Offsets of %(account)s"
|
||||||
|
msgstr "%(account)s遺漏的抵銷項目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:29
|
||||||
|
#, python-format
|
||||||
|
msgid "Unmatched Offsets of %(account)s in %(currency)s"
|
||||||
|
msgstr "%(currency)s%(account)s遺漏的抵銷項目"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:47
|
||||||
|
msgid "Match"
|
||||||
|
msgstr "抵銷"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:57
|
||||||
|
msgid "Confirm Match Offsets"
|
||||||
|
msgstr "確認抵銷"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:61
|
||||||
|
msgid ""
|
||||||
|
"Do you really want to match the following original line items with their "
|
||||||
|
"offsets? This cannot be undone. Please backup your database first, and "
|
||||||
|
"review before you confirm."
|
||||||
|
msgstr "你確定要抵銷下列原始分錄與抵銷分錄嗎?結果無法復原。請先備份資料庫,並仔細核對抵銷分錄是否正確。"
|
||||||
|
|
||||||
|
#: src/accounting/templates/accounting/report/unmatched.html:107
|
||||||
|
#, python-format
|
||||||
|
msgid "Can match %(item)s"
|
||||||
|
msgstr "可抵銷 %(item)s"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:26
|
#: src/accounting/templates/accounting/report/include/period-chooser.html:26
|
||||||
msgid "Period Chooser"
|
msgid "Period Chooser"
|
||||||
msgstr "選擇日期範圍"
|
msgstr "選擇日期範圍"
|
||||||
|
@ -20,22 +20,10 @@ warnings from the IDE.
|
|||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
def be(expression: t.Any) -> sa.BinaryExpression:
|
def s(message: Any) -> str:
|
||||||
"""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.
|
"""Casts the LazyString message to the string type.
|
||||||
|
|
||||||
:param message: The message.
|
:param message: The message.
|
||||||
|
@ -17,12 +17,13 @@
|
|||||||
"""The current assets and liabilities account.
|
"""The current assets and liabilities account.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import Self
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
class CurrentAccount:
|
class CurrentAccount:
|
||||||
@ -54,7 +55,7 @@ class CurrentAccount:
|
|||||||
return self.str
|
return self.str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def current_assets_and_liabilities(cls) -> t.Self:
|
def current_assets_and_liabilities(cls) -> Self:
|
||||||
"""Returns the pseudo account for all current assets and liabilities.
|
"""Returns the pseudo account for all current assets and liabilities.
|
||||||
|
|
||||||
:return: The pseudo account for all current assets and liabilities.
|
:return: The pseudo account for all current assets and liabilities.
|
||||||
@ -67,7 +68,7 @@ class CurrentAccount:
|
|||||||
return account
|
return account
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def accounts(cls) -> list[t.Self]:
|
def accounts(cls) -> list[Self]:
|
||||||
"""Returns the current assets and liabilities accounts.
|
"""Returns the current assets and liabilities accounts.
|
||||||
|
|
||||||
:return: The current assets and liabilities accounts.
|
:return: The current assets and liabilities accounts.
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import Any
|
||||||
|
|
||||||
from flask import flash
|
from flask import flash
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
@ -34,7 +34,7 @@ def flash_form_errors(form: FlaskForm) -> None:
|
|||||||
__flash_errors(form.errors)
|
__flash_errors(form.errors)
|
||||||
|
|
||||||
|
|
||||||
def __flash_errors(error: t.Any) -> None:
|
def __flash_errors(error: Any) -> None:
|
||||||
"""Flash all errors recursively.
|
"""Flash all errors recursively.
|
||||||
|
|
||||||
:param error: The errors.
|
:param error: The errors.
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The SQLAlchemy alias for the offset items.
|
"""The SQLAlchemy alias for the offset items.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
@ -30,10 +30,10 @@ def offset_alias() -> sa.Alias:
|
|||||||
:return: The SQLAlchemy alias for the offset items.
|
:return: The SQLAlchemy alias for the offset items.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def as_from(model_cls: t.Any) -> sa.FromClause:
|
def as_from(model_cls: Any) -> sa.FromClause:
|
||||||
return model_cls
|
return model_cls
|
||||||
|
|
||||||
def as_alias(alias: t.Any) -> sa.Alias:
|
def as_alias(alias: Any) -> sa.Alias:
|
||||||
return alias
|
return alias
|
||||||
|
|
||||||
return as_alias(sa.alias(as_from(JournalEntryLineItem), name="offset"))
|
return as_alias(sa.alias(as_from(JournalEntryLineItem), name="offset"))
|
@ -99,12 +99,12 @@ class Options:
|
|||||||
self.__set_option("default_currency_code", value)
|
self.__set_option("default_currency_code", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_currency_text(self) -> str:
|
def default_currency(self) -> Currency:
|
||||||
"""Returns the text of the default currency code.
|
"""Returns the default currency.
|
||||||
|
|
||||||
:return: The text of the default currency code.
|
:return: The default currency.
|
||||||
"""
|
"""
|
||||||
return str(db.session.get(Currency, self.default_currency_code))
|
return db.session.get(Currency, self.default_currency_code)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_ie_account_code(self) -> str:
|
def default_ie_account_code(self) -> str:
|
||||||
@ -123,22 +123,11 @@ class Options:
|
|||||||
"""
|
"""
|
||||||
self.__set_option("default_ie_account", value)
|
self.__set_option("default_ie_account", value)
|
||||||
|
|
||||||
@property
|
|
||||||
def default_ie_account_code_text(self) -> str:
|
|
||||||
"""Returns the text of the default currency code.
|
|
||||||
|
|
||||||
:return: The text of the default currency code.
|
|
||||||
"""
|
|
||||||
code: str = self.default_ie_account_code
|
|
||||||
if code == CurrentAccount.CURRENT_AL_CODE:
|
|
||||||
return str(CurrentAccount.current_assets_and_liabilities())
|
|
||||||
return str(CurrentAccount(Account.find_by_code(code)))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_ie_account(self) -> CurrentAccount:
|
def default_ie_account(self) -> CurrentAccount:
|
||||||
"""Returns the default account code for the income and expenses log.
|
"""Returns the default account for the income and expenses log.
|
||||||
|
|
||||||
:return: The default account code for the income and expenses log.
|
:return: The default account for the income and expenses log.
|
||||||
"""
|
"""
|
||||||
if self.default_ie_account_code \
|
if self.default_ie_account_code \
|
||||||
== CurrentAccount.CURRENT_AL_CODE:
|
== CurrentAccount.CURRENT_AL_CODE:
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import TypeVar, Generic
|
||||||
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, \
|
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, \
|
||||||
ParseResult
|
ParseResult
|
||||||
|
|
||||||
@ -62,10 +62,10 @@ class Redirection(RequestRedirect):
|
|||||||
DEFAULT_PAGE_SIZE: int = 10
|
DEFAULT_PAGE_SIZE: int = 10
|
||||||
"""The default page size."""
|
"""The default page size."""
|
||||||
|
|
||||||
T = t.TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class Pagination(t.Generic[T]):
|
class Pagination(Generic[T]):
|
||||||
"""The pagination utility."""
|
"""The pagination utility."""
|
||||||
|
|
||||||
def __init__(self, items: list[T], is_reversed: bool = False):
|
def __init__(self, items: list[T], is_reversed: bool = False):
|
||||||
@ -91,7 +91,7 @@ class Pagination(t.Generic[T]):
|
|||||||
"""The options to the number of items in a page."""
|
"""The options to the number of items in a page."""
|
||||||
|
|
||||||
|
|
||||||
class AbstractPagination(t.Generic[T]):
|
class AbstractPagination(Generic[T]):
|
||||||
"""An abstract pagination."""
|
"""An abstract pagination."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user