Compare commits
58
Commits
v0.0.1
...
020555602d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
15ad595ad1 | ||
|
|
47b41a86e3 | ||
|
|
943fd44c44 | ||
|
|
b6ac650bfd |
No files matched your search
+6
-1
@@ -1,9 +1,14 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
dist
|
||||
build
|
||||
*.egg-info
|
||||
migrations
|
||||
*.mo
|
||||
.idea
|
||||
venv
|
||||
mirror
|
||||
excludes
|
||||
zh_Hans
|
||||
|
||||
.scannerwork
|
||||
sonar-project.properties
|
||||
+20
@@ -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/*/*
|
||||
@@ -1,222 +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
|
||||
|
||||
### Requirements
|
||||
|
||||
`mia-accounting` requires Python 3.6 or above to work.
|
||||
|
||||
Install the required packages with `pip`.
|
||||
|
||||
```
|
||||
pip install django django-dirtyfields titlecase django-decorator-include
|
||||
```
|
||||
|
||||
### Download
|
||||
|
||||
The Mia! Accounting project is hosted on GitHub.
|
||||
|
||||
https://github.com/imacat/mia-accounting
|
||||
|
||||
You can download or clone the project from from GitHub
|
||||
|
||||
```
|
||||
git clone git@github.com:imacat/mia-accounting.git
|
||||
```
|
||||
|
||||
Move the `accounting` and `mia_core` directories into your Django project root
|
||||
directory.
|
||||
|
||||
### `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
|
||||
|
||||
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.
|
||||
```
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
======================================
|
||||
The Mia! Django Accounting Application
|
||||
======================================
|
||||
|
||||
|
||||
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! Django accounting 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.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! Django accounting application requires Python 3.7 and Django
|
||||
3.1.
|
||||
|
||||
Install ``mia-accounting`` with ``pip``.
|
||||
|
||||
.. code::
|
||||
|
||||
pip install mia-accounting
|
||||
|
||||
``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
|
||||
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! Django accounting application 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-2021 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.
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
django
|
||||
django-dirtyfields
|
||||
titlecase
|
||||
django-decorator-include
|
||||
File renamed without changes.
File renamed without changes.
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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'
|
||||
copyright = '2022, imacat'
|
||||
author = 'imacat'
|
||||
release = '0.1.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']
|
||||
@@ -0,0 +1,20 @@
|
||||
.. Mia! Accounting 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's documentation!
|
||||
===========================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
@@ -0,0 +1,10 @@
|
||||
mia\_core.migrations package
|
||||
============================
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: mia_core.migrations
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1,51 @@
|
||||
# The setup.cfg
|
||||
# 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.
|
||||
|
||||
[metadata]
|
||||
name = mia-accounting
|
||||
version = 0.1.0
|
||||
author = imacat
|
||||
author_email = imacat@mail.imacat.idv.tw
|
||||
description = A Django accounting application.
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
url = https://github.com/imacat/mia-accounting
|
||||
project_urls =
|
||||
Bug Tracker = https://github.com/imacat/mia-accounting/issues
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
Framework :: 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
|
||||
@@ -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.1",
|
||||
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"],
|
||||
)
|
||||
File renamed without changes.
File renamed without changes.
@@ -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.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+11
-11
@@ -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\
|
||||
File renamed without changes.
@@ -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
|
||||
-2
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+8
-8
@@ -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."));
|
||||
File renamed without changes.
+4
-4
@@ -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]);
|
||||
+14
-14
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
File renamed without changes.
+2
-2
@@ -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>
|
||||
File renamed without changes.
File renamed without changes.
+2
-1
@@ -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>
|
||||
+2
-1
@@ -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>
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+2
-2
@@ -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>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -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>
|
||||
File renamed without changes.
+2
-2
@@ -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>
|
||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -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>
|
||||
File renamed without changes.
File renamed without changes.
@@ -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],
|
||||
File renamed without changes.
File renamed without changes.
@@ -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.
|
||||
"""
|
||||
File renamed without changes.
@@ -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())
|
||||
Whitespace-only changes.
File renamed without changes.
+13
-9
@@ -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 "下一頁"
|
||||
+11
-5
@@ -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(
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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">
|
||||
|
||||
Whitespace-only changes.
@@ -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("星期", "")
|
||||
File renamed without changes.
@@ -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."""
|
||||
@@ -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)
|
||||
Executable
+22
@@ -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()
|
||||
@@ -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>
|
||||
Whitespace-only changes.
@@ -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()
|
||||
@@ -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'
|
||||
@@ -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'),
|
||||
]
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user