Added forms and validators, and applied them to the transaction form in the accounting application.

This commit is contained in:
依瑪貓 2020-08-01 23:56:41 +08:00
parent c1da25b3b5
commit 9d988f17ca
8 changed files with 513 additions and 186 deletions

242
accounting/forms.py Normal file
View File

@ -0,0 +1,242 @@
# The core application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/7/31
# 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 forms of the Mia core application.
"""
import re
from django import forms
from django.utils.translation import pgettext
from .models import Account, Record
from .validators import validate_record_account_code, validate_record_id
class RecordForm(forms.Form):
"""An accounting record form.
Attributes:
transaction (Transaction|None): The current transaction or None.
is_credit (bool): Whether this is a credit record.
"""
id = forms.IntegerField(
required=False,
error_messages={
"invalid": pgettext("Accounting|", "This record is not valid."),
},
validators=[validate_record_id])
account = forms.CharField(
error_messages={
"required": pgettext("Accounting|", "Please select the account."),
},
validators=[validate_record_account_code])
summary = forms.CharField(
required=False,
max_length=128,
error_messages={
"max_length": pgettext("Accounting|", "This summary is too long."),
})
amount = forms.IntegerField(
min_value=1,
error_messages={
"required": pgettext("Accounting|", "Please fill in the amount."),
"invalid": pgettext("Accounting|", "Please fill in a number."),
"min_value": pgettext(
"Accounting|", "The amount must be at least 1."),
})
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.transaction = None
self.is_credit = None
def account_title(self):
"""Returns the title of the specified account, if any.
Returns:
str: The title of the specified account, or None if the specified
account is not available.
"""
try:
return Account.objects.get(code=self["account"].value()).title
except KeyError:
return None
except Account.DoesNotExist:
return None
def clean(self):
"""Validates the form globally.
Raises:
ValidationError: When the validation fails.
"""
errors = []
validators = [self._validate_transaction, self._validate_account_type]
for validator in validators:
try:
validator()
except forms.ValidationError as e:
errors.append(e)
if errors:
print(errors)
raise forms.ValidationError(errors)
def _validate_transaction(self):
"""Validates whether the transaction matches the transaction form.
Raises:
ValidationError: When the validation fails.
"""
if "id" in self.errors:
return
if self.transaction is None:
if "id" in self.data:
error = forms.ValidationError(
pgettext("Accounting|",
"This record is not for this transaction."),
code="not_belong")
self.add_error("id", error)
raise error
else:
if "id" in self.data:
record = Record.objects.get(pk=self.data["id"])
if record.transaction.pk != self.transaction.pk:
error = forms.ValidationError(
pgettext("Accounting|",
"This record is not for this transaction."),
code="not_belong")
self.add_error("id", error)
raise error
def _validate_account_type(self):
"""Validates whether the account is a correct debit or credit account.
Raises:
ValidationError: When the validation fails.
"""
if "account" in self.errors:
return
if self.is_credit:
print(self.data["account"])
if not re.match("^([123489]|7[1234])", self.data["account"]):
error = forms.ValidationError(
pgettext("Accounting|",
"This account is not for credit records."),
code="not_credit")
self.add_error("account", error)
raise error
else:
if not re.match("^([1235689]|7[5678])", self.data["account"]):
error = forms.ValidationError(
pgettext("Accounting|",
"This account is not for debit records."),
code="not_debit")
self.add_error("account", error)
raise error
class TransactionForm(forms.Form):
"""A transaction form.
Attributes:
txn_type (str): The transaction type.
transaction (Transaction|None): The current transaction or None
debit_records (list[RecordForm]): The debit records.
credit_records (list[RecordForm]): The credit records.
"""
date = forms.DateField(
required=True,
error_messages={
"invalid": pgettext("Accounting|", "This date is not valid.")
})
notes = forms.CharField(
required=False,
max_length=128,
error_messages={
"max_length": pgettext("Accounting|", "This notes is too long.")
})
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.txn_type = None
self.transaction = None
self.debit_records = []
self.credit_records = []
def clean(self):
"""Validates the form globally.
Raises:
ValidationError: When the validation fails.
"""
self._validate_balance()
def _validate_balance(self):
"""Validates whether the total amount of debit and credit records are
consistent.
Raises:
ValidationError: When the validation fails.
"""
if self.txn_type != "transfer":
return
if self.debit_total() == self.credit_total():
return
raise forms.ValidationError(pgettext(
"Accounting|",
"The total amount of debit and credit records are inconsistent."),
code="balance")
def is_valid(self):
if not super(TransactionForm, self).is_valid():
return False
for x in self.debit_records + self.credit_records:
if not x.is_valid():
return False
return True
def balance_error(self):
"""Returns the error message when the transaction is imbalanced.
Returns:
str: The error message when the transaction is imbalanced, or
None otherwise.
"""
errors = [x for x in self.non_field_errors().data
if x.code == "balance"]
if errors:
return errors[0].message
return None
def debit_total(self):
"""Returns the total amount of the debit records.
Returns:
int: The total amount of the credit records.
"""
return sum([int(x.data["amount"]) for x in self.debit_records
if "amount" not in x.errors])
def credit_total(self):
"""Returns the total amount of the credit records.
Returns:
int: The total amount of the credit records.
"""
return sum([int(x.data["amount"]) for x in self.credit_records
if "amount" not in x.errors])

View File

@ -33,13 +33,13 @@ First written: 2020/7/23
{% block content %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if item.pk %}{% url_keep_return "accounting:transactions.show" "expense" item %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<a class="btn btn-primary" role="button" href="{% if item.transaction %}{% url_keep_return "accounting:transactions.show" "expense" item.transaction %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{% trans "Back" context "Navigation|" as text %}{{ text|force_escape }}
</a>
</div>
<form id="txn-form" action="{% if item.pk %}{% url_keep_return "accounting:transactions.update" "expense" item %}{% else %}{% url_keep_return "accounting:transactions.store" "expense" %}{% endif %}" method="post">
<form id="txn-form" action="{% if item.transaction %}{% url_keep_return "accounting:transactions.update" "expense" item.transaction %}{% else %}{% url_keep_return "accounting:transactions.store" "expense" %}{% endif %}" method="post">
{% csrf_token %}
{# TODO: To be done #}
<input id="l10n-messages" type="hidden" value="{{ l10n_messages }}" />
@ -52,8 +52,8 @@ First written: 2020/7/23
<label for="txn-date">{% trans "Date:" context "Accounting|" as text %}{{ text|force_escape }}</label>
</div>
<div class="col-sm-10">
<input id="txn-date" class="form-control {% if errors|dict:"date" %} is-invalid {% endif %}" type="date" name="date" value="{{ item.date|date:"Y-m-d" }}" required="required" />
<div id="txn-date-error" class="invalid-feedback">{{ errors|dict:"date"|default:"" }}</div>
<input id="txn-date" class="form-control {% if item.date.errors %} is-invalid {% endif %}" type="date" name="date" value="{{ item.date.value }}" required="required" />
<div id="txn-date-error" class="invalid-feedback">{{ item.date.errors.0|default:"" }}</div>
</div>
</div>
@ -64,32 +64,29 @@ First written: 2020/7/23
<li id="debit-{{ forloop.counter }}" class="list-group-item d-flex justify-content-between draggable-record debit-record">
<div class="row">
<div class="col-lg-6">
{% if x.pk is not None %}
<input type="hidden" name="debit-{{ forloop.counter }}-sn" value="{{ x.pk }}" />
{% if x.id.value %}
<input type="hidden" name="debit-{{ forloop.counter }}-id" value="{{ x.id.value }}" />
{% endif %}
<input id="debit-{{ forloop.counter }}-ord" class="debit-ord" type="hidden" name="debit-{{ forloop.counter }}-ord" value="{{ x.ord }}" />
{% str_format "debit-{}-account" forloop.counter as field %}
<select id="{{ field }}" class="form-control record-account debit-account {% if errors|dict:field %} is-invalid {% endif %}" name="{{ field }}">
<input id="debit-{{ forloop.counter }}-ord" class="debit-ord" type="hidden" name="debit-{{ forloop.counter }}-ord" value="{{ x.ord.value }}" />
<select id="debit-{{ forloop.counter }}-account" class="form-control record-account debit-account {% if x.account.errors %} is-invalid {% endif %}" name="debit-{{ forloop.counter }}-account">
{% if x.account is not None %}
<option value="{{ x.account.code }}" selected="selected">{{ x.account.code }} {{ x.account.title }}</option>
<option value="{{ x.account.value|default:"" }}" selected="selected">{{ x.account.value|default:"" }} {{ x.account_title|default:"" }}</option>
{% else %}
<option value=""></option>
{% endif %}
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
</select>
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<div id="debit-{{ forloop.counter }}-account-error" class="invalid-feedback">{{ x.account.errors.0|default:"" }}</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-sm-8">
{% str_format "debit-{}-summary" forloop.counter as field %}
<input id="{{ field }}" class="form-control record-summary {% if errors|dict:field %} is-invalid {% endif %}" type="text" name="{{ field }}" value="{{ x.summary|default:"" }}" maxlength="128" />
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<input id="debit-{{ forloop.counter }}-summary" class="form-control record-summary {% if x.summary.errors %} is-invalid {% endif %}" type="text" name="debit-{{ forloop.counter }}-summary" value="{{ x.summary.value|default:"" }}" maxlength="128" />
<div id="debit-{{ forloop.counter }}-summary-error" class="invalid-feedback">{{ x.summary.errors.0|default:"" }}</div>
</div>
<div class="col-sm-4">
{% str_format "debit-{}-amount" forloop.counter as field %}
<input id="{{ field }}" class="form-control record-amount debit-to-sum {% if errors|dict:field %} is-invalid {% endif %}" type="number" min="1" name="{{ field }}" value="{{ x.amount|default:"" }}" required="required" />
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<input id="debit-{{ forloop.counter }}-amount" class="form-control record-amount debit-to-sum {% if x.amount.errors %} is-invalid {% endif %}" type="number" min="1" name="debit-{{ forloop.counter }}-amount" value="{{ x.amount.value|default:"" }}" required="required" />
<div id="debit-{{ forloop.counter }}-amount-error" class="invalid-feedback">{{ x.amount.errors.0|default:"" }}</div>
</div>
</div>
</div>
@ -136,8 +133,8 @@ First written: 2020/7/23
<label for="txn-note">{% trans "Notes:" context "Accounting|" as text %}{{ text|force_escape }}</label>
</div>
<div class="col-sm-10">
<textarea id="txn-note" class="form-control {% if errors|dict:"notes" %} is-invalid {% endif %}" name="note">{{ item.notes|default:"" }}</textarea>
<div id="txn-note-error" class="invalid-feedback">{{ errors|dict:"notes"|default:"" }}</div>
<textarea id="txn-note" class="form-control {% if item.notes.errors %} is-invalid {% endif %}" name="notes">{{ item.notes.value|default:"" }}</textarea>
<div id="txn-note-error" class="invalid-feedback">{{ item.notes.errors.0|default:"" }}</div>
</div>
</div>

View File

@ -33,13 +33,13 @@ First written: 2020/7/23
{% block content %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if item.pk %}{% url_keep_return "accounting:transactions.show" "income" item %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<a class="btn btn-primary" role="button" href="{% if item.transaction %}{% url_keep_return "accounting:transactions.show" "income" item.transaction %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{% trans "Back" context "Navigation|" as text %}{{ text|force_escape }}
</a>
</div>
<form id="txn-form" action="{% if item.pk %}{% url_keep_return "accounting:transactions.update" "income" item %}{% else %}{% url_keep_return "accounting:transactions.store" "income" %}{% endif %}" method="post">
<form id="txn-form" action="{% if item.transaction %}{% url_keep_return "accounting:transactions.update" "income" item.transaction %}{% else %}{% url_keep_return "accounting:transactions.store" "income" %}{% endif %}" method="post">
{% csrf_token %}
{# TODO: To be done #}
<input id="l10n-messages" type="hidden" value="{{ l10n_messages }}" />
@ -52,8 +52,8 @@ First written: 2020/7/23
<label for="txn-date">{% trans "Date:" context "Accounting|" as text %}{{ text|force_escape }}</label>
</div>
<div class="col-sm-10">
<input id="txn-date" class="form-control {% if errors|dict:"date" %} is-invalid {% endif %}" type="date" name="date" value="{{ item.date|date:"Y-m-d" }}" required="required" />
<div id="txn-date-error" class="invalid-feedback">{{ errors|dict:"date"|default:"" }}</div>
<input id="txn-date" class="form-control {% if item.date.errors %} is-invalid {% endif %}" type="date" name="date" value="{{ item.date.value }}" required="required" />
<div id="txn-date-error" class="invalid-feedback">{{ item.date.errors.0|default:"" }}</div>
</div>
</div>
@ -64,32 +64,29 @@ First written: 2020/7/23
<li id="credit-{{ forloop.counter }}" class="list-group-item d-flex justify-content-between draggable-record credit-record">
<div class="row">
<div class="col-lg-6">
{% if x.pk is not None %}
<input type="hidden" name="credit-{{ forloop.counter }}-sn" value="{{ x.pk }}" />
{% if x.id.value %}
<input type="hidden" name="credit-{{ forloop.counter }}-id" value="{{ x.id.value }}" />
{% endif %}
<input id="credit-{{ forloop.counter }}-ord" class="credit-ord" type="hidden" name="credit-{{ forloop.counter }}-ord" value="{{ x.ord }}" />
{% str_format "credit-{}-account" forloop.counter as field %}
<select id="{{ field }}" class="form-control record-account credit-account {% if errors|dict:field %} is-invalid {% endif %}" name="{{ field }}">
<input id="credit-{{ forloop.counter }}-ord" class="credit-ord" type="hidden" name="credit-{{ forloop.counter }}-ord" value="{{ x.ord.value }}" />
<select id="credit-{{ forloop.counter }}-account" class="form-control record-account credit-account {% if x.account.errors %} is-invalid {% endif %}" name="credit-{{ forloop.counter }}-account">
{% if x.account is not None %}
<option value="{{ x.account.code }}" selected="selected">{{ x.account.code }} {{ x.account.title }}</option>
<option value="{{ x.account.value|default:"" }}" selected="selected">{{ x.account.value|default:"" }} {{ x.account_title|default:"" }}</option>
{% else %}
<option value=""></option>
{% endif %}
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
</select>
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<div id="credit-{{ forloop.counter }}-account-error" class="invalid-feedback">{{ x.account.errors.0|default:"" }}</div>
</div>
<div class="col-lg-6">
<div class="row">
<div class="col-sm-8">
{% str_format "credit-{}-summary" forloop.counter as field %}
<input id="{{ field }}" class="form-control record-summary {% if errors|dict:field %} is-invalid {% endif %}" type="text" name="{{ field }}" value="{{ x.summary|default:"" }}" maxlength="128" />
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<input id="credit-{{ forloop.counter }}-summary" class="form-control record-summary {% if x.summary.errors %} is-invalid {% endif %}" type="text" name="credit-{{ forloop.counter }}-summary" value="{{ x.summary.value|default:"" }}" maxlength="128" />
<div id="credit-{{ forloop.counter }}-summary-error" class="invalid-feedback">{{ x.summary.errors.0|default:"" }}</div>
</div>
<div class="col-sm-4">
{% str_format "credit-{}-amount" forloop.counter as field %}
<input id="{{ field }}" class="form-control record-amount credit-to-sum {% if errors|dict:field %} is-invalid {% endif %}" type="number" min="1" name="{{ field }}" value="{{ x.amount|default:"" }}" required="required" />
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<input id="credit-{{ forloop.counter }}-amount" class="form-control record-amount credit-to-sum {% if x.amount.errors %} is-invalid {% endif %}" type="number" min="1" name="credit-{{ forloop.counter }}-amount" value="{{ x.amount.value|default:"" }}" required="required" />
<div id="credit-{{ forloop.counter }}-amount-error" class="invalid-feedback">{{ x.amount.errors.0|default:"" }}</div>
</div>
</div>
</div>
@ -136,8 +133,8 @@ First written: 2020/7/23
<label for="txn-note">{% trans "Notes:" context "Accounting|" as text %}{{ text|force_escape }}</label>
</div>
<div class="col-sm-10">
<textarea id="txn-note" class="form-control {% if errors|dict:"notes" %} is-invalid {% endif %}" name="note">{{ item.notes|default:"" }}</textarea>
<div id="txn-note-error" class="invalid-feedback">{{ errors|dict:"notes"|default:"" }}</div>
<textarea id="txn-note" class="form-control {% if item.notes.errors %} is-invalid {% endif %}" name="notes">{{ item.notes.value|default:"" }}</textarea>
<div id="txn-note-error" class="invalid-feedback">{{ item.notes.errors.0|default:"" }}</div>
</div>
</div>

View File

@ -33,13 +33,13 @@ First written: 2020/7/23
{% block content %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if item.pk %}{% url_keep_return "accounting:transactions.show" "transfer" item %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<a class="btn btn-primary" role="button" href="{% if item.transaction %}{% url_keep_return "accounting:transactions.show" "transfer" item.transaction %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{% trans "Back" context "Navigation|" as text %}{{ text|force_escape }}
</a>
</div>
<form id="txn-form" action="{% if item.pk %}{% url_keep_return "accounting:transactions.update" "transfer" item %}{% else %}{% url_keep_return "accounting:transactions.store" "transfer" %}{% endif %}" method="post">
<form id="txn-form" action="{% if item.transaction %}{% url_keep_return "accounting:transactions.update" "transfer" item.transaction %}{% else %}{% url_keep_return "accounting:transactions.store" "transfer" %}{% endif %}" method="post">
{% csrf_token %}
{# TODO: To be done #}
<input id="l10n-messages" type="hidden" value="{{ l10n_messages }}" />
@ -55,8 +55,8 @@ First written: 2020/7/23
<label for="txn-date">{% trans "Date:" context "Accounting|" as text %}{{ text|force_escape }}</label>
</div>
<div class="col-sm-10">
<input id="txn-date" class="form-control {% if errors|dict:"date" %} is-invalid {% endif %}" type="date" name="date" value="{{ item.date|date:"Y-m-d" }}" required="required" />
<div id="txn-date-error" class="invalid-feedback">{{ errors|dict:"date"|default:"" }}</div>
<input id="txn-date" class="form-control {% if item.date.errors %} is-invalid {% endif %}" type="date" name="date" value="{{ item.date.value }}" required="required" />
<div id="txn-date-error" class="invalid-feedback">{{ item.date.errors.0|default:"" }}</div>
</div>
</div>
@ -70,32 +70,29 @@ First written: 2020/7/23
<div>
<div class="row">
<div class="col-sm-12">
{% if x.pk is not None %}
<input type="hidden" name="debit-{{ forloop.counter }}-sn" value="{{ x.pk }}" />
{% if x.id.value %}
<input type="hidden" name="debit-{{ forloop.counter }}-id" value="{{ x.id.value }}" />
{% endif %}
<input id="debit-{{ forloop.counter }}-ord" class="debit-ord" type="hidden" name="debit-{{ forloop.counter }}-ord" value="{{ x.ord }}" />
{% str_format "debit-{}-account" forloop.counter as field %}
<select id="{{ field }}" class="form-control record-account debit-account {% if errors|dict:field %} is-invalid {% endif %}" name="{{ field }}">
<input id="debit-{{ forloop.counter }}-ord" class="debit-ord" type="hidden" name="debit-{{ forloop.counter }}-ord" value="{{ x.ord.value }}" />
<select id="debit-{{ forloop.counter }}-account" class="form-control record-account debit-account {% if x.account.errors %} is-invalid {% endif %}" name="debit-{{ forloop.counter }}-account">
{% if x.account is not None %}
<option value="{{ x.account.code }}" selected="selected">{{ x.account.code }} {{ x.account.title }}</option>
<option value="{{ x.account.value|default:"" }}" selected="selected">{{ x.account.value|default:"" }} {{ x.account_title|default:"" }}</option>
{% else %}
<option value=""></option>
{% endif %}
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
</select>
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<div id="debit-{{ forloop.counter }}-account-error" class="invalid-feedback">{{ x.account.errors.0|default:"" }}</div>
</div>
</div>
<div class="row">
<div class="col-lg-8">
{% str_format "debit-{}-summary" forloop.counter as field %}
<input id="{{ field }}" class="form-control record-summary {% if errors|dict:field %} is-invalid {% endif %}" type="text" name="{{ field }}" value="{{ x.summary|default:"" }}" maxlength="128" />
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<input id="debit-{{ forloop.counter }}-summary" class="form-control record-summary {% if x.summary.errors %} is-invalid {% endif %}" type="text" name="debit-{{ forloop.counter }}-summary" value="{{ x.summary.value|default:"" }}" maxlength="128" />
<div id="debit-{{ forloop.counter }}-summary-error" class="invalid-feedback">{{ x.summary.errors.0|default:"" }}</div>
</div>
<div class="col-lg-4">
{% str_format "debit-{}-amount" forloop.counter as field %}
<input id="{{ field }}" class="form-control record-amount debit-to-sum {% if errors|dict:field %} is-invalid {% endif %}" type="number" min="1" name="{{ field }}" value="{{ x.amount|default:"" }}" required="required" />
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<input id="debit-{{ forloop.counter }}-amount" class="form-control record-amount debit-to-sum {% if x.amount.errors %} is-invalid {% endif %}" type="number" min="1" name="debit-{{ forloop.counter }}-amount" value="{{ x.amount.value|default:"" }}" required="required" />
<div id="debit-{{ forloop.counter }}-amount-error" class="invalid-feedback">{{ x.amount.errors.0|default:"" }}</div>
</div>
</div>
</div>
@ -119,11 +116,11 @@ First written: 2020/7/23
</button>
</li>
<li class="list-group-item">
<div id="debit-total-row" class="d-flex justify-content-between align-items-center form-control {% if errors|dict:"balance" %} is-invalid {% endif %} balance-row">
<div id="debit-total-row" class="d-flex justify-content-between align-items-center form-control {% if item.balance_error %} is-invalid {% endif %} balance-row">
{% trans "Total" context "Accounting|" as text %}{{ text|force_escape }}
<span id="debit-total" class="amount">{{ item.debit_total }}</span>
</div>
<div id="debit-total-error" class="invalid-feedback balance-error">{{ errors|dict:"balance"|default:"" }}</div>
<div id="debit-total-error" class="invalid-feedback balance-error">{{ item.balance_error|default:"" }}</div>
</li>
</ul>
</div>
@ -137,32 +134,29 @@ First written: 2020/7/23
<div>
<div class="row">
<div class="col-sm-12">
{% if x.pk is not None %}
<input type="hidden" name="credit-{{ forloop.counter }}-sn" value="{{ x.pk }}" />
{% if x.id.value %}
<input type="hidden" name="credit-{{ forloop.counter }}-id" value="{{ x.id.value }}" />
{% endif %}
<input id="credit-{{ forloop.counter }}-ord" class="credit-ord" type="hidden" name="credit-{{ forloop.counter }}-ord" value="{{ x.ord }}" />
{% str_format "credit-{}-account" forloop.counter as field %}
<select id="{{ field }}" class="form-control record-account credit-account {% if errors|dict:field %} is-invalid {% endif %}" name="{{ field }}">
<input id="credit-{{ forloop.counter }}-ord" class="credit-ord" type="hidden" name="credit-{{ forloop.counter }}-ord" value="{{ x.ord.value }}" />
<select id="credit-{{ forloop.counter }}-account" class="form-control record-account credit-account {% if x.account.errors %} is-invalid {% endif %}" name="credit-{{ forloop.counter }}-account">
{% if x.account is not None %}
<option value="{{ x.account.code }}" selected="selected">{{ x.account.code }} {{ x.account.title }}</option>
<option value="{{ x.account.value|default:"" }}" selected="selected">{{ x.account.value|default:"" }} {{ x.account_title|default:"" }}</option>
{% else %}
<option value=""></option>
{% endif %}
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
</select>
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<div id="credit-{{ forloop.counter }}-account-error" class="invalid-feedback">{{ x.account.errors.0|default:"" }}</div>
</div>
</div>
<div class="row">
<div class="col-lg-8">
{% str_format "credit-{}-summary" forloop.counter as field %}
<input id="{{ field }}" class="form-control record-summary {% if errors|dict:field %} is-invalid {% endif %}" type="text" name="{{ field }}" value="{{ x.summary|default:"" }}" maxlength="128" />
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<input id="credit-{{ forloop.counter }}-summary" class="form-control record-summary {% if x.summary.errors %} is-invalid {% endif %}" type="text" name="credit-{{ forloop.counter }}-summary" value="{{ x.summary.value|default:"" }}" maxlength="128" />
<div id="credit-{{ forloop.counter }}-summary-error" class="invalid-feedback">{{ x.summary.errors.0|default:"" }}</div>
</div>
<div class="col-lg-4">
{% str_format "credit-{}-amount" forloop.counter as field %}
<input id="{{ field }}" class="form-control record-amount credit-to-sum {% if errors|dict:field %} is-invalid {% endif %}" type="number" min="1" name="{{ field }}" value="{{ x.amount|default:"" }}" required="required" />
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
<input id="credit-{{ forloop.counter }}-amount" class="form-control record-amount credit-to-sum {% if x.amount.errors %} is-invalid {% endif %}" type="number" min="1" name="credit-{{ forloop.counter }}-amount" value="{{ x.amount.value|default:"" }}" required="required" />
<div id="credit-{{ forloop.counter }}-amount-error" class="invalid-feedback">{{ x.amount.errors.0|default:"" }}</div>
</div>
</div>
</div>
@ -186,11 +180,11 @@ First written: 2020/7/23
</button>
</li>
<li class="list-group-item">
<div id="credit-total-row" class="d-flex justify-content-between align-items-center form-control {% if errors|dict:"balance" %} is-invalid {% endif %} balance-row">
<div id="credit-total-row" class="d-flex justify-content-between align-items-center form-control {% if item.balance_error %} is-invalid {% endif %} balance-row">
{% trans "Total" context "Accounting|" as text %}{{ text|force_escape }}
<span id="credit-total" class="amount">{{ item.credit_total }}</span>
</div>
<div id="credit-total-error" class="invalid-feedback balance-error">{{ errors|dict:"balance"|default:"" }}</div>
<div id="credit-total-error" class="invalid-feedback balance-error">{{ item.balance_error|default:"" }}</div>
</li>
</ul>
</div>
@ -201,8 +195,8 @@ First written: 2020/7/23
<label for="txn-note">{% trans "Notes:" context "Accounting|" as text %}{{ text|force_escape }}</label>
</div>
<div class="col-sm-10">
<textarea id="txn-note" class="form-control {% if errors|dict:"notes" %} is-invalid {% endif %}" name="note">{{ item.notes|default:"" }}</textarea>
<div id="txn-note-error" class="invalid-feedback">{{ errors|dict:"notes"|default:"" }}</div>
<textarea id="txn-note" class="form-control {% if item.notes.errors %} is-invalid {% endif %}" name="notes">{{ item.notes.value|default:"" }}</textarea>
<div id="txn-note-error" class="invalid-feedback">{{ item.notes.errors.0|default:"" }}</div>
</div>
</div>

View File

@ -21,13 +21,13 @@
import re
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Q, Sum, Case, When, F, Count, Max, Min
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import pgettext, gettext_noop
from django.utils.translation import pgettext
from accounting.models import Account, Transaction, Record
from .forms import TransactionForm, RecordForm
from .models import Account, Transaction, Record
from mia_core.period import Period
from mia_core.status import retrieve_status
from mia_core.utils import new_pk
@ -289,7 +289,7 @@ def find_order_holes(records):
.filter(~(Q(max=F("count")) & Q(min=1)))] +\
[x["date"] for x in Transaction.objects
.values("date", "ord")
.annotate(count=Count("sn"))
.annotate(count=Count("pk"))
.filter(~Q(count=1))]
for record in records:
record.has_order_hole = record.transaction.date in holes
@ -313,7 +313,7 @@ def fill_transaction_from_form(transaction, form):
}
for key in form.keys():
m = re.match(
"^(debit|credit)-([1-9][0-9]*)-(sn|ord|account|summary|amount)$",
"^(debit|credit)-([1-9][0-9]*)-(id|ord|account|summary|amount)$",
key)
if m is not None:
rec_type = m.group(1)
@ -328,8 +328,8 @@ def fill_transaction_from_form(transaction, form):
ord=no,
is_credit=(rec_type == "credit"),
transaction=transaction)
if F"{rec_type}-{no}-sn" in form:
record.pk = form[F"{rec_type}-{no}-sn"]
if F"{rec_type}-{no}-id" in form:
record.pk = form[F"{rec_type}-{no}-id"]
if F"{rec_type}-{no}-account" in form:
record.account = Account(code=form[F"{rec_type}-{no}-account"])
if F"{rec_type}-{no}-summary" in form:
@ -340,22 +340,6 @@ def fill_transaction_from_form(transaction, form):
transaction.records = records
def fill_transaction_from_previous_form(request, transaction):
"""Fills the transaction from the previously-stored form.
Args:
request (HttpRequest): The request
transaction (Transaction): The transaction.
"""
status = retrieve_status(request)
if status is None:
return
if "form" not in status:
return
form = status["form"]
fill_transaction_from_form(transaction, form)
def sort_form_transaction_records(form):
"""Sorts the records in the form by their specified order, so that the
form can be used to populate the data to return to the user.
@ -370,7 +354,7 @@ def sort_form_transaction_records(form):
}
for key in form.keys():
m = re.match(
"^(debit|credit)-([1-9][0-9]*)-(sn|ord|account|summary|amount)",
"^(debit|credit)-([1-9][0-9]*)-(id|ord|account|summary|amount)",
key)
if m is None:
continue
@ -396,41 +380,122 @@ def sort_form_transaction_records(form):
old_no = record_no[record_type][i]
no = i + 1
new_form[F"{record_type}-{no}-ord"] = no
for attr in ["sn", "account", "summary", "amount"]:
for attr in ["id", "account", "summary", "amount"]:
if F"{record_type}-{old_no}-{attr}" in form:
new_form[F"{record_type}-{no}-{attr}"]\
= form[F"{record_type}-{old_no}-{attr}"]
# Purges the old form and fills it with the new form
for x in [x for x in form.keys() if re.match(
"^(debit|credit)-([1-9][0-9]*)-(sn|ord|account|summary|amount)",
"^(debit|credit)-([1-9][0-9]*)-(id|ord|account|summary|amount)",
x)]:
del form[x]
for key in new_form.keys():
form[key] = new_form[key]
def validate_account_code(record):
"""Validates the account code.
def make_transaction_form_from_model(transaction, exists):
"""Converts a transaction data model to a transaction form.
Args:
record (Record): The accounting record.
transaction (Transaction): The transaction data model.
exists (bool): Whether the current transaction exists.
Exceptions:
ValidationError: Thrown when validation fails.
Returns:
TransactionForm: The transaction form.
"""
if record.account.code is None:
raise ValidationError(gettext_noop(
"Please select the account."))
if record.account.code == "":
raise ValidationError(gettext_noop(
"Please select the account."))
transaction_form = TransactionForm(
{x: str(getattr(transaction, x)) for x in ["date", "notes"]
if getattr(transaction, x) is not None})
transaction_form.transaction = transaction if exists else None
for record in transaction.records:
data = {x: getattr(record, x)
for x in ["summary", "amount"]
if getattr(record, x) is not None}
data["id"] = record.pk
try:
record.account = Account.objects.get(code=record.account.code)
except Account.DoesNotExist:
raise ValidationError(gettext_noop(
"This account does not exist."))
child_account = Account.objects.filter(
code__startswith=record.account.code).first()
if child_account is not None:
raise ValidationError(gettext_noop(
"You cannot choose a parent account."))
data["account"] = record.account.code
except AttributeError:
pass
record_form = RecordForm(data)
record_form.transaction = transaction_form.transaction
record_form.is_credit = record.is_credit
if record.is_credit:
transaction_form.credit_records.append(record_form)
else:
transaction_form.debit_records.append(record_form)
return transaction_form
def make_transaction_form_from_post(post, txn_type, transaction):
"""Converts the POSTed data to a transaction form.
Args:
post (dict[str]): The POSTed data.
txn_type (str): The transaction type.
transaction (Transaction|None): The current transaction, or None
if there is no current transaction.
Returns:
TransactionForm: The transaction form.
"""
transaction_form = TransactionForm(
{x: post[x] for x in ("date", "notes") if x in post})
transaction_form.transaction = transaction
transaction_form.txn_type = txn_type
# The records
max_no = {
"debit": 0,
"credit": 0,
}
for key in post.keys():
m = re.match(
"^(debit|credit)-([1-9][0-9]*)-(id|ord|account|summary|amount)$",
key)
if m is not None:
rec_type = m.group(1)
no = int(m.group(2))
if max_no[rec_type] < no:
max_no[rec_type] = no
if max_no["debit"] == 0:
max_no["debit"] = 1
if max_no["credit"] == 0:
max_no["credit"] = 1
for rec_type in max_no.keys():
records = []
is_credit = (rec_type == "credit")
for i in range(max_no[rec_type]):
no = i + 1
record = RecordForm(
{x: post[F"{rec_type}-{no}-{x}"]
for x in ["id", "account", "summary", "amount"]
if F"{rec_type}-{no}-{x}" in post})
record.transaction = transaction_form.transaction
record.is_credit = is_credit
records.append(record)
if rec_type == "debit":
transaction_form.debit_records = records
else:
transaction_form.credit_records = records
return transaction_form
def make_transaction_form_from_status(request, txn_type, transaction):
"""Converts the previously-stored status to a transaction form.
Args:
request (HttpRequest): The request.
txn_type (str): The transaction type.
transaction (Transaction|None): The current transaction, or None
if there is no current transaction.
Returns:
TransactionForm: The transaction form, or None if there is no
previously-stored status.
"""
status = retrieve_status(request)
if status is None:
return None
if "form" not in status:
return
return make_transaction_form_from_post(
status["form"], txn_type, transaction)

67
accounting/validators.py Normal file
View File

@ -0,0 +1,67 @@
# The core application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/8/1
# 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 validators of the Mia core application.
"""
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import pgettext
from .models import Account, Record
def validate_record_id(value):
"""Validates the record ID.
Args:
value (str): The record ID.
Raises:
ValidationError: When the validation fails.
"""
try:
Record.objects.get(pk=value)
except Record.DoesNotExist:
raise ValidationError(
pgettext("Accounting|", "This record does not exists."),
code="not_exist")
def validate_record_account_code(value):
"""Validates an account code.
Args:
value (str): The account code.
Raises:
ValidationError: When the validation fails.
"""
try:
Account.objects.get(code=value)
except Account.DoesNotExist:
raise ValidationError(
pgettext("Accounting|", "This account does not exist."),
code="not_exist")
child = Account.objects.filter(
Q(code__startswith=value),
~Q(code=value),
).first()
if child is not None:
raise ValidationError(
pgettext("Accounting|", "You cannot select a parent account."),
code="parent_account")

View File

@ -21,7 +21,6 @@
import re
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Sum, Case, When, F, Q
from django.db.models.functions import TruncMonth, Coalesce
from django.shortcuts import render
@ -40,8 +39,8 @@ from mia_core.utils import Pagination, get_multi_lingual_search, UrlBuilder, \
from .models import Record, Transaction, Account, RecordSummary
from .utils import ReportUrl, get_cash_accounts, get_ledger_accounts, \
find_imbalanced, find_order_holes, fill_transaction_from_form, \
sort_form_transaction_records, fill_transaction_from_previous_form, \
validate_account_code
sort_form_transaction_records, make_transaction_form_from_status, \
make_transaction_form_from_model, make_transaction_form_from_post
@method_decorator(require_GET, name="dispatch")
@ -823,15 +822,18 @@ def transaction_edit(request, txn_type, transaction=None):
Returns:
HttpResponse: The response.
"""
form = make_transaction_form_from_status(request, txn_type, transaction)
if form is None:
exists = transaction is not None
if transaction is None:
transaction = Transaction()
fill_transaction_from_previous_form(request, transaction)
transaction = Transaction(date=timezone.localdate())
if len(transaction.debit_records) == 0:
transaction.records.append(Record(ord=1, is_credit=False))
if len(transaction.credit_records) == 0:
transaction.records.append(Record(ord=1, is_credit=True))
form = make_transaction_form_from_model(transaction, exists)
return render(request, F"accounting/transactions/{txn_type}/form.html", {
"item": transaction,
"item": form,
})
@ -848,61 +850,24 @@ def transaction_store(request, txn_type, transaction=None):
Returns:
HttpResponse: The response.
"""
post = request.POST.dict()
strip_form(post)
sort_form_transaction_records(post)
form = make_transaction_form_from_post(post, txn_type, transaction)
if not form.is_valid():
if transaction is None:
transaction = Transaction()
form = request.POST.dict()
strip_form(form)
sort_form_transaction_records(form)
fill_transaction_from_form(transaction, form)
errors = {}
try:
transaction.full_clean(exclude=["sn", "created_by", "updated_by"])
except ValidationError as e:
errors = e.message_dict
records = {
"debit": transaction.debit_records,
"credit": transaction.credit_records,
}
for record_type in records.keys():
no = 0
for x in records[record_type]:
no = no + 1
try:
x.full_clean(exclude=[
"sn", "transaction", "account", "created_by", "updated_by",
])
except ValidationError as e:
for key in e.message_dict:
errors[F"{record_type}-{no}-{key}"] = e.message_dict[key]
# Validates the account
try:
validate_account_code(x)
except ValidationError as e:
errors[F"{record_type}-{no}-account"] = e.message
# Validates the transaction
if x.transaction is None:
x.transaction = transaction
if transaction.pk is None:
if x.transaction.pk is not None:
errors[F"{record_type}-{no}-transaction"] = gettext_noop(
"This record is not of the same transaction.")
else:
if x.transaction.pk is None:
pass
elif x.transaction.pk != transaction.pk:
errors[F"{record_type}-{no}-transaction"] = gettext_noop(
"This record is not of the same transaction.")
if len(errors) > 0:
if transaction.pk is None:
url = reverse("accounting:transactions.create", args=(txn_type,))
else:
url = reverse(
"accounting:transactions.edit", args=(txn_type, transaction))
return error_redirect(
request,
str(UrlBuilder(url).add("r", request.GET.get("r"))),
form,
errors)
str(UrlBuilder(url).set("r", request.GET.get("r"))),
post)
if transaction is None:
transaction = Transaction()
fill_transaction_from_form(transaction, post)
# TODO: Stores the data
return success_redirect(
request,
str(UrlBuilder(reverse("accounting:transactions.show",

View File

@ -22,7 +22,7 @@ import random
from django.http import HttpResponseRedirect
from mia_core.utils import UrlBuilder
from .utils import UrlBuilder
def success_redirect(request, url, success):
@ -39,10 +39,10 @@ def success_redirect(request, url, success):
HttpResponseRedirect: The redirect response.
"""
id = _store(request, {"success": success})
return HttpResponseRedirect(str(UrlBuilder(url).add("s", id)))
return HttpResponseRedirect(str(UrlBuilder(url).set("s", id)))
def error_redirect(request, url, form, errors_by_field):
def error_redirect(request, url, form):
"""Redirects to a specific URL on error, with the status ID appended
as the query parameter "s". The status will be loaded with the
retrieve_status template tag.
@ -57,8 +57,8 @@ def error_redirect(request, url, form, errors_by_field):
Returns:
HttpResponseRedirect: The redirect response.
"""
id = _store(request, {"form": form, "errors_by_field": errors_by_field})
return HttpResponseRedirect(str(UrlBuilder(url).add("s", id)))
id = _store(request, {"form": form})
return HttpResponseRedirect(str(UrlBuilder(url).set("s", id)))
def retrieve_status(request):