Compare commits
58 Commits
Author | SHA1 | Date | |
---|---|---|---|
7086c26ce7 | |||
2ec072854b | |||
a41d381322 | |||
c301f7ca74 | |||
020555602d | |||
9176be3c11 | |||
1d16b250d8 | |||
72b9555e29 | |||
d09255432d | |||
64e81a64ef | |||
e14a0432b2 | |||
7e7d428b52 | |||
353ebd7242 | |||
467739e0c8 | |||
999b593c64 | |||
f6f83fe323 | |||
607b5be9c0 | |||
3792524022 | |||
1c44d51e92 | |||
3d4d20614c | |||
4784100084 | |||
56a9d565c1 | |||
1967359142 | |||
e80aceb8ff | |||
649f76d9db | |||
30b00b1a67 | |||
b3e06e3e5b | |||
8c3d6fd962 | |||
d83a72bb8c | |||
bcb27594ad | |||
3cef0d7009 | |||
24c3b868e0 | |||
f57162a93c | |||
4afd072cc5 | |||
86b84bef7a | |||
a0382ad179 | |||
62b59e7380 | |||
dec53c09f3 | |||
91d22d72cb | |||
bbcedfd366 | |||
bc863869c0 | |||
874cbca320 | |||
df6c961dc3 | |||
|
fc6f1fd18a | ||
|
9af204322a | ||
|
112201ee8b | ||
|
1cc9b1732b | ||
|
ea8af57fcb | ||
|
b4d9a250db | ||
500467432a | |||
fd47f7bd94 | |||
bb93f601ad | |||
21281da3ed | |||
a554974ea0 | |||
54d18ca8b3 | |||
2db018f18b | |||
eb162c95df | |||
|
7358b3ed9d |
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,11 +1,14 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
__pycache__
|
__pycache__
|
||||||
dist
|
dist
|
||||||
|
build
|
||||||
*.egg-info
|
*.egg-info
|
||||||
migrations
|
migrations
|
||||||
*.mo
|
*.mo
|
||||||
.idea
|
.idea
|
||||||
venv
|
venv
|
||||||
mirror
|
|
||||||
excludes
|
excludes
|
||||||
zh_Hans
|
zh_Hans
|
||||||
|
|
||||||
|
.scannerwork
|
||||||
|
sonar-project.properties
|
||||||
|
20
MANIFEST.in
Normal file
20
MANIFEST.in
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# The Flask HTTP Digest Authentication Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/23
|
||||||
|
|
||||||
|
# Copyright (c) 2022 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.
|
||||||
|
|
||||||
|
include tests/*
|
||||||
|
include tests/test_site/*
|
||||||
|
include tests/test_site/*/*
|
211
README.md
211
README.md
@ -1,211 +0,0 @@
|
|||||||
# mia-accounting
|
|
||||||
|
|
||||||
The Django Accounting application.
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`mia-accounting` is a Django accounting application. It was a re-write of my
|
|
||||||
own private accounting application written in Perl for `mod_perl` in 2007. The
|
|
||||||
revision aims to be mobile-friendly with Bootstrap, with a modern back-end
|
|
||||||
framework and front-end technology like jQuery. The first revision was in
|
|
||||||
Perl / Mojolicious in 2019. This is the second revision in Python / Django
|
|
||||||
in 2020.
|
|
||||||
|
|
||||||
`mia-accounting` comes with two parts:
|
|
||||||
|
|
||||||
* The `accounting` application contains the main accounting application.
|
|
||||||
|
|
||||||
* The `mia_core` application contains core shared libraries that are used by the
|
|
||||||
accounting application and my other applications.
|
|
||||||
|
|
||||||
You may try it in live demonstration at
|
|
||||||
https://accounting.imacat.idv.tw/accounting .
|
|
||||||
* Username: `admin`
|
|
||||||
* Password: `12345`
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Install
|
|
||||||
|
|
||||||
`mia-accounting` requires Python 3.6 or above to work.
|
|
||||||
|
|
||||||
Install `mia-accounting` with `pip`.
|
|
||||||
|
|
||||||
```
|
|
||||||
pip install mia-accounting
|
|
||||||
```
|
|
||||||
|
|
||||||
### `settings.py`
|
|
||||||
|
|
||||||
Add these two applications in the `INSTALL_APPS` section of your `settings.py`.
|
|
||||||
|
|
||||||
```
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'mia_core.apps.MiaCoreConfig',
|
|
||||||
'accounting.apps.AccountingConfig',
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure the locale middleware is in the `MIDDLEWARE` section of your
|
|
||||||
`settings.py`, and add it if it is not added yet.
|
|
||||||
|
|
||||||
```
|
|
||||||
MIDDLEWARE = [
|
|
||||||
...
|
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### `urls.py`
|
|
||||||
|
|
||||||
Add the `accounting` application in the `urlpatterns` of your `urls.py`.
|
|
||||||
|
|
||||||
```
|
|
||||||
urlpatterns = [
|
|
||||||
...
|
|
||||||
path('accounting/', decorator_include(login_required, 'accounting.urls')),
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure `i18n` and `jsi18n` are also in the `urlpatterns` of your `urls.py`,
|
|
||||||
and add them if they are not added yet.
|
|
||||||
|
|
||||||
```
|
|
||||||
urlpatterns = [
|
|
||||||
...
|
|
||||||
path('i18n/', include("django.conf.urls.i18n")),
|
|
||||||
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### `base.html`
|
|
||||||
|
|
||||||
Add the following to the very beginning of your base template
|
|
||||||
`base.html`, before your first real HTML tag.
|
|
||||||
|
|
||||||
```
|
|
||||||
{% load mia_core %}
|
|
||||||
{% init_libs %}
|
|
||||||
{% block settings %}{% endblock %}
|
|
||||||
```
|
|
||||||
|
|
||||||
Add the CSS and JavaScripts in the `<head>...</head>` section of your
|
|
||||||
base template `base.html`.
|
|
||||||
|
|
||||||
```
|
|
||||||
{% for css in libs.css %}
|
|
||||||
<link rel="stylesheet" type="text/css" href="{% if css|is_static_url %}{% static css %}{% else %}{{ css }}{% endif %}" />
|
|
||||||
{% endfor %}
|
|
||||||
{% for js in libs.js %}
|
|
||||||
<script src="{% if js|is_static_url %}{% static js %}{% else %}{{ js }}{% endif %}"></script>
|
|
||||||
{% endfor %}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Restart Your Web Project
|
|
||||||
|
|
||||||
## Advanced Settings
|
|
||||||
|
|
||||||
The following advanced settings are available in `settings.py`.
|
|
||||||
|
|
||||||
```
|
|
||||||
# Settings for the accounting application
|
|
||||||
ACCOUNTING = {
|
|
||||||
# The default cash acount, for ex., "0" (current assets and liabilities),
|
|
||||||
# "1111" (cash on hand), "1113" (cash in banks) or any
|
|
||||||
"DEFAULT_CASH_ACCOUNT": "1111",
|
|
||||||
# The shortcut cash accounts
|
|
||||||
"CASH_SHORTCUT_ACCOUNTS": ["0", "1111"],
|
|
||||||
# The default ledger account
|
|
||||||
"DEFAULT_LEDGER_ACCOUNT": "1111",
|
|
||||||
# The payable accounts to track
|
|
||||||
"PAYABLE_ACCOUNTS": ["2141"],
|
|
||||||
# The asset accounts to track
|
|
||||||
"EQUIPMENT_ACCOUNTS": ["1441"],
|
|
||||||
}
|
|
||||||
|
|
||||||
# The local static CSS and JavaScript libraries
|
|
||||||
# The default is to use the libraries from CDN. You may set them to use the
|
|
||||||
# local static copies of these libraries
|
|
||||||
STATIC_LIBS = {
|
|
||||||
"jquery": {"css": [], "js": ["jquery/jquery-3.5.1.min.js"]},
|
|
||||||
"bootstrap4": {"css": ["bootstrap4/css/bootstrap.min.css"],
|
|
||||||
"js": ["bootstrap4/js/bootstrap.bundle.min.js"]},
|
|
||||||
"font-awesome-5": {"css": ["font-awesome-5/css/all.min.css"],
|
|
||||||
"js": []},
|
|
||||||
"bootstrap4-datatables": {
|
|
||||||
"css": ["datatables/css/jquery.dataTables.min.css",
|
|
||||||
"edatatables/css/dataTables.bootstrap4.min.css"],
|
|
||||||
"js": ["datatables/js/jquery.dataTables.min.js",
|
|
||||||
"datatables/js/dataTables.bootstrap4.min.js"]},
|
|
||||||
"jquery-ui": {"css": ["jquery-ui/jquery-ui.min.css"],
|
|
||||||
"js": ["jquery-ui/jquery-ui.min.js"]},
|
|
||||||
"bootstrap4-tempusdominus": {
|
|
||||||
"css": [("tempusdominus-bootstrap-4/css/"
|
|
||||||
"tempusdominus-bootstrap-4.min.css")],
|
|
||||||
"js": ["moment/moment-with-locales.min.js",
|
|
||||||
("tempusdominus-bootstrap-4/js/"
|
|
||||||
"tempusdominus-bootstrap-4.min.js")]},
|
|
||||||
"decimal.js": {"css": [], "js": ["decimal/decimal.min.js"]},
|
|
||||||
}
|
|
||||||
|
|
||||||
# The default static stylesheets to include. Default is none.
|
|
||||||
DEFAULT_CSS = ["css/app.css"]
|
|
||||||
# The default static JavaScript to include. Default is none.
|
|
||||||
DEFAULT_JS = ["js/app.js"]
|
|
||||||
|
|
||||||
# The regular accounts in the summary helper. They should be lists of tuples
|
|
||||||
# of (generic title, title format, account code).
|
|
||||||
#
|
|
||||||
# The following variables are available. Variables are surrounded in brackets.
|
|
||||||
#
|
|
||||||
# month_no: The numeric month of the current date
|
|
||||||
# month_name: The month name of the current date
|
|
||||||
# last_month_no: The numeric previous month of the current date
|
|
||||||
# last_month_name: The previous month name of the current date
|
|
||||||
# last_bimonthly_from_no: The first month number of the last bimonthly period
|
|
||||||
# last_bimonthly_from_name: The first month name of the last bimonthly period
|
|
||||||
# last_bimonthly_to_no: The second month number of the last bimonthly period
|
|
||||||
# last_bimonthly_to_name: The second month name of the last bimonthly period
|
|
||||||
#
|
|
||||||
REGULAR_ACCOUNTS = {
|
|
||||||
"debit": [
|
|
||||||
("Rent", "Rent for (month_name)", "6252"),
|
|
||||||
("Gas bill",
|
|
||||||
"Gas bill for (last_bimonthly_from_name)-(last_bimonthly_to_name)",
|
|
||||||
"6261"),
|
|
||||||
],
|
|
||||||
"credit": [
|
|
||||||
("Payroll", "Payroll for (last_month_name)", "46116"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Bugs and Supports
|
|
||||||
|
|
||||||
The `mia-accounting` project is hosted on GitHub.
|
|
||||||
|
|
||||||
https://github.com/imacat/mia-accounting
|
|
||||||
|
|
||||||
Address all bugs and support requests to imacat@mail.imacat.idv.tw.
|
|
||||||
|
|
||||||
## Copyright
|
|
||||||
|
|
||||||
```
|
|
||||||
Copyright (c) 2020 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.
|
|
||||||
```
|
|
338
README.rst
Normal file
338
README.rst
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
======================================
|
||||||
|
The Mia! Accounting Django Application
|
||||||
|
======================================
|
||||||
|
|
||||||
|
|
||||||
|
Obsolete Project
|
||||||
|
================
|
||||||
|
|
||||||
|
This project is obsolete. No further Django release will be
|
||||||
|
available. Check the current `Mia! Accounting project`_ for Flask_,
|
||||||
|
and the current `Mia! Accounting live demonstration`_.
|
||||||
|
|
||||||
|
.. _Mia! Accounting project: https://github.com/imacat/mia-accounting
|
||||||
|
.. _Flask: https://flask.palletsprojects.com
|
||||||
|
.. _Mia! Accounting live demonstration: https://accounting.imacat.idv.tw
|
||||||
|
|
||||||
|
|
||||||
|
Warning
|
||||||
|
=======
|
||||||
|
|
||||||
|
|
||||||
|
This is the last release of Django. It will be replaced by a new
|
||||||
|
Flask_ release, starting from scratch. Do not upgrade to the next
|
||||||
|
release, as it will not work.
|
||||||
|
|
||||||
|
This was my first large Python project, and at that time, I had zero
|
||||||
|
experience with Django. I ended up in a mess with Django MVT. The
|
||||||
|
code is unnecessarily complicated, and I do not actually know how the
|
||||||
|
views work anymore.
|
||||||
|
|
||||||
|
If you are new to the Mia! Accounting project, please skip this and
|
||||||
|
try the new release. You may contact me if you have problems with
|
||||||
|
the existing installation, but I may not be able to help you.
|
||||||
|
|
||||||
|
|
||||||
|
Description
|
||||||
|
===========
|
||||||
|
|
||||||
|
*mia-accounting* is a Django_ accounting application. It was
|
||||||
|
rewritten from my own private accounting application in Perl/mod_perl_
|
||||||
|
in 2007. The revision aims to be mobile-friendly with Bootstrap, with
|
||||||
|
a modern back-end framework and front-end technology like jQuery. The
|
||||||
|
first revision was in Perl/Mojolicious_ in 2019. This is the second
|
||||||
|
revision in Python/Django in 2020.
|
||||||
|
|
||||||
|
The Mia! Accounting Django application comes with two parts:
|
||||||
|
|
||||||
|
- The ``accounting`` application contains the main accounting
|
||||||
|
application.
|
||||||
|
|
||||||
|
- The ``mia_core`` application contains core shared libraries that are
|
||||||
|
used by the ``accounting`` application and my other applications.
|
||||||
|
|
||||||
|
You may try it in live demonstration at:
|
||||||
|
|
||||||
|
- URL: https://accounting-django.imacat.idv.tw/accounting
|
||||||
|
- Username: ``admin``
|
||||||
|
- Password: ``12345``
|
||||||
|
|
||||||
|
.. _Django: https://www.djangoproject.com
|
||||||
|
.. _mod_perl: https://perl.apache.org
|
||||||
|
.. _Mojolicious: https://mojolicious.org
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Install
|
||||||
|
-------
|
||||||
|
|
||||||
|
The Mia! Accounting Django application requires Python 3.7 and Django
|
||||||
|
3.1.
|
||||||
|
|
||||||
|
Install ``mia-accounting-django`` with ``pip``.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
pip install mia-accounting-django
|
||||||
|
|
||||||
|
``settings.py``
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Add these two applications in the ``INSTALL_APPS`` section of your
|
||||||
|
``settings.py``.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'mia_core.apps.MiaCoreConfig',
|
||||||
|
'accounting.apps.AccountingConfig',
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
Make sure the locale middleware is in the ``MIDDLEWARE`` section of
|
||||||
|
your ``settings.py``, and add it if it is not added yet.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
...
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
``urls.py``
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Add the ``accounting`` application in the ``urlpatterns`` in your
|
||||||
|
``urls.py``.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
...
|
||||||
|
path('accounting/', decorator_include(login_required, 'accounting.urls')),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
Make sure ``i18n`` and ``jsi18n`` are also in the ``urlpatterns`` of
|
||||||
|
your ``urls.py``, and add them if they are not added yet.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
...
|
||||||
|
path('i18n/', include("django.conf.urls.i18n")),
|
||||||
|
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
``base.html``
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Add the following to the very beginning of your base template
|
||||||
|
``base.html``, before your first real HTML tag.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
{% load mia_core %}
|
||||||
|
{% init_libs %}
|
||||||
|
{% block settings %}{% endblock %}
|
||||||
|
|
||||||
|
Add the CSS and JavaScripts in the ``<head>...</head>`` section of your
|
||||||
|
base template ``base.html``.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
{% for css in libs.css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% if css|is_static_url %}{% static css %}{% else %}{{ css }}{% endif %}" />
|
||||||
|
{% endfor %}
|
||||||
|
{% for js in libs.js %}
|
||||||
|
<script src="{% if js|is_static_url %}{% static js %}{% else %}{{ js }}{% endif %}"></script>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
Database Initialization
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Run the management commands to initialize the database.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
./manage.py migrate accounting
|
||||||
|
./manage.py accounting_accounts
|
||||||
|
|
||||||
|
Optionally you can populate the database with some sample data.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
./manage.py accounting_sample
|
||||||
|
|
||||||
|
Restart Your Web Server
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
And you are done.
|
||||||
|
|
||||||
|
|
||||||
|
Management Commands
|
||||||
|
===================
|
||||||
|
|
||||||
|
The following management commands are added by *the Mia! Accounting Django
|
||||||
|
application* to ``manage.py``:
|
||||||
|
|
||||||
|
``accounting_accounts``
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
% ./manage.py accounting_accounts [--user USER]
|
||||||
|
|
||||||
|
Fills the database with the accounting accounts.
|
||||||
|
|
||||||
|
- ``--user`` *USER*
|
||||||
|
|
||||||
|
An optional user to specify which user these initial accounts
|
||||||
|
belongs to. When omitted, the first user found in the system will
|
||||||
|
be used.
|
||||||
|
|
||||||
|
``accounting_sample``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
% ./manage.py accounting_sample [--user USER]
|
||||||
|
|
||||||
|
Fills the database with sample accounting data.
|
||||||
|
|
||||||
|
- ``--user`` *USER*
|
||||||
|
|
||||||
|
An optional user to specify which user these initial accounts
|
||||||
|
belongs to. When omitted, the first user found in the system will
|
||||||
|
be used.
|
||||||
|
|
||||||
|
``make_trans``
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
% ./manage.py make_trans --domain DOMAIN APP_DIR1 [APP_DIR2 ...]
|
||||||
|
|
||||||
|
Updates the revision date, converts the Traditional Chinese
|
||||||
|
translation into Simplified Chinese, and then calls the
|
||||||
|
``compilemessages`` command.
|
||||||
|
|
||||||
|
- ``--domain`` *DOMAIN*
|
||||||
|
|
||||||
|
The message domain, either ``django`` or ``djangojs``.
|
||||||
|
|
||||||
|
- *APP_DIR1* [*APP_DIR2* ...]
|
||||||
|
|
||||||
|
One or more application directories that contains their ``locale``
|
||||||
|
subdirectories.
|
||||||
|
|
||||||
|
|
||||||
|
Advanced Settings
|
||||||
|
=================
|
||||||
|
|
||||||
|
The following advanced settings are available in ``settings.py``.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
# Settings for the accounting application
|
||||||
|
ACCOUNTING = {
|
||||||
|
# The default cash account, for ex., "0" (current assets and liabilities),
|
||||||
|
# "1111" (cash on hand), "1113" (cash in banks) or any
|
||||||
|
"DEFAULT_CASH_ACCOUNT": "1111",
|
||||||
|
# The shortcut cash accounts
|
||||||
|
"CASH_SHORTCUT_ACCOUNTS": ["0", "1111"],
|
||||||
|
# The default ledger account
|
||||||
|
"DEFAULT_LEDGER_ACCOUNT": "1111",
|
||||||
|
# The payable accounts to track
|
||||||
|
"PAYABLE_ACCOUNTS": ["2141"],
|
||||||
|
# The asset accounts to track
|
||||||
|
"EQUIPMENT_ACCOUNTS": ["1441"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# The local static CSS and JavaScript libraries
|
||||||
|
# The default is to use the libraries from CDN. You may set them to use the
|
||||||
|
# local static copies of these libraries
|
||||||
|
STATIC_LIBS = {
|
||||||
|
"jquery": {"css": [], "js": ["jquery/jquery-3.5.1.min.js"]},
|
||||||
|
"bootstrap4": {"css": ["bootstrap4/css/bootstrap.min.css"],
|
||||||
|
"js": ["bootstrap4/js/bootstrap.bundle.min.js"]},
|
||||||
|
"font-awesome-5": {"css": ["font-awesome-5/css/all.min.css"],
|
||||||
|
"js": []},
|
||||||
|
"bootstrap4-datatables": {
|
||||||
|
"css": ["datatables/css/jquery.dataTables.min.css",
|
||||||
|
"datatables/css/dataTables.bootstrap4.min.css"],
|
||||||
|
"js": ["datatables/js/jquery.dataTables.min.js",
|
||||||
|
"datatables/js/dataTables.bootstrap4.min.js"]},
|
||||||
|
"jquery-ui": {"css": ["jquery-ui/jquery-ui.min.css"],
|
||||||
|
"js": ["jquery-ui/jquery-ui.min.js"]},
|
||||||
|
"bootstrap4-tempusdominus": {
|
||||||
|
"css": [("tempusdominus-bootstrap-4/css/"
|
||||||
|
"tempusdominus-bootstrap-4.min.css")],
|
||||||
|
"js": ["moment/moment-with-locales.min.js",
|
||||||
|
("tempusdominus-bootstrap-4/js/"
|
||||||
|
"tempusdominus-bootstrap-4.min.js")]},
|
||||||
|
"decimal.js": {"css": [], "js": ["decimal/decimal.min.js"]},
|
||||||
|
}
|
||||||
|
|
||||||
|
# The default static stylesheets to include. Default is none.
|
||||||
|
DEFAULT_CSS = ["css/app.css"]
|
||||||
|
# The default static JavaScript to include. Default is none.
|
||||||
|
DEFAULT_JS = ["js/app.js"]
|
||||||
|
|
||||||
|
# The regular accounts in the summary helper. They should be lists of tuples
|
||||||
|
# of (generic title, summary format, account code).
|
||||||
|
#
|
||||||
|
# The following variables are available. Variables are surrounded in brackets.
|
||||||
|
#
|
||||||
|
# month_no: The numeric month of the current date
|
||||||
|
# month_name: The month name of the current date
|
||||||
|
# last_month_no: The numeric previous month of the current date
|
||||||
|
# last_month_name: The previous month name of the current date
|
||||||
|
# last_bimonthly_from_no: The first month number of the last bimonthly period
|
||||||
|
# last_bimonthly_from_name: The first month name of the last bimonthly period
|
||||||
|
# last_bimonthly_to_no: The second month number of the last bimonthly period
|
||||||
|
# last_bimonthly_to_name: The second month name of the last bimonthly period
|
||||||
|
#
|
||||||
|
REGULAR_ACCOUNTS = {
|
||||||
|
"debit": [
|
||||||
|
("Rent", "Rent for (month_name)", "6252"),
|
||||||
|
("Gas bill",
|
||||||
|
"Gas bill for (last_bimonthly_from_name)-(last_bimonthly_to_name)",
|
||||||
|
"6261"),
|
||||||
|
],
|
||||||
|
"credit": [
|
||||||
|
("Payroll", "Payroll for (last_month_name)", "46116"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Bugs and Supports
|
||||||
|
=================
|
||||||
|
|
||||||
|
The Mia! Accounting Django application is hosted on GitHub.
|
||||||
|
|
||||||
|
https://github.com/imacat/mia-accounting-django
|
||||||
|
|
||||||
|
Address all bugs and support requests to imacat@mail.imacat.idv.tw.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright
|
||||||
|
=========
|
||||||
|
|
||||||
|
Copyright (c) 2020-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.
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = source
|
||||||
|
BUILDDIR = build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set SOURCEDIR=source
|
||||||
|
set BUILDDIR=build
|
||||||
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.https://www.sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:help
|
||||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
|
||||||
|
:end
|
||||||
|
popd
|
4
docs/requirements.txt
Normal file
4
docs/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
django
|
||||||
|
django-dirtyfields
|
||||||
|
titlecase
|
||||||
|
django-decorator-include
|
21
docs/source/accounting.migrations.rst
Normal file
21
docs/source/accounting.migrations.rst
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
accounting.migrations package
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
accounting.migrations.0001\_initial module
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.migrations.0001_initial
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.migrations
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
94
docs/source/accounting.rst
Normal file
94
docs/source/accounting.rst
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
accounting package
|
||||||
|
==================
|
||||||
|
|
||||||
|
Subpackages
|
||||||
|
-----------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
accounting.migrations
|
||||||
|
accounting.templatetags
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
accounting.apps module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.apps
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.converters module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.converters
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.forms module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.forms
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.models module
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.models
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.tests module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.tests
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.urls module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.urls
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.utils module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.utils
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.validators module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.validators
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.views module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.views
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: accounting
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
21
docs/source/accounting.templatetags.rst
Normal file
21
docs/source/accounting.templatetags.rst
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
accounting.templatetags package
|
||||||
|
===============================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
accounting.templatetags.accounting module
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.templatetags.accounting
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.templatetags
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
36
docs/source/conf.py
Normal file
36
docs/source/conf.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath('../../src/'))
|
||||||
|
sys.path.append(os.path.abspath('../../tests/test_site/'))
|
||||||
|
os.environ["DJANGO_SETTINGS_MODULE"] = "test_site.settings"
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
|
project = 'Mia! Accounting Django'
|
||||||
|
copyright = '2022-2023, imacat'
|
||||||
|
author = 'imacat'
|
||||||
|
release = '0.2.0'
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
|
extensions = ["sphinx.ext.autodoc"]
|
||||||
|
|
||||||
|
templates_path = ['_templates']
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
html_static_path = ['_static']
|
20
docs/source/index.rst
Normal file
20
docs/source/index.rst
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.. Mia! Accounting Django documentation master file, created by
|
||||||
|
sphinx-quickstart on Thu Dec 8 00:42:14 2022.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
Welcome to Mia! Accounting Django's documentation!
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
10
docs/source/mia_core.migrations.rst
Normal file
10
docs/source/mia_core.migrations.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
mia\_core.migrations package
|
||||||
|
============================
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core.migrations
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
70
docs/source/mia_core.rst
Normal file
70
docs/source/mia_core.rst
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
mia\_core package
|
||||||
|
=================
|
||||||
|
|
||||||
|
Subpackages
|
||||||
|
-----------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
mia_core.migrations
|
||||||
|
mia_core.templatetags
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
mia\_core.apps module
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core.apps
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
mia\_core.models module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core.models
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
mia\_core.period module
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core.period
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
mia\_core.tests module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core.tests
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
mia\_core.utils module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core.utils
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
mia\_core.views module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core.views
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
21
docs/source/mia_core.templatetags.rst
Normal file
21
docs/source/mia_core.templatetags.rst
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
mia\_core.templatetags package
|
||||||
|
==============================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
mia\_core.templatetags.mia\_core module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core.templatetags.mia_core
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: mia_core.templatetags
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
8
docs/source/modules.rst
Normal file
8
docs/source/modules.rst
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
src
|
||||||
|
===
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
accounting
|
||||||
|
mia_core
|
@ -1,121 +0,0 @@
|
|||||||
# The core application of the Mia project.
|
|
||||||
# by imacat <imacat@mail.imacat.idv.tw>, 2020/7/24
|
|
||||||
|
|
||||||
# Copyright (c) 2020 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 session-based POST data storage management of the Mia core application.
|
|
||||||
|
|
||||||
"""
|
|
||||||
import random
|
|
||||||
from typing import Dict, Mapping, Any, Optional
|
|
||||||
|
|
||||||
from django.http import HttpResponseRedirect, HttpRequest
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
|
|
||||||
from .utils import UrlBuilder
|
|
||||||
|
|
||||||
STORAGE_KEY: str = "stored_post"
|
|
||||||
|
|
||||||
|
|
||||||
def error_redirect(request: HttpRequest, url: str,
|
|
||||||
post: Dict[str, str]) -> HttpResponseRedirect:
|
|
||||||
"""Redirects to a specific URL on error, with the POST data ID appended
|
|
||||||
as the query parameter "s". The POST data can be loaded with the
|
|
||||||
get_previous_post() utility.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The request.
|
|
||||||
url (str): The destination URL.
|
|
||||||
post (dict[str]): The POST data.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HttpResponseRedirect: The redirect response.
|
|
||||||
"""
|
|
||||||
post_id = _store(request, post)
|
|
||||||
return redirect(str(UrlBuilder(url).query(s=post_id)))
|
|
||||||
|
|
||||||
|
|
||||||
def get_previous_post(request: HttpRequest) -> Optional[Dict[str, str]]:
|
|
||||||
"""Retrieves the previously-stored POST data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The request.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: The previously-stored POST data.
|
|
||||||
"""
|
|
||||||
if "s" not in request.GET:
|
|
||||||
return None
|
|
||||||
return _retrieve(request, request.GET["s"])
|
|
||||||
|
|
||||||
|
|
||||||
def _store(request: HttpRequest, post: Dict[str, str]) -> str:
|
|
||||||
"""Stores the POST data into the session, and returns the POST data ID that
|
|
||||||
can be used to retrieve it later with _retrieve().
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: The request.
|
|
||||||
post: The POST data.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The POST data ID
|
|
||||||
"""
|
|
||||||
if STORAGE_KEY not in request.session:
|
|
||||||
request.session[STORAGE_KEY] = {}
|
|
||||||
post_id = _new_post_id(request.session[STORAGE_KEY])
|
|
||||||
request.session[STORAGE_KEY][post_id] = post
|
|
||||||
return post_id
|
|
||||||
|
|
||||||
|
|
||||||
def _retrieve(request: HttpRequest, post_id: str) -> Optional[Dict[str, str]]:
|
|
||||||
"""Retrieves the POST data from the storage.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: The request.
|
|
||||||
post_id: The POST data ID.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The POST data, or None if the corresponding data does not exist.
|
|
||||||
"""
|
|
||||||
if STORAGE_KEY not in request.session:
|
|
||||||
return None
|
|
||||||
if post_id not in request.session[STORAGE_KEY]:
|
|
||||||
return None
|
|
||||||
return request.session[STORAGE_KEY][post_id]
|
|
||||||
|
|
||||||
|
|
||||||
def _new_post_id(post_store: Mapping[int, Any]) -> str:
|
|
||||||
"""Generates and returns a new POST ID that does not exist yet.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
post_store (dict): The POST storage.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The newly-generated POST ID.
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
post_id = ""
|
|
||||||
while len(post_id) < 16:
|
|
||||||
n = random.randint(1, 64)
|
|
||||||
if n < 26:
|
|
||||||
post_id = post_id + chr(ord("a") + n)
|
|
||||||
elif n < 52:
|
|
||||||
post_id = post_id + chr(ord("a") + (n - 26))
|
|
||||||
elif n < 62:
|
|
||||||
post_id = post_id + chr(ord("0") + (n - 52))
|
|
||||||
else:
|
|
||||||
post_id = post_id + "-_."[n - 62]
|
|
||||||
if post_id not in post_store:
|
|
||||||
return post_id
|
|
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# The accounting application of the Mia project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/23
|
||||||
|
|
||||||
|
# Copyright (c) 2022 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.
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=42"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
51
setup.cfg
Normal file
51
setup.cfg
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# The setup.cfg
|
||||||
|
# by imacat <imacat@mail.imacat.idv.tw>, 2020/9/7
|
||||||
|
|
||||||
|
# Copyright (c) 2020-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.2.0
|
||||||
|
author = imacat
|
||||||
|
author_email = imacat@mail.imacat.idv.tw
|
||||||
|
description = The Mia! Accounting Django project.
|
||||||
|
long_description = file: README.rst
|
||||||
|
long_description_content_type = text/x-rst
|
||||||
|
url = https://github.com/imacat/mia-accounting-django
|
||||||
|
project_urls =
|
||||||
|
Bug Tracker = https://github.com/imacat/mia-accounting-django/issues
|
||||||
|
classifiers =
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
License :: OSI Approved :: Apache Software License
|
||||||
|
Operating System :: OS Independent
|
||||||
|
Framework :: Django
|
||||||
|
Topic :: Office/Business :: Financial :: Accounting
|
||||||
|
Intended Audience :: End Users/Desktop
|
||||||
|
|
||||||
|
[options]
|
||||||
|
package_dir =
|
||||||
|
= src
|
||||||
|
packages = find:
|
||||||
|
python_requires = >=3.6
|
||||||
|
install_requires =
|
||||||
|
django
|
||||||
|
django-dirtyfields
|
||||||
|
titlecase
|
||||||
|
django-decorator-include
|
||||||
|
tests_require =
|
||||||
|
unittest
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
where = src
|
42
setup.py
42
setup.py
@ -1,42 +0,0 @@
|
|||||||
# The setup.py
|
|
||||||
# by imacat <imacat@mail.imacat.idv.tw>, 2020/9/7
|
|
||||||
|
|
||||||
# Copyright (c) 2020 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.
|
|
||||||
import setuptools
|
|
||||||
|
|
||||||
with open("README.md", "r") as fh:
|
|
||||||
long_description = fh.read()
|
|
||||||
|
|
||||||
setuptools.setup(
|
|
||||||
name="mia-accounting",
|
|
||||||
version="0.0.2",
|
|
||||||
author="imacat",
|
|
||||||
author_email="imacat@mail.imacat.idv.tw",
|
|
||||||
description="A Django accounting application",
|
|
||||||
long_description=long_description,
|
|
||||||
long_description_content_type="text/markdown",
|
|
||||||
url="https://github.com/imacat/mia-accounting",
|
|
||||||
packages=setuptools.find_packages(),
|
|
||||||
classifiers=[
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"License :: OSI Approved :: Apache Software License",
|
|
||||||
"Framework :: Django :: 3.0",
|
|
||||||
"Topic :: Office/Business :: Financial :: Accounting",
|
|
||||||
"Operating System :: OS Independent",
|
|
||||||
],
|
|
||||||
python_requires=">=3.6",
|
|
||||||
install_requires=["django", "django-dirtyfields", "titlecase",
|
|
||||||
"django-decorator-include"],
|
|
||||||
)
|
|
@ -40,9 +40,9 @@ class TransactionTypeConverter:
|
|||||||
|
|
||||||
class PeriodConverter:
|
class PeriodConverter:
|
||||||
"""The path converter for the period."""
|
"""The path converter for the period."""
|
||||||
regex = ("([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)|"
|
regex = (r"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)|"
|
||||||
"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?-"
|
r"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?-"
|
||||||
"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?")
|
r"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?")
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
"""Returns the period by the period specification.
|
"""Returns the period by the period specification.
|
||||||
@ -90,7 +90,7 @@ class DateConverter:
|
|||||||
Returns:
|
Returns:
|
||||||
datetime.date: The date.
|
datetime.date: The date.
|
||||||
"""
|
"""
|
||||||
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$", value)
|
m = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", value)
|
||||||
year = int(m.group(1))
|
year = int(m.group(1))
|
||||||
month = int(m.group(2))
|
month = int(m.group(2))
|
||||||
day = int(m.group(3))
|
day = int(m.group(3))
|
@ -205,8 +205,8 @@ class TransactionForm(forms.Form):
|
|||||||
by_rec_id = {}
|
by_rec_id = {}
|
||||||
for key in args[0].keys():
|
for key in args[0].keys():
|
||||||
m = re.match(
|
m = re.match(
|
||||||
("^((debit|credit)-([1-9][0-9]*))-"
|
(r"^((debit|credit)-([1-9]\d*))-"
|
||||||
"(id|ord|account|summary|amount)$"),
|
r"(id|ord|account|summary|amount)$"),
|
||||||
key)
|
key)
|
||||||
if m is None:
|
if m is None:
|
||||||
continue
|
continue
|
||||||
@ -271,7 +271,7 @@ class TransactionForm(forms.Form):
|
|||||||
}
|
}
|
||||||
for key in post.keys():
|
for key in post.keys():
|
||||||
m = re.match(
|
m = re.match(
|
||||||
"^(debit|credit)-([1-9][0-9]*)-(id|ord|account|summary|amount)",
|
r"^(debit|credit)-([1-9]\d*)-(id|ord|account|summary|amount)",
|
||||||
key)
|
key)
|
||||||
if m is None:
|
if m is None:
|
||||||
continue
|
continue
|
||||||
@ -303,7 +303,7 @@ class TransactionForm(forms.Form):
|
|||||||
= post[F"{record_type}-{old_no}-{attr}"]
|
= post[F"{record_type}-{old_no}-{attr}"]
|
||||||
# Purges the old form and fills it with the new form
|
# Purges the old form and fills it with the new form
|
||||||
for x in [x for x in post.keys() if re.match(
|
for x in [x for x in post.keys() if re.match(
|
||||||
"^(debit|credit)-([1-9][0-9]*)-(id|ord|account|summary|amount)",
|
r"^(debit|credit)-([1-9]\d*)-(id|ord|account|summary|amount)",
|
||||||
x)]:
|
x)]:
|
||||||
del post[x]
|
del post[x]
|
||||||
for key in new_post.keys():
|
for key in new_post.keys():
|
||||||
@ -475,11 +475,11 @@ class TransactionSortForm(forms.Form):
|
|||||||
key = F"transaction-{txn.pk}-ord"
|
key = F"transaction-{txn.pk}-ord"
|
||||||
if key not in post:
|
if key not in post:
|
||||||
post_orders.append(form.Order(txn, 9999))
|
post_orders.append(form.Order(txn, 9999))
|
||||||
elif not re.match("^[0-9]+$", post[key]):
|
elif not re.match(r"^\d+$", post[key]):
|
||||||
post_orders.append(form.Order(txn, 9999))
|
post_orders.append(form.Order(txn, 9999))
|
||||||
else:
|
else:
|
||||||
post_orders.append(form.Order(txn, int(post[key])))
|
post_orders.append(form.Order(txn, int(post[key])))
|
||||||
post_orders.sort(key=lambda x: (x.order, x.txn.ord))
|
post_orders.sort(key=lambda x: (x.ord, x.txn.ord))
|
||||||
form.txn_orders = []
|
form.txn_orders = []
|
||||||
for i in range(len(post_orders)):
|
for i in range(len(post_orders)):
|
||||||
form.txn_orders.append(form.Order(post_orders[i].txn, i + 1))
|
form.txn_orders.append(form.Order(post_orders[i].txn, i + 1))
|
||||||
@ -488,9 +488,9 @@ class TransactionSortForm(forms.Form):
|
|||||||
|
|
||||||
class Order:
|
class Order:
|
||||||
"""A transaction order"""
|
"""A transaction order"""
|
||||||
def __init__(self, txn: Transaction, order: int):
|
def __init__(self, txn: Transaction, ord: int):
|
||||||
self.txn = txn
|
self.txn = txn
|
||||||
self.order = order
|
self.ord = ord
|
||||||
|
|
||||||
|
|
||||||
class AccountForm(forms.Form):
|
class AccountForm(forms.Form):
|
||||||
@ -603,7 +603,6 @@ class AccountForm(forms.Form):
|
|||||||
code="code_unique")
|
code="code_unique")
|
||||||
self.add_error("code", error)
|
self.add_error("code", error)
|
||||||
raise error
|
raise error
|
||||||
return
|
|
||||||
|
|
||||||
def _validate_code_descendant_code_size(self) -> None:
|
def _validate_code_descendant_code_size(self) -> None:
|
||||||
"""Validates whether the codes of the descendants will be too long.
|
"""Validates whether the codes of the descendants will be too long.
|
@ -20,7 +20,7 @@
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import getpass
|
import getpass
|
||||||
import random
|
from secrets import randbelow
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
@ -98,20 +98,20 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
self._filler.add_expense_transaction(
|
self._filler.add_expense_transaction(
|
||||||
-2,
|
-2,
|
||||||
[(6272, _("Lunch—Spaghetti"), random.randint(40, 200)),
|
[(6272, _("Lunch—Spaghetti"), 40 + randbelow(160)),
|
||||||
(6272, _("Drink—Tea"), random.randint(40, 200))])
|
(6272, _("Drink—Tea"), 40 + randbelow(160))])
|
||||||
self._filler.add_expense_transaction(
|
self._filler.add_expense_transaction(
|
||||||
-1,
|
-1,
|
||||||
([(6272, _("Lunch—Pizza"), random.randint(40, 200)),
|
([(6272, _("Lunch—Pizza"), 40 + randbelow(160)),
|
||||||
(6272, _("Drink—Tea"), random.randint(40, 200))]))
|
(6272, _("Drink—Tea"), 40 + randbelow(160))]))
|
||||||
self._filler.add_expense_transaction(
|
self._filler.add_expense_transaction(
|
||||||
-1,
|
-1,
|
||||||
[(6272, _("Lunch—Spaghetti"), random.randint(40, 200)),
|
[(6272, _("Lunch—Spaghetti"), 40 + randbelow(160)),
|
||||||
(6272, _("Drink—Soda"), random.randint(40, 200))])
|
(6272, _("Drink—Soda"), 40 + randbelow(160))])
|
||||||
self._filler.add_expense_transaction(
|
self._filler.add_expense_transaction(
|
||||||
0,
|
0,
|
||||||
[(6272, _("Lunch—Salad"), random.randint(40, 200)),
|
[(6272, _("Lunch—Salad"), 40 + randbelow(160)),
|
||||||
(6272, _("Drink—Coffee"), random.randint(40, 200))])
|
(6272, _("Drink—Coffee"), 40 + randbelow(160))])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user(username_option):
|
def get_user(username_option):
|
||||||
@ -155,7 +155,7 @@ class Command(BaseCommand):
|
|||||||
payday = today.replace(day=5)
|
payday = today.replace(day=5)
|
||||||
if payday > today:
|
if payday > today:
|
||||||
payday = self.previous_month(payday)
|
payday = self.previous_month(payday)
|
||||||
for i in range(months):
|
for _ in range(months):
|
||||||
self.add_payroll(payday)
|
self.add_payroll(payday)
|
||||||
payday = self.previous_month(payday)
|
payday = self.previous_month(payday)
|
||||||
|
|
||||||
@ -181,7 +181,7 @@ class Command(BaseCommand):
|
|||||||
Args:
|
Args:
|
||||||
payday: The payday.
|
payday: The payday.
|
||||||
"""
|
"""
|
||||||
income = random.randint(40000, 50000)
|
income = 40000 + randbelow(10000)
|
||||||
pension = 882 if income <= 40100\
|
pension = 882 if income <= 40100\
|
||||||
else 924 if income <= 42000\
|
else 924 if income <= 42000\
|
||||||
else 966 if income <= 43900\
|
else 966 if income <= 43900\
|
@ -173,7 +173,8 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
|||||||
self.ord = 1 if max_ord is None else max_ord + 1
|
self.ord = 1 if max_ord is None else max_ord + 1
|
||||||
# Collects the records to be deleted
|
# Collects the records to be deleted
|
||||||
to_keep = [x.pk for x in self.records if x.pk is not None]
|
to_keep = [x.pk for x in self.records if x.pk is not None]
|
||||||
to_delete = [x for x in self.record_set.all() if x.pk not in to_keep]
|
to_delete = [] if self.pk is None \
|
||||||
|
else [x for x in self.record_set.all() if x.pk not in to_keep]
|
||||||
to_save = [x for x in self.records
|
to_save = [x for x in self.records
|
||||||
if x.is_dirty(check_relationship=True)]
|
if x.is_dirty(check_relationship=True)]
|
||||||
for record in to_save:
|
for record in to_save:
|
||||||
@ -215,7 +216,7 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
|||||||
txn_type: The transaction type.
|
txn_type: The transaction type.
|
||||||
"""
|
"""
|
||||||
self.old_date = self.date
|
self.old_date = self.date
|
||||||
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$", post["date"])
|
m = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", post["date"])
|
||||||
self.date = datetime.date(
|
self.date = datetime.date(
|
||||||
int(m.group(1)),
|
int(m.group(1)),
|
||||||
int(m.group(2)),
|
int(m.group(2)),
|
||||||
@ -228,7 +229,8 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
|||||||
for i in range(max_no[record_type]):
|
for i in range(max_no[record_type]):
|
||||||
no = i + 1
|
no = i + 1
|
||||||
if F"{record_type}-{no}-id" in post:
|
if F"{record_type}-{no}-id" in post:
|
||||||
record = Record.objects.get(pk=post[F"{record_type}-{no}-id"])
|
record = Record.objects.get(
|
||||||
|
pk=post[F"{record_type}-{no}-id"])
|
||||||
else:
|
else:
|
||||||
record = Record(
|
record = Record(
|
||||||
is_credit=(record_type == "credit"),
|
is_credit=(record_type == "credit"),
|
||||||
@ -267,12 +269,11 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
|||||||
"""Finds the max debit and record numbers from the POSTed form.
|
"""Finds the max debit and record numbers from the POSTed form.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
txn_type (str): The transaction type.
|
txn_type: The transaction type.
|
||||||
post (dict[str,str]): The POSTed data.
|
post: The POSTed data.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict[str,int]: The max debit and record numbers from the POSTed form.
|
The max debit and record numbers from the POSTed form.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
max_no = {}
|
max_no = {}
|
||||||
if txn_type != "credit":
|
if txn_type != "credit":
|
||||||
@ -281,7 +282,8 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
|||||||
max_no["credit"] = 0
|
max_no["credit"] = 0
|
||||||
for key in post.keys():
|
for key in post.keys():
|
||||||
m = re.match(
|
m = re.match(
|
||||||
"^(debit|credit)-([1-9][0-9]*)-(id|ord|account|summary|amount)$",
|
(r"^(debit|credit)-([1-9]\d*)-"
|
||||||
|
r"(id|ord|account|summary|amount)$"),
|
||||||
key)
|
key)
|
||||||
if m is None:
|
if m is None:
|
||||||
continue
|
continue
|
||||||
@ -301,8 +303,11 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
|||||||
List[Record]: The records.
|
List[Record]: The records.
|
||||||
"""
|
"""
|
||||||
if self._records is None:
|
if self._records is None:
|
||||||
self._records = list(self.record_set.all())
|
if self.pk is None:
|
||||||
self._records.sort(key=lambda x: (x.is_credit, x.ord))
|
self._records = []
|
||||||
|
else:
|
||||||
|
self._records = list(self.record_set.all())
|
||||||
|
self._records.sort(key=lambda x: (x.is_credit, x.ord))
|
||||||
return self._records
|
return self._records
|
||||||
|
|
||||||
@records.setter
|
@records.setter
|
@ -161,8 +161,6 @@
|
|||||||
font-size: 1.21em;
|
font-size: 1.21em;
|
||||||
border-bottom: thick double slategray;
|
border-bottom: thick double slategray;
|
||||||
}
|
}
|
||||||
.balance-sheet-table tbody {
|
|
||||||
}
|
|
||||||
.balance-sheet-table .group-title {
|
.balance-sheet-table .group-title {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
@ -50,8 +50,8 @@ let accounts;
|
|||||||
*/
|
*/
|
||||||
function getAllAccounts() {
|
function getAllAccounts() {
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
request.onreadystatechange = function() {
|
request.onload = function() {
|
||||||
if (this.readyState === 4 && this.status === 200) {
|
if (this.status === 200) {
|
||||||
accounts = JSON.parse(this.responseText);
|
accounts = JSON.parse(this.responseText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -75,7 +75,7 @@ function updateParent(code) {
|
|||||||
parent.text(gettext("Topmost"));
|
parent.text(gettext("Topmost"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentCode = code.value.substr(0, code.value.length - 1);
|
const parentCode = code.value.substring(0, code.value.length - 1);
|
||||||
if (parentCode in accounts) {
|
if (parentCode in accounts) {
|
||||||
parent.text(parentCode + " " + accounts[parentCode]);
|
parent.text(parentCode + " " + accounts[parentCode]);
|
||||||
return;
|
return;
|
||||||
@ -96,10 +96,10 @@ function updateParent(code) {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function validateForm() {
|
function validateForm() {
|
||||||
let isValidated = true;
|
let isValid = true;
|
||||||
isValidated = isValidated && validateCode();
|
isValid = validateCode() && isValid;
|
||||||
isValidated = isValidated && validateTitle();
|
isValid = validateTitle() && isValid;
|
||||||
return isValidated;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,7 +136,7 @@ function validateCode() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const parentCode = code.value.substr(0, code.value.length - 1);
|
const parentCode = code.value.substring(0, code.value.length - 1);
|
||||||
if (!(parentCode in accounts)) {
|
if (!(parentCode in accounts)) {
|
||||||
code.classList.add("is-invalid");
|
code.classList.add("is-invalid");
|
||||||
errorMessage.text(gettext("The parent account of this code does not exist."));
|
errorMessage.text(gettext("The parent account of this code does not exist."));
|
@ -298,7 +298,7 @@ function parseSummaryForHelper(summary) {
|
|||||||
const pos = summary.lastIndexOf("×");
|
const pos = summary.lastIndexOf("×");
|
||||||
let count = 1;
|
let count = 1;
|
||||||
if (pos !== -1) {
|
if (pos !== -1) {
|
||||||
count = parseInt(summary.substr(pos + 1));
|
count = parseInt(summary.substring(pos + 1));
|
||||||
}
|
}
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
count = 1;
|
count = 1;
|
||||||
@ -329,7 +329,7 @@ function parseSummaryForCategoryHelpers(summary) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// A bus route
|
// A bus route
|
||||||
const matchBus = summary.match(/^(.+)—(.+)—(.+)→(.+?)(?:×[0-9]+)?$/);
|
const matchBus = summary.match(/^([^—]+)—([^—]+)—([^—]+)→([^—]+?)(?:×\d+)?$/);
|
||||||
if (matchBus !== null) {
|
if (matchBus !== null) {
|
||||||
$("#summary-bus-category").get(0).value = matchBus[1];
|
$("#summary-bus-category").get(0).value = matchBus[1];
|
||||||
setSummaryBusCategoryButtons(matchBus[1]);
|
setSummaryBusCategoryButtons(matchBus[1]);
|
||||||
@ -342,7 +342,7 @@ function parseSummaryForCategoryHelpers(summary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A general travel route
|
// A general travel route
|
||||||
const matchTravel = summary.match(/^(.+)—(.+)([→|↔])(.+?)(?:×[0-9]+)?$/);
|
const matchTravel = summary.match(/^([^—]+)—([^—]+)([→|↔])([^—]+?)(?:×\d+)?$/);
|
||||||
if (matchTravel !== null) {
|
if (matchTravel !== null) {
|
||||||
$("#summary-travel-category").get(0).value = matchTravel[1];
|
$("#summary-travel-category").get(0).value = matchTravel[1];
|
||||||
setSummaryTravelCategoryButtons(matchTravel[1]);
|
setSummaryTravelCategoryButtons(matchTravel[1]);
|
||||||
@ -357,7 +357,7 @@ function parseSummaryForCategoryHelpers(summary) {
|
|||||||
|
|
||||||
// A general category
|
// A general category
|
||||||
const generalCategoryTab = $("#summary-tab-category");
|
const generalCategoryTab = $("#summary-tab-category");
|
||||||
const matchCategory = summary.match(/^(.+)—.+(?:×[0-9]+)?$/);
|
const matchCategory = summary.match(/^([^—]+)—.+(?:×\d+)?$/);
|
||||||
if (matchCategory !== null) {
|
if (matchCategory !== null) {
|
||||||
$("#summary-general-category").get(0).value = matchCategory[1];
|
$("#summary-general-category").get(0).value = matchCategory[1];
|
||||||
setSummaryGeneralCategoryButtons(matchCategory[1]);
|
setSummaryGeneralCategoryButtons(matchCategory[1]);
|
@ -91,8 +91,8 @@ let accountOptions;
|
|||||||
*/
|
*/
|
||||||
function getAccountOptions() {
|
function getAccountOptions() {
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
request.onreadystatechange = function() {
|
request.onload = function() {
|
||||||
if (this.readyState === 4 && this.status === 200) {
|
if (this.status === 200) {
|
||||||
accountOptions = JSON.parse(this.responseText);
|
accountOptions = JSON.parse(this.responseText);
|
||||||
$(".record-account").each(function () {
|
$(".record-account").each(function () {
|
||||||
initializeAccountOptions($(this));
|
initializeAccountOptions($(this));
|
||||||
@ -184,8 +184,8 @@ function updateTotalAmount(element) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
total = String(total);
|
total = String(total);
|
||||||
while (total.match(/^[1-9][0-9]*[0-9]{3}/)) {
|
while (total.match(/^[1-9]\d*\d{3}/)) {
|
||||||
total = total.replace(/^([1-9][0-9]*)([0-9]{3})/, "$1,$2");
|
total = total.replace(/^([1-9]\d*)(\d{3})/, "$1,$2");
|
||||||
}
|
}
|
||||||
$("#" + type + "-total").text(total);
|
$("#" + type + "-total").text(total);
|
||||||
}
|
}
|
||||||
@ -337,28 +337,28 @@ function resetRecordButtons() {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function validateForm() {
|
function validateForm() {
|
||||||
let isValidated = true;
|
let isValid = true;
|
||||||
isValidated = isValidated && validateDate();
|
isValid = validateDate() && isValid;
|
||||||
$(".debit-record").each(function () {
|
$(".debit-record").each(function () {
|
||||||
isValidated = isValidated && validateRecord(this);
|
isValid = validateRecord(this) && isValid;
|
||||||
});
|
});
|
||||||
$(".credit-account").each(function () {
|
$(".credit-account").each(function () {
|
||||||
isValidated = isValidated && validateRecord(this);
|
isValid = validateRecord(this) && isValid;
|
||||||
});
|
});
|
||||||
$(".record-account").each(function () {
|
$(".record-account").each(function () {
|
||||||
isValidated = isValidated && validateAccount(this);
|
isValid = validateAccount(this) && isValid;
|
||||||
});
|
});
|
||||||
$(".record-summary").each(function () {
|
$(".record-summary").each(function () {
|
||||||
isValidated = isValidated && validateSummary(this);
|
isValid = validateSummary(this) && isValid;
|
||||||
});
|
});
|
||||||
$(".record-amount").each(function () {
|
$(".record-amount").each(function () {
|
||||||
isValidated = isValidated && validateAmount(this);
|
isValid = validateAmount(this) && isValid;
|
||||||
});
|
});
|
||||||
if (isTransfer()) {
|
if (isTransfer()) {
|
||||||
isValidated = isValidated && validateBalance();
|
isValid = validateBalance() && isValid;
|
||||||
}
|
}
|
||||||
isValidated = isValidated && validateNote();
|
isValid = validateNote() && isValid;
|
||||||
return isValidated;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -42,7 +42,7 @@ First written: 2020/8/8
|
|||||||
<form action="{% url "accounting:accounts.delete" account as url %}{% url_keep_return url %}" method="post">
|
<form action="{% url "accounting:accounts.delete" account as url %}{% url_keep_return url %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<!-- The Modal -->
|
<!-- The Modal -->
|
||||||
<div class="modal" id="del-modal">
|
<div class="modal fade" id="del-modal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
||||||
@ -57,8 +57,8 @@ First written: 2020/8/8
|
|||||||
|
|
||||||
<!-- Modal footer -->
|
<!-- Modal footer -->
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
||||||
|
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -19,6 +19,7 @@ form-record-non-transfer.html: The template for a record in the non-transfer tra
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2020/8/5
|
First written: 2020/8/5
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
{% load accounting %}
|
||||||
|
|
||||||
<li id="{{ record_type }}-{{ no }}" class="list-group-item {% if record.non_field_errors %} list-group-item-danger {% endif %} draggable-record {{ record_type }}-record" data-no="{{ no }}">
|
<li id="{{ record_type }}-{{ no }}" class="list-group-item {% if record.non_field_errors %} list-group-item-danger {% endif %} draggable-record {{ record_type }}-record" data-no="{{ no }}">
|
||||||
<div id="{{ record_type }}-{{ no }}-error">{% if record.non_field_errors %}{{ record.non_field_errors.0 }}{% endif %}</div>
|
<div id="{{ record_type }}-{{ no }}-error">{% if record.non_field_errors %}{{ record.non_field_errors.0 }}{% endif %}</div>
|
||||||
@ -37,7 +38,7 @@ First written: 2020/8/5
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<label for="{{ record_type }}-{{ no }}-amount" class="record-label">{{ _("Amount:")|force_escape }}</label>
|
<label for="{{ record_type }}-{{ no }}-amount" class="record-label">{{ _("Amount:")|force_escape }}</label>
|
||||||
<input id="{{ record_type }}-{{ no }}-amount" class="form-control record-amount {{ record_type }}-to-sum {% if record.amount.errors %} is-invalid {% endif %}" type="number" step="0.01" min="0.01" name="{{ record_type }}-{{ no }}-amount" value="{{ record.amount.value|default:"" }}" required="required" data-type="{{ record_type }}" />
|
<input id="{{ record_type }}-{{ no }}-amount" class="form-control record-amount {{ record_type }}-to-sum {% if record.amount.errors %} is-invalid {% endif %}" type="number" step="0.01" min="0.01" name="{{ record_type }}-{{ no }}-amount" value="{{ record.amount.value|short_value }}" required="required" data-type="{{ record_type }}" />
|
||||||
<div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div>
|
<div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -19,6 +19,7 @@ form-record-transfer.html: The template for a record in the transfer transaction
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2020/8/5
|
First written: 2020/8/5
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
{% load accounting %}
|
||||||
|
|
||||||
<li id="{{ record_type }}-{{ no }}" class="list-group-item {% if record.non_field_errors %} list-group-item-danger {% endif %} draggable-record {{ record_type }}-record" data-no="{{ no }}">
|
<li id="{{ record_type }}-{{ no }}" class="list-group-item {% if record.non_field_errors %} list-group-item-danger {% endif %} draggable-record {{ record_type }}-record" data-no="{{ no }}">
|
||||||
<div id="{{ record_type }}-{{ no }}-error">{% if record.non_field_errors %}{{ record.non_field_errors.0 }}{% endif %}</div>
|
<div id="{{ record_type }}-{{ no }}-error">{% if record.non_field_errors %}{{ record.non_field_errors.0 }}{% endif %}</div>
|
||||||
@ -36,7 +37,7 @@ First written: 2020/8/5
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<label for="{{ record_type }}-{{ no }}-amount" class="record-label">{{ _("Amount:")|force_escape }}</label>
|
<label for="{{ record_type }}-{{ no }}-amount" class="record-label">{{ _("Amount:")|force_escape }}</label>
|
||||||
<input id="{{ record_type }}-{{ no }}-amount" class="form-control record-amount {{ record_type }}-to-sum {% if record.amount.errors %} is-invalid {% endif %}" type="number" step="0.01" min="0.01" name="{{ record_type }}-{{ no }}-amount" value="{{ record.amount.value|default:"" }}" required="required" data-type="{{ record_type }}" />
|
<input id="{{ record_type }}-{{ no }}-amount" class="form-control record-amount {{ record_type }}-to-sum {% if record.amount.errors %} is-invalid {% endif %}" type="number" step="0.01" min="0.01" name="{{ record_type }}-{{ no }}-amount" value="{{ record.amount.value|short_value }}" required="required" data-type="{{ record_type }}" />
|
||||||
<div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div>
|
<div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -25,7 +25,7 @@ First written: 2020/7/9
|
|||||||
<!-- the accounting record search dialog -->
|
<!-- the accounting record search dialog -->
|
||||||
<form action="{% url "accounting:search" %}" method="GET">
|
<form action="{% url "accounting:search" %}" method="GET">
|
||||||
<!-- The Modal -->
|
<!-- The Modal -->
|
||||||
<div class="modal" id="accounting-search-modal">
|
<div class="modal fade" id="accounting-search-modal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
@ -25,7 +25,7 @@ First written: 2020/4/3
|
|||||||
<!-- The Modal -->
|
<!-- The Modal -->
|
||||||
<form id="summary-helper-form" action="" method="get">
|
<form id="summary-helper-form" action="" method="get">
|
||||||
<input id="summary-record" type="hidden" value="" />
|
<input id="summary-record" type="hidden" value="" />
|
||||||
<div class="modal" id="summary-modal">
|
<div class="modal fade" id="summary-modal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
||||||
@ -155,8 +155,8 @@ First written: 2020/4/3
|
|||||||
|
|
||||||
<!-- Modal footer -->
|
<!-- Modal footer -->
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button id="summary-confirm" class="btn btn-danger" type="submit" data-dismiss="modal">{{ _("Confirm")|force_escape }}</button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
||||||
|
<button id="summary-confirm" class="btn btn-danger" type="submit" data-dismiss="modal">{{ _("Confirm")|force_escape }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -44,7 +44,7 @@ First written: 2020/7/23
|
|||||||
<form action="{% url "accounting:transactions.delete" txn as url %}{% url_keep_return url %}" method="post">
|
<form action="{% url "accounting:transactions.delete" txn as url %}{% url_keep_return url %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<!-- The Modal -->
|
<!-- The Modal -->
|
||||||
<div class="modal" id="del-modal">
|
<div class="modal fade" id="del-modal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
||||||
@ -59,8 +59,8 @@ First written: 2020/7/23
|
|||||||
|
|
||||||
<!-- Modal footer -->
|
<!-- Modal footer -->
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
||||||
|
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -44,7 +44,7 @@ First written: 2020/7/23
|
|||||||
<form action="{% url "accounting:transactions.delete" txn as url %}{% url_keep_return url %}" method="post">
|
<form action="{% url "accounting:transactions.delete" txn as url %}{% url_keep_return url %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<!-- The Modal -->
|
<!-- The Modal -->
|
||||||
<div class="modal" id="del-modal">
|
<div class="modal fade" id="del-modal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
||||||
@ -59,8 +59,8 @@ First written: 2020/7/23
|
|||||||
|
|
||||||
<!-- Modal footer -->
|
<!-- Modal footer -->
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
||||||
|
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -44,7 +44,7 @@ First written: 2020/7/23
|
|||||||
<form action="{% url "accounting:transactions.delete" txn as url %}{% url_keep_return url %}" method="post">
|
<form action="{% url "accounting:transactions.delete" txn as url %}{% url_keep_return url %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<!-- The Modal -->
|
<!-- The Modal -->
|
||||||
<div class="modal" id="del-modal">
|
<div class="modal fade" id="del-modal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
||||||
@ -59,8 +59,8 @@ First written: 2020/7/23
|
|||||||
|
|
||||||
<!-- Modal footer -->
|
<!-- Modal footer -->
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
|
||||||
|
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -20,7 +20,7 @@
|
|||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Union, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
@ -32,28 +32,41 @@ from mia_core.period import Period
|
|||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
def _format_positive_amount(value: Union[str, Decimal]) -> str:
|
def _strip_decimal_zeros(value: Decimal) -> str:
|
||||||
|
"""Formats a decimal value, stripping excess decimal zeros.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The value with excess decimal zeros stripped.
|
||||||
|
"""
|
||||||
|
s = str(value)
|
||||||
|
s = re.sub(r"^(.*\.\d*?)0+$", r"\1", s)
|
||||||
|
s = re.sub(r"^(.*)\.$", r"\1", s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _format_positive_amount(value: Decimal) -> str:
|
||||||
"""Formats a positive amount, groups every 3 digits by commas.
|
"""Formats a positive amount, groups every 3 digits by commas.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value: The amount.
|
value: The amount.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ReportUrl: The formatted amount.
|
str: The amount in the desired format.
|
||||||
"""
|
"""
|
||||||
s = str(value)
|
s = _strip_decimal_zeros(value)
|
||||||
while True:
|
while True:
|
||||||
m = re.match("^([1-9][0-9]*)([0-9]{3}.*)", s)
|
m = re.match(r"^([1-9]\d*)(\d{3}.*)", s)
|
||||||
if m is None:
|
if m is None:
|
||||||
break
|
break
|
||||||
s = m.group(1) + "," + m.group(2)
|
s = m.group(1) + "," + m.group(2)
|
||||||
s = re.sub(r"^(.*\.[0-9]*?)0+$", r"\1", s)
|
|
||||||
s = re.sub(r"^(.*)\.$", r"\1", s)
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def accounting_amount(value: Union[Decimal]) -> str:
|
def accounting_amount(value: Optional[Decimal]) -> str:
|
||||||
"""Formats an amount with the accounting notation, grouping every 3 digits
|
"""Formats an amount with the accounting notation, grouping every 3 digits
|
||||||
by commas, and marking negative numbers with brackets instead of signs.
|
by commas, and marking negative numbers with brackets instead of signs.
|
||||||
|
|
||||||
@ -61,7 +74,7 @@ def accounting_amount(value: Union[Decimal]) -> str:
|
|||||||
value: The amount.
|
value: The amount.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ReportUrl: The formatted amount.
|
str: The amount in the desired format.
|
||||||
"""
|
"""
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
@ -74,14 +87,14 @@ def accounting_amount(value: Union[Decimal]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def short_amount(value: Union[Decimal]) -> str:
|
def short_amount(value: Optional[Decimal]) -> str:
|
||||||
"""Formats an amount, groups every 3 digits by commas.
|
"""Formats an amount, groups every 3 digits by commas.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value: The amount.
|
value: The amount.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ReportUrl: The formatted amount.
|
str: The amount in the desired format.
|
||||||
"""
|
"""
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
@ -93,6 +106,21 @@ def short_amount(value: Union[Decimal]) -> str:
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def short_value(value: Optional[Decimal]) -> str:
|
||||||
|
"""Formats a decimal value, stripping excess decimal zeros.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The value with excess decimal zeroes stripped.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
return _strip_decimal_zeros(value)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def report_url(context: RequestContext,
|
def report_url(context: RequestContext,
|
||||||
cash_account: Optional[Account],
|
cash_account: Optional[Account],
|
@ -80,10 +80,9 @@ class ReportUrl:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
namespace: The namespace of the current application.
|
namespace: The namespace of the current application.
|
||||||
cash: The currently-specified account of the
|
cash: The currently-specified account of the cash account or cash
|
||||||
cash account or cash summary.
|
summary.
|
||||||
ledger: The currently-specified account of the
|
ledger: The currently-specified account of the ledger or leger summary.
|
||||||
ledger or leger summary.
|
|
||||||
period: The currently-specified period.
|
period: The currently-specified period.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -145,9 +144,8 @@ class DataFiller:
|
|||||||
"""Adds accounts.
|
"""Adds accounts.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
accounts (tuple[tuple[any]]): Tuples of
|
accounts: Tuples of (code, English, Traditional Chinese, Simplified
|
||||||
(code, English, Traditional Chinese, Simplified Chinese)
|
Chinese) of the accounts.
|
||||||
of the accounts.
|
|
||||||
"""
|
"""
|
||||||
for data in accounts:
|
for data in accounts:
|
||||||
code = data[0]
|
code = data[0]
|
||||||
@ -167,8 +165,7 @@ class DataFiller:
|
|||||||
"""Adds a transfer transaction.
|
"""Adds a transfer transaction.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
date: The date, or the number of days from
|
date: The date, or the number of days from today.
|
||||||
today.
|
|
||||||
debit: Tuples of (account, summary, amount) of the debit records.
|
debit: Tuples of (account, summary, amount) of the debit records.
|
||||||
credit: Tuples of (account, summary, amount) of the credit records.
|
credit: Tuples of (account, summary, amount) of the credit records.
|
||||||
"""
|
"""
|
@ -28,7 +28,7 @@ from django.conf import settings
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Sum, Case, When, F, Q, Count, BooleanField, \
|
from django.db.models import Sum, Case, When, F, Q, Count, BooleanField, \
|
||||||
ExpressionWrapper, Exists, OuterRef, Value, CharField
|
ExpressionWrapper, Exists, OuterRef, Value, CharField, DecimalField
|
||||||
from django.db.models.functions import TruncMonth, Coalesce, Left, StrIndex
|
from django.db.models.functions import TruncMonth, Coalesce, Left, StrIndex
|
||||||
from django.http import JsonResponse, HttpResponseRedirect, Http404, \
|
from django.http import JsonResponse, HttpResponseRedirect, Http404, \
|
||||||
HttpRequest, HttpResponse
|
HttpRequest, HttpResponse
|
||||||
@ -94,7 +94,7 @@ def cash(request: HttpRequest, account: Account,
|
|||||||
~Q(account__code__startswith="21"),
|
~Q(account__code__startswith="21"),
|
||||||
~Q(account__code__startswith="22"))
|
~Q(account__code__startswith="22"))
|
||||||
.order_by("transaction__date", "transaction__ord",
|
.order_by("transaction__date", "transaction__ord",
|
||||||
"is_credit", "ord"))
|
"-is_credit", "ord"))
|
||||||
balance_before = Record.objects \
|
balance_before = Record.objects \
|
||||||
.filter(
|
.filter(
|
||||||
Q(transaction__date__lt=period.start),
|
Q(transaction__date__lt=period.start),
|
||||||
@ -103,9 +103,10 @@ def cash(request: HttpRequest, account: Account,
|
|||||||
Q(account__code__startswith="21") |
|
Q(account__code__startswith="21") |
|
||||||
Q(account__code__startswith="22"))) \
|
Q(account__code__startswith="22"))) \
|
||||||
.aggregate(
|
.aggregate(
|
||||||
balance=Coalesce(Sum(Case(
|
balance=Coalesce(Sum(Case(When(is_credit=True, then=-1),
|
||||||
When(is_credit=True, then=-1),
|
default=1) * F("amount"),
|
||||||
default=1) * F("amount")), 0))["balance"]
|
output_field=DecimalField()),
|
||||||
|
0, output_field=DecimalField()))["balance"]
|
||||||
else:
|
else:
|
||||||
records = list(
|
records = list(
|
||||||
Record.objects
|
Record.objects
|
||||||
@ -116,15 +117,16 @@ def cash(request: HttpRequest, account: Account,
|
|||||||
Q(record__account__code__startswith=account.code))),
|
Q(record__account__code__startswith=account.code))),
|
||||||
~Q(account__code__startswith=account.code))
|
~Q(account__code__startswith=account.code))
|
||||||
.order_by("transaction__date", "transaction__ord",
|
.order_by("transaction__date", "transaction__ord",
|
||||||
"is_credit", "ord"))
|
"-is_credit", "ord"))
|
||||||
balance_before = Record.objects \
|
balance_before = Record.objects \
|
||||||
.filter(
|
.filter(
|
||||||
transaction__date__lt=period.start,
|
transaction__date__lt=period.start,
|
||||||
account__code__startswith=account.code) \
|
account__code__startswith=account.code) \
|
||||||
.aggregate(
|
.aggregate(
|
||||||
balance=Coalesce(Sum(Case(When(
|
balance=Coalesce(Sum(Case(When(is_credit=True, then=-1),
|
||||||
is_credit=True, then=-1),
|
default=1) * F("amount"),
|
||||||
default=1) * F("amount")), 0))["balance"]
|
output_field=DecimalField()),
|
||||||
|
0, output_field=DecimalField()))["balance"]
|
||||||
balance = balance_before
|
balance = balance_before
|
||||||
for record in records:
|
for record in records:
|
||||||
sign = 1 if record.is_credit else -1
|
sign = 1 if record.is_credit else -1
|
||||||
@ -211,15 +213,14 @@ def cash_summary(request: HttpRequest, account: Account) -> HttpResponse:
|
|||||||
.values("month")
|
.values("month")
|
||||||
.order_by("month")
|
.order_by("month")
|
||||||
.annotate(
|
.annotate(
|
||||||
debit=Coalesce(
|
debit=Coalesce(Sum(Case(When(is_credit=False,
|
||||||
Sum(Case(When(is_credit=False, then=F("amount")))),
|
then=F("amount")))),
|
||||||
0),
|
0, output_field=DecimalField()),
|
||||||
credit=Coalesce(
|
credit=Coalesce(Sum(Case(When(is_credit=True,
|
||||||
Sum(Case(When(is_credit=True, then=F("amount")))),
|
then=F("amount")))),
|
||||||
0),
|
0, output_field=DecimalField()),
|
||||||
balance=Sum(Case(
|
balance=Sum(Case(When(is_credit=False, then=-F("amount")),
|
||||||
When(is_credit=False, then=-F("amount")),
|
default=F("amount"))))]
|
||||||
default=F("amount"))))]
|
|
||||||
else:
|
else:
|
||||||
months = [utils.MonthlySummary(**x) for x in Record.objects
|
months = [utils.MonthlySummary(**x) for x in Record.objects
|
||||||
.filter(
|
.filter(
|
||||||
@ -230,15 +231,14 @@ def cash_summary(request: HttpRequest, account: Account) -> HttpResponse:
|
|||||||
.values("month")
|
.values("month")
|
||||||
.order_by("month")
|
.order_by("month")
|
||||||
.annotate(
|
.annotate(
|
||||||
debit=Coalesce(
|
debit=Coalesce(Sum(Case(When(is_credit=False,
|
||||||
Sum(Case(When(is_credit=False, then=F("amount")))),
|
then=F("amount")))),
|
||||||
0),
|
0, output_field=DecimalField()),
|
||||||
credit=Coalesce(
|
credit=Coalesce(Sum(Case(When(is_credit=True,
|
||||||
Sum(Case(When(is_credit=True, then=F("amount")))),
|
then=F("amount")))),
|
||||||
0),
|
0, output_field=DecimalField()),
|
||||||
balance=Sum(Case(
|
balance=Sum(Case(When(is_credit=False, then=-F("amount")),
|
||||||
When(is_credit=False, then=-F("amount")),
|
default=F("amount"))))]
|
||||||
default=F("amount"))))]
|
|
||||||
cumulative_balance = 0
|
cumulative_balance = 0
|
||||||
for month in months:
|
for month in months:
|
||||||
cumulative_balance = cumulative_balance + month.balance
|
cumulative_balance = cumulative_balance + month.balance
|
||||||
@ -305,9 +305,10 @@ def ledger(request: HttpRequest, account: Account,
|
|||||||
transaction__date__lt=period.start,
|
transaction__date__lt=period.start,
|
||||||
account__code__startswith=account.code) \
|
account__code__startswith=account.code) \
|
||||||
.aggregate(
|
.aggregate(
|
||||||
balance=Coalesce(Sum(Case(When(
|
balance=Coalesce(Sum(Case(When(is_credit=True, then=-1),
|
||||||
is_credit=True, then=-1),
|
default=1) * F("amount"),
|
||||||
default=1) * F("amount")), 0))["balance"]
|
output_field=DecimalField()),
|
||||||
|
0, output_field=DecimalField()))["balance"]
|
||||||
record_brought_forward = Record(
|
record_brought_forward = Record(
|
||||||
transaction=Transaction(date=period.start),
|
transaction=Transaction(date=period.start),
|
||||||
account=account,
|
account=account,
|
||||||
@ -370,15 +371,14 @@ def ledger_summary(request: HttpRequest, account: Account) -> HttpResponse:
|
|||||||
.values("month")
|
.values("month")
|
||||||
.order_by("month")
|
.order_by("month")
|
||||||
.annotate(
|
.annotate(
|
||||||
debit=Coalesce(
|
debit=Coalesce(Sum(Case(When(is_credit=False,
|
||||||
Sum(Case(When(is_credit=False, then=F("amount")))),
|
then=F("amount")))),
|
||||||
0),
|
0, output_field=DecimalField()),
|
||||||
credit=Coalesce(
|
credit=Coalesce(Sum(Case(When(is_credit=True,
|
||||||
Sum(Case(When(is_credit=True, then=F("amount")))),
|
then=F("amount")))),
|
||||||
0),
|
0, output_field=DecimalField()),
|
||||||
balance=Sum(Case(
|
balance=Sum(Case(When(is_credit=False, then=F("amount")),
|
||||||
When(is_credit=False, then=F("amount")),
|
default=-F("amount"))))]
|
||||||
default=-F("amount"))))]
|
|
||||||
cumulative_balance = 0
|
cumulative_balance = 0
|
||||||
for month in months:
|
for month in months:
|
||||||
cumulative_balance = cumulative_balance + month.balance
|
cumulative_balance = cumulative_balance + month.balance
|
||||||
@ -436,11 +436,10 @@ def journal(request: HttpRequest, period: Period) -> HttpResponse:
|
|||||||
| Q(code__startswith="2")
|
| Q(code__startswith="2")
|
||||||
| Q(code__startswith="3")) \
|
| Q(code__startswith="3")) \
|
||||||
.annotate(balance=Sum(
|
.annotate(balance=Sum(
|
||||||
Case(
|
Case(When(record__is_credit=True, then=-1),
|
||||||
When(record__is_credit=True, then=-1),
|
default=1) * F("record__amount"),
|
||||||
default=1
|
filter=Q(record__transaction__date__lt=period.start),
|
||||||
) * F("record__amount"),
|
output_field=DecimalField())) \
|
||||||
filter=Q(record__transaction__date__lt=period.start))) \
|
|
||||||
.filter(~Q(balance=0))
|
.filter(~Q(balance=0))
|
||||||
debit_records = [Record(
|
debit_records = [Record(
|
||||||
transaction=Transaction(date=period.start),
|
transaction=Transaction(date=period.start),
|
||||||
@ -513,9 +512,9 @@ def trial_balance(request: HttpRequest, period: Period) -> HttpResponse:
|
|||||||
| Q(code__startswith="2")
|
| Q(code__startswith="2")
|
||||||
| Q(code__startswith="3")))
|
| Q(code__startswith="3")))
|
||||||
.annotate(
|
.annotate(
|
||||||
amount=Sum(Case(
|
amount=Sum(Case(When(record__is_credit=True, then=-1),
|
||||||
When(record__is_credit=True, then=-1),
|
default=1) * F("record__amount"),
|
||||||
default=1) * F("record__amount")))
|
output_field=DecimalField()))
|
||||||
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
||||||
.annotate(
|
.annotate(
|
||||||
debit_amount=Case(
|
debit_amount=Case(
|
||||||
@ -534,9 +533,9 @@ def trial_balance(request: HttpRequest, period: Period) -> HttpResponse:
|
|||||||
| Q(code__startswith="3")),
|
| Q(code__startswith="3")),
|
||||||
~Q(code=Account.ACCUMULATED_BALANCE))
|
~Q(code=Account.ACCUMULATED_BALANCE))
|
||||||
.annotate(
|
.annotate(
|
||||||
amount=Sum(Case(
|
amount=Sum(Case(When(record__is_credit=True, then=-1),
|
||||||
When(record__is_credit=True, then=-1),
|
default=1) * F("record__amount"),
|
||||||
default=1) * F("record__amount")))
|
output_field=DecimalField()))
|
||||||
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
||||||
.annotate(
|
.annotate(
|
||||||
debit_amount=Case(
|
debit_amount=Case(
|
||||||
@ -555,9 +554,9 @@ def trial_balance(request: HttpRequest, period: Period) -> HttpResponse:
|
|||||||
| (Q(transaction__date__lte=period.end)
|
| (Q(transaction__date__lte=period.end)
|
||||||
& Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
& Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
||||||
.aggregate(
|
.aggregate(
|
||||||
balance=Sum(Case(
|
balance=Sum(Case(When(is_credit=True, then=-1),
|
||||||
When(is_credit=True, then=-1),
|
default=1) * F("amount"),
|
||||||
default=1) * F("amount")))["balance"]
|
output_field=DecimalField()))["balance"]
|
||||||
if balance is not None and balance != 0:
|
if balance is not None and balance != 0:
|
||||||
brought_forward = Account.objects.get(
|
brought_forward = Account.objects.get(
|
||||||
code=Account.ACCUMULATED_BALANCE)
|
code=Account.ACCUMULATED_BALANCE)
|
||||||
@ -614,9 +613,9 @@ def income_statement(request: HttpRequest, period: Period) -> HttpResponse:
|
|||||||
| Q(code__startswith="2")
|
| Q(code__startswith="2")
|
||||||
| Q(code__startswith="3")))
|
| Q(code__startswith="3")))
|
||||||
.annotate(
|
.annotate(
|
||||||
amount=Sum(Case(
|
amount=Sum(Case(When(record__is_credit=True, then=1),
|
||||||
When(record__is_credit=True, then=1),
|
default=-1) * F("record__amount"),
|
||||||
default=-1) * F("record__amount")))
|
output_field=DecimalField()))
|
||||||
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
||||||
.order_by("code"))
|
.order_by("code"))
|
||||||
groups = list(Account.objects.filter(
|
groups = list(Account.objects.filter(
|
||||||
@ -687,9 +686,9 @@ def balance_sheet(request: HttpRequest, period: Period) -> HttpResponse:
|
|||||||
| Q(code__startswith="3")),
|
| Q(code__startswith="3")),
|
||||||
~Q(code=Account.ACCUMULATED_BALANCE))
|
~Q(code=Account.ACCUMULATED_BALANCE))
|
||||||
.annotate(
|
.annotate(
|
||||||
amount=Sum(Case(
|
amount=Sum(Case(When(record__is_credit=True, then=-1),
|
||||||
When(record__is_credit=True, then=-1),
|
default=1) * F("record__amount"),
|
||||||
default=1) * F("record__amount")))
|
output_field=DecimalField()))
|
||||||
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
||||||
.order_by("code"))
|
.order_by("code"))
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
@ -703,9 +702,9 @@ def balance_sheet(request: HttpRequest, period: Period) -> HttpResponse:
|
|||||||
| Q(account__code__startswith="3"))
|
| Q(account__code__startswith="3"))
|
||||||
& ~Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
& ~Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
||||||
.aggregate(
|
.aggregate(
|
||||||
balance=Sum(Case(
|
balance=Sum(Case(When(is_credit=True, then=-1),
|
||||||
When(is_credit=True, then=-1),
|
default=1) * F("amount"),
|
||||||
default=1) * F("amount")))["balance"]
|
output_field=DecimalField()))["balance"]
|
||||||
if balance is not None and balance != 0:
|
if balance is not None and balance != 0:
|
||||||
brought_forward = Account.objects.get(
|
brought_forward = Account.objects.get(
|
||||||
code=Account.ACCUMULATED_BALANCE)
|
code=Account.ACCUMULATED_BALANCE)
|
||||||
@ -723,9 +722,9 @@ def balance_sheet(request: HttpRequest, period: Period) -> HttpResponse:
|
|||||||
| Q(account__code__startswith="3"))
|
| Q(account__code__startswith="3"))
|
||||||
& ~Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
& ~Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
||||||
.aggregate(
|
.aggregate(
|
||||||
balance=Sum(Case(
|
balance=Sum(Case(When(is_credit=True, then=-1),
|
||||||
When(is_credit=True, then=-1),
|
default=1) * F("amount"),
|
||||||
default=1) * F("amount")))["balance"]
|
output_field=DecimalField()))["balance"]
|
||||||
if balance is not None and balance != 0:
|
if balance is not None and balance != 0:
|
||||||
net_change = Account.objects.get(code=Account.NET_CHANGE)
|
net_change = Account.objects.get(code=Account.NET_CHANGE)
|
||||||
net_change.amount = balance
|
net_change.amount = balance
|
||||||
@ -781,12 +780,12 @@ class SearchListView(TemplateView):
|
|||||||
if len(terms) == 0:
|
if len(terms) == 0:
|
||||||
return []
|
return []
|
||||||
conditions = [self._get_conditions_for_term(x) for x in terms]
|
conditions = [self._get_conditions_for_term(x) for x in terms]
|
||||||
if len(conditions) == 1:
|
|
||||||
return Record.objects.filter(conditions[0])
|
|
||||||
combined = conditions[0]
|
combined = conditions[0]
|
||||||
for x in conditions[1:]:
|
for x in conditions[1:]:
|
||||||
combined = combined & x
|
combined = combined & x
|
||||||
return Record.objects.filter(combined)
|
return Record.objects.filter(combined)\
|
||||||
|
.order_by("transaction__date", "transaction__ord", "is_credit",
|
||||||
|
"ord")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_conditions_for_term(term: str) -> Q:
|
def _get_conditions_for_term(term: str) -> Q:
|
||||||
@ -805,9 +804,9 @@ class SearchListView(TemplateView):
|
|||||||
| Q(code=term)))\
|
| Q(code=term)))\
|
||||||
| Q(summary__icontains=term)\
|
| Q(summary__icontains=term)\
|
||||||
| Q(transaction__notes__icontains=term)
|
| Q(transaction__notes__icontains=term)
|
||||||
if re.match("^[0-9]+(?:\\.[0-9]+)?$", term):
|
if re.match(r"^\d+(?:\.\d+)?$", term):
|
||||||
conditions = conditions | Q(amount=Decimal(term))
|
conditions = conditions | Q(amount=Decimal(term))
|
||||||
if re.match("^[1-9][0-8]{9}$", term):
|
if re.match(r"^[1-9][0-8]{9}$", term):
|
||||||
conditions = conditions\
|
conditions = conditions\
|
||||||
| Q(pk=int(term))\
|
| Q(pk=int(term))\
|
||||||
| Q(transaction__pk=int(term))\
|
| Q(transaction__pk=int(term))\
|
||||||
@ -1087,13 +1086,13 @@ class TransactionSortFormView(FormView):
|
|||||||
|
|
||||||
def form_valid(self, form: TransactionSortForm) -> HttpResponseRedirect:
|
def form_valid(self, form: TransactionSortForm) -> HttpResponseRedirect:
|
||||||
"""Handles the action when the POST form is valid."""
|
"""Handles the action when the POST form is valid."""
|
||||||
modified = [x for x in form.txn_orders if x.txn.ord != x.order]
|
modified = [x for x in form.txn_orders if x.txn.ord != x.ord]
|
||||||
if len(modified) == 0:
|
if len(modified) == 0:
|
||||||
message = self.get_not_modified_message(form.cleaned_data)
|
message = self.get_not_modified_message(form.cleaned_data)
|
||||||
else:
|
else:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for x in modified:
|
for x in modified:
|
||||||
Transaction.objects.filter(pk=x.txn.pk).update(ord=x.order)
|
Transaction.objects.filter(pk=x.txn.pk).update(ord=x.ord)
|
||||||
message = self.get_success_message(form.cleaned_data)
|
message = self.get_success_message(form.cleaned_data)
|
||||||
messages.success(self.request, message)
|
messages.success(self.request, message)
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
0
src/mia_core/__init__.py
Normal file
0
src/mia_core/__init__.py
Normal file
@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: mia-core 3.0\n"
|
"Project-Id-Version: mia-core 3.0\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-08-17 23:56+0800\n"
|
"POT-Creation-Date: 2021-01-17 00:24+0800\n"
|
||||||
"PO-Revision-Date: 2020-08-18 00:02+0800\n"
|
"PO-Revision-Date: 2021-01-17 00:29+0800\n"
|
||||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
||||||
"Language-Team: Traditional Chinese <imacat@mail.imacat.idv.tw>\n"
|
"Language-Team: Traditional Chinese <imacat@mail.imacat.idv.tw>\n"
|
||||||
"Language: Traditional Chinese\n"
|
"Language: Traditional Chinese\n"
|
||||||
@ -38,13 +38,13 @@ msgstr "全部"
|
|||||||
|
|
||||||
#: mia_core/period.py:588 mia_core/period.py:621
|
#: mia_core/period.py:588 mia_core/period.py:621
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:60
|
#: mia_core/templates/mia_core/include/period-chooser.html:60
|
||||||
#: mia_core/templatetags/mia_core.py:173
|
#: mia_core/templatetags/mia_core.py:219
|
||||||
msgid "This Month"
|
msgid "This Month"
|
||||||
msgstr "這個月"
|
msgstr "這個月"
|
||||||
|
|
||||||
#: mia_core/period.py:629
|
#: mia_core/period.py:629
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:63
|
#: mia_core/templates/mia_core/include/period-chooser.html:63
|
||||||
#: mia_core/templatetags/mia_core.py:180
|
#: mia_core/templatetags/mia_core.py:226
|
||||||
msgid "Last Month"
|
msgid "Last Month"
|
||||||
msgstr "上個月"
|
msgstr "上個月"
|
||||||
|
|
||||||
@ -60,13 +60,13 @@ msgstr "去年"
|
|||||||
|
|
||||||
#: mia_core/period.py:661
|
#: mia_core/period.py:661
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:95
|
#: mia_core/templates/mia_core/include/period-chooser.html:95
|
||||||
#: mia_core/templatetags/mia_core.py:153
|
#: mia_core/templatetags/mia_core.py:187
|
||||||
msgid "Today"
|
msgid "Today"
|
||||||
msgstr "今天"
|
msgstr "今天"
|
||||||
|
|
||||||
#: mia_core/period.py:663
|
#: mia_core/period.py:663
|
||||||
#: mia_core/templates/mia_core/include/period-chooser.html:98
|
#: mia_core/templates/mia_core/include/period-chooser.html:98
|
||||||
#: mia_core/templatetags/mia_core.py:155
|
#: mia_core/templatetags/mia_core.py:190
|
||||||
msgid "Yesterday"
|
msgid "Yesterday"
|
||||||
msgstr "昨天"
|
msgstr "昨天"
|
||||||
|
|
||||||
@ -115,17 +115,21 @@ msgstr "從:"
|
|||||||
msgid "To:"
|
msgid "To:"
|
||||||
msgstr "到:"
|
msgstr "到:"
|
||||||
|
|
||||||
#: mia_core/utils.py:347
|
#: mia_core/templatetags/mia_core.py:192
|
||||||
|
msgid "Tomorrow"
|
||||||
|
msgstr "明天"
|
||||||
|
|
||||||
|
#: mia_core/utils.py:342
|
||||||
msgctxt "Pagination|"
|
msgctxt "Pagination|"
|
||||||
msgid "Previous"
|
msgid "Previous"
|
||||||
msgstr "上一頁"
|
msgstr "上一頁"
|
||||||
|
|
||||||
#: mia_core/utils.py:375 mia_core/utils.py:396
|
#: mia_core/utils.py:370 mia_core/utils.py:391
|
||||||
msgctxt "Pagination|"
|
msgctxt "Pagination|"
|
||||||
msgid "..."
|
msgid "..."
|
||||||
msgstr "…"
|
msgstr "…"
|
||||||
|
|
||||||
#: mia_core/utils.py:415
|
#: mia_core/utils.py:410
|
||||||
msgctxt "Pagination|"
|
msgctxt "Pagination|"
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr "下一頁"
|
msgstr "下一頁"
|
@ -30,8 +30,13 @@ from opencc import OpenCC
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""Populates the database with sample accounting data."""
|
"""Updates the revision date, converts the Traditional Chinese
|
||||||
help = "Fills the database with the accounting accounts."
|
translation into Simplified Chinese, and then calls the
|
||||||
|
compilemessages command.
|
||||||
|
"""
|
||||||
|
help = ("Updates the revision date, converts the Traditional Chinese"
|
||||||
|
" translation into Simplified Chinese, and then calls the"
|
||||||
|
" compilemessages command.")
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -44,8 +49,9 @@ class Command(BaseCommand):
|
|||||||
Args:
|
Args:
|
||||||
parser (CommandParser): The command line argument parser.
|
parser (CommandParser): The command line argument parser.
|
||||||
"""
|
"""
|
||||||
parser.add_argument("proj_dir", nargs="+",
|
parser.add_argument("app_dir", nargs="+",
|
||||||
help="The domain, either django or djangojs")
|
help=("One or more application directories that"
|
||||||
|
" contains their locale subdirectories"))
|
||||||
parser.add_argument("--domain", "-d", action="append",
|
parser.add_argument("--domain", "-d", action="append",
|
||||||
choices=["django", "djangojs"], required=True,
|
choices=["django", "djangojs"], required=True,
|
||||||
help="The domain, either django or djangojs")
|
help="The domain, either django or djangojs")
|
||||||
@ -58,7 +64,7 @@ class Command(BaseCommand):
|
|||||||
**options (dict[str,str]): The command line switches.
|
**options (dict[str,str]): The command line switches.
|
||||||
"""
|
"""
|
||||||
locale_dirs = [os.path.join(settings.BASE_DIR, x, "locale")
|
locale_dirs = [os.path.join(settings.BASE_DIR, x, "locale")
|
||||||
for x in options["proj_dir"]]
|
for x in options["app_dir"]]
|
||||||
missing = [x for x in locale_dirs if not os.path.isdir(x)]
|
missing = [x for x in locale_dirs if not os.path.isdir(x)]
|
||||||
if len(missing) > 0:
|
if len(missing) > 0:
|
||||||
error = "Directories not exist: " + ", ".join(missing)
|
error = "Directories not exist: " + ", ".join(missing)
|
@ -69,7 +69,7 @@ class StampedModel(models.Model):
|
|||||||
F"Missing current_user in {self.__class__.__name__}")
|
F"Missing current_user in {self.__class__.__name__}")
|
||||||
try:
|
try:
|
||||||
self.created_by
|
self.created_by
|
||||||
except ObjectDoesNotExist as e:
|
except ObjectDoesNotExist:
|
||||||
self.created_by = self.current_user
|
self.created_by = self.current_user
|
||||||
self.updated_by = self.current_user
|
self.updated_by = self.current_user
|
||||||
super().save(force_insert=force_insert, force_update=force_update,
|
super().save(force_insert=force_insert, force_update=force_update,
|
||||||
@ -126,9 +126,12 @@ class LocalizedModel(models.Model):
|
|||||||
current_value = getattr(self, name + "_l10n")
|
current_value = getattr(self, name + "_l10n")
|
||||||
if current_value is None or current_value == "":
|
if current_value is None or current_value == "":
|
||||||
setattr(self, name + "_l10n", new_value)
|
setattr(self, name + "_l10n", new_value)
|
||||||
l10n_rec = self._get_l10n_set()\
|
if self.pk is None:
|
||||||
.filter(name=name, language=language)\
|
l10n_rec = None
|
||||||
.first()
|
else:
|
||||||
|
l10n_rec = self._get_l10n_set()\
|
||||||
|
.filter(name=name, language=language)\
|
||||||
|
.first()
|
||||||
if l10n_rec is None:
|
if l10n_rec is None:
|
||||||
l10n_to_save.append(self._get_l10n_set().model(
|
l10n_to_save.append(self._get_l10n_set().model(
|
||||||
master=self, name=name,
|
master=self, name=name,
|
@ -195,7 +195,7 @@ class Period:
|
|||||||
"""
|
"""
|
||||||
if self._data_start is None:
|
if self._data_start is None:
|
||||||
return None
|
return None
|
||||||
m = re.match("^[0-9]{4}-[0-2]{2}", self._period.spec)
|
m = re.match(r"^\d{4}-\d{2}", self._period.spec)
|
||||||
if m is None:
|
if m is None:
|
||||||
return None
|
return None
|
||||||
if self._period.end < self._data_start:
|
if self._period.end < self._data_start:
|
||||||
@ -379,9 +379,9 @@ class Period:
|
|||||||
if self.start <= self._data_start:
|
if self.start <= self._data_start:
|
||||||
return None
|
return None
|
||||||
previous_day = self.start - datetime.timedelta(days=1)
|
previous_day = self.start - datetime.timedelta(days=1)
|
||||||
if re.match("^[0-9]{4}$", self.spec):
|
if re.match(r"^\d{4}$", self.spec):
|
||||||
return "-" + str(previous_day.year)
|
return "-" + str(previous_day.year)
|
||||||
if re.match("^[0-9]{4}-[0-9]{2}$", self.spec):
|
if re.match(r"^\d{4}-\d{2}$", self.spec):
|
||||||
return dateformat.format(previous_day, "-Y-m")
|
return dateformat.format(previous_day, "-Y-m")
|
||||||
return dateformat.format(previous_day, "-Y-m-d")
|
return dateformat.format(previous_day, "-Y-m-d")
|
||||||
|
|
||||||
@ -441,7 +441,7 @@ class Period:
|
|||||||
return
|
return
|
||||||
self.spec = spec
|
self.spec = spec
|
||||||
# A specific month
|
# A specific month
|
||||||
m = re.match("^([0-9]{4})-([0-9]{2})$", spec)
|
m = re.match(r"^(\d{4})-(\d{2})$", spec)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
year = int(m.group(1))
|
year = int(m.group(1))
|
||||||
month = int(m.group(2))
|
month = int(m.group(2))
|
||||||
@ -452,7 +452,7 @@ class Period:
|
|||||||
self.prep_desc = gettext("In %s") % self.description
|
self.prep_desc = gettext("In %s") % self.description
|
||||||
return
|
return
|
||||||
# From a specific month
|
# From a specific month
|
||||||
m = re.match("^([0-9]{4})-([0-9]{2})-$", spec)
|
m = re.match(r"^(\d{4})-(\d{2})-$", spec)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
year = int(m.group(1))
|
year = int(m.group(1))
|
||||||
month = int(m.group(2))
|
month = int(m.group(2))
|
||||||
@ -464,7 +464,7 @@ class Period:
|
|||||||
self.prep_desc = self.description
|
self.prep_desc = self.description
|
||||||
return
|
return
|
||||||
# Until a specific month
|
# Until a specific month
|
||||||
m = re.match("^-([0-9]{4})-([0-9]{2})$", spec)
|
m = re.match(r"^-(\d{4})-(\d{2})$", spec)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
year = int(m.group(1))
|
year = int(m.group(1))
|
||||||
month = int(m.group(2))
|
month = int(m.group(2))
|
||||||
@ -477,7 +477,7 @@ class Period:
|
|||||||
self.prep_desc = self.description
|
self.prep_desc = self.description
|
||||||
return
|
return
|
||||||
# A specific year
|
# A specific year
|
||||||
m = re.match("^([0-9]{4})$", spec)
|
m = re.match(r"^(\d{4})$", spec)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
year = int(m.group(1))
|
year = int(m.group(1))
|
||||||
# Raises ValueError
|
# Raises ValueError
|
||||||
@ -487,7 +487,7 @@ class Period:
|
|||||||
self.prep_desc = gettext("In %s") % self.description
|
self.prep_desc = gettext("In %s") % self.description
|
||||||
return
|
return
|
||||||
# Until a specific year
|
# Until a specific year
|
||||||
m = re.match("^-([0-9]{4})$", spec)
|
m = re.match(r"^-(\d{4})$", spec)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
year = int(m.group(1))
|
year = int(m.group(1))
|
||||||
# Raises ValueError
|
# Raises ValueError
|
||||||
@ -505,7 +505,7 @@ class Period:
|
|||||||
self.prep_desc = gettext("In %s") % self.description
|
self.prep_desc = gettext("In %s") % self.description
|
||||||
return
|
return
|
||||||
# A specific date
|
# A specific date
|
||||||
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$",
|
m = re.match(r"^(\d{4})-(\d{2})-(\d{2})$",
|
||||||
spec)
|
spec)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
# Raises ValueError
|
# Raises ValueError
|
||||||
@ -518,8 +518,8 @@ class Period:
|
|||||||
self.prep_desc = gettext("In %s") % self.description
|
self.prep_desc = gettext("In %s") % self.description
|
||||||
return
|
return
|
||||||
# A specific date period
|
# A specific date period
|
||||||
m = re.match(("^([0-9]{4})-([0-9]{2})-([0-9]{2})"
|
m = re.match((r"^(\d{4})-(\d{2})-(\d{2})"
|
||||||
"-([0-9]{4})-([0-9]{2})-([0-9]{2})$"),
|
r"-(\d{4})-(\d{2})-(\d{2})$"),
|
||||||
spec)
|
spec)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
# Raises ValueError
|
# Raises ValueError
|
||||||
@ -564,7 +564,7 @@ class Period:
|
|||||||
self.prep_desc = gettext("In %s") % self.description
|
self.prep_desc = gettext("In %s") % self.description
|
||||||
return
|
return
|
||||||
# Until a specific day
|
# Until a specific day
|
||||||
m = re.match("^-([0-9]{4})-([0-9]{2})-([0-9]{2})$", spec)
|
m = re.match(r"^-(\d{4})-(\d{2})-(\d{2})$", spec)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
# Raises ValueError
|
# Raises ValueError
|
||||||
self.end = datetime.date(
|
self.end = datetime.date(
|
@ -26,7 +26,7 @@ First written: 2020/7/10
|
|||||||
<!-- The Modal -->
|
<!-- The Modal -->
|
||||||
<input id="period-url" type="hidden" value="{% url_period "0000-00-00" %}" />
|
<input id="period-url" type="hidden" value="{% url_period "0000-00-00" %}" />
|
||||||
<input id="period-month-picker-params" type="hidden" value="{{ period.month_picker_params }}" />
|
<input id="period-month-picker-params" type="hidden" value="{{ period.month_picker_params }}" />
|
||||||
<div class="modal" id="period-modal">
|
<div class="modal fade" id="period-modal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
0
src/mia_core/templatetags/__init__.py
Normal file
0
src/mia_core/templatetags/__init__.py
Normal file
@ -30,7 +30,7 @@ from django.template import defaultfilters, RequestContext
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext, get_language
|
||||||
|
|
||||||
from mia_core.utils import UrlBuilder, CssAndJavaScriptLibraries
|
from mia_core.utils import UrlBuilder, CssAndJavaScriptLibraries
|
||||||
|
|
||||||
@ -185,8 +185,20 @@ def smart_date(value: datetime.date) -> str:
|
|||||||
"""
|
"""
|
||||||
if value == date.today():
|
if value == date.today():
|
||||||
return gettext("Today")
|
return gettext("Today")
|
||||||
if (date.today() - value).days == 1:
|
prev_days = (value - date.today()).days
|
||||||
|
if prev_days == -1:
|
||||||
return gettext("Yesterday")
|
return gettext("Yesterday")
|
||||||
|
if prev_days == 1:
|
||||||
|
return gettext("Tomorrow")
|
||||||
|
if get_language() == "zh-hant":
|
||||||
|
if prev_days == -2:
|
||||||
|
return "前天"
|
||||||
|
if prev_days == -3:
|
||||||
|
return "大前天"
|
||||||
|
if prev_days == 2:
|
||||||
|
return "後天"
|
||||||
|
if prev_days == 3:
|
||||||
|
return "大後天"
|
||||||
if date.today().year == value.year:
|
if date.today().year == value.year:
|
||||||
return defaultfilters.date(value, "n/j(D)").replace("星期", "")
|
return defaultfilters.date(value, "n/j(D)").replace("星期", "")
|
||||||
return defaultfilters.date(value, "Y/n/j(D)").replace("星期", "")
|
return defaultfilters.date(value, "Y/n/j(D)").replace("星期", "")
|
@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import random
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Dict, List, Any, Type
|
from secrets import randbelow
|
||||||
|
from typing import Dict, List, Any, Type, Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
@ -40,7 +40,7 @@ def new_pk(cls: Type[Model]) -> int:
|
|||||||
The new random ID.
|
The new random ID.
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
pk = random.randint(100000000, 999999999)
|
pk = 100000000 + randbelow(900000000)
|
||||||
try:
|
try:
|
||||||
cls.objects.get(pk=pk)
|
cls.objects.get(pk=pk)
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
@ -59,6 +59,35 @@ def strip_post(post: Dict[str, str]) -> None:
|
|||||||
del post[key]
|
del post[key]
|
||||||
|
|
||||||
|
|
||||||
|
STORAGE_KEY: str = "stored_post"
|
||||||
|
|
||||||
|
|
||||||
|
def store_post(request: HttpRequest, post: Dict[str, str]):
|
||||||
|
"""Stores the POST data into the session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The request.
|
||||||
|
post: The POST data.
|
||||||
|
"""
|
||||||
|
request.session[STORAGE_KEY] = post
|
||||||
|
|
||||||
|
|
||||||
|
def retrieve_store(request: HttpRequest) -> Optional[Dict[str, str]]:
|
||||||
|
"""Retrieves the POST data from the storage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The POST data, or None if the previously-stored data does not exist.
|
||||||
|
"""
|
||||||
|
if STORAGE_KEY not in request.session:
|
||||||
|
return None
|
||||||
|
post = request.session[STORAGE_KEY]
|
||||||
|
del request.session[STORAGE_KEY]
|
||||||
|
return post
|
||||||
|
|
||||||
|
|
||||||
def parse_date(s: str):
|
def parse_date(s: str):
|
||||||
"""Parses a string for a date. The date can be either YYYY-MM-DD,
|
"""Parses a string for a date. The date can be either YYYY-MM-DD,
|
||||||
Y/M/D, or M/D/Y.
|
Y/M/D, or M/D/Y.
|
||||||
@ -134,8 +163,8 @@ class UrlBuilder:
|
|||||||
self.params = []
|
self.params = []
|
||||||
for piece in start_url[pos + 1:].split("&"):
|
for piece in start_url[pos + 1:].split("&"):
|
||||||
pos = piece.find("=")
|
pos = piece.find("=")
|
||||||
name = urllib.parse.unquote(piece[:pos])
|
name = urllib.parse.unquote_plus(piece[:pos])
|
||||||
value = urllib.parse.unquote(piece[pos + 1:])
|
value = urllib.parse.unquote_plus(piece[pos + 1:])
|
||||||
self.params.append(self.Param(name, value))
|
self.params.append(self.Param(name, value))
|
||||||
|
|
||||||
def add(self, name, value):
|
def add(self, name, value):
|
||||||
@ -400,8 +429,7 @@ class Pagination:
|
|||||||
url (str): The link URL, or for a non-link slot.
|
url (str): The link URL, or for a non-link slot.
|
||||||
title (str): The title of the link.
|
title (str): The title of the link.
|
||||||
is_active (bool): Whether this link is currently active.
|
is_active (bool): Whether this link is currently active.
|
||||||
is_small_screen (bool): Whether this link is for small
|
is_small_screen (bool): Whether this link is for small screens.
|
||||||
screens
|
|
||||||
"""
|
"""
|
||||||
def __int__(self):
|
def __int__(self):
|
||||||
self.url = None
|
self.url = None
|
@ -34,7 +34,7 @@ from django.views.generic import DeleteView as CoreDeleteView, \
|
|||||||
RedirectView as CoreRedirectView
|
RedirectView as CoreRedirectView
|
||||||
from django.views.generic.base import View
|
from django.views.generic.base import View
|
||||||
|
|
||||||
from . import stored_post, utils
|
from . import utils
|
||||||
from .models import StampedModel
|
from .models import StampedModel
|
||||||
from .utils import UrlBuilder
|
from .utils import UrlBuilder
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ class FormView(View):
|
|||||||
utils.strip_post(post)
|
utils.strip_post(post)
|
||||||
return self.make_form_from_post(post)
|
return self.make_form_from_post(post)
|
||||||
else:
|
else:
|
||||||
previous_post = stored_post.get_previous_post(self.request)
|
previous_post = utils.retrieve_store(self.request)
|
||||||
if previous_post is not None:
|
if previous_post is not None:
|
||||||
return self.make_form_from_post(previous_post)
|
return self.make_form_from_post(previous_post)
|
||||||
if self.object is not None:
|
if self.object is not None:
|
||||||
@ -146,8 +146,8 @@ class FormView(View):
|
|||||||
|
|
||||||
def form_invalid(self, form: forms.Form) -> HttpResponseRedirect:
|
def form_invalid(self, form: forms.Form) -> HttpResponseRedirect:
|
||||||
"""Handles the action when the POST form is invalid."""
|
"""Handles the action when the POST form is invalid."""
|
||||||
return stored_post.error_redirect(
|
utils.store_post(self.request, form.data)
|
||||||
self.request, self.get_error_url(), form.data)
|
return redirect(self.get_error_url())
|
||||||
|
|
||||||
def form_valid(self, form: forms.Form) -> HttpResponseRedirect:
|
def form_valid(self, form: forms.Form) -> HttpResponseRedirect:
|
||||||
"""Handles the action when the POST form is valid."""
|
"""Handles the action when the POST form is valid."""
|
158
tests/test_run.py
Normal file
158
tests/test_run.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# The accounting application of the Mia project.
|
||||||
|
# by imacat <imacat@mail.imacat.idv.tw>, 2022/12/5
|
||||||
|
|
||||||
|
# Copyright (c) 2022 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 tests to run the accounting application.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
from django.test import Client
|
||||||
|
|
||||||
|
|
||||||
|
class RunTestCase(unittest.TestCase):
|
||||||
|
"""The test case to run the accounting application."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Sets up the test.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None.
|
||||||
|
"""
|
||||||
|
test_site_path: str = str(Path(__file__).parent / "test_site")
|
||||||
|
if test_site_path not in sys.path:
|
||||||
|
sys.path.append(test_site_path)
|
||||||
|
self.username: str = "admin"
|
||||||
|
self.password: str = token_urlsafe(16)
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_site.settings")
|
||||||
|
get_wsgi_application()
|
||||||
|
call_command("migrate")
|
||||||
|
os.getlogin()
|
||||||
|
call_command("createsuperuser", interactive=False,
|
||||||
|
username=self.username, email="test@example.com")
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
user = User.objects.get(username=self.username)
|
||||||
|
user.set_password(self.password)
|
||||||
|
user.save()
|
||||||
|
call_command("accounting_accounts")
|
||||||
|
call_command("accounting_sample")
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
def test_run(self):
|
||||||
|
"""Tests the accounting application.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None.
|
||||||
|
"""
|
||||||
|
response = self.client.get("/accounting/", follow=True)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
response = self.client.post("/admin/login/",
|
||||||
|
{"username": self.username,
|
||||||
|
"password": self.password,
|
||||||
|
"next": "/ok"})
|
||||||
|
# 200 for errors, 302 for success.
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
response = self.client.get("/accounting/", follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
today: str = datetime.today().strftime("%Y-%m-%d")
|
||||||
|
expense_add_uri: str = "/accounting/transactions/expense/create?r=/ok"
|
||||||
|
income_add_uri: str = "/accounting/transactions/income/create?r=/ok"
|
||||||
|
transfer_add_uri: str \
|
||||||
|
= "/accounting/transactions/transfer/create?r=/ok"
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
expense_add_uri,
|
||||||
|
{"date": today,
|
||||||
|
"debit-2-ord": 6,
|
||||||
|
"debit-2-summary": "lunch",
|
||||||
|
"debit-2-amount": 80,
|
||||||
|
"debit-2-account": "6272",
|
||||||
|
"debit-8-ord": 4,
|
||||||
|
"debit-8-summary": "movies",
|
||||||
|
"debit-8-amount": 320,
|
||||||
|
"debit-8-account": "6273",
|
||||||
|
"notes": "yammy"})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertNotEqual(response.headers["Location"], expense_add_uri)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
income_add_uri,
|
||||||
|
{"date": today,
|
||||||
|
"credit-3-ord": 7,
|
||||||
|
"credit-3-summary": "withdrawal",
|
||||||
|
"credit-3-amount": 1000,
|
||||||
|
"credit-3-account": "1113",
|
||||||
|
"credit-6-ord": 3,
|
||||||
|
"credit-6-summary": "payroll",
|
||||||
|
"credit-6-amount": 10000,
|
||||||
|
"credit-6-account": "4611",
|
||||||
|
"notes": "wonderful"})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertNotEqual(response.headers["Location"], income_add_uri)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
transfer_add_uri,
|
||||||
|
{"date": today,
|
||||||
|
"debit-2-ord": 6,
|
||||||
|
"debit-2-summary": "lunch",
|
||||||
|
"debit-2-amount": 80,
|
||||||
|
"debit-2-account": "6272",
|
||||||
|
"debit-8-ord": 4,
|
||||||
|
"debit-8-summary": "movies",
|
||||||
|
"debit-8-amount": 320,
|
||||||
|
"debit-8-account": "6273",
|
||||||
|
"credit-3-ord": 7,
|
||||||
|
"credit-3-summary": "withdrawal",
|
||||||
|
"credit-3-amount": 100,
|
||||||
|
"credit-3-account": "1113",
|
||||||
|
"credit-6-ord": 3,
|
||||||
|
"credit-6-summary": "",
|
||||||
|
"credit-6-amount": 320,
|
||||||
|
"credit-6-account": "1111",
|
||||||
|
"notes": "nothing"})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.headers["Location"], transfer_add_uri)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
transfer_add_uri,
|
||||||
|
{"date": today,
|
||||||
|
"debit-2-ord": 6,
|
||||||
|
"debit-2-summary": "lunch",
|
||||||
|
"debit-2-amount": 80,
|
||||||
|
"debit-2-account": "6272",
|
||||||
|
"debit-8-ord": 4,
|
||||||
|
"debit-8-summary": "movies",
|
||||||
|
"debit-8-amount": 320,
|
||||||
|
"debit-8-account": "6273",
|
||||||
|
"credit-3-ord": 7,
|
||||||
|
"credit-3-summary": "withdrawal",
|
||||||
|
"credit-3-amount": 100,
|
||||||
|
"credit-3-account": "1113",
|
||||||
|
"credit-6-ord": 3,
|
||||||
|
"credit-6-summary": "",
|
||||||
|
"credit-6-amount": 300,
|
||||||
|
"credit-6-account": "1111",
|
||||||
|
"notes": "nothing"})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertNotEqual(response.headers["Location"], transfer_add_uri)
|
22
tests/test_site/manage.py
Executable file
22
tests/test_site/manage.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_site.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
25
tests/test_site/templates/base.html
Normal file
25
tests/test_site/templates/base.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
{% load mia_core %}
|
||||||
|
{% init_libs %}
|
||||||
|
{% block settings %}{% endblock %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/html" lang="{% get_current_language as language %}{{ language }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
{% for css in libs.css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% if css|is_static_url %}{% static css %}{% else %}{{ css }}{% endif %}" />
|
||||||
|
{% endfor %}
|
||||||
|
{% for js in libs.js %}
|
||||||
|
<script src="{% if js|is_static_url %}{% static js %}{% else %}{{ js }}{% endif %}"></script>
|
||||||
|
{% endfor %}
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
</body>
|
0
tests/test_site/test_site/__init__.py
Normal file
0
tests/test_site/test_site/__init__.py
Normal file
16
tests/test_site/test_site/asgi.py
Normal file
16
tests/test_site/test_site/asgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for test_site project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_site.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
126
tests/test_site/test_site/settings.py
Normal file
126
tests/test_site/test_site/settings.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
"""
|
||||||
|
Django settings for test_site project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 3.2.16.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = token_urlsafe(48)
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ["testserver"]
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'mia_core.apps.MiaCoreConfig',
|
||||||
|
'accounting.apps.AccountingConfig',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'test_site.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [(os.path.join(BASE_DIR, 'templates'))],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'test_site.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': ':memory:',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
27
tests/test_site/test_site/urls.py
Normal file
27
tests/test_site/test_site/urls.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"""test_site URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/3.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from decorator_include import decorator_include
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.urls import include, path
|
||||||
|
from django.views.i18n import JavaScriptCatalog
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('accounting/', decorator_include(login_required, 'accounting.urls')),
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('i18n/', include("django.conf.urls.i18n")),
|
||||||
|
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||||
|
]
|
16
tests/test_site/test_site/wsgi.py
Normal file
16
tests/test_site/test_site/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for test_site project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_site.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
Loading…
Reference in New Issue
Block a user