Added forms and validators, and applied them to the transaction form in the accounting application.
This commit is contained in:
parent
c1da25b3b5
commit
9d988f17ca
242
accounting/forms.py
Normal file
242
accounting/forms.py
Normal 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])
|
@ -33,13 +33,13 @@ First written: 2020/7/23
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="btn-group btn-actions">
|
<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>
|
<i class="fas fa-chevron-circle-left"></i>
|
||||||
{% trans "Back" context "Navigation|" as text %}{{ text|force_escape }}
|
{% trans "Back" context "Navigation|" as text %}{{ text|force_escape }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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 %}
|
{% csrf_token %}
|
||||||
{# TODO: To be done #}
|
{# TODO: To be done #}
|
||||||
<input id="l10n-messages" type="hidden" value="{{ l10n_messages }}" />
|
<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>
|
<label for="txn-date">{% trans "Date:" context "Accounting|" as text %}{{ text|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-10">
|
<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" />
|
<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">{{ errors|dict:"date"|default:"" }}</div>
|
<div id="txn-date-error" class="invalid-feedback">{{ item.date.errors.0|default:"" }}</div>
|
||||||
</div>
|
</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">
|
<li id="debit-{{ forloop.counter }}" class="list-group-item d-flex justify-content-between draggable-record debit-record">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
{% if x.pk is not None %}
|
{% if x.id.value %}
|
||||||
<input type="hidden" name="debit-{{ forloop.counter }}-sn" value="{{ x.pk }}" />
|
<input type="hidden" name="debit-{{ forloop.counter }}-id" value="{{ x.id.value }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="debit-{{ forloop.counter }}-ord" class="debit-ord" type="hidden" name="debit-{{ forloop.counter }}-ord" value="{{ x.ord }}" />
|
<input id="debit-{{ forloop.counter }}-ord" class="debit-ord" type="hidden" name="debit-{{ forloop.counter }}-ord" value="{{ x.ord.value }}" />
|
||||||
{% str_format "debit-{}-account" forloop.counter as field %}
|
<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">
|
||||||
<select id="{{ field }}" class="form-control record-account debit-account {% if errors|dict:field %} is-invalid {% endif %}" name="{{ field }}">
|
|
||||||
{% if x.account is not None %}
|
{% 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 %}
|
{% else %}
|
||||||
<option value=""></option>
|
<option value=""></option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
|
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
|
||||||
</select>
|
</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="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
{% str_format "debit-{}-summary" forloop.counter as field %}
|
<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" />
|
||||||
<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="debit-{{ forloop.counter }}-summary-error" class="invalid-feedback">{{ x.summary.errors.0|default:"" }}</div>
|
||||||
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
{% str_format "debit-{}-amount" forloop.counter as field %}
|
<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" />
|
||||||
<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="debit-{{ forloop.counter }}-amount-error" class="invalid-feedback">{{ x.amount.errors.0|default:"" }}</div>
|
||||||
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<label for="txn-note">{% trans "Notes:" context "Accounting|" as text %}{{ text|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-10">
|
<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>
|
<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">{{ errors|dict:"notes"|default:"" }}</div>
|
<div id="txn-note-error" class="invalid-feedback">{{ item.notes.errors.0|default:"" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -33,13 +33,13 @@ First written: 2020/7/23
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="btn-group btn-actions">
|
<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>
|
<i class="fas fa-chevron-circle-left"></i>
|
||||||
{% trans "Back" context "Navigation|" as text %}{{ text|force_escape }}
|
{% trans "Back" context "Navigation|" as text %}{{ text|force_escape }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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 %}
|
{% csrf_token %}
|
||||||
{# TODO: To be done #}
|
{# TODO: To be done #}
|
||||||
<input id="l10n-messages" type="hidden" value="{{ l10n_messages }}" />
|
<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>
|
<label for="txn-date">{% trans "Date:" context "Accounting|" as text %}{{ text|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-10">
|
<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" />
|
<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">{{ errors|dict:"date"|default:"" }}</div>
|
<div id="txn-date-error" class="invalid-feedback">{{ item.date.errors.0|default:"" }}</div>
|
||||||
</div>
|
</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">
|
<li id="credit-{{ forloop.counter }}" class="list-group-item d-flex justify-content-between draggable-record credit-record">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
{% if x.pk is not None %}
|
{% if x.id.value %}
|
||||||
<input type="hidden" name="credit-{{ forloop.counter }}-sn" value="{{ x.pk }}" />
|
<input type="hidden" name="credit-{{ forloop.counter }}-id" value="{{ x.id.value }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="credit-{{ forloop.counter }}-ord" class="credit-ord" type="hidden" name="credit-{{ forloop.counter }}-ord" value="{{ x.ord }}" />
|
<input id="credit-{{ forloop.counter }}-ord" class="credit-ord" type="hidden" name="credit-{{ forloop.counter }}-ord" value="{{ x.ord.value }}" />
|
||||||
{% str_format "credit-{}-account" forloop.counter as field %}
|
<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">
|
||||||
<select id="{{ field }}" class="form-control record-account credit-account {% if errors|dict:field %} is-invalid {% endif %}" name="{{ field }}">
|
|
||||||
{% if x.account is not None %}
|
{% 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 %}
|
{% else %}
|
||||||
<option value=""></option>
|
<option value=""></option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
|
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
|
||||||
</select>
|
</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="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
{% str_format "credit-{}-summary" forloop.counter as field %}
|
<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" />
|
||||||
<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="credit-{{ forloop.counter }}-summary-error" class="invalid-feedback">{{ x.summary.errors.0|default:"" }}</div>
|
||||||
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
{% str_format "credit-{}-amount" forloop.counter as field %}
|
<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" />
|
||||||
<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="credit-{{ forloop.counter }}-amount-error" class="invalid-feedback">{{ x.amount.errors.0|default:"" }}</div>
|
||||||
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<label for="txn-note">{% trans "Notes:" context "Accounting|" as text %}{{ text|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-10">
|
<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>
|
<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">{{ errors|dict:"notes"|default:"" }}</div>
|
<div id="txn-note-error" class="invalid-feedback">{{ item.notes.errors.0|default:"" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -33,13 +33,13 @@ First written: 2020/7/23
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="btn-group btn-actions">
|
<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>
|
<i class="fas fa-chevron-circle-left"></i>
|
||||||
{% trans "Back" context "Navigation|" as text %}{{ text|force_escape }}
|
{% trans "Back" context "Navigation|" as text %}{{ text|force_escape }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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 %}
|
{% csrf_token %}
|
||||||
{# TODO: To be done #}
|
{# TODO: To be done #}
|
||||||
<input id="l10n-messages" type="hidden" value="{{ l10n_messages }}" />
|
<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>
|
<label for="txn-date">{% trans "Date:" context "Accounting|" as text %}{{ text|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-10">
|
<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" />
|
<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">{{ errors|dict:"date"|default:"" }}</div>
|
<div id="txn-date-error" class="invalid-feedback">{{ item.date.errors.0|default:"" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -70,32 +70,29 @@ First written: 2020/7/23
|
|||||||
<div>
|
<div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
{% if x.pk is not None %}
|
{% if x.id.value %}
|
||||||
<input type="hidden" name="debit-{{ forloop.counter }}-sn" value="{{ x.pk }}" />
|
<input type="hidden" name="debit-{{ forloop.counter }}-id" value="{{ x.id.value }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="debit-{{ forloop.counter }}-ord" class="debit-ord" type="hidden" name="debit-{{ forloop.counter }}-ord" value="{{ x.ord }}" />
|
<input id="debit-{{ forloop.counter }}-ord" class="debit-ord" type="hidden" name="debit-{{ forloop.counter }}-ord" value="{{ x.ord.value }}" />
|
||||||
{% str_format "debit-{}-account" forloop.counter as field %}
|
<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">
|
||||||
<select id="{{ field }}" class="form-control record-account debit-account {% if errors|dict:field %} is-invalid {% endif %}" name="{{ field }}">
|
|
||||||
{% if x.account is not None %}
|
{% 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 %}
|
{% else %}
|
||||||
<option value=""></option>
|
<option value=""></option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
|
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
{% str_format "debit-{}-summary" forloop.counter as field %}
|
<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" />
|
||||||
<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="debit-{{ forloop.counter }}-summary-error" class="invalid-feedback">{{ x.summary.errors.0|default:"" }}</div>
|
||||||
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
{% str_format "debit-{}-amount" forloop.counter as field %}
|
<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" />
|
||||||
<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="debit-{{ forloop.counter }}-amount-error" class="invalid-feedback">{{ x.amount.errors.0|default:"" }}</div>
|
||||||
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -119,11 +116,11 @@ First written: 2020/7/23
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item">
|
<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 }}
|
{% trans "Total" context "Accounting|" as text %}{{ text|force_escape }}
|
||||||
<span id="debit-total" class="amount">{{ item.debit_total }}</span>
|
<span id="debit-total" class="amount">{{ item.debit_total }}</span>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -137,32 +134,29 @@ First written: 2020/7/23
|
|||||||
<div>
|
<div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
{% if x.pk is not None %}
|
{% if x.id.value %}
|
||||||
<input type="hidden" name="credit-{{ forloop.counter }}-sn" value="{{ x.pk }}" />
|
<input type="hidden" name="credit-{{ forloop.counter }}-id" value="{{ x.id.value }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="credit-{{ forloop.counter }}-ord" class="credit-ord" type="hidden" name="credit-{{ forloop.counter }}-ord" value="{{ x.ord }}" />
|
<input id="credit-{{ forloop.counter }}-ord" class="credit-ord" type="hidden" name="credit-{{ forloop.counter }}-ord" value="{{ x.ord.value }}" />
|
||||||
{% str_format "credit-{}-account" forloop.counter as field %}
|
<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">
|
||||||
<select id="{{ field }}" class="form-control record-account credit-account {% if errors|dict:field %} is-invalid {% endif %}" name="{{ field }}">
|
|
||||||
{% if x.account is not None %}
|
{% 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 %}
|
{% else %}
|
||||||
<option value=""></option>
|
<option value=""></option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
|
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
{% str_format "credit-{}-summary" forloop.counter as field %}
|
<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" />
|
||||||
<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="credit-{{ forloop.counter }}-summary-error" class="invalid-feedback">{{ x.summary.errors.0|default:"" }}</div>
|
||||||
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
{% str_format "credit-{}-amount" forloop.counter as field %}
|
<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" />
|
||||||
<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="credit-{{ forloop.counter }}-amount-error" class="invalid-feedback">{{ x.amount.errors.0|default:"" }}</div>
|
||||||
<div id="{{ field }}-error" class="invalid-feedback">{{ errors|dict:field|default:"" }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -186,11 +180,11 @@ First written: 2020/7/23
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item">
|
<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 }}
|
{% trans "Total" context "Accounting|" as text %}{{ text|force_escape }}
|
||||||
<span id="credit-total" class="amount">{{ item.credit_total }}</span>
|
<span id="credit-total" class="amount">{{ item.credit_total }}</span>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -201,8 +195,8 @@ First written: 2020/7/23
|
|||||||
<label for="txn-note">{% trans "Notes:" context "Accounting|" as text %}{{ text|force_escape }}</label>
|
<label for="txn-note">{% trans "Notes:" context "Accounting|" as text %}{{ text|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-10">
|
<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>
|
<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">{{ errors|dict:"notes"|default:"" }}</div>
|
<div id="txn-note-error" class="invalid-feedback">{{ item.notes.errors.0|default:"" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -21,13 +21,13 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
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.db.models import Q, Sum, Case, When, F, Count, Max, Min
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
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.period import Period
|
||||||
from mia_core.status import retrieve_status
|
from mia_core.status import retrieve_status
|
||||||
from mia_core.utils import new_pk
|
from mia_core.utils import new_pk
|
||||||
@ -289,7 +289,7 @@ def find_order_holes(records):
|
|||||||
.filter(~(Q(max=F("count")) & Q(min=1)))] +\
|
.filter(~(Q(max=F("count")) & Q(min=1)))] +\
|
||||||
[x["date"] for x in Transaction.objects
|
[x["date"] for x in Transaction.objects
|
||||||
.values("date", "ord")
|
.values("date", "ord")
|
||||||
.annotate(count=Count("sn"))
|
.annotate(count=Count("pk"))
|
||||||
.filter(~Q(count=1))]
|
.filter(~Q(count=1))]
|
||||||
for record in records:
|
for record in records:
|
||||||
record.has_order_hole = record.transaction.date in holes
|
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():
|
for key in form.keys():
|
||||||
m = re.match(
|
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)
|
key)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
rec_type = m.group(1)
|
rec_type = m.group(1)
|
||||||
@ -328,8 +328,8 @@ def fill_transaction_from_form(transaction, form):
|
|||||||
ord=no,
|
ord=no,
|
||||||
is_credit=(rec_type == "credit"),
|
is_credit=(rec_type == "credit"),
|
||||||
transaction=transaction)
|
transaction=transaction)
|
||||||
if F"{rec_type}-{no}-sn" in form:
|
if F"{rec_type}-{no}-id" in form:
|
||||||
record.pk = form[F"{rec_type}-{no}-sn"]
|
record.pk = form[F"{rec_type}-{no}-id"]
|
||||||
if F"{rec_type}-{no}-account" in form:
|
if F"{rec_type}-{no}-account" in form:
|
||||||
record.account = Account(code=form[F"{rec_type}-{no}-account"])
|
record.account = Account(code=form[F"{rec_type}-{no}-account"])
|
||||||
if F"{rec_type}-{no}-summary" in form:
|
if F"{rec_type}-{no}-summary" in form:
|
||||||
@ -340,22 +340,6 @@ def fill_transaction_from_form(transaction, form):
|
|||||||
transaction.records = records
|
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):
|
def sort_form_transaction_records(form):
|
||||||
"""Sorts the records in the form by their specified order, so that the
|
"""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.
|
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():
|
for key in form.keys():
|
||||||
m = re.match(
|
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)
|
key)
|
||||||
if m is None:
|
if m is None:
|
||||||
continue
|
continue
|
||||||
@ -396,41 +380,122 @@ def sort_form_transaction_records(form):
|
|||||||
old_no = record_no[record_type][i]
|
old_no = record_no[record_type][i]
|
||||||
no = i + 1
|
no = i + 1
|
||||||
new_form[F"{record_type}-{no}-ord"] = no
|
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:
|
if F"{record_type}-{old_no}-{attr}" in form:
|
||||||
new_form[F"{record_type}-{no}-{attr}"]\
|
new_form[F"{record_type}-{no}-{attr}"]\
|
||||||
= form[F"{record_type}-{old_no}-{attr}"]
|
= form[F"{record_type}-{old_no}-{attr}"]
|
||||||
# Purges the old form and fills it with the new form
|
# Purges the old form and fills it with the new form
|
||||||
for x in [x for x in form.keys() if re.match(
|
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)]:
|
x)]:
|
||||||
del form[x]
|
del form[x]
|
||||||
for key in new_form.keys():
|
for key in new_form.keys():
|
||||||
form[key] = new_form[key]
|
form[key] = new_form[key]
|
||||||
|
|
||||||
|
|
||||||
def validate_account_code(record):
|
def make_transaction_form_from_model(transaction, exists):
|
||||||
"""Validates the account code.
|
"""Converts a transaction data model to a transaction form.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
record (Record): The accounting record.
|
transaction (Transaction): The transaction data model.
|
||||||
|
exists (bool): Whether the current transaction exists.
|
||||||
|
|
||||||
Exceptions:
|
Returns:
|
||||||
ValidationError: Thrown when validation fails.
|
TransactionForm: The transaction form.
|
||||||
"""
|
"""
|
||||||
if record.account.code is None:
|
transaction_form = TransactionForm(
|
||||||
raise ValidationError(gettext_noop(
|
{x: str(getattr(transaction, x)) for x in ["date", "notes"]
|
||||||
"Please select the account."))
|
if getattr(transaction, x) is not None})
|
||||||
if record.account.code == "":
|
transaction_form.transaction = transaction if exists else None
|
||||||
raise ValidationError(gettext_noop(
|
for record in transaction.records:
|
||||||
"Please select the account."))
|
data = {x: getattr(record, x)
|
||||||
|
for x in ["summary", "amount"]
|
||||||
|
if getattr(record, x) is not None}
|
||||||
|
data["id"] = record.pk
|
||||||
try:
|
try:
|
||||||
record.account = Account.objects.get(code=record.account.code)
|
data["account"] = record.account.code
|
||||||
except Account.DoesNotExist:
|
except AttributeError:
|
||||||
raise ValidationError(gettext_noop(
|
pass
|
||||||
"This account does not exist."))
|
record_form = RecordForm(data)
|
||||||
child_account = Account.objects.filter(
|
record_form.transaction = transaction_form.transaction
|
||||||
code__startswith=record.account.code).first()
|
record_form.is_credit = record.is_credit
|
||||||
if child_account is not None:
|
if record.is_credit:
|
||||||
raise ValidationError(gettext_noop(
|
transaction_form.credit_records.append(record_form)
|
||||||
"You cannot choose a parent account."))
|
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
67
accounting/validators.py
Normal 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")
|
@ -21,7 +21,6 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
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 import Sum, Case, When, F, Q
|
||||||
from django.db.models.functions import TruncMonth, Coalesce
|
from django.db.models.functions import TruncMonth, Coalesce
|
||||||
from django.shortcuts import render
|
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 .models import Record, Transaction, Account, RecordSummary
|
||||||
from .utils import ReportUrl, get_cash_accounts, get_ledger_accounts, \
|
from .utils import ReportUrl, get_cash_accounts, get_ledger_accounts, \
|
||||||
find_imbalanced, find_order_holes, fill_transaction_from_form, \
|
find_imbalanced, find_order_holes, fill_transaction_from_form, \
|
||||||
sort_form_transaction_records, fill_transaction_from_previous_form, \
|
sort_form_transaction_records, make_transaction_form_from_status, \
|
||||||
validate_account_code
|
make_transaction_form_from_model, make_transaction_form_from_post
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(require_GET, name="dispatch")
|
@method_decorator(require_GET, name="dispatch")
|
||||||
@ -823,15 +822,18 @@ def transaction_edit(request, txn_type, transaction=None):
|
|||||||
Returns:
|
Returns:
|
||||||
HttpResponse: The response.
|
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:
|
if transaction is None:
|
||||||
transaction = Transaction()
|
transaction = Transaction(date=timezone.localdate())
|
||||||
fill_transaction_from_previous_form(request, transaction)
|
|
||||||
if len(transaction.debit_records) == 0:
|
if len(transaction.debit_records) == 0:
|
||||||
transaction.records.append(Record(ord=1, is_credit=False))
|
transaction.records.append(Record(ord=1, is_credit=False))
|
||||||
if len(transaction.credit_records) == 0:
|
if len(transaction.credit_records) == 0:
|
||||||
transaction.records.append(Record(ord=1, is_credit=True))
|
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", {
|
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:
|
Returns:
|
||||||
HttpResponse: The response.
|
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:
|
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,))
|
url = reverse("accounting:transactions.create", args=(txn_type,))
|
||||||
else:
|
else:
|
||||||
url = reverse(
|
url = reverse(
|
||||||
"accounting:transactions.edit", args=(txn_type, transaction))
|
"accounting:transactions.edit", args=(txn_type, transaction))
|
||||||
return error_redirect(
|
return error_redirect(
|
||||||
request,
|
request,
|
||||||
str(UrlBuilder(url).add("r", request.GET.get("r"))),
|
str(UrlBuilder(url).set("r", request.GET.get("r"))),
|
||||||
form,
|
post)
|
||||||
errors)
|
if transaction is None:
|
||||||
|
transaction = Transaction()
|
||||||
|
fill_transaction_from_form(transaction, post)
|
||||||
|
# TODO: Stores the data
|
||||||
return success_redirect(
|
return success_redirect(
|
||||||
request,
|
request,
|
||||||
str(UrlBuilder(reverse("accounting:transactions.show",
|
str(UrlBuilder(reverse("accounting:transactions.show",
|
||||||
|
@ -22,7 +22,7 @@ import random
|
|||||||
|
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
from mia_core.utils import UrlBuilder
|
from .utils import UrlBuilder
|
||||||
|
|
||||||
|
|
||||||
def success_redirect(request, url, success):
|
def success_redirect(request, url, success):
|
||||||
@ -39,10 +39,10 @@ def success_redirect(request, url, success):
|
|||||||
HttpResponseRedirect: The redirect response.
|
HttpResponseRedirect: The redirect response.
|
||||||
"""
|
"""
|
||||||
id = _store(request, {"success": success})
|
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
|
"""Redirects to a specific URL on error, with the status ID appended
|
||||||
as the query parameter "s". The status will be loaded with the
|
as the query parameter "s". The status will be loaded with the
|
||||||
retrieve_status template tag.
|
retrieve_status template tag.
|
||||||
@ -57,8 +57,8 @@ def error_redirect(request, url, form, errors_by_field):
|
|||||||
Returns:
|
Returns:
|
||||||
HttpResponseRedirect: The redirect response.
|
HttpResponseRedirect: The redirect response.
|
||||||
"""
|
"""
|
||||||
id = _store(request, {"form": form, "errors_by_field": errors_by_field})
|
id = _store(request, {"form": form})
|
||||||
return HttpResponseRedirect(str(UrlBuilder(url).add("s", id)))
|
return HttpResponseRedirect(str(UrlBuilder(url).set("s", id)))
|
||||||
|
|
||||||
|
|
||||||
def retrieve_status(request):
|
def retrieve_status(request):
|
||||||
|
Loading…
Reference in New Issue
Block a user