dded the account_store() view in the accounting application.

This commit is contained in:
依瑪貓 2020-08-09 16:22:51 +08:00
parent 7c218cbc76
commit 00ee0cc3bb
4 changed files with 163 additions and 6 deletions

View File

@ -22,6 +22,8 @@ import re
from django import forms from django import forms
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db.models import Q, Max
from django.db.models.functions import Length
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from .models import Account, Record from .models import Account, Record
@ -317,7 +319,6 @@ class AccountForm(forms.Form):
RegexValidator( RegexValidator(
regex="^[1-9]+$", regex="^[1-9]+$",
message=_("You can only use numbers 1-9 in the code.")), message=_("You can only use numbers 1-9 in the code.")),
validate_account_code,
]) ])
title = forms.CharField( title = forms.CharField(
max_length=128, max_length=128,
@ -328,6 +329,7 @@ class AccountForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AccountForm, self).__init__(*args, **kwargs) super(AccountForm, self).__init__(*args, **kwargs)
self.account = None
@property @property
def parent(self): def parent(self):
@ -335,3 +337,109 @@ class AccountForm(forms.Form):
if code is None or len(code) < 2: if code is None or len(code) < 2:
return None return None
return Account.objects.get(code=code[:-1]) return Account.objects.get(code=code[:-1])
def clean(self):
"""Validates the form globally.
Raises:
ValidationError: When the validation fails.
"""
errors = []
validators = [self._validate_code_not_under_myself,
self._validate_code_unique,
self._validate_code_parent_exists,
self._validate_code_descendant_code_size]
for validator in validators:
try:
validator()
except forms.ValidationError as e:
errors.append(e)
if errors:
raise forms.ValidationError(errors)
def _validate_code_not_under_myself(self):
"""Validates whether the code is under itself.
Raises:
ValidationError: When the validation fails.
"""
if self.account is None:
return
if "code" not in self.data:
return
if self.data["code"] == self.account.code:
return
if not self.data["code"].startswith(self.account.code):
return
error = forms.ValidationError(
_("You cannot set the code under itself."),
code="not_under_myself")
self.add_error("code", error)
raise error
def _validate_code_unique(self):
"""Validates whether the code is unique.
Raises:
ValidationError: When the validation fails.
"""
if "code" not in self.data:
return
try:
if self.account is None:
Account.objects.get(code=self.data["code"])
else:
Account.objects.get(Q(code=self.data["code"]),
~Q(pk=self.account.pk))
except Account.DoesNotExist:
return
error = forms.ValidationError(_("This code is already in use."),
code="code_unique")
self.add_error("code", error)
raise error
def _validate_code_parent_exists(self):
"""Validates whether the parent account exists.
Raises:
ValidationError: When the validation fails.
"""
if "code" not in self.data:
return
if len(self.data["code"]) < 2:
return
try:
Account.objects.get(code=self.data["code"][:-1])
except Account.DoesNotExist:
error = forms.ValidationError(
_("The parent account of this code does not exist."),
code="code_unique")
self.add_error("code", error)
raise error
return
def _validate_code_descendant_code_size(self):
"""Validates whether the codes of the descendants will be too long.
Raises:
ValidationError: When the validation fails.
"""
if "code" not in self.data:
return
if self.account is None:
return
cur_max_len = Account.objects\
.filter(Q(code__startswith=self.account.code),
~Q(pk=self.account.pk))\
.aggregate(max_len=Max(Length("code")))["max_len"]
if cur_max_len is None:
return True
new_max_len = cur_max_len - len(self.account.code)\
+ len(self.data["code"])
if new_max_len <= 5:
return
error = forms.ValidationError(
_("The descendant account codes will be too long (max. 5)."),
code="descendant_code_size")
self.add_error("code", error)
raise error

View File

@ -24,7 +24,8 @@ from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from mia_core.utils import get_multi_lingual_attr, set_multi_lingual_attr from mia_core.utils import get_multi_lingual_attr, set_multi_lingual_attr, \
new_pk
class Account(DirtyFieldsMixin, models.Model): class Account(DirtyFieldsMixin, models.Model):
@ -70,6 +71,19 @@ class Account(DirtyFieldsMixin, models.Model):
"""Returns the string representation of this account.""" """Returns the string representation of this account."""
return self.code.__str__() + " " + self.title return self.code.__str__() + " " + self.title
def save(self, current_user=None, force_insert=False, force_update=False,
using=None, update_fields=None):
self.parent = None if len(self.code) == 1\
else Account.objects.get(code=self.code[:-1])
if self.pk is None:
self.pk = new_pk(Account)
self.created_by = current_user
self.updated_by = current_user
with transaction.atomic():
super(Account, self).save(
force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields)
class Meta: class Meta:
db_table = "accounting_accounts" db_table = "accounting_accounts"

View File

@ -94,9 +94,8 @@ urlpatterns = [
views.AccountListView.as_view(), name="accounts"), views.AccountListView.as_view(), name="accounts"),
path("accounts/create", path("accounts/create",
views.account_form, name="accounts.create"), views.account_form, name="accounts.create"),
# TODO: To be done
path("accounts/store", path("accounts/store",
mia_core_views.todo, name="accounts.store"), views.account_store, name="accounts.store"),
path("api/accounts", path("api/accounts",
views.api_account_list, name="api.accounts"), views.api_account_list, name="api.accounts"),
path("api/accounts/options", path("api/accounts/options",
@ -105,9 +104,8 @@ urlpatterns = [
views.AccountView.as_view(), name="accounts.detail"), views.AccountView.as_view(), name="accounts.detail"),
path("accounts/<account:account>/edit", path("accounts/<account:account>/edit",
views.account_form, name="accounts.edit"), views.account_form, name="accounts.edit"),
# TODO: To be done
path("accounts/<account:account>/update", path("accounts/<account:account>/update",
mia_core_views.todo, name="accounts.update"), views.account_store, name="accounts.update"),
# TODO: To be done # TODO: To be done
path("accounts/<account:account>/delete", path("accounts/<account:account>/delete",
mia_core_views.todo, name="accounts.delete"), mia_core_views.todo, name="accounts.delete"),

View File

@ -1053,11 +1053,48 @@ def account_form(request, account=None):
}) })
else: else:
form = AccountForm() form = AccountForm()
form.account = account
return render(request, "accounting/account_form.html", { return render(request, "accounting/account_form.html", {
"form": form, "form": form,
}) })
@require_POST
@login_required
def account_store(request, account=None):
"""The view to edit an accounting transaction.
Args:
request (HttpRequest): The request.
account (Account): The account.
Returns:
HttpResponseRedirect: The response.
"""
post = request.POST.dict()
strip_post(post)
form = AccountForm(post)
form.account = account
if not form.is_valid():
if account is None:
url = reverse("accounting:accounts.create")
else:
url = reverse("accounting:accounts.edit", args=(account,))
return stored_post.error_redirect(request, url, post)
if account is None:
account = Account()
account.code = form["code"].value()
account.title = form["title"].value()
if not account.is_dirty():
message = gettext_noop("This account was not modified.")
else:
account.save(current_user=request.user)
message = gettext_noop("This account was saved successfully.")
messages.success(request, message)
return HttpResponseRedirect(reverse("accounting:accounts.detail",
args=(account,)))
@require_GET @require_GET
@login_required @login_required
def api_account_list(request): def api_account_list(request):