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
|
||||
__pycache__
|
||||
dist
|
||||
build
|
||||
*.egg-info
|
||||
migrations
|
||||
*.mo
|
||||
.idea
|
||||
venv
|
||||
mirror
|
||||
excludes
|
||||
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:
|
||||
"""The path converter for the period."""
|
||||
regex = ("([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)|"
|
||||
"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?-"
|
||||
"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?")
|
||||
regex = (r"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)|"
|
||||
r"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?-"
|
||||
r"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?")
|
||||
|
||||
def to_python(self, value):
|
||||
"""Returns the period by the period specification.
|
||||
@ -90,7 +90,7 @@ class DateConverter:
|
||||
Returns:
|
||||
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))
|
||||
month = int(m.group(2))
|
||||
day = int(m.group(3))
|
@ -205,8 +205,8 @@ class TransactionForm(forms.Form):
|
||||
by_rec_id = {}
|
||||
for key in args[0].keys():
|
||||
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)
|
||||
if m is None:
|
||||
continue
|
||||
@ -271,7 +271,7 @@ class TransactionForm(forms.Form):
|
||||
}
|
||||
for key in post.keys():
|
||||
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)
|
||||
if m is None:
|
||||
continue
|
||||
@ -303,7 +303,7 @@ class TransactionForm(forms.Form):
|
||||
= post[F"{record_type}-{old_no}-{attr}"]
|
||||
# Purges the old form and fills it with the new form
|
||||
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)]:
|
||||
del post[x]
|
||||
for key in new_post.keys():
|
||||
@ -475,11 +475,11 @@ class TransactionSortForm(forms.Form):
|
||||
key = F"transaction-{txn.pk}-ord"
|
||||
if key not in post:
|
||||
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))
|
||||
else:
|
||||
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 = []
|
||||
for i in range(len(post_orders)):
|
||||
form.txn_orders.append(form.Order(post_orders[i].txn, i + 1))
|
||||
@ -488,9 +488,9 @@ class TransactionSortForm(forms.Form):
|
||||
|
||||
class Order:
|
||||
"""A transaction order"""
|
||||
def __init__(self, txn: Transaction, order: int):
|
||||
def __init__(self, txn: Transaction, ord: int):
|
||||
self.txn = txn
|
||||
self.order = order
|
||||
self.ord = ord
|
||||
|
||||
|
||||
class AccountForm(forms.Form):
|
||||
@ -603,7 +603,6 @@ class AccountForm(forms.Form):
|
||||
code="code_unique")
|
||||
self.add_error("code", error)
|
||||
raise error
|
||||
return
|
||||
|
||||
def _validate_code_descendant_code_size(self) -> None:
|
||||
"""Validates whether the codes of the descendants will be too long.
|
@ -20,7 +20,7 @@
|
||||
"""
|
||||
import datetime
|
||||
import getpass
|
||||
import random
|
||||
from secrets import randbelow
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
@ -98,20 +98,20 @@ class Command(BaseCommand):
|
||||
|
||||
self._filler.add_expense_transaction(
|
||||
-2,
|
||||
[(6272, _("Lunch—Spaghetti"), random.randint(40, 200)),
|
||||
(6272, _("Drink—Tea"), random.randint(40, 200))])
|
||||
[(6272, _("Lunch—Spaghetti"), 40 + randbelow(160)),
|
||||
(6272, _("Drink—Tea"), 40 + randbelow(160))])
|
||||
self._filler.add_expense_transaction(
|
||||
-1,
|
||||
([(6272, _("Lunch—Pizza"), random.randint(40, 200)),
|
||||
(6272, _("Drink—Tea"), random.randint(40, 200))]))
|
||||
([(6272, _("Lunch—Pizza"), 40 + randbelow(160)),
|
||||
(6272, _("Drink—Tea"), 40 + randbelow(160))]))
|
||||
self._filler.add_expense_transaction(
|
||||
-1,
|
||||
[(6272, _("Lunch—Spaghetti"), random.randint(40, 200)),
|
||||
(6272, _("Drink—Soda"), random.randint(40, 200))])
|
||||
[(6272, _("Lunch—Spaghetti"), 40 + randbelow(160)),
|
||||
(6272, _("Drink—Soda"), 40 + randbelow(160))])
|
||||
self._filler.add_expense_transaction(
|
||||
0,
|
||||
[(6272, _("Lunch—Salad"), random.randint(40, 200)),
|
||||
(6272, _("Drink—Coffee"), random.randint(40, 200))])
|
||||
[(6272, _("Lunch—Salad"), 40 + randbelow(160)),
|
||||
(6272, _("Drink—Coffee"), 40 + randbelow(160))])
|
||||
|
||||
@staticmethod
|
||||
def get_user(username_option):
|
||||
@ -155,7 +155,7 @@ class Command(BaseCommand):
|
||||
payday = today.replace(day=5)
|
||||
if payday > today:
|
||||
payday = self.previous_month(payday)
|
||||
for i in range(months):
|
||||
for _ in range(months):
|
||||
self.add_payroll(payday)
|
||||
payday = self.previous_month(payday)
|
||||
|
||||
@ -181,7 +181,7 @@ class Command(BaseCommand):
|
||||
Args:
|
||||
payday: The payday.
|
||||
"""
|
||||
income = random.randint(40000, 50000)
|
||||
income = 40000 + randbelow(10000)
|
||||
pension = 882 if income <= 40100\
|
||||
else 924 if income <= 42000\
|
||||
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
|
||||
# Collects the records to be deleted
|
||||
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
|
||||
if x.is_dirty(check_relationship=True)]
|
||||
for record in to_save:
|
||||
@ -215,7 +216,7 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
||||
txn_type: The transaction type.
|
||||
"""
|
||||
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(
|
||||
int(m.group(1)),
|
||||
int(m.group(2)),
|
||||
@ -228,7 +229,8 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
||||
for i in range(max_no[record_type]):
|
||||
no = i + 1
|
||||
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:
|
||||
record = Record(
|
||||
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.
|
||||
|
||||
Args:
|
||||
txn_type (str): The transaction type.
|
||||
post (dict[str,str]): The POSTed data.
|
||||
txn_type: The transaction type.
|
||||
post: The POSTed data.
|
||||
|
||||
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 = {}
|
||||
if txn_type != "credit":
|
||||
@ -281,7 +282,8 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
||||
max_no["credit"] = 0
|
||||
for key in post.keys():
|
||||
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)
|
||||
if m is None:
|
||||
continue
|
||||
@ -301,8 +303,11 @@ class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
|
||||
List[Record]: The records.
|
||||
"""
|
||||
if self._records is None:
|
||||
self._records = list(self.record_set.all())
|
||||
self._records.sort(key=lambda x: (x.is_credit, x.ord))
|
||||
if self.pk is None:
|
||||
self._records = []
|
||||
else:
|
||||
self._records = list(self.record_set.all())
|
||||
self._records.sort(key=lambda x: (x.is_credit, x.ord))
|
||||
return self._records
|
||||
|
||||
@records.setter
|
@ -161,8 +161,6 @@
|
||||
font-size: 1.21em;
|
||||
border-bottom: thick double slategray;
|
||||
}
|
||||
.balance-sheet-table tbody {
|
||||
}
|
||||
.balance-sheet-table .group-title {
|
||||
font-size: 1.1em;
|
||||
font-weight: bolder;
|
@ -50,8 +50,8 @@ let accounts;
|
||||
*/
|
||||
function getAllAccounts() {
|
||||
const request = new XMLHttpRequest();
|
||||
request.onreadystatechange = function() {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
request.onload = function() {
|
||||
if (this.status === 200) {
|
||||
accounts = JSON.parse(this.responseText);
|
||||
}
|
||||
};
|
||||
@ -75,7 +75,7 @@ function updateParent(code) {
|
||||
parent.text(gettext("Topmost"));
|
||||
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) {
|
||||
parent.text(parentCode + " " + accounts[parentCode]);
|
||||
return;
|
||||
@ -96,10 +96,10 @@ function updateParent(code) {
|
||||
* @private
|
||||
*/
|
||||
function validateForm() {
|
||||
let isValidated = true;
|
||||
isValidated = isValidated && validateCode();
|
||||
isValidated = isValidated && validateTitle();
|
||||
return isValidated;
|
||||
let isValid = true;
|
||||
isValid = validateCode() && isValid;
|
||||
isValid = validateTitle() && isValid;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,7 +136,7 @@ function validateCode() {
|
||||
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)) {
|
||||
code.classList.add("is-invalid");
|
||||
errorMessage.text(gettext("The parent account of this code does not exist."));
|
@ -298,7 +298,7 @@ function parseSummaryForHelper(summary) {
|
||||
const pos = summary.lastIndexOf("×");
|
||||
let count = 1;
|
||||
if (pos !== -1) {
|
||||
count = parseInt(summary.substr(pos + 1));
|
||||
count = parseInt(summary.substring(pos + 1));
|
||||
}
|
||||
if (count === 0) {
|
||||
count = 1;
|
||||
@ -329,7 +329,7 @@ function parseSummaryForCategoryHelpers(summary) {
|
||||
});
|
||||
|
||||
// A bus route
|
||||
const matchBus = summary.match(/^(.+)—(.+)—(.+)→(.+?)(?:×[0-9]+)?$/);
|
||||
const matchBus = summary.match(/^([^—]+)—([^—]+)—([^—]+)→([^—]+?)(?:×\d+)?$/);
|
||||
if (matchBus !== null) {
|
||||
$("#summary-bus-category").get(0).value = matchBus[1];
|
||||
setSummaryBusCategoryButtons(matchBus[1]);
|
||||
@ -342,7 +342,7 @@ function parseSummaryForCategoryHelpers(summary) {
|
||||
}
|
||||
|
||||
// A general travel route
|
||||
const matchTravel = summary.match(/^(.+)—(.+)([→|↔])(.+?)(?:×[0-9]+)?$/);
|
||||
const matchTravel = summary.match(/^([^—]+)—([^—]+)([→|↔])([^—]+?)(?:×\d+)?$/);
|
||||
if (matchTravel !== null) {
|
||||
$("#summary-travel-category").get(0).value = matchTravel[1];
|
||||
setSummaryTravelCategoryButtons(matchTravel[1]);
|
||||
@ -357,7 +357,7 @@ function parseSummaryForCategoryHelpers(summary) {
|
||||
|
||||
// A general category
|
||||
const generalCategoryTab = $("#summary-tab-category");
|
||||
const matchCategory = summary.match(/^(.+)—.+(?:×[0-9]+)?$/);
|
||||
const matchCategory = summary.match(/^([^—]+)—.+(?:×\d+)?$/);
|
||||
if (matchCategory !== null) {
|
||||
$("#summary-general-category").get(0).value = matchCategory[1];
|
||||
setSummaryGeneralCategoryButtons(matchCategory[1]);
|
@ -91,8 +91,8 @@ let accountOptions;
|
||||
*/
|
||||
function getAccountOptions() {
|
||||
const request = new XMLHttpRequest();
|
||||
request.onreadystatechange = function() {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
request.onload = function() {
|
||||
if (this.status === 200) {
|
||||
accountOptions = JSON.parse(this.responseText);
|
||||
$(".record-account").each(function () {
|
||||
initializeAccountOptions($(this));
|
||||
@ -184,8 +184,8 @@ function updateTotalAmount(element) {
|
||||
}
|
||||
});
|
||||
total = String(total);
|
||||
while (total.match(/^[1-9][0-9]*[0-9]{3}/)) {
|
||||
total = total.replace(/^([1-9][0-9]*)([0-9]{3})/, "$1,$2");
|
||||
while (total.match(/^[1-9]\d*\d{3}/)) {
|
||||
total = total.replace(/^([1-9]\d*)(\d{3})/, "$1,$2");
|
||||
}
|
||||
$("#" + type + "-total").text(total);
|
||||
}
|
||||
@ -337,28 +337,28 @@ function resetRecordButtons() {
|
||||
* @private
|
||||
*/
|
||||
function validateForm() {
|
||||
let isValidated = true;
|
||||
isValidated = isValidated && validateDate();
|
||||
let isValid = true;
|
||||
isValid = validateDate() && isValid;
|
||||
$(".debit-record").each(function () {
|
||||
isValidated = isValidated && validateRecord(this);
|
||||
isValid = validateRecord(this) && isValid;
|
||||
});
|
||||
$(".credit-account").each(function () {
|
||||
isValidated = isValidated && validateRecord(this);
|
||||
isValid = validateRecord(this) && isValid;
|
||||
});
|
||||
$(".record-account").each(function () {
|
||||
isValidated = isValidated && validateAccount(this);
|
||||
isValid = validateAccount(this) && isValid;
|
||||
});
|
||||
$(".record-summary").each(function () {
|
||||
isValidated = isValidated && validateSummary(this);
|
||||
isValid = validateSummary(this) && isValid;
|
||||
});
|
||||
$(".record-amount").each(function () {
|
||||
isValidated = isValidated && validateAmount(this);
|
||||
isValid = validateAmount(this) && isValid;
|
||||
});
|
||||
if (isTransfer()) {
|
||||
isValidated = isValidated && validateBalance();
|
||||
isValid = validateBalance() && isValid;
|
||||
}
|
||||
isValidated = isValidated && validateNote();
|
||||
return isValidated;
|
||||
isValid = validateNote() && isValid;
|
||||
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">
|
||||
{% csrf_token %}
|
||||
<!-- The Modal -->
|
||||
<div class="modal" id="del-modal">
|
||||
<div class="modal fade" id="del-modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
@ -57,8 +57,8 @@ First written: 2020/8/8
|
||||
|
||||
<!-- 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 class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
||||
</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)
|
||||
First written: 2020/8/5
|
||||
{% 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 }}">
|
||||
<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 class="col-sm-4">
|
||||
<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>
|
||||
</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)
|
||||
First written: 2020/8/5
|
||||
{% 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 }}">
|
||||
<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 class="col-lg-4">
|
||||
<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>
|
||||
</div>
|
@ -25,7 +25,7 @@ First written: 2020/7/9
|
||||
<!-- the accounting record search dialog -->
|
||||
<form action="{% url "accounting:search" %}" method="GET">
|
||||
<!-- The Modal -->
|
||||
<div class="modal" id="accounting-search-modal">
|
||||
<div class="modal fade" id="accounting-search-modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
@ -25,7 +25,7 @@ First written: 2020/4/3
|
||||
<!-- The Modal -->
|
||||
<form id="summary-helper-form" action="" method="get">
|
||||
<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-content">
|
||||
|
||||
@ -155,8 +155,8 @@ First written: 2020/4/3
|
||||
|
||||
<!-- 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 id="summary-confirm" class="btn btn-danger" type="submit" data-dismiss="modal">{{ _("Confirm")|force_escape }}</button>
|
||||
</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">
|
||||
{% csrf_token %}
|
||||
<!-- The Modal -->
|
||||
<div class="modal" id="del-modal">
|
||||
<div class="modal fade" id="del-modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
@ -59,8 +59,8 @@ First written: 2020/7/23
|
||||
|
||||
<!-- 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 class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
||||
</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">
|
||||
{% csrf_token %}
|
||||
<!-- The Modal -->
|
||||
<div class="modal" id="del-modal">
|
||||
<div class="modal fade" id="del-modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
@ -59,8 +59,8 @@ First written: 2020/7/23
|
||||
|
||||
<!-- 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 class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
||||
</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">
|
||||
{% csrf_token %}
|
||||
<!-- The Modal -->
|
||||
<div class="modal" id="del-modal">
|
||||
<div class="modal fade" id="del-modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
@ -59,8 +59,8 @@ First written: 2020/7/23
|
||||
|
||||
<!-- 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 class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -20,7 +20,7 @@
|
||||
"""
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Union, Optional
|
||||
from typing import Optional
|
||||
|
||||
from django import template
|
||||
from django.template import RequestContext
|
||||
@ -32,28 +32,41 @@ from mia_core.period import Period
|
||||
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.
|
||||
|
||||
Args:
|
||||
value: The amount.
|
||||
|
||||
Returns:
|
||||
ReportUrl: The formatted amount.
|
||||
str: The amount in the desired format.
|
||||
"""
|
||||
s = str(value)
|
||||
s = _strip_decimal_zeros(value)
|
||||
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:
|
||||
break
|
||||
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
|
||||
|
||||
|
||||
@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
|
||||
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.
|
||||
|
||||
Returns:
|
||||
ReportUrl: The formatted amount.
|
||||
str: The amount in the desired format.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
@ -74,14 +87,14 @@ def accounting_amount(value: Union[Decimal]) -> str:
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
Args:
|
||||
value: The amount.
|
||||
|
||||
Returns:
|
||||
ReportUrl: The formatted amount.
|
||||
str: The amount in the desired format.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
@ -93,6 +106,21 @@ def short_amount(value: Union[Decimal]) -> str:
|
||||
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)
|
||||
def report_url(context: RequestContext,
|
||||
cash_account: Optional[Account],
|
@ -80,10 +80,9 @@ class ReportUrl:
|
||||
|
||||
Args:
|
||||
namespace: The namespace of the current application.
|
||||
cash: The currently-specified account of the
|
||||
cash account or cash summary.
|
||||
ledger: The currently-specified account of the
|
||||
ledger or leger summary.
|
||||
cash: The currently-specified account of the cash account or cash
|
||||
summary.
|
||||
ledger: The currently-specified account of the ledger or leger summary.
|
||||
period: The currently-specified period.
|
||||
"""
|
||||
|
||||
@ -145,9 +144,8 @@ class DataFiller:
|
||||
"""Adds accounts.
|
||||
|
||||
Args:
|
||||
accounts (tuple[tuple[any]]): Tuples of
|
||||
(code, English, Traditional Chinese, Simplified Chinese)
|
||||
of the accounts.
|
||||
accounts: Tuples of (code, English, Traditional Chinese, Simplified
|
||||
Chinese) of the accounts.
|
||||
"""
|
||||
for data in accounts:
|
||||
code = data[0]
|
||||
@ -167,8 +165,7 @@ class DataFiller:
|
||||
"""Adds a transfer transaction.
|
||||
|
||||
Args:
|
||||
date: The date, or the number of days from
|
||||
today.
|
||||
date: The date, or the number of days from today.
|
||||
debit: Tuples of (account, summary, amount) of the debit 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.db import transaction
|
||||
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.http import JsonResponse, HttpResponseRedirect, Http404, \
|
||||
HttpRequest, HttpResponse
|
||||
@ -94,7 +94,7 @@ def cash(request: HttpRequest, account: Account,
|
||||
~Q(account__code__startswith="21"),
|
||||
~Q(account__code__startswith="22"))
|
||||
.order_by("transaction__date", "transaction__ord",
|
||||
"is_credit", "ord"))
|
||||
"-is_credit", "ord"))
|
||||
balance_before = Record.objects \
|
||||
.filter(
|
||||
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="22"))) \
|
||||
.aggregate(
|
||||
balance=Coalesce(Sum(Case(
|
||||
When(is_credit=True, then=-1),
|
||||
default=1) * F("amount")), 0))["balance"]
|
||||
balance=Coalesce(Sum(Case(When(is_credit=True, then=-1),
|
||||
default=1) * F("amount"),
|
||||
output_field=DecimalField()),
|
||||
0, output_field=DecimalField()))["balance"]
|
||||
else:
|
||||
records = list(
|
||||
Record.objects
|
||||
@ -116,15 +117,16 @@ def cash(request: HttpRequest, account: Account,
|
||||
Q(record__account__code__startswith=account.code))),
|
||||
~Q(account__code__startswith=account.code))
|
||||
.order_by("transaction__date", "transaction__ord",
|
||||
"is_credit", "ord"))
|
||||
"-is_credit", "ord"))
|
||||
balance_before = Record.objects \
|
||||
.filter(
|
||||
transaction__date__lt=period.start,
|
||||
account__code__startswith=account.code) \
|
||||
.aggregate(
|
||||
balance=Coalesce(Sum(Case(When(
|
||||
is_credit=True, then=-1),
|
||||
default=1) * F("amount")), 0))["balance"]
|
||||
balance=Coalesce(Sum(Case(When(is_credit=True, then=-1),
|
||||
default=1) * F("amount"),
|
||||
output_field=DecimalField()),
|
||||
0, output_field=DecimalField()))["balance"]
|
||||
balance = balance_before
|
||||
for record in records:
|
||||
sign = 1 if record.is_credit else -1
|
||||
@ -211,15 +213,14 @@ def cash_summary(request: HttpRequest, account: Account) -> HttpResponse:
|
||||
.values("month")
|
||||
.order_by("month")
|
||||
.annotate(
|
||||
debit=Coalesce(
|
||||
Sum(Case(When(is_credit=False, then=F("amount")))),
|
||||
0),
|
||||
credit=Coalesce(
|
||||
Sum(Case(When(is_credit=True, then=F("amount")))),
|
||||
0),
|
||||
balance=Sum(Case(
|
||||
When(is_credit=False, then=-F("amount")),
|
||||
default=F("amount"))))]
|
||||
debit=Coalesce(Sum(Case(When(is_credit=False,
|
||||
then=F("amount")))),
|
||||
0, output_field=DecimalField()),
|
||||
credit=Coalesce(Sum(Case(When(is_credit=True,
|
||||
then=F("amount")))),
|
||||
0, output_field=DecimalField()),
|
||||
balance=Sum(Case(When(is_credit=False, then=-F("amount")),
|
||||
default=F("amount"))))]
|
||||
else:
|
||||
months = [utils.MonthlySummary(**x) for x in Record.objects
|
||||
.filter(
|
||||
@ -230,15 +231,14 @@ def cash_summary(request: HttpRequest, account: Account) -> HttpResponse:
|
||||
.values("month")
|
||||
.order_by("month")
|
||||
.annotate(
|
||||
debit=Coalesce(
|
||||
Sum(Case(When(is_credit=False, then=F("amount")))),
|
||||
0),
|
||||
credit=Coalesce(
|
||||
Sum(Case(When(is_credit=True, then=F("amount")))),
|
||||
0),
|
||||
balance=Sum(Case(
|
||||
When(is_credit=False, then=-F("amount")),
|
||||
default=F("amount"))))]
|
||||
debit=Coalesce(Sum(Case(When(is_credit=False,
|
||||
then=F("amount")))),
|
||||
0, output_field=DecimalField()),
|
||||
credit=Coalesce(Sum(Case(When(is_credit=True,
|
||||
then=F("amount")))),
|
||||
0, output_field=DecimalField()),
|
||||
balance=Sum(Case(When(is_credit=False, then=-F("amount")),
|
||||
default=F("amount"))))]
|
||||
cumulative_balance = 0
|
||||
for month in months:
|
||||
cumulative_balance = cumulative_balance + month.balance
|
||||
@ -305,9 +305,10 @@ def ledger(request: HttpRequest, account: Account,
|
||||
transaction__date__lt=period.start,
|
||||
account__code__startswith=account.code) \
|
||||
.aggregate(
|
||||
balance=Coalesce(Sum(Case(When(
|
||||
is_credit=True, then=-1),
|
||||
default=1) * F("amount")), 0))["balance"]
|
||||
balance=Coalesce(Sum(Case(When(is_credit=True, then=-1),
|
||||
default=1) * F("amount"),
|
||||
output_field=DecimalField()),
|
||||
0, output_field=DecimalField()))["balance"]
|
||||
record_brought_forward = Record(
|
||||
transaction=Transaction(date=period.start),
|
||||
account=account,
|
||||
@ -370,15 +371,14 @@ def ledger_summary(request: HttpRequest, account: Account) -> HttpResponse:
|
||||
.values("month")
|
||||
.order_by("month")
|
||||
.annotate(
|
||||
debit=Coalesce(
|
||||
Sum(Case(When(is_credit=False, then=F("amount")))),
|
||||
0),
|
||||
credit=Coalesce(
|
||||
Sum(Case(When(is_credit=True, then=F("amount")))),
|
||||
0),
|
||||
balance=Sum(Case(
|
||||
When(is_credit=False, then=F("amount")),
|
||||
default=-F("amount"))))]
|
||||
debit=Coalesce(Sum(Case(When(is_credit=False,
|
||||
then=F("amount")))),
|
||||
0, output_field=DecimalField()),
|
||||
credit=Coalesce(Sum(Case(When(is_credit=True,
|
||||
then=F("amount")))),
|
||||
0, output_field=DecimalField()),
|
||||
balance=Sum(Case(When(is_credit=False, then=F("amount")),
|
||||
default=-F("amount"))))]
|
||||
cumulative_balance = 0
|
||||
for month in months:
|
||||
cumulative_balance = cumulative_balance + month.balance
|
||||
@ -436,11 +436,10 @@ def journal(request: HttpRequest, period: Period) -> HttpResponse:
|
||||
| Q(code__startswith="2")
|
||||
| Q(code__startswith="3")) \
|
||||
.annotate(balance=Sum(
|
||||
Case(
|
||||
When(record__is_credit=True, then=-1),
|
||||
default=1
|
||||
) * F("record__amount"),
|
||||
filter=Q(record__transaction__date__lt=period.start))) \
|
||||
Case(When(record__is_credit=True, then=-1),
|
||||
default=1) * F("record__amount"),
|
||||
filter=Q(record__transaction__date__lt=period.start),
|
||||
output_field=DecimalField())) \
|
||||
.filter(~Q(balance=0))
|
||||
debit_records = [Record(
|
||||
transaction=Transaction(date=period.start),
|
||||
@ -513,9 +512,9 @@ def trial_balance(request: HttpRequest, period: Period) -> HttpResponse:
|
||||
| Q(code__startswith="2")
|
||||
| Q(code__startswith="3")))
|
||||
.annotate(
|
||||
amount=Sum(Case(
|
||||
When(record__is_credit=True, then=-1),
|
||||
default=1) * F("record__amount")))
|
||||
amount=Sum(Case(When(record__is_credit=True, then=-1),
|
||||
default=1) * F("record__amount"),
|
||||
output_field=DecimalField()))
|
||||
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
||||
.annotate(
|
||||
debit_amount=Case(
|
||||
@ -534,9 +533,9 @@ def trial_balance(request: HttpRequest, period: Period) -> HttpResponse:
|
||||
| Q(code__startswith="3")),
|
||||
~Q(code=Account.ACCUMULATED_BALANCE))
|
||||
.annotate(
|
||||
amount=Sum(Case(
|
||||
When(record__is_credit=True, then=-1),
|
||||
default=1) * F("record__amount")))
|
||||
amount=Sum(Case(When(record__is_credit=True, then=-1),
|
||||
default=1) * F("record__amount"),
|
||||
output_field=DecimalField()))
|
||||
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
||||
.annotate(
|
||||
debit_amount=Case(
|
||||
@ -555,9 +554,9 @@ def trial_balance(request: HttpRequest, period: Period) -> HttpResponse:
|
||||
| (Q(transaction__date__lte=period.end)
|
||||
& Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
||||
.aggregate(
|
||||
balance=Sum(Case(
|
||||
When(is_credit=True, then=-1),
|
||||
default=1) * F("amount")))["balance"]
|
||||
balance=Sum(Case(When(is_credit=True, then=-1),
|
||||
default=1) * F("amount"),
|
||||
output_field=DecimalField()))["balance"]
|
||||
if balance is not None and balance != 0:
|
||||
brought_forward = Account.objects.get(
|
||||
code=Account.ACCUMULATED_BALANCE)
|
||||
@ -614,9 +613,9 @@ def income_statement(request: HttpRequest, period: Period) -> HttpResponse:
|
||||
| Q(code__startswith="2")
|
||||
| Q(code__startswith="3")))
|
||||
.annotate(
|
||||
amount=Sum(Case(
|
||||
When(record__is_credit=True, then=1),
|
||||
default=-1) * F("record__amount")))
|
||||
amount=Sum(Case(When(record__is_credit=True, then=1),
|
||||
default=-1) * F("record__amount"),
|
||||
output_field=DecimalField()))
|
||||
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
||||
.order_by("code"))
|
||||
groups = list(Account.objects.filter(
|
||||
@ -687,9 +686,9 @@ def balance_sheet(request: HttpRequest, period: Period) -> HttpResponse:
|
||||
| Q(code__startswith="3")),
|
||||
~Q(code=Account.ACCUMULATED_BALANCE))
|
||||
.annotate(
|
||||
amount=Sum(Case(
|
||||
When(record__is_credit=True, then=-1),
|
||||
default=1) * F("record__amount")))
|
||||
amount=Sum(Case(When(record__is_credit=True, then=-1),
|
||||
default=1) * F("record__amount"),
|
||||
output_field=DecimalField()))
|
||||
.filter(Q(amount__isnull=False), ~Q(amount=0))
|
||||
.order_by("code"))
|
||||
for account in accounts:
|
||||
@ -703,9 +702,9 @@ def balance_sheet(request: HttpRequest, period: Period) -> HttpResponse:
|
||||
| Q(account__code__startswith="3"))
|
||||
& ~Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
||||
.aggregate(
|
||||
balance=Sum(Case(
|
||||
When(is_credit=True, then=-1),
|
||||
default=1) * F("amount")))["balance"]
|
||||
balance=Sum(Case(When(is_credit=True, then=-1),
|
||||
default=1) * F("amount"),
|
||||
output_field=DecimalField()))["balance"]
|
||||
if balance is not None and balance != 0:
|
||||
brought_forward = Account.objects.get(
|
||||
code=Account.ACCUMULATED_BALANCE)
|
||||
@ -723,9 +722,9 @@ def balance_sheet(request: HttpRequest, period: Period) -> HttpResponse:
|
||||
| Q(account__code__startswith="3"))
|
||||
& ~Q(account__code=Account.ACCUMULATED_BALANCE))) \
|
||||
.aggregate(
|
||||
balance=Sum(Case(
|
||||
When(is_credit=True, then=-1),
|
||||
default=1) * F("amount")))["balance"]
|
||||
balance=Sum(Case(When(is_credit=True, then=-1),
|
||||
default=1) * F("amount"),
|
||||
output_field=DecimalField()))["balance"]
|
||||
if balance is not None and balance != 0:
|
||||
net_change = Account.objects.get(code=Account.NET_CHANGE)
|
||||
net_change.amount = balance
|
||||
@ -781,12 +780,12 @@ class SearchListView(TemplateView):
|
||||
if len(terms) == 0:
|
||||
return []
|
||||
conditions = [self._get_conditions_for_term(x) for x in terms]
|
||||
if len(conditions) == 1:
|
||||
return Record.objects.filter(conditions[0])
|
||||
combined = conditions[0]
|
||||
for x in conditions[1:]:
|
||||
combined = combined & x
|
||||
return Record.objects.filter(combined)
|
||||
return Record.objects.filter(combined)\
|
||||
.order_by("transaction__date", "transaction__ord", "is_credit",
|
||||
"ord")
|
||||
|
||||
@staticmethod
|
||||
def _get_conditions_for_term(term: str) -> Q:
|
||||
@ -805,9 +804,9 @@ class SearchListView(TemplateView):
|
||||
| Q(code=term)))\
|
||||
| Q(summary__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))
|
||||
if re.match("^[1-9][0-8]{9}$", term):
|
||||
if re.match(r"^[1-9][0-8]{9}$", term):
|
||||
conditions = conditions\
|
||||
| Q(pk=int(term))\
|
||||
| Q(transaction__pk=int(term))\
|
||||
@ -1087,13 +1086,13 @@ class TransactionSortFormView(FormView):
|
||||
|
||||
def form_valid(self, form: TransactionSortForm) -> HttpResponseRedirect:
|
||||
"""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:
|
||||
message = self.get_not_modified_message(form.cleaned_data)
|
||||
else:
|
||||
with transaction.atomic():
|
||||
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)
|
||||
messages.success(self.request, message)
|
||||
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 ""
|
||||
"Project-Id-Version: mia-core 3.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-08-17 23:56+0800\n"
|
||||
"PO-Revision-Date: 2020-08-18 00:02+0800\n"
|
||||
"POT-Creation-Date: 2021-01-17 00:24+0800\n"
|
||||
"PO-Revision-Date: 2021-01-17 00:29+0800\n"
|
||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
||||
"Language-Team: Traditional Chinese <imacat@mail.imacat.idv.tw>\n"
|
||||
"Language: Traditional Chinese\n"
|
||||
@ -38,13 +38,13 @@ msgstr "全部"
|
||||
|
||||
#: mia_core/period.py:588 mia_core/period.py:621
|
||||
#: 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"
|
||||
msgstr "這個月"
|
||||
|
||||
#: mia_core/period.py:629
|
||||
#: 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"
|
||||
msgstr "上個月"
|
||||
|
||||
@ -60,13 +60,13 @@ msgstr "去年"
|
||||
|
||||
#: mia_core/period.py:661
|
||||
#: 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"
|
||||
msgstr "今天"
|
||||
|
||||
#: mia_core/period.py:663
|
||||
#: 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"
|
||||
msgstr "昨天"
|
||||
|
||||
@ -115,17 +115,21 @@ msgstr "從:"
|
||||
msgid "To:"
|
||||
msgstr "到:"
|
||||
|
||||
#: mia_core/utils.py:347
|
||||
#: mia_core/templatetags/mia_core.py:192
|
||||
msgid "Tomorrow"
|
||||
msgstr "明天"
|
||||
|
||||
#: mia_core/utils.py:342
|
||||
msgctxt "Pagination|"
|
||||
msgid "Previous"
|
||||
msgstr "上一頁"
|
||||
|
||||
#: mia_core/utils.py:375 mia_core/utils.py:396
|
||||
#: mia_core/utils.py:370 mia_core/utils.py:391
|
||||
msgctxt "Pagination|"
|
||||
msgid "..."
|
||||
msgstr "…"
|
||||
|
||||
#: mia_core/utils.py:415
|
||||
#: mia_core/utils.py:410
|
||||
msgctxt "Pagination|"
|
||||
msgid "Next"
|
||||
msgstr "下一頁"
|
@ -30,8 +30,13 @@ from opencc import OpenCC
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Populates the database with sample accounting data."""
|
||||
help = "Fills the database with the accounting accounts."
|
||||
"""Updates the revision date, converts the Traditional Chinese
|
||||
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):
|
||||
super().__init__()
|
||||
@ -44,8 +49,9 @@ class Command(BaseCommand):
|
||||
Args:
|
||||
parser (CommandParser): The command line argument parser.
|
||||
"""
|
||||
parser.add_argument("proj_dir", nargs="+",
|
||||
help="The domain, either django or djangojs")
|
||||
parser.add_argument("app_dir", nargs="+",
|
||||
help=("One or more application directories that"
|
||||
" contains their locale subdirectories"))
|
||||
parser.add_argument("--domain", "-d", action="append",
|
||||
choices=["django", "djangojs"], required=True,
|
||||
help="The domain, either django or djangojs")
|
||||
@ -58,7 +64,7 @@ class Command(BaseCommand):
|
||||
**options (dict[str,str]): The command line switches.
|
||||
"""
|
||||
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)]
|
||||
if len(missing) > 0:
|
||||
error = "Directories not exist: " + ", ".join(missing)
|
@ -69,7 +69,7 @@ class StampedModel(models.Model):
|
||||
F"Missing current_user in {self.__class__.__name__}")
|
||||
try:
|
||||
self.created_by
|
||||
except ObjectDoesNotExist as e:
|
||||
except ObjectDoesNotExist:
|
||||
self.created_by = self.current_user
|
||||
self.updated_by = self.current_user
|
||||
super().save(force_insert=force_insert, force_update=force_update,
|
||||
@ -126,9 +126,12 @@ class LocalizedModel(models.Model):
|
||||
current_value = getattr(self, name + "_l10n")
|
||||
if current_value is None or current_value == "":
|
||||
setattr(self, name + "_l10n", new_value)
|
||||
l10n_rec = self._get_l10n_set()\
|
||||
.filter(name=name, language=language)\
|
||||
.first()
|
||||
if self.pk is None:
|
||||
l10n_rec = None
|
||||
else:
|
||||
l10n_rec = self._get_l10n_set()\
|
||||
.filter(name=name, language=language)\
|
||||
.first()
|
||||
if l10n_rec is None:
|
||||
l10n_to_save.append(self._get_l10n_set().model(
|
||||
master=self, name=name,
|
@ -195,7 +195,7 @@ class Period:
|
||||
"""
|
||||
if self._data_start is 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:
|
||||
return None
|
||||
if self._period.end < self._data_start:
|
||||
@ -379,9 +379,9 @@ class Period:
|
||||
if self.start <= self._data_start:
|
||||
return None
|
||||
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)
|
||||
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-d")
|
||||
|
||||
@ -441,7 +441,7 @@ class Period:
|
||||
return
|
||||
self.spec = spec
|
||||
# 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:
|
||||
year = int(m.group(1))
|
||||
month = int(m.group(2))
|
||||
@ -452,7 +452,7 @@ class Period:
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# 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:
|
||||
year = int(m.group(1))
|
||||
month = int(m.group(2))
|
||||
@ -464,7 +464,7 @@ class Period:
|
||||
self.prep_desc = self.description
|
||||
return
|
||||
# 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:
|
||||
year = int(m.group(1))
|
||||
month = int(m.group(2))
|
||||
@ -477,7 +477,7 @@ class Period:
|
||||
self.prep_desc = self.description
|
||||
return
|
||||
# A specific year
|
||||
m = re.match("^([0-9]{4})$", spec)
|
||||
m = re.match(r"^(\d{4})$", spec)
|
||||
if m is not None:
|
||||
year = int(m.group(1))
|
||||
# Raises ValueError
|
||||
@ -487,7 +487,7 @@ class Period:
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# Until a specific year
|
||||
m = re.match("^-([0-9]{4})$", spec)
|
||||
m = re.match(r"^-(\d{4})$", spec)
|
||||
if m is not None:
|
||||
year = int(m.group(1))
|
||||
# Raises ValueError
|
||||
@ -505,7 +505,7 @@ class Period:
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# 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)
|
||||
if m is not None:
|
||||
# Raises ValueError
|
||||
@ -518,8 +518,8 @@ class Period:
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# A specific date period
|
||||
m = re.match(("^([0-9]{4})-([0-9]{2})-([0-9]{2})"
|
||||
"-([0-9]{4})-([0-9]{2})-([0-9]{2})$"),
|
||||
m = re.match((r"^(\d{4})-(\d{2})-(\d{2})"
|
||||
r"-(\d{4})-(\d{2})-(\d{2})$"),
|
||||
spec)
|
||||
if m is not None:
|
||||
# Raises ValueError
|
||||
@ -564,7 +564,7 @@ class Period:
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# 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:
|
||||
# Raises ValueError
|
||||
self.end = datetime.date(
|
@ -26,7 +26,7 @@ First written: 2020/7/10
|
||||
<!-- The Modal -->
|
||||
<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 }}" />
|
||||
<div class="modal" id="period-modal">
|
||||
<div class="modal fade" id="period-modal">
|
||||
<div class="modal-dialog">
|
||||
<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.utils import timezone
|
||||
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
|
||||
|
||||
@ -185,8 +185,20 @@ def smart_date(value: datetime.date) -> str:
|
||||
"""
|
||||
if value == date.today():
|
||||
return gettext("Today")
|
||||
if (date.today() - value).days == 1:
|
||||
prev_days = (value - date.today()).days
|
||||
if prev_days == -1:
|
||||
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:
|
||||
return defaultfilters.date(value, "n/j(D)").replace("星期", "")
|
||||
return defaultfilters.date(value, "Y/n/j(D)").replace("星期", "")
|
@ -19,9 +19,9 @@
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import random
|
||||
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.db.models import Model
|
||||
@ -40,7 +40,7 @@ def new_pk(cls: Type[Model]) -> int:
|
||||
The new random ID.
|
||||
"""
|
||||
while True:
|
||||
pk = random.randint(100000000, 999999999)
|
||||
pk = 100000000 + randbelow(900000000)
|
||||
try:
|
||||
cls.objects.get(pk=pk)
|
||||
except cls.DoesNotExist:
|
||||
@ -59,6 +59,35 @@ def strip_post(post: Dict[str, str]) -> None:
|
||||
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):
|
||||
"""Parses a string for a date. The date can be either YYYY-MM-DD,
|
||||
Y/M/D, or M/D/Y.
|
||||
@ -134,8 +163,8 @@ class UrlBuilder:
|
||||
self.params = []
|
||||
for piece in start_url[pos + 1:].split("&"):
|
||||
pos = piece.find("=")
|
||||
name = urllib.parse.unquote(piece[:pos])
|
||||
value = urllib.parse.unquote(piece[pos + 1:])
|
||||
name = urllib.parse.unquote_plus(piece[:pos])
|
||||
value = urllib.parse.unquote_plus(piece[pos + 1:])
|
||||
self.params.append(self.Param(name, value))
|
||||
|
||||
def add(self, name, value):
|
||||
@ -400,8 +429,7 @@ class Pagination:
|
||||
url (str): The link URL, or for a non-link slot.
|
||||
title (str): The title of the link.
|
||||
is_active (bool): Whether this link is currently active.
|
||||
is_small_screen (bool): Whether this link is for small
|
||||
screens
|
||||
is_small_screen (bool): Whether this link is for small screens.
|
||||
"""
|
||||
def __int__(self):
|
||||
self.url = None
|
@ -34,7 +34,7 @@ from django.views.generic import DeleteView as CoreDeleteView, \
|
||||
RedirectView as CoreRedirectView
|
||||
from django.views.generic.base import View
|
||||
|
||||
from . import stored_post, utils
|
||||
from . import utils
|
||||
from .models import StampedModel
|
||||
from .utils import UrlBuilder
|
||||
|
||||
@ -109,7 +109,7 @@ class FormView(View):
|
||||
utils.strip_post(post)
|
||||
return self.make_form_from_post(post)
|
||||
else:
|
||||
previous_post = stored_post.get_previous_post(self.request)
|
||||
previous_post = utils.retrieve_store(self.request)
|
||||
if previous_post is not None:
|
||||
return self.make_form_from_post(previous_post)
|
||||
if self.object is not None:
|
||||
@ -146,8 +146,8 @@ class FormView(View):
|
||||
|
||||
def form_invalid(self, form: forms.Form) -> HttpResponseRedirect:
|
||||
"""Handles the action when the POST form is invalid."""
|
||||
return stored_post.error_redirect(
|
||||
self.request, self.get_error_url(), form.data)
|
||||
utils.store_post(self.request, form.data)
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
def form_valid(self, form: forms.Form) -> HttpResponseRedirect:
|
||||
"""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