Added the account form in the accounting application.
This commit is contained in:
parent
008def227d
commit
724ba44a71
@ -21,10 +21,13 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from mia_core.status import retrieve_status
|
||||
from .models import Account, Record
|
||||
from .validators import validate_record_account_code, validate_record_id
|
||||
from .validators import validate_record_account_code, validate_record_id, \
|
||||
validate_account_code
|
||||
|
||||
|
||||
class RecordForm(forms.Form):
|
||||
@ -301,3 +304,35 @@ class TransactionForm(forms.Form):
|
||||
"""
|
||||
return sum([int(x.data["amount"]) for x in self.credit_records
|
||||
if "amount" not in x.errors])
|
||||
|
||||
|
||||
class AccountForm(forms.Form):
|
||||
"""An account form."""
|
||||
code = forms.CharField(
|
||||
error_messages={
|
||||
"required": _("Please fill in the code."),
|
||||
"invalid": _("Please fill in a number."),
|
||||
"max_length": _("This code is too long (max. 5)."),
|
||||
"min_value": _("This code is too long (max. 5)."),
|
||||
}, validators=[
|
||||
RegexValidator(
|
||||
regex="^[1-9]+$",
|
||||
message=_("You can only use numbers 1-9 in the code")),
|
||||
validate_account_code,
|
||||
])
|
||||
title = forms.CharField(
|
||||
max_length=128,
|
||||
error_messages={
|
||||
"required": _("Please fill in the title."),
|
||||
"max_length": _("This title is too long (max. 128)."),
|
||||
})
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AccountForm, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
code = self["code"].value()
|
||||
if code is None or len(code) < 2:
|
||||
return None
|
||||
return Account.objects.get(code=code[:-1])
|
||||
|
189
accounting/static/accounting/js/account-form.js
Normal file
189
accounting/static/accounting/js/account-form.js
Normal file
@ -0,0 +1,189 @@
|
||||
/* The Mia Website
|
||||
* account-form.js: The JavaScript to edit an account
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2019-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.
|
||||
*/
|
||||
|
||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
* First written: 2020/3/23
|
||||
*/
|
||||
|
||||
// Initializes the page JavaScript.
|
||||
$(function () {
|
||||
getAllAccounts();
|
||||
$("#account-code").on("blur", function () {
|
||||
updateParent(this);
|
||||
validateCode();
|
||||
});
|
||||
$("#account-title").on("blur", function () {
|
||||
validateTitle();
|
||||
});
|
||||
$("#account-form").on("submit", function () {
|
||||
return validateForm();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* All the accounts
|
||||
* @type {Array.}
|
||||
* @private
|
||||
*/
|
||||
let accounts;
|
||||
|
||||
/**
|
||||
* Obtains all the accounts.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function getAllAccounts() {
|
||||
const request = new XMLHttpRequest();
|
||||
request.onreadystatechange = function() {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
accounts = JSON.parse(this.responseText);
|
||||
}
|
||||
};
|
||||
request.open("GET", $("#all-account-url").val(), true);
|
||||
request.send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the parent account.
|
||||
*
|
||||
* @param {HTMLInputElement} code the code input element
|
||||
* @private
|
||||
*/
|
||||
function updateParent(code) {
|
||||
const parent = $("#account-parent");
|
||||
if (code.value.length === 0) {
|
||||
parent.text("");
|
||||
return;
|
||||
}
|
||||
if (code.value.length === 1) {
|
||||
parent.text(gettext("Topmost"));
|
||||
return;
|
||||
}
|
||||
const parentCode = code.value.substr(0, code.value.length - 1);
|
||||
if (parentCode in accounts) {
|
||||
parent.text(parentCode + " " + accounts[parentCode]);
|
||||
return;
|
||||
}
|
||||
parent.text(gettext("(Unknown)"));
|
||||
}
|
||||
|
||||
|
||||
/*******************
|
||||
* Form Validation *
|
||||
*******************/
|
||||
|
||||
/**
|
||||
* Validates the form.
|
||||
*
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateForm() {
|
||||
let isValidated = true;
|
||||
isValidated = isValidated && validateCode();
|
||||
isValidated = isValidated && validateTitle();
|
||||
return isValidated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the code column.
|
||||
*
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateCode() {
|
||||
const code = $("#account-code")[0];
|
||||
const errorMessage = $("#account-code-error");
|
||||
code.value = code.value.trim();
|
||||
if (code.value === "") {
|
||||
code.classList.add("is-invalid");
|
||||
errorMessage.text(gettext("Please fill in the code."));
|
||||
return false;
|
||||
}
|
||||
if (!code.value.match(/^[1-9][0-9]*$/)) {
|
||||
code.classList.add("is-invalid");
|
||||
errorMessage.text(gettext("You can only use a number as the code"));
|
||||
return false;
|
||||
}
|
||||
const originalCode = $("#account-code-original").val();
|
||||
if (code.value !== originalCode) {
|
||||
if (originalCode !== "" && code.value.startsWith(originalCode)) {
|
||||
code.classList.add("is-invalid");
|
||||
errorMessage.text(gettext("You cannot set the code under itself."));
|
||||
return false;
|
||||
}
|
||||
if (code.value in accounts) {
|
||||
code.classList.add("is-invalid");
|
||||
errorMessage.text(gettext("This code is already in use."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const parentCode = code.value.substr(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."));
|
||||
return false;
|
||||
}
|
||||
if (originalCode !== "" && code.value !== originalCode) {
|
||||
const descendants = [];
|
||||
Object.keys(accounts).forEach(function (key) {
|
||||
if (key.startsWith(originalCode) && key !== originalCode) {
|
||||
descendants.push(key);
|
||||
}
|
||||
});
|
||||
if (descendants.length > 0) {
|
||||
descendants.sort(function (a, b) {
|
||||
return b.length - a.length;
|
||||
});
|
||||
if (descendants[0].length
|
||||
- originalCode.length
|
||||
+ code.value.length > code.maxLength) {
|
||||
code.classList.add("is-invalid");
|
||||
errorMessage.text(gettext("The descendant account codes will be too long (max. 5)."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
code.classList.remove("is-invalid");
|
||||
errorMessage.text("");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the title column.
|
||||
*
|
||||
* @returns {boolean} true if the validation succeed, or false
|
||||
* otherwise
|
||||
* @private
|
||||
*/
|
||||
function validateTitle() {
|
||||
const title = $("#account-title")[0];
|
||||
const errorMessage = $("#account-title-error");
|
||||
title.value = title.value.trim();
|
||||
if (title.value === "") {
|
||||
title.classList.add("is-invalid");
|
||||
errorMessage.text(gettext("Please fill in the title."));
|
||||
return false;
|
||||
}
|
||||
title.classList.remove("is-invalid");
|
||||
errorMessage.text("");
|
||||
return true;
|
||||
}
|
83
accounting/templates/accounting/account_form.html
Normal file
83
accounting/templates/accounting/account_form.html
Normal file
@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
The Mia Accounting Application
|
||||
account_detail.html: The template for the account detail
|
||||
|
||||
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.
|
||||
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2020/8/8
|
||||
{% endcomment %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load mia_core %}
|
||||
{% load accounting %}
|
||||
|
||||
{% block settings %}
|
||||
{% setvar "title" account %}
|
||||
{% static "accounting/js/account-form.js" as file %}{% add_js file %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="btn-group btn-actions">
|
||||
<a class="btn btn-primary" role="button" href="{% if account %}{% url "accounting:accounts.detail" request.resolver_match.kwargs.account %}{% else %}{% url "accounting:accounts" %}{% endif %}">
|
||||
<i class="fas fa-chevron-circle-left"></i>
|
||||
{{ _("Back")|force_escape }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form id="account-form" action="{% if request.resolver_match.kwargs.account %}{% url "accounting:accounts.update" request.resolver_match.kwargs.account %}{% else %}{% url "accounting:accounts.store" %}{% endif %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input id="all-account-url" type="hidden" value="{% url "accounting:api.accounts.all" %}" />
|
||||
<input id="account-code-original" type="hidden" value="{% if request.resolver_match.kwargs.account %}{{ request.resolver_match.kwargs.account.code }}{% endif %}" />
|
||||
<div class="row form-group">
|
||||
<label class="col-sm-2" for="account-parent">{{ _("Parent Account:")|force_escape }}</label>
|
||||
<div id="account-parent" class="col-sm-10">
|
||||
{% if form.parent %}
|
||||
{{ form.parent }}
|
||||
{% else %}
|
||||
{{ _("Topmost")|force_escape }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row form-group">
|
||||
<label class="col-sm-2 col-form-label" for="account-code">{{ _("Code:")|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="account-code" class="form-control {% if form.code.errors %} is-invalid {% endif %}" type="text" name="code" value="{{ form.code.value|default:"" }}" maxlength="5" required="required" />
|
||||
<div id="account-code-error" class="invalid-feedback">{% if form.code.errors %}{{ form.code.errors.0 }}{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row form-group">
|
||||
<label class="col-sm-2 col-form-label" for="account-title">{{ _("Title:")|force_escape }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="account-title" class="form-control {% if form.title.errors %} is-invalid {% endif %}" type="text" name="title" value="{{ form.title.value|default:"" }}" required="required" />
|
||||
<div id="account-title-error" class="invalid-feedback">{% if form.title.errors %}{{ form.title.errors.0 }}{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row form-group">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<i class="fas fa-save"></i>
|
||||
{{ _("Save")|force_escape }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -92,19 +92,19 @@ urlpatterns = [
|
||||
views.txn_sort, name="transactions.sort"),
|
||||
path("accounts",
|
||||
views.AccountListView.as_view(), name="accounts"),
|
||||
# TODO: To be done
|
||||
path("accounts/create",
|
||||
mia_core_views.todo, name="accounts.create"),
|
||||
views.account_form, name="accounts.create"),
|
||||
# TODO: To be done
|
||||
path("accounts/store",
|
||||
mia_core_views.todo, name="accounts.store"),
|
||||
path("api/accounts/all",
|
||||
views.api_account_all, name="api.accounts.all"),
|
||||
path("accounts/options",
|
||||
views.account_options, name="accounts.options"),
|
||||
path("accounts/<account:account>",
|
||||
views.AccountView.as_view(), name="accounts.detail"),
|
||||
# TODO: To be done
|
||||
path("accounts/<account:account>/edit",
|
||||
mia_core_views.todo, name="accounts.edit"),
|
||||
views.account_form, name="accounts.edit"),
|
||||
# TODO: To be done
|
||||
path("accounts/<account:account>/update",
|
||||
mia_core_views.todo, name="accounts.update"),
|
||||
|
@ -62,3 +62,22 @@ def validate_record_account_code(value):
|
||||
if child is not None:
|
||||
raise ValidationError(_("You cannot select a parent account."),
|
||||
code="parent_account")
|
||||
|
||||
|
||||
def validate_account_code(value):
|
||||
"""Validates an account code.
|
||||
|
||||
Args:
|
||||
value (str): The account code.
|
||||
|
||||
Raises:
|
||||
ValidationError: When the validation fails.
|
||||
"""
|
||||
if len(value) > 1:
|
||||
try:
|
||||
Account.objects.get(code=value[:-1])
|
||||
except Account.DoesNotExist:
|
||||
raise ValidationError(
|
||||
_("The parent account of this code does not exist"),
|
||||
code="parent_not_exist")
|
||||
|
||||
|
@ -26,6 +26,7 @@ from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.db.models import Sum, Case, When, F, Q, Max, Count, BooleanField
|
||||
from django.db.models.functions import TruncMonth, Coalesce, Now
|
||||
from django.forms import model_to_dict
|
||||
from django.http import JsonResponse, HttpResponseRedirect, Http404
|
||||
from django.shortcuts import render
|
||||
from django.template.loader import render_to_string
|
||||
@ -38,9 +39,10 @@ from django.views.generic import RedirectView, ListView, DetailView
|
||||
|
||||
from mia_core.digest_auth import login_required
|
||||
from mia_core.period import Period
|
||||
from mia_core.status import error_redirect
|
||||
from mia_core.status import error_redirect, get_previous_post
|
||||
from mia_core.utils import Pagination, get_multi_lingual_search, UrlBuilder, \
|
||||
strip_form, new_pk, PaginationException
|
||||
from .forms import AccountForm
|
||||
from .models import Record, Transaction, Account
|
||||
from .utils import get_cash_accounts, get_ledger_accounts, \
|
||||
find_imbalanced, find_order_holes, fill_txn_from_post, \
|
||||
@ -1048,6 +1050,47 @@ class AccountView(DetailView):
|
||||
return self.request.resolver_match.kwargs["account"]
|
||||
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def account_form(request, account=None):
|
||||
"""The view to edit an accounting transaction.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request.
|
||||
account (Account): The account.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The response.
|
||||
"""
|
||||
previous_post = get_previous_post(request)
|
||||
if previous_post is not None:
|
||||
form = AccountForm(previous_post)
|
||||
elif account is not None:
|
||||
form = AccountForm({
|
||||
"code": account.code,
|
||||
"title": account.title,
|
||||
})
|
||||
else:
|
||||
form = AccountForm()
|
||||
return render(request, "accounting/account_form.html", {
|
||||
"form": form,
|
||||
})
|
||||
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def api_account_all(request):
|
||||
"""The API view to return all the accounts.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request.
|
||||
|
||||
Returns:
|
||||
JsonResponse: The response.
|
||||
"""
|
||||
return JsonResponse({x.code: x.title for x in Account.objects.all()})
|
||||
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def account_options(request):
|
||||
|
@ -42,6 +42,23 @@ def error_redirect(request, url, form):
|
||||
return HttpResponseRedirect(str(UrlBuilder(url).query(s=status_id)))
|
||||
|
||||
|
||||
def get_previous_post(request):
|
||||
"""Retrieves the previously-stored status.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The request.
|
||||
|
||||
Returns:
|
||||
dict: The previously-stored status.
|
||||
"""
|
||||
if "s" not in request.GET:
|
||||
return None
|
||||
status = _retrieve(request, request.GET["s"])
|
||||
if "form" not in status:
|
||||
return None
|
||||
return status["form"]
|
||||
|
||||
|
||||
def retrieve_status(request):
|
||||
"""Retrieves the previously-stored status.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user