Moved the source files to the "src" subdirectory.

This commit is contained in:
2022-12-05 08:46:20 +08:00
parent 24c3b868e0
commit 3cef0d7009
65 changed files with 2 additions and 1 deletions

View File

5
src/accounting/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountingConfig(AppConfig):
name = 'accounting'

View File

@ -0,0 +1,248 @@
# The accounting application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/7/23
# 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 URL converters.
"""
import datetime
import re
from django.utils.translation import gettext as _
from .models import Transaction, Record, Account
from mia_core.period import Period
class TransactionTypeConverter:
"""The path converter for the transaction types."""
regex = "income|expense|transfer"
def to_python(self, value):
return value
def to_url(self, value):
return value
class PeriodConverter:
"""The path converter for the period."""
regex = ("([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)|"
"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?-"
"([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?)?")
def to_python(self, value):
"""Returns the period by the period specification.
Args:
value (str): The period specification.
Returns:
Period: The period.
Raises:
ValueError: When the period specification is invalid.
"""
first_txn = Transaction.objects.order_by("date").first()
data_start = first_txn.date if first_txn is not None else None
last_txn = Transaction.objects.order_by("-date").first()
data_end = last_txn.date if last_txn is not None else None
# Raises ValueError
return Period(value, data_start, data_end)
def to_url(self, value):
"""Returns the specification of a period.
Args:
value (Period|str): The period.
Returns:
str: The period specification.
"""
if isinstance(value, Period):
return value.spec
return value
class DateConverter:
"""The path converter for the date."""
regex = "([0-9]{4})-([0-9]{2})-([0-9]{2})"
def to_python(self, value):
"""Returns the date by the date specification.
Args:
value (str): The date specification.
Returns:
datetime.date: The date.
"""
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$", value)
year = int(m.group(1))
month = int(m.group(2))
day = int(m.group(3))
return datetime.date(year, month, day)
def to_url(self, value):
"""Returns the specification of a date.
Args:
value (datetime.date): The date.
Returns:
str: The date specification.
"""
return value.strftime("%Y-%m-%d")
class AccountConverter:
"""The path converter for the account."""
regex = "[1-9]{1,5}"
def to_python(self, value):
"""Returns the account by the account code.
Args:
value (str): The account code.
Returns:
Account: The account.
"""
try:
return Account.objects.get(code=value)
except Account.DoesNotExist:
raise ValueError
def to_url(self, value):
"""Returns the code of an account.
Args:
value (Account): The account.
Returns:
str: The account code.
"""
return value.code
class CashAccountConverter:
"""The path converter for the cash account."""
regex = "0|(11|12|21|22)[1-9]{1,3}"
def to_python(self, value: str) -> Account:
"""Returns the cash account by the account code.
Args:
value: The account code.
Returns:
The account.
Raises:
ValueError: When the value is invalid
"""
if value == "0":
return Account(
code="0",
title=_("current assets and liabilities"),
)
try:
account = Account.objects.get(code=value)
except Account.DoesNotExist:
raise ValueError
if Record.objects.filter(account=account).count() == 0:
raise ValueError
return account
def to_url(self, value: Account) -> str:
"""Returns the code of an account.
Args:
value: The account.
Returns:
The account code.
"""
return value.code
class LedgerAccountConverter:
"""The path converter for the ledger account."""
regex = "[1-9]{1,5}"
def to_python(self, value: str) -> Account:
"""Returns the ledger account by the account code.
Args:
value: The account code.
Returns:
The account.
Raises:
ValueError: When the value is invalid
"""
try:
account = Account.objects.get(code=value)
except Account.DoesNotExist:
raise ValueError
if Record.objects.filter(account__code__startswith=value).count() == 0:
raise ValueError
return account
def to_url(self, value: Account) -> str:
"""Returns the code of an account.
Args:
value: The account.
Returns:
The account code.
"""
return value.code
class TransactionConverter:
"""The path converter for the accounting transactions."""
regex = "[1-9][0-9]{8}"
def to_python(self, value: str) -> Transaction:
"""Returns the transaction by the transaction ID.
Args:
value: The transaction ID.
Returns:
The account.
Raises:
ValueError: When the value is invalid
"""
try:
return Transaction.objects.get(pk=value)
except Transaction.DoesNotExist:
raise ValueError
def to_url(self, value: Transaction) -> str:
"""Returns the ID of a transaction.
Args:
value: The transaction.
Returns:
The transaction ID.
"""
return value.pk

632
src/accounting/forms.py Normal file
View File

@ -0,0 +1,632 @@
# 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 datetime
import re
from decimal import Decimal
from typing import Optional, List, Dict
from django import forms
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import RegexValidator
from django.db.models import Q, Max, Model
from django.db.models.functions import Length
from django.utils.translation import gettext as _
from .models import Account, Record, Transaction
from .validators import validate_record_account_code, validate_record_id
class RecordForm(forms.Form):
"""An accounting record form.
Attributes:
txn_form (TransactionForm): The parent transaction form.
is_credit (bool): Whether this is a credit record.
"""
id = forms.IntegerField(
required=False,
error_messages={
"invalid": _("This accounting record is not valid."),
},
validators=[validate_record_id])
account = forms.CharField(
error_messages={
"required": _("Please select the account."),
},
validators=[validate_record_account_code])
summary = forms.CharField(
required=False,
max_length=128,
error_messages={
"max_length": _("This summary is too long (max. 128 characters)."),
})
amount = forms.DecimalField(
max_digits=18,
decimal_places=2,
min_value=0.01,
error_messages={
"required": _("Please fill in the amount."),
"invalid": _("Please fill in a number."),
"min_value": _("The amount must be more than 0."),
})
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.txn_form = None
self.is_credit = None
def account_title(self) -> Optional[str]:
"""Returns the title of the specified account, if any.
Returns:
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:
forms.ValidationError: When the validation fails.
"""
errors = []
if "id" in self.errors:
errors = errors + self.errors["id"].as_data()
validators = [self._validate_transaction,
self._validate_account_type,
self._validate_is_credit]
for validator in validators:
try:
validator()
except forms.ValidationError as e:
errors.append(e)
if errors:
raise forms.ValidationError(errors)
def _validate_transaction(self) -> None:
"""Validates whether the transaction matches the transaction form.
Raises:
forms.ValidationError: When the validation fails.
"""
if "id" in self.errors:
return
if self.txn_form.transaction is None:
if "id" in self.data:
raise forms.ValidationError(
_("This record is not for this transaction."),
code="not_belong")
else:
if "id" in self.data:
record = Record.objects.get(pk=self.data["id"])
if record.transaction.pk != self.txn_form.transaction.pk:
raise forms.ValidationError(
_("This record is not for this transaction."),
code="not_belong")
def _validate_account_type(self) -> None:
"""Validates whether the account is a correct debit or credit account.
Raises:
forms.ValidationError: When the validation fails.
"""
if "account" in self.errors:
return
if self.is_credit:
if not re.match("^([123489]|7[1234])", self.data["account"]):
error = forms.ValidationError(
_("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(
_("This account is not for debit records."),
code="not_debit")
self.add_error("account", error)
raise error
def _validate_is_credit(self) -> None:
"""Validates whether debit and credit records are submitted correctly
as corresponding debit and credit records.
Raises:
forms.ValidationError: When the validation fails.
"""
if "id" in self.errors:
return
if "id" not in self.data:
return
record = Record.objects.get(pk=self.data["id"])
if record.is_credit != self.is_credit:
if self.is_credit:
raise forms.ValidationError(
_("This accounting record is not a credit record."),
code="not_credit")
else:
raise forms.ValidationError(
_("This accounting record is not a debit record."),
code="not_debit")
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={
"required": _("Please fill in the date."),
"invalid": _("This date is not valid.")
})
notes = forms.CharField(
required=False,
max_length=128,
error_messages={
"max_length": _("These notes are too long (max. 128 characters).")
})
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Populates the belonging record forms
self.debit_records = []
self.credit_records = []
if len(args) > 0 and isinstance(args[0], dict):
by_rec_id = {}
for key in args[0].keys():
m = re.match(
("^((debit|credit)-([1-9][0-9]*))-"
"(id|ord|account|summary|amount)$"),
key)
if m is None:
continue
rec_id = m.group(1)
column = m.group(4)
if rec_id not in by_rec_id:
by_rec_id[rec_id] = {
"is_credit": m.group(2) == "credit",
"no": int(m.group(3)),
"data": {},
}
by_rec_id[rec_id]["data"][column] = args[0][key]
debit_data_list = [x for x in by_rec_id.values()
if not x["is_credit"]]
debit_data_list.sort(key=lambda x: x["no"])
for x in debit_data_list:
record_form = RecordForm(x["data"])
record_form.txn_form = self
record_form.is_credit = False
self.debit_records.append(record_form)
credit_data_list = [x for x in by_rec_id.values()
if x["is_credit"]]
credit_data_list.sort(key=lambda x: x["no"])
for x in credit_data_list:
record_form = RecordForm(x["data"])
record_form.txn_form = self
record_form.is_credit = True
self.credit_records.append(record_form)
self.txn_type = None
self.transaction = None
@staticmethod
def from_post(post: Dict[str, str], txn_type: str, txn: Model):
"""Constructs a transaction form from the POST data.
Args:
post: The post data.
txn_type: The transaction type.
txn: The transaction data model.
Returns:
The transaction form.
"""
TransactionForm._sort_post_txn_records(post)
form = TransactionForm(post)
form.txn_type = txn_type
form.transaction = txn
return form
@staticmethod
def _sort_post_txn_records(post: Dict[str, str]) -> None:
"""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.
Args:
post: The POSTed form.
"""
# Collects the available record numbers
record_no = {
"debit": [],
"credit": [],
}
for key in post.keys():
m = re.match(
"^(debit|credit)-([1-9][0-9]*)-(id|ord|account|summary|amount)",
key)
if m is None:
continue
record_type = m.group(1)
no = int(m.group(2))
if no not in record_no[record_type]:
record_no[record_type].append(no)
# Sorts these record numbers by their specified orders
for record_type in record_no.keys():
orders = {}
for no in record_no[record_type]:
try:
orders[no] = int(post[F"{record_type}-{no}-ord"])
except KeyError:
orders[no] = 9999
except ValueError:
orders[no] = 9999
record_no[record_type].sort(key=lambda n: orders[n])
# Constructs the sorted new form
new_post = {}
for record_type in record_no.keys():
for i in range(len(record_no[record_type])):
old_no = record_no[record_type][i]
no = i + 1
new_post[F"{record_type}-{no}-ord"] = str(no)
for attr in ["id", "account", "summary", "amount"]:
if F"{record_type}-{old_no}-{attr}" in post:
new_post[F"{record_type}-{no}-{attr}"] \
= post[F"{record_type}-{old_no}-{attr}"]
# Purges the old form and fills it with the new form
for x in [x for x in post.keys() if re.match(
"^(debit|credit)-([1-9][0-9]*)-(id|ord|account|summary|amount)",
x)]:
del post[x]
for key in new_post.keys():
post[key] = new_post[key]
@staticmethod
def from_model(txn: Transaction, txn_type: str):
"""Constructs a transaction form from the transaction data model.
Args:
txn: The transaction data model.
txn_type: The transaction type.
Returns:
The transaction form.
"""
form = TransactionForm(
{x: str(getattr(txn, x)) for x in ["date", "notes"]
if getattr(txn, x) is not None})
form.transaction = txn if txn.pk is not None else None
form.txn_type = txn_type
records = []
if txn_type != "income":
records = records + txn.debit_records
if txn_type != "expense":
records = records + txn.credit_records
for record in records:
data = {x: getattr(record, x)
for x in ["summary", "amount"]
if getattr(record, x) is not None}
if record.pk is not None:
data["id"] = record.pk
try:
data["account"] = record.account.code
except ObjectDoesNotExist:
pass
record_form = RecordForm(data)
record_form.txn_form = form
record_form.is_credit = record.is_credit
if record.is_credit:
form.credit_records.append(record_form)
else:
form.debit_records.append(record_form)
return form
def clean(self):
"""Validates the form globally.
Raises:
forms.ValidationError: When the validation fails.
"""
errors = []
validators = [self._validate_has_debit_records,
self._validate_has_credit_records,
self._validate_balance]
for validator in validators:
try:
validator()
except forms.ValidationError as e:
errors.append(e)
if errors:
raise forms.ValidationError(errors)
def _validate_has_debit_records(self) -> None:
"""Validates whether there is any debit record.
Raises:
forms.ValidationError: When the validation fails.
"""
if self.txn_type == "income":
return
if len(self.debit_records) > 0:
return
if self.txn_type == "transfer":
raise forms.ValidationError(
_("Please fill in debit accounting records."),
code="has_debit_records")
raise forms.ValidationError(
_("Please fill in accounting records."),
code="has_debit_records")
def _validate_has_credit_records(self) -> None:
"""Validates whether there is any credit record.
Raises:
forms.ValidationError: When the validation fails.
"""
if self.txn_type == "expense":
return
if len(self.credit_records) > 0:
return
if self.txn_type == "transfer":
raise forms.ValidationError(
_("Please fill in credit accounting records."),
code="has_debit_records")
raise forms.ValidationError(
_("Please fill in accounting records."),
code="has_debit_records")
def _validate_balance(self) -> None:
"""Validates whether the total amount of debit and credit records are
consistent.
Raises:
forms.ValidationError: When the validation fails.
"""
if self.txn_type != "transfer":
return
if self.debit_total() == self.credit_total():
return
raise forms.ValidationError(
_("The total of the debit and credit amounts are inconsistent."),
code="balance")
def is_valid(self) -> bool:
if not super().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) -> Optional[str]:
"""Returns the error message when the transaction is imbalanced.
Returns:
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) -> Decimal:
"""Returns the total amount of the debit records.
Returns:
The total amount of the credit records.
"""
return sum([Decimal(x.data["amount"]) for x in self.debit_records
if "amount" in x.data and "amount" not in x.errors])
def credit_total(self) -> Decimal:
"""Returns the total amount of the credit records.
Returns:
The total amount of the credit records.
"""
return sum([Decimal(x.data["amount"]) for x in self.credit_records
if "amount" in x.data and "amount" not in x.errors])
class TransactionSortForm(forms.Form):
"""A form to sort the transactions in a same day."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.date = None
self.txn_list: Optional[List[Transaction]] = None
self.txn_orders: List[TransactionSortForm.Order] = []
@staticmethod
def from_post(date: datetime.date, post: Dict[str, str]):
form = TransactionSortForm({})
form.date = date
post_orders: List[TransactionSortForm.Order] = []
for txn in Transaction.objects.filter(date=date).all():
key = F"transaction-{txn.pk}-ord"
if key not in post:
post_orders.append(form.Order(txn, 9999))
elif not re.match("^[0-9]+$", post[key]):
post_orders.append(form.Order(txn, 9999))
else:
post_orders.append(form.Order(txn, int(post[key])))
post_orders.sort(key=lambda x: (x.order, x.txn.ord))
form.txn_orders = []
for i in range(len(post_orders)):
form.txn_orders.append(form.Order(post_orders[i].txn, i + 1))
form.txn_list = [x.txn for x in form.txn_orders]
return form
class Order:
"""A transaction order"""
def __init__(self, txn: Transaction, order: int):
self.txn = txn
self.order = order
class AccountForm(forms.Form):
"""An account form."""
code = forms.CharField(
error_messages={
"required": _("Please fill in the code."),
"invalid": _("Please fill in a number."),
"max_length": _("This code is too long (max. 5)."),
"min_value": _("This code is too long (max. 5)."),
}, validators=[
RegexValidator(
regex="^[1-9]+$",
message=_("You can only use numbers 1-9 in the code.")),
])
title = forms.CharField(
max_length=128,
error_messages={
"required": _("Please fill in the title."),
"max_length": _("This title is too long (max. 128)."),
})
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.account = None
@property
def parent(self) -> Optional[Account]:
"""The parent account, or None if this is the topmost account."""
code = self["code"].value()
if code is None or len(code) < 2:
return None
return Account.objects.get(code=code[:-1])
def clean(self):
"""Validates the form globally.
Raises:
forms.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) -> None:
"""Validates whether the code is under itself.
Raises:
forms.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) -> None:
"""Validates whether the code is unique.
Raises:
forms.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) -> None:
"""Validates whether the parent account exists.
Raises:
forms.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) -> None:
"""Validates whether the codes of the descendants will be too long.
Raises:
forms.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
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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,126 @@
# Traditional Chinese PO file for the JavaScript on the Mia Website
# Copyright (C) 2020 imacat
# This file is distributed under the same license as the Mia package.
# imacat <imacat@mail.imacat.idv.tw>, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: mia-accounting-js 3.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-08-31 09:42+0800\n"
"PO-Revision-Date: 2020-08-31 09:58+0800\n"
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
"Language-Team: Traditional Chinese <imacat@mail.imacat.idv.tw>\n"
"Language: Traditional Chinese\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: accounting/static/accounting/js/account-form.js:75
msgid "Topmost"
msgstr "最上層"
#: accounting/static/accounting/js/account-form.js:83
msgid "(Unknown)"
msgstr "(不可考)"
#: accounting/static/accounting/js/account-form.js:118
msgid "Please fill in the code."
msgstr "請填寫代碼。"
#: accounting/static/accounting/js/account-form.js:123
msgid "You can only use numbers 1-9 in the code."
msgstr "代碼限填數字1-9。"
#: accounting/static/accounting/js/account-form.js:130
msgid "You cannot set the code under itself."
msgstr "代碼不可設在自己之下。"
#: accounting/static/accounting/js/account-form.js:135
msgid "This code is already in use."
msgstr "代碼和其他會計科目重複。"
#: accounting/static/accounting/js/account-form.js:142
msgid "The parent account of this code does not exist."
msgstr "找不到上層會計科目。"
#: accounting/static/accounting/js/account-form.js:160
msgid "The descendant account codes will be too long (max. 5)."
msgstr "子科目的代碼會太長最長5位數字。"
#: accounting/static/accounting/js/account-form.js:183
msgid "Please fill in the title."
msgstr "請填寫標題。"
#: accounting/static/accounting/js/summary-helper.js:254
msgid "January"
msgstr "一月"
#: accounting/static/accounting/js/summary-helper.js:255
msgid "February"
msgstr "二月"
#: accounting/static/accounting/js/summary-helper.js:256
msgid "March"
msgstr "三月"
#: accounting/static/accounting/js/summary-helper.js:257
msgid "April"
msgstr "四月"
#: accounting/static/accounting/js/summary-helper.js:258
msgid "May"
msgstr "五月"
#: accounting/static/accounting/js/summary-helper.js:259
msgid "June"
msgstr "六月"
#: accounting/static/accounting/js/summary-helper.js:260
msgid "July"
msgstr "七月"
#: accounting/static/accounting/js/summary-helper.js:261
msgid "August"
msgstr "八月"
#: accounting/static/accounting/js/summary-helper.js:262
msgid "September"
msgstr "九月"
#: accounting/static/accounting/js/summary-helper.js:263
msgid "October"
msgstr "十月"
#: accounting/static/accounting/js/summary-helper.js:264
msgid "November"
msgstr "十一月"
#: accounting/static/accounting/js/summary-helper.js:265
msgid "December"
msgstr "十二月"
#: accounting/static/accounting/js/transaction-form.js:376
msgid "Please fill in the date."
msgstr "請填寫日期。"
#: accounting/static/accounting/js/transaction-form.js:408
msgid "Please select the account."
msgstr "請選擇會計科目。"
#: accounting/static/accounting/js/transaction-form.js:429
msgid "This summary is too long (max. 128 characters)."
msgstr "摘要太長了最長128個字。"
#: accounting/static/accounting/js/transaction-form.js:450
msgid "Please fill in the amount."
msgstr "請填寫金額。"
#: accounting/static/accounting/js/transaction-form.js:482
msgid "The total amount of debit and credit records are inconsistent."
msgstr "借方和貸方合計不符。"
#: accounting/static/accounting/js/transaction-form.js:503
msgid "These notes are too long (max. 128 characters)."
msgstr "註記太長了最長128個字。"

View File

@ -0,0 +1,889 @@
# The accounting application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/9/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 command to populate the database with the accounts.
"""
import getpass
from typing import Optional
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.core.management import BaseCommand, CommandParser, CommandError, \
call_command
from django.db import transaction
from accounting.models import Account
from accounting.utils import DataFiller
class Command(BaseCommand):
"""Populates the database with sample accounting data."""
help = "Fills the database with the accounting accounts."
def __init__(self):
super().__init__()
self._filler: Optional[DataFiller] = None
def add_arguments(self, parser):
"""Adds command line arguments to the parser.
Args:
parser (CommandParser): The command line argument parser.
"""
parser.add_argument("--user", "-u", help="User")
def handle(self, *args, **options):
"""Runs the command.
Args:
*args (list[str]): The command line arguments.
**options (dict[str,str]): The command line switches.
"""
if Account.objects.count() > 0:
error = "Refuse to initialize the account data with existing data."
raise CommandError(error, returncode=1)
# Gets the user to use
user = self.get_user(options["user"])
self.stdout.write(F"Initializing accounting accounts as \"{user}\"")
with transaction.atomic():
self._filler = DataFiller(user)
self._filler.add_accounts([
(1, "assets", "資產", "资产"),
(2, "liabilities", "負債", "负债"),
(3, "owners equity", "業主權益", "业主权益"),
(4, "operating revenue", "營業收入", "营业收入"),
(5, "operating costs", "營業成本", "营业成本"),
(6, "operating expenses", "營業費用", "营业费用"),
(7,
"non-operating revenue and expenses, other income (expense)",
"營業外收入及費用", "营业外收入及费用"),
(8, "income tax expense (or benefit)", "所得稅費用(或利益)",
"所得税费用(或利益)"),
(9, "nonrecurring gain or loss", "非經常營業損益",
"非经常营业损益"),
(11, "current assets", "流動資產", "流动资产"),
(12, "current assets", "流動資產", "流动资产"),
(13, "funds and long-term investments", "基金及長期投資",
"基金及长期投资"),
(14, "property , plant, and equipment", "固定資產", "固定资产"),
(15, "property , plant, and equipment", "固定資產", "固定资产"),
(16, "depletable assets", "遞耗資產", "递耗资产"),
(17, "intangible assets", "無形資產", "无形资产"),
(18, "other assets", "其他資產", "其他资产"),
(21, "current liabilities", "流動負債", "流动负债"),
(22, "current liabilities", "流動負債", "流动负债"),
(23, "long-term liabilities", "長期負債", "长期负债"),
(28, "other liabilities", "其他負債", "其他负债"),
(31, "capital", "資本", "资本"),
(32, "additional paid-in capital", "資本公積", "资本公积"),
(33, "retained earnings (accumulated deficit)",
"保留盈餘(或累積虧損)", "保留盈余(或累积亏损)"),
(34, "equity adjustments", "權益調整", "权益调整"),
(35, "treasury stock", "庫藏股", "库藏股"),
(36, "minority interest", "少數股權", "少数股权"),
(41, "sales revenue", "銷貨收入", "销货收入"),
(46, "service revenue", "勞務收入", "劳务收入"),
(47, "agency revenue", "業務收入", "业务收入"),
(48, "other operating revenue", "其他營業收入", "其他营业收入"),
(51, "cost of goods sold", "銷貨成本", "销货成本"),
(56, "service costs", "勞務成本", "劳务成本"),
(57, "agency costs", "業務成本", "业务成本"),
(58, "other operating costs", "其他營業成本", "其他营业成本"),
(61, "selling expenses", "推銷費用", "推销费用"),
(62, "general & administrative expenses", "管理及總務費用",
"管理及总务费用"),
(63, "research and development expenses", "研究發展費用",
"研究发展费用"),
(71, "non-operating revenue", "營業外收入", "营业外收入"),
(72, "non-operating revenue", "營業外收入", "营业外收入"),
(73, "non-operating revenue", "營業外收入", "营业外收入"),
(74, "non-operating revenue", "營業外收入", "营业外收入"),
(75, "non-operating expenses", "營業外費用", "营业外费用"),
(76, "non-operating expenses", "營業外費用", "营业外费用"),
(77, "non-operating expenses", "營業外費用", "营业外费用"),
(78, "non-operating expenses", "營業外費用", "营业外费用"),
(81, "income tax expense (or benefit)", "所得稅費用(或利益)",
"所得税费用(或利益)"),
(91, "gain (loss) from discontinued operations", "停業部門損益",
"停业部门损益"),
(92, "extraordinary gain or loss", "非常損益", "非常损益"),
(93, "cumulative effect of changes in accounting principles",
"會計原則變動累積影響數", "会计原则变动累积影响数"),
(94, "minority interest income", "少數股權淨利", "少数股权净利"),
(111, "cash and cash equivalents", "現金及約當現金",
"现金及约当现金"),
(112, "short-term investments", "短期投資", "短期投资"),
(113, "notes receivable", "應收票據", "应收票据"),
(114, "accounts receivable", "應收帳款", "应收帐款"),
(118, "other receivables", "其他應收款", "其他应收款"),
(121, "inventories", "存貨", "存货"),
(122, "inventories", "存貨", "存货"),
(125, "prepaid expenses", "預付費用", "预付费用"),
(126, "prepayments", "預付款項", "预付款项"),
(128, "other current assets", "其他流動資產", "其他流动资产"),
(129, "other current assets", "其他流動資產", "其他流动资产"),
(131, "funds", "基金", "基金"),
(132, "long-term investments", "長期投資", "长期投资"),
(141, "land", "土地", "土地"),
(142, "land improvements", "土地改良物", "土地改良物"),
(143, "buildings", "房屋及建物", "房屋及建物"),
(144, "machinery and equipment", "機(器)具及設備",
"机(器)具及设备"),
(145, "machinery and equipment", "機(器)具及設備",
"机(器)具及设备"),
(146, "machinery and equipment", "機(器)具及設備",
"机(器)具及设备"),
(151, "leased assets", "租賃資產", "租赁资产"),
(152, "leasehold improvements", "租賃權益改良", "租赁权益改良"),
(156, "construction in progress and prepayments for equipment",
"未完工程及預付購置設備款", "未完工程及预付购置设备款"),
(158, "miscellaneous property, plant, and equipment",
"雜項固定資產", "杂项固定资产"),
(161, "depletable assets", "遞耗資產", "递耗资产"),
(171, "trademarks", "商標權", "商标权"),
(172, "patents", "專利權", "专利权"),
(173, "franchise", "特許權", "特许权"),
(174, "copyright", "著作權", "著作权"),
(175, "computer software", "電腦軟體", "电脑软体"),
(176, "goodwill", "商譽", "商誉"),
(177, "organization costs", "開辦費", "开办费"),
(178, "other intangibles", "其他無形資產", "其他无形资产"),
(181, "deferred assets", "遞延資產", "递延资产"),
(182, "idle assets", "閒置資產", "闲置资产"),
(184, "long-term notes , accounts and overdue receivables",
"長期應收票據及款項與催收帳款", "长期应收票据及款项与催收帐款"),
(185, "assets leased to others", "出租資產", "出租资产"),
(186, "refundable deposit", "存出保證金", "存出保证金"),
(188, "miscellaneous assets", "雜項資產", "杂项资产"),
(211, "short-term borrowings (debt)", "短期借款", "短期借款"),
(212, "short-term notes and bills payable", "應付短期票券",
"应付短期票券"),
(213, "notes payable", "應付票據", "应付票据"),
(214, "accounts pay able", "應付帳款", "应付帐款"),
(216, "income taxes payable", "應付所得稅", "应付所得税"),
(217, "accrued expenses", "應付費用", "应付费用"),
(218, "other payables", "其他應付款", "其他应付款"),
(219, "other payables", "其他應付款", "其他应付款"),
(226, "advance receipts", "預收款項", "预收款项"),
(227, "long-term liabilities -current portion",
"一年或一營業週期內到期長期負債", "一年或一营业周期内到期长期负债"),
(228, "other current liabilities", "其他流動負債",
"其他流动负债"),
(229, "other current liabilities", "其他流動負債",
"其他流动负债"),
(231, "corporate bonds payable", "應付公司債", "应付公司债"),
(232, "long-term loans payable", "長期借款", "长期借款"),
(233, "long-term notes and accounts payable",
"長期應付票據及款項", "长期应付票据及款项"),
(234, "accrued liabilities for land value increment tax",
"估計應付土地增值稅", "估计应付土地增值税"),
(235, "accrued pension liabilities", "應計退休金負債",
"应计退休金负债"),
(238, "other long-term liabilities", "其他長期負債",
"其他长期负债"),
(281, "deferred liabilities", "遞延負債", "递延负债"),
(286, "deposits received", "存入保證金", "存入保证金"),
(288, "miscellaneous liabilities", "雜項負債", "杂项负债"),
(311, "capital", "資本(或股本)", "资本(或股本)"),
(321, "paid-in capital in excess of par", "股票溢價", "股票溢价"),
(323, "capital surplus from assets revaluation",
"資產重估增值準備", "资产重估增值准备"),
(324, "capital surplus from gain on disposal of assets",
"處分資產溢價公積", "处分资产溢价公积"),
(325, "capital surplus from business combination", "合併公積",
"合并公积"),
(326, "donated surplus", "受贈公積", "受赠公积"),
(328, "other additional paid-in capital", "其他資本公積",
"其他资本公积"),
(331, "legal reserve", "法定盈餘公積", "法定盈余公积"),
(332, "special reserve", "特別盈餘公積", "特别盈余公积"),
(335,
"retained earnings-unappropriated (or accumulated deficit)",
"未分配盈餘(或累積虧損)", "未分配盈余(或累积亏损)"),
(341,
("unrealized loss on market value decline"
" of long-term equity investments"),
"長期股權投資未實現跌價損失", "长期股权投资未实现跌价损失"),
(342, "cumulative translation adjustment", "累積換算調整數",
"累积换算调整数"),
(343, "net loss not recognized as pension cost",
"未認列為退休金成本之淨損失", "未认列为退休金成本之净损失"),
(351, "treasury stock", "庫藏股", "库藏股"),
(361, "minority interest", "少數股權", "少数股权"),
(411, "sales revenue", "銷貨收入", "销货收入"),
(417, "sales return", "銷貨退回", "销货退回"),
(419, "sales allowances", "銷貨折讓", "销货折让"),
(461, "service revenue", "勞務收入", "劳务收入"),
(471, "agency revenue", "業務收入", "业务收入"),
(488, "other operating revenue", "其他營業收入—其他",
"其他营业收入—其他"),
(511, "cost of goods sold", "銷貨成本", "销货成本"),
(512, "purchases", "進貨", "进货"),
(513, "materials purchased", "進料", "进料"),
(514, "direct labor", "直接人工", "直接人工"),
(515, "manufacturing overhead", "製造費用", "制造费用"),
(516, "manufacturing overhead", "製造費用", "制造费用"),
(517, "manufacturing overhead", "製造費用", "制造费用"),
(518, "manufacturing overhead", "製造費用", "制造费用"),
(561, "service costs", "勞務成本", "劳务成本"),
(571, "agency costs", "業務成本", "业务成本"),
(588, "other operating costs-other", "其他營業成本—其他",
"其他营业成本—其他"),
(615, "selling expenses", "推銷費用", "推销费用"),
(616, "selling expenses", "推銷費用", "推销费用"),
(617, "selling expenses", "推銷費用", "推销费用"),
(618, "selling expenses", "推銷費用", "推销费用"),
(625, "general & administrative expenses", "管理及總務費用",
"管理及总务费用"),
(626, "general & administrative expenses", "管理及總務費用",
"管理及总务费用"),
(627, "general & administrative expenses", "管理及總務費用",
"管理及总务费用"),
(628, "general & administrative expenses", "管理及總務費用",
"管理及总务费用"),
(635, "research and development expenses", "研究發展費用",
"研究发展费用"),
(636, "research and development expenses", "研究發展費用",
"研究发展费用"),
(637, "research and development expenses", "研究發展費用",
"研究发展费用"),
(638, "research and development expenses", "研究發展費用",
"研究发展费用"),
(711, "interest revenue", "利息收入", "利息收入"),
(712, "investment income", "投資收益", "投资收益"),
(713, "foreign exchange gain", "兌換利益", "兑换利益"),
(714, "gain on disposal of investments", "處分投資收益",
"处分投资收益"),
(715, "gain on disposal of assets", "處分資產溢價收入",
"处分资产溢价收入"),
(748, "other non-operating revenue", "其他營業外收入",
"其他营业外收入"),
(751, "interest expense", "利息費用", "利息费用"),
(752, "investment loss", "投資損失", "投资损失"),
(753, "foreign exchange loss", "兌換損失", "兑换损失"),
(754, "loss on disposal of investments", "處分投資損失",
"处分投资损失"),
(755, "loss on disposal of assets", "處分資產損失",
"处分资产损失"),
(788, "other non-operating expenses", "其他營業外費用",
"其他营业外费用"),
(811, "income tax expense (or benefit)", "所得稅費用(或利益)",
"所得税费用(或利益)"),
(911, "income (loss) from operations of discontinued segments",
"停業部門損益—停業前營業損益", "停业部门损益—停业前营业损益"),
(912, "gain (loss) from disposal of discontinued segments",
"停業部門損益—處分損益", "停业部门损益—处分损益"),
(921, "extraordinary gain or loss", "非常損益", "非常损益"),
(931, "cumulative effect of changes in accounting principles",
"會計原則變動累積影響數", "会计原则变动累积影响数"),
(941, "minority interest income", "少數股權淨利", "少数股权净利"),
(1111, "cash on hand", "庫存現金", "库存现金"),
(1112, "petty cash/revolving funds", "零用金/週轉金",
"零用金/周转金"),
(1113, "cash in banks", "銀行存款", "银行存款"),
(1116, "cash in transit", "在途現金", "在途现金"),
(1117, "cash equivalents", "約當現金", "约当现金"),
(1118, "other cash and cash equivalents", "其他現金及約當現金",
"其他现金及约当现金"),
(1121, "short-term investments stock", "短期投資—股票",
"短期投资—股票"),
(1122, "short-term investments short-term notes and bills",
"短期投資—短期票券", "短期投资—短期票券"),
(1123, "short-term investments government bonds",
"短期投資—政府債券", "短期投资—政府债券"),
(1124, "short-term investments beneficiary certificates",
"短期投資—受益憑證", "短期投资—受益凭证"),
(1125, "short-term investments corporate bonds",
"短期投資—公司債", "短期投资—公司债"),
(1128, "short-term investments other", "短期投資—其他",
"短期投资—其他"),
(1129,
"allowance for reduction of short-term investment to market",
"備抵短期投資跌價損失", "备抵短期投资跌价损失"),
(1131, "notes receivable", "應收票據", "应收票据"),
(1132, "discounted notes receivable", "應收票據貼現",
"应收票据贴现"),
(1137, "notes receivable related parties", "應收票據—關係人",
"应收票据—关系人"),
(1138, "other notes receivable", "其他應收票據", "其他应收票据"),
(1139,
"allowance for uncollectible accounts notes receivable",
"備抵呆帳-應收票據", "备抵呆帐-应收票据"),
(1141, "accounts receivable", "應收帳款", "应收帐款"),
(1142, "installment accounts receivable", "應收分期帳款",
"应收分期帐款"),
(1147, "accounts receivable related parties",
"應收帳款—關係人", "应收帐款—关系人"),
(1149,
"allowance for uncollectible accounts accounts receivable",
"備抵呆帳-應收帳款", "备抵呆帐-应收帐款"),
(1181, "forward exchange contract receivable", "應收出售遠匯款",
"应收出售远汇款"),
(1182,
"forward exchange contract receivable foreign currencies",
"應收遠匯款—外幣", "应收远汇款—外币"),
(1183, "discount on forward ex-change contract", "買賣遠匯折價",
"买卖远汇折价"),
(1184, "earned revenue receivable", "應收收益", "应收收益"),
(1185, "income tax refund receivable", "應收退稅款",
"应收退税款"),
(1187, "other receivables related parties",
"其他應收款—關係人", "其他应收款—关系人"),
(1188, "other receivables other", "其他應收款—其他",
"其他应收款—其他"),
(1189,
"allowance for uncollectible accounts other receivables",
"備抵呆帳—其他應收款", "备抵呆帐—其他应收款"),
(1211, "merchandise inventory", "商品存貨", "商品存货"),
(1212, "consigned goods", "寄銷商品", "寄销商品"),
(1213, "goods in transit", "在途商品", "在途商品"),
(1219, "allowance for reduction of inventory to market",
"備抵存貨跌價損失", "备抵存货跌价损失"),
(1221, "finished goods", "製成品", "制成品"),
(1222, "consigned finished goods", "寄銷製成品", "寄销制成品"),
(1223, "by-products", "副產品", "副产品"),
(1224, "work in process", "在製品", "在制品"),
(1225, "work in process outsourced", "委外加工", "委外加工"),
(1226, "raw materials", "原料", "原料"),
(1227, "supplies", "物料", "物料"),
(1228, "materials and supplies in transit", "在途原物料",
"在途原物料"),
(1229, "allowance for reduction of inventory to market",
"備抵存貨跌價損失", "备抵存货跌价损失"),
(1251, "prepaid payroll", "預付薪資", "预付薪资"),
(1252, "prepaid rents", "預付租金", "预付租金"),
(1253, "prepaid insurance", "預付保險費", "预付保险费"),
(1254, "office supplies", "用品盤存", "用品盘存"),
(1255, "prepaid income tax", "預付所得稅", "预付所得税"),
(1258, "other prepaid expenses", "其他預付費用", "其他预付费用"),
(1261, "prepayment for purchases", "預付貨款", "预付货款"),
(1268, "other prepayments", "其他預付款項", "其他预付款项"),
(1281, "VAT paid ( or input tax)", "進項稅額", "进项税额"),
(1282, "excess VAT paid (or overpaid VAT)", "留抵稅額",
"留抵税额"),
(1283, "temporary payments", "暫付款", "暂付款"),
(1284, "payment on behalf of others", "代付款", "代付款"),
(1285, "advances to employees", "員工借支", "员工借支"),
(1286, "refundable deposits", "存出保證金", "存出保证金"),
(1287, "certificate of deposit-restricted", "受限制存款",
"受限制存款"),
(1291, "deferred income tax assets", "遞延所得稅資產",
"递延所得税资产"),
(1292, "deferred foreign exchange losses", "遞延兌換損失",
"递延兑换损失"),
(1293, "owners (stockholders) current account",
"業主(股東)往來", "业主(股东)往来"),
(1294, "current account with others", "同業往來", "同业往来"),
(1298, "other current assets other", "其他流動資產—其他",
"其他流动资产—其他"),
(1311, "redemption fund (or sinking fund)", "償債基金",
"偿债基金"),
(1312, "fund for improvement and expansion", "改良及擴充基金",
"改良及扩充基金"),
(1313, "contingency fund", "意外損失準備基金", "意外损失准备基金"),
(1314, "pension fund", "退休基金", "退休基金"),
(1318, "other funds", "其他基金", "其他基金"),
(1321, "long-term equity investments", "長期股權投資",
"长期股权投资"),
(1322, "long-term bond investments", "長期債券投資",
"长期债券投资"),
(1323, "long-term real estate in-vestments", "長期不動產投資",
"长期不动产投资"),
(1324, "cash surrender value of life insurance",
"人壽保險現金解約價值", "人寿保险现金解约价值"),
(1328, "other long-term investments", "其他長期投資",
"其他长期投资"),
(1329,
("allowance for excess of cost over market value"
" of long-term investments"),
"備抵長期投資跌價損失", "备抵长期投资跌价损失"),
(1411, "land", "土地", "土地"),
(1418, "land revaluation increments", "土地—重估增值",
"土地—重估增值"),
(1421, "land improvements", "土地改良物", "土地改良物"),
(1428, "land improvements revaluation increments",
"土地改良物—重估增值", "土地改良物—重估增值"),
(1429, "accumulated depreciation land improvements",
"累積折舊—土地改良物", "累积折旧—土地改良物"),
(1431, "buildings", "房屋及建物", "房屋及建物"),
(1438, "buildings revaluation increments",
"房屋及建物—重估增值", "房屋及建物—重估增值"),
(1439, "accumulated depreciation buildings",
"累積折舊—房屋及建物", "累积折旧—房屋及建物"),
(1441, "machinery", "機(器)具", "机(器)具"),
(1448, "machinery revaluation increments", "機(器)具—重估增值",
"机(器)具—重估增值"),
(1449, "accumulated depreciation machinery",
"累積折舊—機(器)具", "累积折旧—机(器)具"),
(1511, "leased assets", "租賃資產", "租赁资产"),
(1519, "accumulated depreciation leased assets",
"累積折舊—租賃資產", "累积折旧—租赁资产"),
(1521, "leasehold improvements", "租賃權益改良", "租赁权益改良"),
(1529, "accumulated depreciation leasehold improvements",
"累積折舊—租賃權益改良", "累积折旧—租赁权益改良"),
(1561, "construction in progress", "未完工程", "未完工程"),
(1562, "prepayment for equipment", "預付購置設備款",
"预付购置设备款"),
(1581, "miscellaneous property, plant, and equipment",
"雜項固定資產", "杂项固定资产"),
(1588,
("miscellaneous property, plant, and equipment"
" revaluation increments"),
"雜項固定資產—重估增值", "杂项固定资产—重估增值"),
(1589,
("accumulated depreciation"
" miscellaneous property, plant, and equipment"),
"累積折舊—雜項固定資產", "累积折旧—杂项固定资产"),
(1611, "natural resources", "天然資源", "天然资源"),
(1618, "natural resources revaluation increments",
"天然資源—重估增值", "天然资源—重估增值"),
(1619, "accumulated depletion natural resources",
"累積折耗—天然資源", "累积折耗—天然资源"),
(1711, "trademarks", "商標權", "商标权"),
(1721, "patents", "專利權", "专利权"),
(1731, "franchise", "特許權", "特许权"),
(1741, "copyright", "著作權", "著作权"),
(1751, "computer software cost", "電腦軟體", "电脑软体"),
(1761, "goodwill", "商譽", "商誉"),
(1771, "organization costs", "開辦費", "开办费"),
(1781, "deferred pension costs", "遞延退休金成本",
"递延退休金成本"),
(1782, "leasehold improvements", "租賃權益改良", "租赁权益改良"),
(1788, "other intangible assets other", "其他無形資產—其他",
"其他无形资产—其他"),
(1811, "deferred bond issuance costs", "債券發行成本",
"债券发行成本"),
(1812, "long-term prepaid rent", "長期預付租金", "长期预付租金"),
(1813, "long-term prepaid insurance", "長期預付保險費",
"长期预付保险费"),
(1814, "deferred income tax assets", "遞延所得稅資產",
"递延所得税资产"),
(1815, "prepaid pension cost", "預付退休金", "预付退休金"),
(1818, "other deferred assets", "其他遞延資產", "其他递延资产"),
(1821, "idle assets", "閒置資產", "闲置资产"),
(1841, "long-term notes receivable", "長期應收票據",
"长期应收票据"),
(1842, "long-term accounts receivable", "長期應收帳款",
"长期应收帐款"),
(1843, "overdue receivables", "催收帳款", "催收帐款"),
(1847,
("long-term notes, accounts and overdue receivables"
" related parties"),
"長期應收票據及款項與催收帳款—關係人",
"长期应收票据及款项与催收帐款—关系人"),
(1848, "other long-term receivables", "其他長期應收款項",
"其他长期应收款项"),
(1849,
("allowance for uncollectible accounts"
" long-term notes, accounts and overdue receivables"),
"備抵呆帳—長期應收票據及款項與催收帳款",
"备抵呆帐—长期应收票据及款项与催收帐款"),
(1851, "assets leased to others", "出租資產", "出租资产"),
(1858,
("assets leased to others"
" incremental value from revaluation"),
"出租資產—重估增值", "出租资产—重估增值"),
(1859, "accumulated depreciation assets leased to others",
"累積折舊—出租資產", "累积折旧—出租资产"),
(1861, "refundable deposits", "存出保證金", "存出保证金"),
(1881, "certificate of deposit restricted", "受限制存款",
"受限制存款"),
(1888, "miscellaneous assets other", "雜項資產—其他",
"杂项资产—其他"),
(2111, "bank overdraft", "銀行透支", "银行透支"),
(2112, "bank loan", "銀行借款", "银行借款"),
(2114, "short-term borrowings owners", "短期借款—業主",
"短期借款—业主"),
(2115, "short-term borrowings employees", "短期借款—員工",
"短期借款—员工"),
(2117, "short-term borrowings related parties",
"短期借款—關係人", "短期借款—关系人"),
(2118, "short-term borrowings other", "短期借款—其他",
"短期借款—其他"),
(2121, "commercial paper payable", "應付商業本票",
"应付商业本票"),
(2122, "bank acceptance", "銀行承兌匯票", "银行承兑汇票"),
(2128, "other short-term notes and bills payable",
"其他應付短期票券",
"其他应付短期票券"),
(2129, "discount on short-term notes and bills payable",
"應付短期票券折價", "应付短期票券折价"),
(2131, "notes payable", "應付票據", "应付票据"),
(2137, "notes payable related parties", "應付票據—關係人",
"应付票据—关系人"),
(2138, "other notes payable", "其他應付票據", "其他应付票据"),
(2141, "accounts payable", "應付帳款", "应付帐款"),
(2147, "accounts payable related parties", "應付帳款—關係人",
"应付帐款—关系人"),
(2161, "income tax payable", "應付所得稅", "应付所得税"),
(2171, "accrued payroll", "應付薪工", "应付薪工"),
(2172, "accrued rent payable", "應付租金", "应付租金"),
(2173, "accrued interest payable", "應付利息", "应付利息"),
(2174, "accrued VAT payable", "應付營業稅", "应付营业税"),
(2175, "accrued taxes payable other", "應付稅捐—其他",
"应付税捐—其他"),
(2178, "other accrued expenses payable", "其他應付費用",
"其他应付费用"),
(2181, "forward exchange contract payable", "應付購入遠匯款",
"应付购入远汇款"),
(2182,
"forward exchange contract payable foreign currencies",
"應付遠匯款—外幣", "应付远汇款—外币"),
(2183, "premium on forward exchange contract", "買賣遠匯溢價",
"买卖远汇溢价"),
(2184, "payables on land and building purchased",
"應付土地房屋款", "应付土地房屋款"),
(2185, "Payables on equipment", "應付設備款", "应付设备款"),
(2187, "other payables related parties", "其他應付款—關係人",
"其他应付款—关系人"),
(2191, "dividend payable", "應付股利", "应付股利"),
(2192, "bonus payable", "應付紅利", "应付红利"),
(2193, "compensation payable to directors and supervisors",
"應付董監事酬勞", "应付董监事酬劳"),
(2198, "other payables other", "其他應付款—其他",
"其他应付款—其他"),
(2261, "sales revenue received in advance", "預收貨款",
"预收货款"),
(2262, "revenue received in advance", "預收收入", "预收收入"),
(2268, "other advance receipts", "其他預收款", "其他预收款"),
(2271, "corporate bonds payable current portion",
"一年或一營業週期內到期公司債", "一年或一营业周期内到期公司债"),
(2272, "long-term loans payable current portion",
"一年或一營業週期內到期長期借款", "一年或一营业周期内到期长期借款"),
(2273,
("long-term notes and accounts payable due"
" within one year or one operating cycle"),
"一年或一營業週期內到期長期應付票據及款項",
"一年或一营业周期内到期长期应付票据及款项"),
(2277,
("long-term notes and accounts payables to related parties"
" current portion"),
"一年或一營業週期內到期長期應付票據及款項—關係人",
"一年或一营业周期内到期长期应付票据及款项—关系人"),
(2278, "other long-term liabilities current portion",
"其他一年或一營業週期內到期長期負債",
"其他一年或一营业周期内到期长期负债"),
(2281, "VAT received (or output tax)", "銷項稅額", "销项税额"),
(2283, "temporary receipts", "暫收款", "暂收款"),
(2284, "receipts under custody", "代收款", "代收款"),
(2285, "estimated warranty liabilities", "估計售後服務/保固負債",
"估计售后服务/保固负债"),
(2291, "deferred income tax liabilities", "遞延所得稅負債",
"递延所得税负债"),
(2292, "deferred foreign exchange gain", "遞延兌換利益",
"递延兑换利益"),
(2293, "owners current account", "業主(股東)往來",
"业主(股东)往来"),
(2294, "current account with others", "同業往來", "同业往来"),
(2298, "other current liabilities others", "其他流動負債—其他",
"其他流动负债—其他"),
(2311, "corporate bonds payable", "應付公司債", "应付公司债"),
(2319, "premium (discount) on corporate bonds payable",
"應付公司債溢(折)價", "应付公司债溢(折)价"),
(2321, "long-term loans payable bank", "長期銀行借款",
"长期银行借款"),
(2324, "long-term loans payable owners", "長期借款—業主",
"长期借款—业主"),
(2325, "long-term loans payable employees", "長期借款—員工",
"长期借款—员工"),
(2327, "long-term loans payable related parties",
"長期借款—關係人", "长期借款—关系人"),
(2328, "long-term loans payable other", "長期借款—其他",
"长期借款—其他"),
(2331, "long-term notes payable", "長期應付票據", "长期应付票据"),
(2332, "long-term accounts pay-able", "長期應付帳款",
"长期应付帐款"),
(2333, "long-term capital lease liabilities", "長期應付租賃負債",
"长期应付租赁负债"),
(2337,
"Long-term notes and accounts payable related parties",
"長期應付票據及款項—關係人", "长期应付票据及款项—关系人"),
(2338, "other long-term payables", "其他長期應付款項",
"其他长期应付款项"),
(2341, "estimated accrued land value incremental tax pay-able",
"估計應付土地增值稅", "估计应付土地增值税"),
(2351, "accrued pension liabilities", "應計退休金負債",
"应计退休金负债"),
(2388, "other long-term liabilities other",
"其他長期負債—其他", "其他长期负债—其他"),
(2811, "deferred revenue", "遞延收入", "递延收入"),
(2814, "deferred income tax liabilities", "遞延所得稅負債",
"递延所得税负债"),
(2818, "other deferred liabilities", "其他遞延負債",
"其他递延负债"),
(2861, "guarantee deposit received", "存入保證金", "存入保证金"),
(2888, "miscellaneous liabilities other", "雜項負債—其他",
"杂项负债—其他"),
(3111, "capital common stock", "普通股股本", "普通股股本"),
(3112, "capital preferred stock", "特別股股本", "特别股股本"),
(3113, "capital collected in advance", "預收股本", "预收股本"),
(3114, "stock dividends to be distributed", "待分配股票股利",
"待分配股票股利"),
(3115, "capital", "資本", "资本"),
(3211, "paid-in capital in excess of par- common stock",
"普通股股票溢價", "普通股股票溢价"),
(3212, "paid-in capital in excess of par- preferred stock",
"特別股股票溢價", "特别股股票溢价"),
(3231, "capital surplus from assets revaluation",
"資產重估增值準備", "资产重估增值准备"),
(3241, "capital surplus from gain on disposal of assets",
"處分資產溢價公積", "处分资产溢价公积"),
(3251, "capital surplus from business combination", "合併公積",
"合并公积"),
(3261, "donated surplus", "受贈公積", "受赠公积"),
(3281,
("additional paid-in capital from investee"
" under equity method"),
"權益法長期股權投資資本公積", "权益法长期股权投资资本公积"),
(3282,
"additional paid-in capital treasury stock trans-actions",
"資本公積—庫藏股票交易", "资本公积—库藏股票交易"),
(3311, "legal reserve", "法定盈餘公積", "法定盈余公积"),
(3321, "contingency reserve", "意外損失準備", "意外损失准备"),
(3322, "improvement and expansion reserve", "改良擴充準備",
"改良扩充准备"),
(3323, "special reserve for redemption of liabilities",
"償債準備", "偿债准备"),
(3328, "other special reserve", "其他特別盈餘公積",
"其他特别盈余公积"),
(3351, "accumulated profit or loss", "累積盈虧",
"累积盈亏"),
(3352, "prior period adjustments", "前期損益調整",
"前期损益调整"),
(3353, "net income or loss for current period", "本期損益",
"本期损益"),
(3411,
("unrealized loss on market value decline"
" of long-term equity investments"),
"長期股權投資未實現跌價損失", "长期股权投资未实现跌价损失"),
(3421, "cumulative translation adjustments", "累積換算調整數",
"累积换算调整数"),
(3431, "net loss not recognized as pension costs",
"未認列為退休金成本之淨損失", "未认列为退休金成本之净损失"),
(3511, "treasury stock", "庫藏股", "库藏股"),
(3611, "minority interest", "少數股權", "少数股权"),
(4111, "sales revenue", "銷貨收入", "销货收入"),
(4112, "installment sales revenue", "分期付款銷貨收入",
"分期付款销货收入"),
(4171, "sales return", "銷貨退回", "销货退回"),
(4191, "sales discounts and allowances", "銷貨折讓", "销货折让"),
(4611, "service revenue", "勞務收入", "劳务收入"),
(4711, "agency revenue", "業務收入", "业务收入"),
(4888, "other operating revenue other", "其他營業收入—其他",
"其他营业收入—其他"),
(5111, "cost of goods sold", "銷貨成本", "销货成本"),
(5112, "installment cost of goods sold", "分期付款銷貨成本",
"分期付款销货成本"),
(5121, "purchases", "進貨", "进货"),
(5122, "purchase expenses", "進貨費用", "进货费用"),
(5123, "purchase returns", "進貨退出", "进货退出"),
(5124, "charges on purchased merchandise", "進貨折讓",
"进货折让"),
(5131, "material purchased", "進料", "进料"),
(5132, "charges on purchased material", "進料費用", "进料费用"),
(5133, "material purchase returns", "進料退出", "进料退出"),
(5134, "material purchase allowances", "進料折讓", "进料折让"),
(5141, "direct labor", "直接人工", "直接人工"),
(5151, "indirect labor", "間接人工", "间接人工"),
(5152, "rent expense, rent", "租金支出", "租金支出"),
(5153, "office supplies (expense)", "文具用品", "文具用品"),
(5154, "travelling expense, travel", "旅費", "旅费"),
(5155, "shipping expenses, freight", "運費", "运费"),
(5156, "postage (expenses)", "郵電費", "邮电费"),
(5157, "repair (s) and maintenance (expense )", "修繕費",
"修缮费"),
(5158, "packing expenses", "包裝費", "包装费"),
(5161, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
(5162, "insurance (expense)", "保險費", "保险费"),
(5163, "manufacturing overhead outsourced", "加工費",
"加工费"),
(5166, "taxes", "稅捐", "税捐"),
(5168, "depreciation expense", "折舊", "折旧"),
(5169, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
(5172, "meal (expenses)", "伙食費", "伙食费"),
(5173, "employee benefits/welfare", "職工福利", "职工福利"),
(5176, "training (expense)", "訓練費", "训练费"),
(5177, "indirect materials", "間接材料", "间接材料"),
(5188, "other manufacturing expenses", "其他製造費用",
"其他制造费用"),
(5611, "service costs", "勞務成本", "劳务成本"),
(5711, "agency costs", "業務成本", "业务成本"),
(5888, "other operating costs other", "其他營業成本—其他",
"其他营业成本—其他"),
(6151, "payroll expense", "薪資支出", "薪资支出"),
(6152, "rent expense, rent", "租金支出", "租金支出"),
(6153, "office supplies (expense)", "文具用品", "文具用品"),
(6154, "travelling expense, travel", "旅費", "旅费"),
(6155, "shipping expenses, freight", "運費", "运费"),
(6156, "postage (expenses)", "郵電費", "邮电费"),
(6157, "repair (s) and maintenance (expense)", "修繕費",
"修缮费"),
(6159, "advertisement expense, advertisement", "廣告費",
"广告费"),
(6161, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
(6162, "insurance (expense)", "保險費", "保险费"),
(6164, "entertainment (expense)", "交際費", "交际费"),
(6165, "donation (expense)", "捐贈", "捐赠"),
(6166, "taxes", "稅捐", "税捐"),
(6167, "loss on uncollectible accounts", "呆帳損失", "呆帐损失"),
(6168, "depreciation expense", "折舊", "折旧"),
(6169, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
(6172, "meal (expenses)", "伙食費", "伙食费"),
(6173, "employee benefits/welfare", "職工福利", "职工福利"),
(6175, "commission (expense)", "佣金支出", "佣金支出"),
(6176, "training (expense)", "訓練費", "训练费"),
(6188, "other selling expenses", "其他推銷費用", "其他推销费用"),
(6251, "payroll expense", "薪資支出", "薪资支出"),
(6252, "rent expense, rent", "租金支出", "租金支出"),
(6253, "office supplies", "文具用品", "文具用品"),
(6254, "travelling expense, travel", "旅費", "旅费"),
(6255, "shipping expenses,freight", "運費", "运费"),
(6256, "postage (expenses)", "郵電費", "邮电费"),
(6257, "repair (s) and maintenance (expense)", "修繕費",
"修缮费"),
(6259, "advertisement expense, advertisement", "廣告費",
"广告费"),
(6261, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
(6262, "insurance (expense)", "保險費", "保险费"),
(6264, "entertainment (expense)", "交際費", "交际费"),
(6265, "donation (expense)", "捐贈", "捐赠"),
(6266, "taxes", "稅捐", "税捐"),
(6267, "loss on uncollectible accounts", "呆帳損失", "呆帐损失"),
(6268, "depreciation expense", "折舊", "折旧"),
(6269, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
(6271, "loss on export sales", "外銷損失", "外销损失"),
(6272, "meal (expenses)", "伙食費", "伙食费"),
(6273, "employee benefits/welfare", "職工福利", "职工福利"),
(6274, "research and development expense", "研究發展費用",
"研究发展费用"),
(6275, "commission (expense)", "佣金支出", "佣金支出"),
(6276, "training (expense)", "訓練費", "训练费"),
(6278, "professional service fees", "勞務費", "劳务费"),
(6288, "other general and administrative expenses",
"其他管理及總務費用", "其他管理及总务费用"),
(6351, "payroll expense", "薪資支出", "薪资支出"),
(6352, "rent expense, rent", "租金支出", "租金支出"),
(6353, "office supplies", "文具用品", "文具用品"),
(6354, "travelling expense, travel", "旅費", "旅费"),
(6355, "shipping expenses, freight", "運費", "运费"),
(6356, "postage (expenses)", "郵電費", "邮电费"),
(6357, "repair (s) and maintenance (expense)", "修繕費",
"修缮费"),
(6361, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
(6362, "insurance (expense)", "保險費", "保险费"),
(6364, "entertainment (expense)", "交際費", "交际费"),
(6366, "taxes", "稅捐", "税捐"),
(6368, "depreciation expense", "折舊", "折旧"),
(6369, "various amortization", "各項耗竭及攤提",
"各项耗竭及摊提"),
(6372, "meal (expenses)", "伙食費", "伙食费"),
(6373, "employee benefits/welfare", "職工福利", "职工福利"),
(6376, "training (expense)", "訓練費", "训练费"),
(6378, "other research and development expenses",
"其他研究發展費用", "其他研究发展费用"),
(7111, "interest revenue/income", "利息收入", "利息收入"),
(7121, "investment income recognized under equity method",
"權益法認列之投資收益", "权益法认列之投资收益"),
(7122, "dividends income", "股利收入", "股利收入"),
(7123,
"gain on market price recovery of short-term investment",
"短期投資市價回升利益", "短期投资市价回升利益"),
(7131, "foreign exchange gain", "兌換利益", "兑换利益"),
(7141, "gain on disposal of investments", "處分投資收益",
"处分投资收益"),
(7151, "gain on disposal of assets", "處分資產溢價收入",
"处分资产溢价收入"),
(7481, "donation income", "捐贈收入", "捐赠收入"),
(7482, "rent revenue/income", "租金收入", "租金收入"),
(7483, "commission revenue/income", "佣金收入", "佣金收入"),
(7484, "revenue from sale of scraps", "出售下腳及廢料收入",
"出售下脚及废料收入"),
(7485, "gain on physical inventory", "存貨盤盈", "存货盘盈"),
(7486, "gain from price recovery of inventory",
"存貨跌價回升利益", "存货跌价回升利益"),
(7487, "gain on reversal of bad debts", "壞帳轉回利益",
"坏帐转回利益"),
(7488, "other non-operating revenue other items",
"其他營業外收入—其他",
"其他营业外收入—其他"),
(7511, "interest expense", "利息費用", "利息费用"),
(7521, "investment loss recognized under equity method",
"權益法認列之投資損失", "权益法认列之投资损失"),
(7523,
("unrealized loss on reduction"
" of short-term investments to market"),
"短期投資未實現跌價損失", "短期投资未实现跌价损失"),
(7531, "foreign exchange loss", "兌換損失", "兑换损失"),
(7541, "loss on disposal of investments", "處分投資損失",
"处分投资损失"),
(7551, "loss on disposal of assets", "處分資產損失",
"处分资产损失"),
(7881, "loss on work stoppages", "停工損失", "停工损失"),
(7882, "casualty loss", "災害損失", "灾害损失"),
(7885, "loss on physical inventory", "存貨盤損", "存货盘损"),
(7886,
("loss for market price decline"
" and obsolete and slow-moving inventories"),
"存貨跌價及呆滯損失", "存货跌价及呆滞损失"),
(7888, "other non-operating expenses other",
"其他營業外費用—其他", "其他营业外费用—其他"),
(8111, "income tax expense ( or benefit)", "所得稅費用(或利益)",
"所得税费用(或利益)"),
(9111, "income (loss) from operations of discontinued segment",
"停業部門損益—停業前營業損益", "停业部门损益—停业前营业损益"),
(9121, "gain (loss) from disposal of discontinued segment",
"停業部門損益—處分損益", "停业部门损益—处分损益"),
(9211, "extraordinary gain or loss", "非常損益", "非常损益"),
(9311, "cumulative effect of changes in accounting principles",
"會計原則變動累積影響數", "会计原则变动累积影响数"),
(9411, "minority interest income", "少數股權淨利",
"少数股权净利"),
])
@staticmethod
def get_user(username_option):
"""Returns the current user.
Args:
username_option: The username specified in the options.
Returns:
The current user.
"""
user_model = get_user_model()
if username_option is not None:
try:
return user_model.objects.get(**{
user_model.USERNAME_FIELD: username_option
})
except ObjectDoesNotExist:
error = F"User \"{username_option}\" does not exist."
raise CommandError(error, returncode=1)
if user_model.objects.count() == 0:
call_command("createsuperuser")
return user_model.objects.first()
if user_model.objects.count() == 1:
return user_model.objects.first()
try:
return user_model.objects.get(**{
user_model.USERNAME_FIELD: getpass.getuser()
})
except ObjectDoesNotExist:
error = "Please specify the user with -u."
raise CommandError(error, returncode=1)

View File

@ -0,0 +1,206 @@
# The accounting application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/7/22
# 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 command to populate the database with sample accounting data.
"""
import datetime
import getpass
import random
from typing import Optional
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.core.management import BaseCommand, CommandParser, CommandError, \
call_command
from django.db import transaction
from django.utils import timezone, formats
from django.utils.translation import gettext as _
from accounting.models import Account, Record
from accounting.utils import DataFiller
class Command(BaseCommand):
"""Populates the database with sample accounting data."""
help = "Fills the database with sample accounting data."
def __init__(self):
super().__init__()
self._filler: Optional[DataFiller] = None
def add_arguments(self, parser):
"""Adds command line arguments to the parser.
Args:
parser (CommandParser): The command line argument parser.
"""
parser.add_argument("--user", "-u", help="User")
def handle(self, *args, **options):
"""Runs the command.
Args:
*args (list[str]): The command line arguments.
**options (dict[str,str]): The command line switches.
"""
if Record.objects.count() > 0:
error = "Refuse to fill in sample data with existing data."
raise CommandError(error, returncode=1)
# Gets the user to use
user = self.get_user(options["user"])
if Account.objects.count() == 0:
username = getattr(user, user.USERNAME_FIELD)
call_command("accounting_accounts", F"-u={username}")
self.stdout.write(F"Filling sample data as \"{user}\"")
with transaction.atomic():
self._filler = DataFiller(user)
self.add_payrolls(5)
self._filler.add_income_transaction(
-15,
[(1113, _("ATM withdrawal"), 2000)])
self._filler.add_transfer_transaction(
-14,
[(6254, _("HSR—New Land→South Lake City"), 1490)],
[(2141, _("HSR—New Land→South Lake City"), 1490)])
self._filler.add_transfer_transaction(
-14,
[(6273, _("Movies—The Avengers"), 80)],
[(2141, _("Movies—The Avengers"), 80)])
self._filler.add_transfer_transaction(
-13,
[(6273, _("Movies—2001: A Space Odyssey"), 80)],
[(2141, _("Movies—2001: A Space Odyssey"), 80)])
self._filler.add_transfer_transaction(
-11,
[(2141, _("Movies—The Avengers"), 80)],
[(1113, _("Movies—The Avengers"), 80)])
self._filler.add_expense_transaction(
-13,
[(6273, _("Bus—2623—Uptown→City Park"), 15.5)])
self._filler.add_expense_transaction(
-2,
[(6272, _("Lunch—Spaghetti"), random.randint(40, 200)),
(6272, _("Drink—Tea"), random.randint(40, 200))])
self._filler.add_expense_transaction(
-1,
([(6272, _("Lunch—Pizza"), random.randint(40, 200)),
(6272, _("Drink—Tea"), random.randint(40, 200))]))
self._filler.add_expense_transaction(
-1,
[(6272, _("Lunch—Spaghetti"), random.randint(40, 200)),
(6272, _("Drink—Soda"), random.randint(40, 200))])
self._filler.add_expense_transaction(
0,
[(6272, _("Lunch—Salad"), random.randint(40, 200)),
(6272, _("Drink—Coffee"), random.randint(40, 200))])
@staticmethod
def get_user(username_option):
"""Returns the current user.
Args:
username_option: The username specified in the options.
Returns:
The current user.
"""
user_model = get_user_model()
if username_option is not None:
try:
return user_model.objects.get(**{
user_model.USERNAME_FIELD: username_option
})
except ObjectDoesNotExist:
error = F"User \"{username_option}\" does not exist."
raise CommandError(error, returncode=1)
if user_model.objects.count() == 0:
call_command("createsuperuser")
return user_model.objects.first()
if user_model.objects.count() == 1:
return user_model.objects.first()
try:
return user_model.objects.get(**{
user_model.USERNAME_FIELD: getpass.getuser()
})
except ObjectDoesNotExist:
error = "Please specify the user with -u."
raise CommandError(error, returncode=1)
def add_payrolls(self, months: int):
"""Adds the payrolls for certain number of months.
Args:
months: The number of months to add.
"""
today = timezone.localdate()
payday = today.replace(day=5)
if payday > today:
payday = self.previous_month(payday)
for i in range(months):
self.add_payroll(payday)
payday = self.previous_month(payday)
@staticmethod
def previous_month(date: datetime.date):
"""Obtain the same day in the previous month.
Args:
date: The date.
Returns:
The same day in the previous month.
"""
month = date.month - 1
if month < 1:
year = date.year - 1
return date.replace(year=year, month=12)
return date.replace(month=month)
def add_payroll(self, payday: datetime.date):
"""Adds the payroll for a payday.
Args:
payday: The payday.
"""
income = random.randint(40000, 50000)
pension = 882 if income <= 40100\
else 924 if income <= 42000\
else 966 if income <= 43900\
else 1008
insurance = 564 if income <= 40100\
else 591 if income <= 42000\
else 618 if income <= 43900\
else 644 if income <= 45800\
else 678 if income <= 48200\
else 712
tax = round(income * 0.05)
savings = income - pension - insurance - tax
previous_month = self.previous_month(payday)
month = formats.date_format(previous_month, format="F")
self._filler.add_transfer_transaction(
payday,
[(1113, _("Payroll Transfer"), savings),
(1314, _("Pension for {month}").format(month=month), pension),
(6262, _("Health insurance for {month}").format(month=month),
insurance),
(1255, _("Income Tax"), tax)],
[(4611, _("Payroll for {month}").format(month=month), income)])

View File

506
src/accounting/models.py Normal file
View File

@ -0,0 +1,506 @@
# The accounting application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/6/29
# 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 data models of the accounting application.
"""
import datetime
import re
from decimal import Decimal
from typing import Dict, List, Optional, Mapping
from dirtyfields import DirtyFieldsMixin
from django.db import models
from django.db.models import Q, Max
from django.http import HttpRequest
from mia_core.models import L10nModel, LocalizedModel, StampedModel, \
RandomPkModel
class Account(DirtyFieldsMixin, LocalizedModel, StampedModel, RandomPkModel):
"""An account."""
parent = models.ForeignKey(
"self", on_delete=models.PROTECT, null=True,
related_name="child_set")
code = models.CharField(max_length=5, unique=True)
title_l10n = models.CharField(max_length=32, db_column="title")
CASH = "1111"
ACCUMULATED_BALANCE = "3351"
NET_CHANGE = "3353"
def __init__(self, *args, **kwargs):
if "title" in kwargs:
self.title = kwargs["title"]
del kwargs["title"]
super().__init__(*args, **kwargs)
self.url = None
self.debit_amount = None
self.credit_amount = None
self.amount = None
self.is_for_debit = None
self.is_for_credit = None
self._is_in_use = None
self._is_parent_and_in_use = None
def __str__(self):
"""Returns the string representation of this account."""
return self.code.__str__() + " " + self.title
def save(self, 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])
super().save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields)
@property
def title(self) -> str:
return self.get_l10n("title")
@title.setter
def title(self, value):
self.set_l10n("title", value)
@property
def option_data(self) -> Dict[str, str]:
"""The data as an option."""
return {
"code": self.code,
"title": self.title,
}
@property
def is_parent_and_in_use(self) -> bool:
"""Whether this is a parent account and is in use."""
if self._is_parent_and_in_use is None:
self._is_parent_and_in_use = self.child_set.count() > 0\
and self.record_set.count() > 0
return self._is_parent_and_in_use
@is_parent_and_in_use.setter
def is_parent_and_in_use(self, value: bool) -> None:
self._is_parent_and_in_use = value
@property
def is_in_use(self) -> bool:
"""Whether this account is in use."""
if self._is_in_use is None:
self._is_in_use = self.child_set.count() > 0\
or self.record_set.count() > 0
return self._is_in_use
@is_in_use.setter
def is_in_use(self, value: bool) -> None:
self._is_in_use = value
class AccountL10n(DirtyFieldsMixin, L10nModel, StampedModel, RandomPkModel):
"""The localization content of an account."""
master = models.ForeignKey(
Account, on_delete=models.CASCADE, related_name="l10n_set")
class Transaction(DirtyFieldsMixin, StampedModel, RandomPkModel):
"""An accounting transaction."""
date = models.DateField()
ord = models.PositiveSmallIntegerField(default=1)
notes = models.CharField(max_length=128, null=True, blank=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._records = None
self._is_balanced = None
self._has_order_hole = None
self.old_date = None
def __str__(self):
"""Returns the string representation of this accounting
transaction."""
return self.date.__str__() + " #" + self.ord.__str__()
def is_dirty(self, check_relationship=False, check_m2m=None) -> bool:
"""Returns whether the data of this transaction is changed and need
to be saved into the database.
Returns:
True if the data of this transaction is changed and need to be
saved into the database, or False otherwise.
"""
if super().is_dirty(check_relationship=check_relationship,
check_m2m=check_m2m):
return True
if len([x for x in self.records
if x.is_dirty(check_relationship=check_relationship,
check_m2m=check_m2m)]) > 0:
return True
kept = [x.pk for x in self.records]
if len([x for x in self.record_set.all() if x.pk not in kept]) > 0:
return True
return False
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
# When the date is changed, the orders of the transactions in the same
# day need to be reordered
txn_to_sort = []
if self.date != self.old_date:
if self.old_date is not None:
txn_same_day = list(
Transaction.objects
.filter(Q(date=self.old_date), ~Q(pk=self.pk))
.order_by("ord"))
for i in range(len(txn_same_day)):
if txn_same_day[i].ord != i + 1:
txn_to_sort.append([txn_same_day[i], i + 1])
max_ord = Transaction.objects\
.filter(date=self.date)\
.aggregate(max=Max("ord"))["max"]
self.ord = 1 if max_ord is None else max_ord + 1
# Collects the records to be deleted
to_keep = [x.pk for x in self.records if x.pk is not None]
to_delete = [] if self.pk is None \
else [x for x in self.record_set.all() if x.pk not in to_keep]
to_save = [x for x in self.records
if x.is_dirty(check_relationship=True)]
for record in to_save:
record.current_user = self.current_user
# Runs the update
super().save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields)
for record in to_delete:
record.delete()
for record in to_save:
record.save(force_insert=force_insert,
force_update=force_update,
using=using, update_fields=update_fields)
for x in txn_to_sort:
Transaction.objects.filter(pk=x[0].pk).update(ord=x[1])
def delete(self, using=None, keep_parents=False):
txn_same_day = list(
Transaction.objects
.filter(Q(date=self.date), ~Q(pk=self.pk))
.order_by("ord"))
txn_to_sort = []
for i in range(len(txn_same_day)):
if txn_same_day[i].ord != i + 1:
txn_to_sort.append([txn_same_day[i], i + 1])
Record.objects.filter(transaction=self).delete()
super().delete(using=using, keep_parents=keep_parents)
for x in txn_to_sort:
Transaction.objects.filter(pk=x[0].pk).update(ord=x[1])
def fill_from_post(self, post: Dict[str, str], request: HttpRequest,
txn_type: str):
"""Fills the transaction from the POST data. The POST data must be
validated and clean at this moment.
Args:
post: The POST data.
request: The request.
txn_type: The transaction type.
"""
self.old_date = self.date
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$", post["date"])
self.date = datetime.date(
int(m.group(1)),
int(m.group(2)),
int(m.group(3)))
self.notes = post.get("notes")
# The records
max_no = self._find_max_record_no(txn_type, post)
records = []
for record_type in max_no.keys():
for i in range(max_no[record_type]):
no = i + 1
if F"{record_type}-{no}-id" in post:
record = Record.objects.get(
pk=post[F"{record_type}-{no}-id"])
else:
record = Record(
is_credit=(record_type == "credit"),
transaction=self)
record.ord = no
record.account = Account.objects.get(
code=post[F"{record_type}-{no}-account"])
if F"{record_type}-{no}-summary" in post:
record.summary = post[F"{record_type}-{no}-summary"]
else:
record.summary = None
record.amount = Decimal(post[F"{record_type}-{no}-amount"])
records.append(record)
if txn_type != "transfer":
if txn_type == "expense":
if len(self.credit_records) > 0:
record = self.credit_records[0]
else:
record = Record(is_credit=True, transaction=self)
else:
if len(self.debit_records) > 0:
record = self.debit_records[0]
else:
record = Record(is_credit=False, transaction=self)
record.ord = 1
record.account = Account.objects.get(code=Account.CASH)
record.summary = None
record.amount = sum([x.amount for x in records])
records.append(record)
self.records = records
self.current_user = request.user
@staticmethod
def _find_max_record_no(txn_type: str,
post: Mapping[str, str]) -> Dict[str, int]:
"""Finds the max debit and record numbers from the POSTed form.
Args:
txn_type: The transaction type.
post: The POSTed data.
Returns:
The max debit and record numbers from the POSTed form.
"""
max_no = {}
if txn_type != "credit":
max_no["debit"] = 0
if txn_type != "debit":
max_no["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 None:
continue
record_type = m.group(1)
if record_type not in max_no:
continue
no = int(m.group(2))
if max_no[record_type] < no:
max_no[record_type] = no
return max_no
@property
def records(self):
"""The records of the transaction.
Returns:
List[Record]: The records.
"""
if self._records is None:
if self.pk is None:
self._records = []
else:
self._records = list(self.record_set.all())
self._records.sort(key=lambda x: (x.is_credit, x.ord))
return self._records
@records.setter
def records(self, value):
self._records = value
@property
def debit_records(self):
"""The debit records of this transaction.
Returns:
List[Record]: The records.
"""
return [x for x in self.records if not x.is_credit]
def debit_total(self) -> Decimal:
"""The total amount of the debit records."""
return sum([x.amount for x in self.debit_records
if isinstance(x.amount, Decimal)])
@property
def debit_summaries(self) -> List[str]:
"""The summaries of the debit records."""
return [x.account.title if x.summary is None else x.summary
for x in self.debit_records]
@property
def credit_records(self):
"""The credit records of this transaction.
Returns:
List[Record]: The records.
"""
return [x for x in self.records if x.is_credit]
def credit_total(self) -> Decimal:
"""The total amount of the credit records."""
return sum([x.amount for x in self.credit_records
if isinstance(x.amount, Decimal)])
@property
def credit_summaries(self) -> List[str]:
"""The summaries of the credit records."""
return [x.account.title if x.summary is None else x.summary
for x in self.credit_records]
@property
def amount(self) -> Decimal:
"""The amount of this transaction."""
return self.debit_total()
@property
def is_balanced(self) -> bool:
"""Whether the sum of the amounts of the debit records is the
same as the sum of the amounts of the credit records. """
if self._is_balanced is None:
debit_sum = sum([x.amount for x in self.debit_records])
credit_sum = sum([x.amount for x in self.credit_records])
self._is_balanced = debit_sum == credit_sum
return self._is_balanced
@is_balanced.setter
def is_balanced(self, value: bool) -> None:
self._is_balanced = value
def has_many_same_day(self) -> bool:
"""whether there are more than one transactions at this day,
so that the user can sort their orders. """
return Transaction.objects.filter(date=self.date).count() > 1
@property
def has_order_hole(self) -> bool:
"""Whether the order of the transactions on this day is not
1, 2, 3, 4, 5..., and should be reordered. """
if self._has_order_hole is None:
orders = [x.ord for x in Transaction.objects.filter(
date=self.date)]
if len(orders) == 0:
self._has_order_hole = False
if max(orders) != len(orders):
self._has_order_hole = True
elif min(orders) != 1:
self._has_order_hole = True
elif len(orders) != len(set(orders)):
self._has_order_hole = True
else:
self._has_order_hole = False
return self._has_order_hole
@has_order_hole.setter
def has_order_hole(self, value: bool) -> None:
self._has_order_hole = value
@property
def is_cash_income(self) -> bool:
"""Whether this transaction is a cash income transaction."""
debit_records = self.debit_records
return (len(debit_records) == 1
and debit_records[0].account.code == Account.CASH
and debit_records[0].summary is None)
@property
def is_cash_expense(self) -> bool:
"""Whether this transaction is a cash expense transaction."""
credit_records = self.credit_records
return (len(credit_records) == 1
and credit_records[0].account.code == Account.CASH
and credit_records[0].summary is None)
@property
def type(self) -> str:
"""The transaction type."""
if self.is_cash_expense:
return "expense"
elif self.is_cash_income:
return "income"
else:
return "transfer"
class Record(DirtyFieldsMixin, StampedModel, RandomPkModel):
"""An accounting record."""
transaction = models.ForeignKey(
Transaction, on_delete=models.CASCADE)
is_credit = models.BooleanField()
ord = models.PositiveSmallIntegerField()
account = models.ForeignKey(
Account, on_delete=models.PROTECT)
summary = models.CharField(max_length=128, blank=True, null=True)
amount = models.DecimalField(max_digits=18, decimal_places=2)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._debit_amount: Optional[Decimal] = None
self._credit_amount: Optional[Decimal] = None
self.balance: Optional[Decimal] = None
self._is_balanced = None
self._has_order_hole = None
self._is_payable = None
self._is_existing_equipment = None
self.is_payable = False
self.is_existing_equipment = False
def __str__(self):
"""Returns the string representation of this accounting
record."""
return "%s %s %s %s" % (
self.transaction.date,
self.account.title,
self.summary,
self.amount)
@property
def debit_amount(self) -> Optional[Decimal]:
"""The debit amount of this accounting record."""
if self._debit_amount is None:
self._debit_amount = self.amount if not self.is_credit else None
return self._debit_amount
@debit_amount.setter
def debit_amount(self, value: Optional[Decimal]) -> None:
self._debit_amount = value
@property
def credit_amount(self) -> Optional[Decimal]:
"""The credit amount of this accounting record."""
if self._credit_amount is None:
self._credit_amount = self.amount if self.is_credit else None
return self._credit_amount
@credit_amount.setter
def credit_amount(self, value: Optional[Decimal]):
self._credit_amount = value
@property
def is_balanced(self) -> bool:
"""Whether the transaction of this record is balanced. """
if self._is_balanced is None:
self._is_balanced = self.transaction.is_balanced
return self._is_balanced
@is_balanced.setter
def is_balanced(self, value: bool) -> None:
self._is_balanced = value
@property
def has_order_hole(self) -> bool:
"""Whether the order of the transactions on this day is not
1, 2, 3, 4, 5..., and should be reordered. """
if self._has_order_hole is None:
self._has_order_hole = self.transaction.has_order_hole
return self._has_order_hole
@has_order_hole.setter
def has_order_hole(self, value: bool) -> None:
self._has_order_hole = value

View File

@ -0,0 +1,223 @@
/* The Mia Website
* report.css: The style sheet for the accounting report
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2019/9/12
*/
/* The report tables */
.table .amount {
text-align: right;
}
.table td.amount {
font-style: italic;
}
.table .actions {
text-align: center;
}
.table .actions .btn {
white-space: nowrap;
}
/* The list view for small screens*/
.account-picker {
height: auto;
max-height: 400px;
overflow-x: hidden;
}
.date-account-line {
font-size: 0.833em;
}
.journal-credit {
padding-left: 1em;
}
/* The general journal tables */
.general-journal-table th, .general-journal-table td {
vertical-align: middle;
height: 50px;
}
/* The report block */
.report-block {
margin: 1em;
background-color: #E9ECEF;
border-radius: 0.3em;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.report-block .table {
background-color: transparent;
}
.report-block h2 {
border-bottom: thick double slategray;
}
.report-block-lg {
padding: 2em 1.5em;
}
.report-block-sm {
padding: 1em 1em;
}
.report-block-lg table th, .report-block-lg table td {
vertical-align: middle;
height: 50px;
}
.report-block-sm .list-group-item {
background-color: transparent;
border: none;
}
/* The trial balance */
.trial-balance-table thead {
font-size: 1.1em;
}
.trial-balance-table tbody {
border-top: thick double slategray;
border-bottom: thick double slategray;
}
.trial-balance-table tfoot {
font-size: 1.1em;
font-weight: bolder;
}
.trial-balance-list .total {
border-top: thick double slategray;
font-weight: bolder;
font-size: 1.1em;
}
/* The income statement */
.income-statement-table thead {
font-size: 1.21em;
}
.income-statement-table tbody {
border-top: thick double slategray;
border-bottom: thick double slategray;
}
.income-statement-table tr {
height: 50px;
}
.income-statement-table td .account {
text-indent: 2em;
}
.income-statement-table tr.section-title {
font-weight: bolder;
font-size: 1.21em;
}
.income-statement-table tr.group-title {
font-weight: bolder;
font-size: 1.1em;
}
.income-statement-table td .group-title {
text-indent: 1em;
}
.income-statement-table .total {
border-top: 1px solid slategray;
font-size: 1.1em;
font-weight: bolder;
}
.income-statement-table .cumulative-total {
font-size: 1.21em;
font-weight: bolder;
}
.income-statement-list .list-group-item {
background-color: transparent;
border: none;
}
.income-statement-list .section-title {
font-weight: bolder;
font-size: 1.21em;
}
.income-statement-list .group-title {
font-weight: bolder;
font-size: 1.1em;
}
.income-statement-list .total {
border-top: 1px solid slategray;
font-size: 1.1em;
font-weight: bolder;
}
.income-statement-list .cumulative-total {
font-weight: bolder;
font-size: 1.21em;
}
/* The balance sheet */
.balance-sheet-table thead {
font-size: 1.21em;
border-bottom: thick double slategray;
}
.balance-sheet-table tbody {
}
.balance-sheet-table .group-title {
font-size: 1.1em;
font-weight: bolder;
}
.balance-sheet-table td .account {
text-indent: 1em;
}
.balance-sheet-table .total {
border-top: thick double slategray;
font-size: 1.1em;
font-weight: bolder;
}
.balance-sheet-total-table .total {
border-top: thick double slategray;
font-size: 1.21em;
font-weight: bolder;
}
.balance-sheet-list {
margin-bottom: 1em;
}
.balance-sheet-list .list-group-item {
background-color: transparent;
border: none;
}
.balance-sheet-list .section-title {
font-size: 1.21em;
font-weight: bolder;
border-bottom: thick double slategray;
}
.balance-sheet-list .group-title {
font-size: 1.1em;
font-weight: bolder;
}
.balance-sheet-list .total {
font-size: 1.1em;
font-weight: bolder;
border-top: thick double slategray;
}
.balance-sheet-list .grand-total {
font-size: 1.21em;
font-weight: bolder;
border-top: thick double slategray;
}
/* The search */
.btn-actions .btn .search-input {
height: calc(1em + .5rem + 2px);
border-radius: .2rem;
}
.btn-actions .btn .search-label {
margin-bottom: 0;
}
.btn-actions .btn .search-label button {
border: none;
background-color: transparent;
color: inherit;
padding-right: 0;
}

View File

@ -0,0 +1,36 @@
/* The Mia Website
* summary-helper.css: The style sheet for the summary helper
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2020/4/3
*/
.summary-container {
padding: 0 0 1em 0;
}
.summary-tab-content {
padding-top: 1em;
}
.summary-categories-known {
max-height: 200px;
overflow-y: scroll;
}
.summary-categories-known .btn-summary-helper {
margin: 0.1em;
}

View File

@ -0,0 +1,43 @@
/* The Mia Website
* sort.css: The style sheet to reorder the transaction
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2019/10/12
*/
ul.txn-content-expense {
margin: 0;
padding: 0 0 0 0;
}
ul.txn-content-expense li {
list-style-type: none;
}
.txn-content-income {
margin: 0 0 0 1em;
}
ul.txn-content-income {
margin: 0 0 0 1em;
padding: 0 0 0 0;
}
ul.txn-content-income li {
list-style-type: none;
}
.amount {
text-align: right;
}

View File

@ -0,0 +1,40 @@
/* The Mia Website
* transaction.css: The style sheet for the current transaction
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2019/9/17
*/
.account-line {
font-size: 0.833em;
}
.amount {
font-style: italic;
text-align: right;
}
#txn-form .amount {
font-style: italic;
}
.balance-row {
border-color: transparent;
}
.record-label {
margin-top: 0.25rem;
margin-bottom: 0.1rem;
}

View File

@ -0,0 +1,189 @@
/* The Mia Website
* account-form.js: The JavaScript to edit an account
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2020/3/23
*/
// Initializes the page JavaScript.
$(function () {
getAllAccounts();
$("#account-code").on("blur", function () {
updateParent(this);
validateCode();
});
$("#account-title").on("blur", function () {
validateTitle();
});
$("#account-form").on("submit", function () {
return validateForm();
});
});
/**
* All the accounts
* @type {Array.}
* @private
*/
let accounts;
/**
* Obtains all the accounts.
*
* @private
*/
function getAllAccounts() {
const request = new XMLHttpRequest();
request.onload = function() {
if (this.status === 200) {
accounts = JSON.parse(this.responseText);
}
};
request.open("GET", $("#all-account-url").val(), true);
request.send();
}
/**
* Updates the parent account.
*
* @param {HTMLInputElement} code the code input element
* @private
*/
function updateParent(code) {
const parent = $("#account-parent");
if (code.value.length === 0) {
parent.text("");
return;
}
if (code.value.length === 1) {
parent.text(gettext("Topmost"));
return;
}
const parentCode = code.value.substr(0, code.value.length - 1);
if (parentCode in accounts) {
parent.text(parentCode + " " + accounts[parentCode]);
return;
}
parent.text(gettext("(Unknown)"));
}
/*******************
* Form Validation *
*******************/
/**
* Validates the form.
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateForm() {
let isValid = true;
isValid = validateCode() && isValid;
isValid = validateTitle() && isValid;
return isValid;
}
/**
* Validates the code column.
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateCode() {
const code = $("#account-code")[0];
const errorMessage = $("#account-code-error");
code.value = code.value.trim();
if (code.value === "") {
code.classList.add("is-invalid");
errorMessage.text(gettext("Please fill in the code."));
return false;
}
if (!code.value.match(/^[1-9]+$/)) {
code.classList.add("is-invalid");
errorMessage.text(gettext("You can only use numbers 1-9 in the code."));
return false;
}
const originalCode = $("#account-code-original").val();
if (code.value !== originalCode) {
if (originalCode !== "" && code.value.startsWith(originalCode)) {
code.classList.add("is-invalid");
errorMessage.text(gettext("You cannot set the code under itself."));
return false;
}
if (code.value in accounts) {
code.classList.add("is-invalid");
errorMessage.text(gettext("This code is already in use."));
return false;
}
}
const parentCode = code.value.substr(0, code.value.length - 1);
if (!(parentCode in accounts)) {
code.classList.add("is-invalid");
errorMessage.text(gettext("The parent account of this code does not exist."));
return false;
}
if (originalCode !== "" && code.value !== originalCode) {
const descendants = [];
Object.keys(accounts).forEach(function (key) {
if (key.startsWith(originalCode) && key !== originalCode) {
descendants.push(key);
}
});
if (descendants.length > 0) {
descendants.sort(function (a, b) {
return b.length - a.length;
});
if (descendants[0].length
- originalCode.length
+ code.value.length > code.maxLength) {
code.classList.add("is-invalid");
errorMessage.text(gettext("The descendant account codes will be too long (max. 5)."));
return false;
}
}
}
code.classList.remove("is-invalid");
errorMessage.text("");
return true;
}
/**
* Validates the title column.
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateTitle() {
const title = $("#account-title")[0];
const errorMessage = $("#account-title-error");
title.value = title.value.trim();
if (title.value === "") {
title.classList.add("is-invalid");
errorMessage.text(gettext("Please fill in the title."));
return false;
}
title.classList.remove("is-invalid");
errorMessage.text("");
return true;
}

View File

@ -0,0 +1,29 @@
/* The Mia Website
* account-list.js: The JavaScript for the account list
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2020/8/7
*/
// Initializes the page JavaScript.
$(function () {
$('#accounts').DataTable({
"ordering": false,
});
});

View File

@ -0,0 +1,601 @@
/* The Mia Website
* summary-helper.js: The JavaScript for the summary helper
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2020/4/3
*/
// Initializes the summary helper JavaScript.
$(function () {
loadSummaryCategoryData();
$("#summary-helper-form")
.on("submit", function () {
return false;
});
$(".record-summary")
.on("click", function () {
startSummaryHelper($(this));
});
$("#summary-summary")
.on("change", function () {
this.value = this.value.trim();
// Replaced common substitution character "*" with "×"
this.value = this.value.replace(/\*(\d+)$/, "×$1");
parseSummaryForHelper(this.value);
});
$(".summary-tab")
.on("click", function () {
switchSummaryTab($(this));
});
// The general categories
$("#summary-general-category")
.on("change", function () {
setSummaryGeneralCategoryButtons(this.value);
setGeneralCategorySummary();
setSummaryAccount("general", this.value);
});
// The travel routes
$("#summary-travel-category")
.on("change", function () {
setSummaryTravelCategoryButtons(this.value);
setSummaryAccount("travel", this.value);
});
$(".summary-travel-part")
.on("change", function () {
this.value = this.value.trim();
setTravelSummary();
});
$(".btn-summary-travel-direction")
.on("click", function () {
$("#summary-travel-direction").get(0).value = this.innerText;
setSummaryTravelDirectionButtons(this.innerText);
setTravelSummary();
});
// The bus routes
$("#summary-bus-category")
.on("change", function () {
setSummaryBusCategoryButtons(this.value);
setSummaryAccount("bus", this.value);
});
$(".summary-bus-part")
.on("change", function () {
this.value = this.value.trim();
setBusSummary();
});
$("#summary-count")
.on("change", function () {
updateSummaryCount();
});
$("#summary-confirm")
.on("click", function () {
applySummaryToAccountingRecord();
});
});
/**
* The known categories
* @type {object}
* @private
*/
let summaryCategories = null;
/**
* The known categories and their corresponding accounts
* @type {object}
* @private
*/
let summaryAccounts = null;
/**
* The account that corresponds to this category
* @type {null|string}
* @private
*/
let summaryAccount = null;
/**
* Loads the summary category data.
*
* @private
*/
function loadSummaryCategoryData() {
const data = JSON.parse($("#summary-categories").val());
summaryCategories = {};
summaryAccounts = {};
["debit", "credit"].forEach(function (type) {
summaryCategories[type] = {};
summaryAccounts[type] = {};
["general", "travel", "bus"].forEach(function (format) {
summaryCategories[type][format] = [];
summaryAccounts[type][format] = {};
if (type + "-" + format in data) {
data[type + "-" + format]
.forEach(function (item) {
summaryCategories[type][format].push(item[0]);
summaryAccounts[type][format][item[0]] = item[1];
});
}
});
});
}
/**
* Starts the summary helper.
*
* @param {jQuery} summary the summary input element
*/
function startSummaryHelper(summary) {
// Replaced common substitution character "*" with "×"
let summary_content = summary.val();
summary_content = summary_content.replace(/\*(\d+)$/, "×$1");
const type = summary.data("type");
const no = summary.data("no");
$("#summary-record").val(type + "-" + no);
$("#summary-summary").val(summary_content);
// Loads the know summary categories into the summary helper
loadKnownSummaryCategories(type);
// Parses the summary and sets up the summary helper
parseSummaryForHelper(summary_content);
// Focus on the summary input
setTimeout(function () {
$("#summary-summary").get(0).focus();
}, 100);
}
/**
* Loads the known summary categories into the summary helper.
*
* @param {string} type the record type, either "debit" or "credit"
* @private
*/
function loadKnownSummaryCategories(type) {
["general", "travel", "bus"].forEach(function (format) {
const knownCategories = $("#summary-" + format + "-categories-known");
knownCategories.html("");
summaryCategories[type][format].forEach(function (item) {
knownCategories.append(
$("<span/>")
.addClass("btn btn-outline-primary")
.addClass("btn-summary-helper")
.addClass("btn-summary-" + format + "-category")
.text(item));
});
});
// The regular accounts
loadRegularAccounts(type);
$(".btn-summary-general-category")
.on("click", function () {
$("#summary-general-category").get(0).value = this.innerText;
setSummaryGeneralCategoryButtons(this.innerText);
setGeneralCategorySummary();
setSummaryAccount("general", this.innerText);
});
$(".btn-summary-travel-category")
.on("click", function () {
$("#summary-travel-category").get(0).value = this.innerText;
setSummaryTravelCategoryButtons(this.innerText);
setTravelSummary();
setSummaryAccount("travel", this.innerText);
});
$(".btn-summary-bus-category")
.on("click", function () {
$("#summary-bus-category").get(0).value = this.innerText;
setSummaryBusCategoryButtons(this.innerText);
setBusSummary();
setSummaryAccount("bus", this.innerText);
});
}
/**
* Loads the regular accounts.
*
* @param {string} type the record type
* @private
*/
function loadRegularAccounts(type) {
const regularAccounts = JSON.parse(document.getElementById("regular-accounts").value);
setRegularAccountSummary(regularAccounts);
Object.keys(regularAccounts).forEach(function (type) {
summaryCategories[type].regular = [];
summaryAccounts[type].regular = {};
regularAccounts[type].forEach(function (item) {
summaryCategories[type].regular.push(item);
summaryAccounts[type].regular[item.title] = item.account;
});
console.log(summaryAccounts[type].regular);
});
const regularAccountButtons = $("#summary-regular-accounts");
regularAccountButtons.html("");
summaryCategories[type].regular.forEach(function (item) {
regularAccountButtons.append(
$("<span/>")
.attr("title", item.summary)
.addClass("btn btn-outline-primary")
.addClass("btn-summary-helper")
.addClass("btn-summary-regular")
.text(item.title));
});
$(".btn-summary-regular")
.on("click", function () {
$("#summary-summary").get(0).value = this.title;
setSummaryRegularAccountButtons(this.innerText);
setSummaryAccount("regular", this.innerText);
});
}
/**
* Sets the summary of the regular accounts according to the date
*
* @param {{}} regularAccounts the regular account data
* @private
*/
function setRegularAccountSummary(regularAccounts)
{
const monthNames = [
"",
gettext("January"),
gettext("February"),
gettext("March"),
gettext("April"),
gettext("May"),
gettext("June"),
gettext("July"),
gettext("August"),
gettext("September"),
gettext("October"),
gettext("November"),
gettext("December"),
];
const today = new Date($("#txn-date").get(0).value);
const thisMonth = today.getMonth() + 1;
const lastMonth = (thisMonth + 10) % 12 + 1;
const lastBimonthlyFrom = ((thisMonth + thisMonth % 2 + 8) % 12 + 1);
const lastBimonthlyTo = ((thisMonth + thisMonth % 2 + 9) % 12 + 1);
Object.keys(regularAccounts).forEach(function (type) {
regularAccounts[type].forEach(function (item) {
item.summary = item.format
.replaceAll("(month_no)", String(thisMonth))
.replaceAll("(month_name)", monthNames[thisMonth])
.replaceAll("(last_month_no)", String(lastMonth))
.replaceAll("(last_month_name)", monthNames[lastMonth])
.replaceAll("(last_bimonthly_from_no)", String(lastBimonthlyFrom))
.replaceAll("(last_bimonthly_from_name)", monthNames[lastBimonthlyFrom])
.replaceAll("(last_bimonthly_to_no)", String(lastBimonthlyTo))
.replaceAll("(last_bimonthly_to_name)", monthNames[lastBimonthlyTo]);
});
});
}
/**
* Parses the summary and sets up the summary helper.
*
* @param {string} summary the summary
* @private
*/
function parseSummaryForHelper(summary) {
// Parses the summary and sets up the category helpers.
parseSummaryForCategoryHelpers(summary);
// The number of items
const pos = summary.lastIndexOf("×");
let count = 1;
if (pos !== -1) {
count = parseInt(summary.substr(pos + 1));
}
if (count === 0) {
count = 1;
}
$("#summary-count").get(0).value = count;
}
/**
* Parses the summary and sets up the category helpers.
*
* @param {string} summary the summary
* @private
*/
function parseSummaryForCategoryHelpers(summary) {
$(".btn-summary-helper")
.removeClass("btn-primary")
.addClass("btn-outline-primary");
$("#btn-summary-one-way")
.removeClass("btn-outline-primary")
.addClass("btn-primary");
$(".summary-helper-input").each(function () {
this.classList.remove("is-invalid");
if (this.id === "summary-travel-direction") {
this.value = $("#btn-summary-one-way").text();
} else {
this.value = "";
}
});
// A bus route
const matchBus = summary.match(/^(.+)—(.+)—(.+)→(.+?)(?:×[0-9]+)?$/);
if (matchBus !== null) {
$("#summary-bus-category").get(0).value = matchBus[1];
setSummaryBusCategoryButtons(matchBus[1]);
setSummaryAccount("bus", matchBus[1]);
$("#summary-bus-route").get(0).value = matchBus[2];
$("#summary-bus-from").get(0).value = matchBus[3];
$("#summary-bus-to").get(0).value = matchBus[4];
switchSummaryTab($("#summary-tab-bus"));
return;
}
// A general travel route
const matchTravel = summary.match(/^(.+)—(.+)([→|↔])(.+?)(?:×[0-9]+)?$/);
if (matchTravel !== null) {
$("#summary-travel-category").get(0).value = matchTravel[1];
setSummaryTravelCategoryButtons(matchTravel[1]);
setSummaryAccount("travel", matchTravel[1]);
$("#summary-travel-from").get(0).value = matchTravel[2];
$("#summary-travel-direction").get(0).value = matchTravel[3];
setSummaryTravelDirectionButtons(matchTravel[3]);
$("#summary-travel-to").get(0).value = matchTravel[4];
switchSummaryTab($("#summary-tab-travel"));
return;
}
// A general category
const generalCategoryTab = $("#summary-tab-category");
const matchCategory = summary.match(/^(.+)—.+(?:×[0-9]+)?$/);
if (matchCategory !== null) {
$("#summary-general-category").get(0).value = matchCategory[1];
setSummaryGeneralCategoryButtons(matchCategory[1]);
setSummaryAccount("general", matchCategory[1]);
switchSummaryTab(generalCategoryTab);
return;
}
// A general summary text
setSummaryGeneralCategoryButtons(null);
setSummaryAccount("general", null);
switchSummaryTab(generalCategoryTab);
}
/**
* Switch the summary helper to tab.
*
* @param {jQuery} tab the navigation tab corresponding to a type
* of helper
* @private
*/
function switchSummaryTab(tab) {
$(".summary-tab-content").addClass("d-none");
$("#summary-tab-content-" + tab.data("tab")).removeClass("d-none");
$(".summary-tab").removeClass("active");
tab.addClass("active");
}
/**
* Sets the known general category buttons.
*
* @param {string|null} category the general category
* @private
*/
function setSummaryGeneralCategoryButtons(category) {
$(".btn-summary-general-category").each(function () {
if (this.innerText === category) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the summary of a general category.
*
* @private
*/
function setGeneralCategorySummary() {
const summary = $("#summary-summary").get(0);
const dashPos = summary.value.indexOf("—");
if (dashPos !== -1) {
summary.value = summary.value.substring(dashPos + 1);
}
const category = $("#summary-general-category").get(0).value;
if (category !== "") {
summary.value = category + "—" + summary.value;
}
}
/**
* Sets the known travel category buttons.
*
* @param {string} category the travel category
* @private
*/
function setSummaryTravelCategoryButtons(category) {
$(".btn-summary-travel-category").each(function () {
if (this.innerText === category) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the summary of a general travel.
*
* @private
*/
function setTravelSummary() {
$(".summary-travel-part").each(function () {
if (this.value === "") {
this.classList.add("is-invalid");
} else {
this.classList.remove("is-invalid");
}
});
let summary = $("#summary-travel-category").get(0).value
+ "—" + $("#summary-travel-from").get(0).value
+ $("#summary-travel-direction").get(0).value
+ $("#summary-travel-to").get(0).value;
const count = parseInt($("#summary-count").get(0).value);
if (count !== 1) {
summary = summary + "×" + count;
}
$("#summary-summary").get(0).value = summary;
}
/**
* Sets the known summary travel direction buttons.
*
* @param {string} direction the known summary travel direction
* @private
*/
function setSummaryTravelDirectionButtons(direction) {
$(".btn-summary-travel-direction").each(function () {
if (this.innerText === direction) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the known bus category buttons.
*
* @param {string} category the bus category
* @private
*/
function setSummaryBusCategoryButtons(category) {
$(".btn-summary-bus-category").each(function () {
if (this.innerText === category) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the summary of a bus travel.
*
* @private
*/
function setBusSummary() {
$(".summary-bus-part").each(function () {
if (this.value === "") {
this.classList.add("is-invalid");
} else {
this.classList.remove("is-invalid");
}
});
let summary = $("#summary-bus-category").get(0).value
+ "—" + $("#summary-bus-route").get(0).value
+ "—" + $("#summary-bus-from").get(0).value
+ "→" + $("#summary-bus-to").get(0).value;
const count = parseInt($("#summary-count").get(0).value);
if (count !== 1) {
summary = summary + "×" + count;
}
$("#summary-summary").get(0).value = summary;
}
/**
* Sets the regular account buttons.
*
* @param {string} category the regular account
* @private
*/
function setSummaryRegularAccountButtons(category) {
$(".btn-summary-regular").each(function () {
if (this.innerText === category) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the account for this summary category.
*
* @param {string} format the category format, either "general",
* "travel", or "bus".
* @param {string|null} category the category
* @private
*/
function setSummaryAccount(format, category) {
const recordId = $("#summary-record").get(0).value;
const type = recordId.substring(0, recordId.indexOf("-"));
if (category in summaryAccounts[type][format]) {
summaryAccount = summaryAccounts[type][format][category];
} else {
summaryAccount = null;
}
}
/**
* Updates the count.
*
* @private
*/
function updateSummaryCount() {
const count = parseInt($("#summary-count").val());
const summary = $("#summary-summary").get(0);
const pos = summary.value.lastIndexOf("×");
if (pos === -1) {
if (count !== 1) {
summary.value = summary.value + "×" + count;
}
} else {
const content = summary.value.substring(0, pos);
if (count === 1) {
summary.value = content;
} else {
summary.value = content + "×" + count;
}
}
}
/**
* Applies the summary to the accounting record.
*
* @private
*/
function applySummaryToAccountingRecord() {
const recordId = $("#summary-record").get(0).value;
const summary = $("#" + recordId + "-summary").get(0);
summary.value = $("#summary-summary").get(0).value.trim();
const account = $("#" + recordId + "-account").get(0);
if (summaryAccount !== null && account.value === "") {
account.value = summaryAccount;
}
setTimeout(function () {
summary.blur();
}, 100);
}

View File

@ -0,0 +1,509 @@
/* The Mia Website
* transaction-form.js: The JavaScript for the transaction form
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2019/9/19
*/
// Initializes the page JavaScript.
$(function () {
getAccountOptions();
resetRecordButtons();
$("#txn-date")
.on("blur", function () {
validateDate();
});
$(".record-account")
.on("focus", function () {
removeBlankOption(this);
})
.on("blur", function () {
validateAccount(this);
});
$(".record-summary")
.on("blur", function () {
validateSummary(this);
});
$(".record-amount")
.on("blur", function () {
validateAmount(this);
})
.on("change", function () {
updateTotalAmount($(this));
validateBalance();
});
$("#txn-note")
.on("blur", function () {
validateNote();
});
$("#txn-form")
.on("submit", function () {
return validateForm();
});
$(".btn-new")
.on("click", function () {
addNewRecord($(this));
});
$(".btn-del-record")
.on("click", function () {
deleteRecord($(this));
});
});
/**
* Returns whether this is a transfer transaction.
*
* @returns {boolean} true if this is a transfer transaction, or false
* otherwise
* @private
*/
function isTransfer() {
return $("#debit-records").length > 0 && $("#credit-records").length > 0;
}
/**
* The account options
* @type {Array.}
* @private
*/
let accountOptions;
/**
* Obtains the account options.
*
* @private
*/
function getAccountOptions() {
const request = new XMLHttpRequest();
request.onload = function() {
if (this.status === 200) {
accountOptions = JSON.parse(this.responseText);
$(".record-account").each(function () {
initializeAccountOptions($(this));
});
}
};
request.open("GET", $("#account-option-url").val(), true);
request.send();
}
/**
* Initialize the account options.
*
* @param {jQuery} account the account select element
* @private
*/
function initializeAccountOptions(account) {
const type = account.data("type");
const selectedAccount = account.val();
let isCash = false;
if (type === "debit") {
isCash = ($(".credit-record").length === 0);
} else if (type === "credit") {
isCash = ($(".debit-record").length === 0);
}
account.html("");
if (selectedAccount === "") {
account.append($("<option/>"));
}
const headerInUse = $("<option/>")
.attr("disabled", "disabled")
.text(accountOptions["header_in_use"]);
account.append(headerInUse);
accountOptions[type + "_in_use"].forEach(function (item) {
// Skips the cash account on cash transactions.
if (item["code"] === 1111 && isCash) {
return;
}
const option = $("<option/>")
.attr("value", item["code"])
.text(item["code"] + " " + item["title"]);
if (String(item["code"]) === selectedAccount) {
option.attr("selected", "selected");
}
account.append(option);
});
const headerNotInUse = $("<option/>")
.attr("disabled", "disabled")
.text(accountOptions["header_not_in_use"]);
account.append(headerNotInUse);
accountOptions[type + "_not_in_use"].forEach(function (item) {
const option = $("<option/>")
.attr("value", item["code"])
.text(item["code"] + " " + item["title"]);
if (String(item["code"]) === selectedAccount) {
option.attr("selected", "selected");
}
account.append(option);
});
}
/**
* Removes the dummy blank option.
*
* @param {HTMLSelectElement} select the select element
* @private
*/
function removeBlankOption(select) {
$(select).children().each(function () {
if (this.value === "" && !this.disabled) {
$(this).remove();
}
});
}
/**
* Updates the total amount.
*
* @param {jQuery} element the amount element that changed, or the
* button that was hit to delete a record
* @private
*/
function updateTotalAmount(element) {
const type = element.data("type")
let total = new Decimal("0");
$("." + type + "-to-sum").each(function () {
if (this.value !== "") {
total = total.plus(new Decimal(this.value));
}
});
total = String(total);
while (total.match(/^[1-9][0-9]*[0-9]{3}/)) {
total = total.replace(/^([1-9][0-9]*)([0-9]{3})/, "$1,$2");
}
$("#" + type + "-total").text(total);
}
/**
* Adds a new accounting record.
*
* @param {jQuery} button the button element that was hit to add a
* new record
* @private
*/
function addNewRecord(button) {
const type = button.data("type");
// Finds the new number that is the maximum number plus 1.
let newNo = 0;
$("." + type + "-record").each(function () {
const no = parseInt($(this).data("no"));
if (newNo < no) {
newNo = no;
}
});
newNo++;
// Inserts a new table row for the new accounting record.
insertNewRecord(type, newNo);
// Resets the order of the records.
resetRecordOrders(type);
// Resets the sort and delete buttons for the records.
resetRecordButtons();
}
/**
* Inserts a new accounting record.
*
* @param {string} type the record type, either "debit" or "credit"
* @param {number} newNo the number of this new accounting record
* @private
*/
function insertNewRecord(type, newNo) {
$("#" + type + "-records").append(
JSON.parse($("#new-record-template").val())
.replace(/TTT/g, type)
.replace(/NNN/g, String(newNo)));
$("#" + type + "-" + newNo + "-account")
.on("focus", function () {
removeBlankOption(this);
})
.on("blur", function () {
validateAccount(this);
})
.each(function () {
initializeAccountOptions($(this));
});
$("#" + type + "-" + newNo + "-summary")
.on("blur", function () {
validateSummary(this);
})
.on("click", function () {
if (typeof startSummaryHelper === "function") {
startSummaryHelper($(this));
}
});
$("#" + type + "-" + newNo + "-amount")
.on("blur", function () {
validateAmount(this);
})
.on("change", function () {
updateTotalAmount($(this));
validateBalance();
});
$("#" + type + "-" + newNo + "-delete")
.on("click", function () {
deleteRecord($(this));
});
$("#" + type + "-" + newNo + "-m-delete")
.on("click", function () {
deleteRecord($(this));
});
}
/**
* Deletes a record.
*
* @param {jQuery} button the button element that was hit to delete
* this record
* @private
*/
function deleteRecord(button) {
const type = button.data("type");
const no = button.data("no");
console.log("#" + type + "-" + no);
$("#" + type + "-" + no).remove();
resetRecordOrders(type);
resetRecordButtons();
updateTotalAmount(button);
validateBalance();
}
/**
* Resets the order of the records according to their appearance.
*
* @param {string} type the record type, either "debit" or "credit".
* @private
*/
function resetRecordOrders(type) {
const sorted = $("#" + type + "-records").sortable("toArray");
for (let i = 0; i < sorted.length; i++) {
$("#" + sorted[i] + "-ord")[0].value = i + 1;
}
}
/**
* Resets the sort and delete buttons for the records.
*
* @private
*/
function resetRecordButtons() {
["debit", "credit"].forEach(function (type) {
const records = $("." + type + "-record");
if (records.length > 1) {
$("#" + type + "-records").sortable({
classes: {
"ui-sortable-helper": "list-group-item-secondary",
},
cursor: "move",
cancel: "input, select",
stop: function () {
resetRecordOrders(type);
},
}).sortable("enable");
$(".btn-actions-" + type).removeClass("invisible");
} else if (records.length === 1) {
$("#" + type + "-records").sortable().sortable("disable");
$(".btn-actions-" + type).addClass("invisible");
}
});
}
/*******************
* Form Validation *
*******************/
/**
* Validates the form.
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateForm() {
let isValid = true;
isValid = validateDate() && isValid;
$(".debit-record").each(function () {
isValid = validateRecord(this) && isValid;
});
$(".credit-account").each(function () {
isValid = validateRecord(this) && isValid;
});
$(".record-account").each(function () {
isValid = validateAccount(this) && isValid;
});
$(".record-summary").each(function () {
isValid = validateSummary(this) && isValid;
});
$(".record-amount").each(function () {
isValid = validateAmount(this) && isValid;
});
if (isTransfer()) {
isValid = validateBalance() && isValid;
}
isValid = validateNote() && isValid;
return isValid;
}
/**
* Validates the date column.
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateDate() {
const date = $("#txn-date")[0];
const errorMessage = $("#txn-date-error");
if (date.value === "") {
date.classList.add("is-invalid");
errorMessage.text(gettext("Please fill in the date."));
return false;
}
date.classList.remove("is-invalid");
errorMessage.text("");
return true;
}
/**
* Validates the record.
*
* @param {HTMLLIElement} record the record
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateRecord(record) {
return !record.classList.contains("list-group-item-danger");
}
/**
* Validates the account column.
*
* @param {HTMLSelectElement} account the account selection element
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateAccount(account) {
const errorMessage = $("#" + account.id + "-error");
if (account.value === "") {
account.classList.add("is-invalid");
errorMessage.text(gettext("Please select the account."));
return false;
}
account.classList.remove("is-invalid");
errorMessage.text("");
return true;
}
/**
* Validates the summary column.
*
* @param {HTMLInputElement} summary the summary input element
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateSummary(summary) {
const errorMessage = $("#" + summary.id + "-error");
summary.value = summary.value.trim();
if (summary.value.length > 128) {
summary.classList.add("is-invalid");
errorMessage.text(gettext("This summary is too long (max. 128 characters)."));
return false;
}
summary.classList.remove("is-invalid");
errorMessage.text("");
return true;
}
/**
* Validates the amount column.
*
* @param {HTMLInputElement} amount the amount input element
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateAmount(amount) {
const errorMessage = $("#" + amount.id + "-error");
amount.value = amount.value.trim();
if (amount.value === "") {
amount.classList.add("is-invalid");
errorMessage.text(gettext("Please fill in the amount."));
return false;
}
amount.classList.remove("is-invalid");
errorMessage.text("");
return true;
}
/**
* Validates the balance between debit and credit records
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateBalance() {
const balanceRows = $(".balance-row");
const errorMessages = $(".balance-error");
let debitTotal = new Decimal("0");
$(".debit-to-sum").each(function () {
if (this.value !== "") {
debitTotal = debitTotal.plus(new Decimal(this.value));
}
});
let creditTotal = new Decimal("0");
$(".credit-to-sum").each(function () {
if (this.value !== "") {
creditTotal = creditTotal.plus(new Decimal(this.value));
}
});
if (!debitTotal.equals(creditTotal)) {
balanceRows.addClass("is-invalid");
errorMessages.text(gettext("The total amount of debit and credit records are inconsistent."))
return false;
}
balanceRows.removeClass("is-invalid");
errorMessages.text("");
return true;
}
/**
* Validates the note column.
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateNote() {
const note = $("#txn-note")[0];
const errorMessage = $("#txn-note-error");
note.value = note.value.trim();
if (note.value.length > 128) {
note.classList.add("is-invalid");
errorMessage.text(gettext("These notes are too long (max. 128 characters)."));
return false;
}
note.classList.remove("is-invalid");
errorMessage.text("");
return true;
}

View File

@ -0,0 +1,47 @@
/* The Mia Website
* sort.js: The JavaScript to reorder the transactions
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2019/10/13
*/
// Initializes the page JavaScript.
$(function () {
$("#transactions").sortable({
classes: {
"ui-sortable-helper": "table-active",
},
cursor: "move",
stop: function () {
resetTransactionOrders();
},
});
});
/**
* Resets the order of the transactions according to their appearance.
*
* @private
*/
function resetTransactionOrders() {
const sorted = $("#transactions").sortable("toArray");
for (let i = 0; i < sorted.length; i++) {
$("#" + sorted[i] + "-ord")[0].value = i + 1;
}
}

View File

@ -0,0 +1,173 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
account_detail.html: The template for the account detail
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/8/8
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% setvar "title" account %}
{% endblock %}
{% block content %}
{% if account.is_parent_and_in_use %}
<div class="alert alert-danger alert-dismissible fade show">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>{{ _("Error:")|force_escape }}</strong> {{ _("The account is a parent account but is also used in the accounting records.")|force_escape }}
</div>
{% endif %}
<!-- the delete confirmation dialog -->
<form action="{% url "accounting:accounts.delete" account as url %}{% url_keep_return url %}" method="post">
{% csrf_token %}
<!-- The Modal -->
<div class="modal fade" id="del-modal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">{{ _("Account Deletion Confirmation")|force_escape }}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<!-- Modal body -->
<div class="modal-body">{{ _("Do you really want to delete this account?")|force_escape }}</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
</div>
</div>
</div>
</div>
</form>
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if "r" in request.GET %}{{ request.GET.r }}{% else %}{% url "accounting:accounts" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{{ _("Back")|force_escape }}
</a>
<a class="btn btn-primary" role="button" href="{% url "accounting:accounts.update" account %}">
<i class="fas fa-user-cog"></i>
{{ _("Settings")|force_escape }}
</a>
{% if not account.is_in_use %}
<button type="button" class="btn btn-secondary d-none d-sm-inline" disabled="disabled" title="{{ _("This account is not used in the accounting records.")|force_escape }}">
<i class="fas fa-file-invoice-dollar"></i>
{{ _("Ledger")|force_escape }}
</button>
{% else %}
<a class="btn btn-primary d-none d-sm-inline" role="button" href="{% url "accounting:ledger" account "-" %}">
<i class="fas fa-file-invoice-dollar"></i>
{{ _("Ledger")|force_escape }}
</a>
{% endif %}
<div class="btn-group d-sm-none">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
<i class="fas fa-bars"></i>
</button>
<div class="dropdown-menu">
{% if not account.is_in_use %}
<span class="dropdown-item disabled" title="{{ _("This account is not used in the accounting records.")|force_escape }}">
<i class="fas fa-file-invoice-dollar"></i>
{{ _("Ledger")|force_escape }}
</span>
{% else %}
<a class="dropdown-item" href="{% url "accounting:ledger" account "-" %}">
<i class="fas fa-file-invoice-dollar"></i>
{{ _("Ledger")|force_escape }}
</a>
{% endif %}
</div>
</div>
{% if account.is_in_use %}
<button class="btn btn-secondary" type="button" disabled="disabled" title="{{ _("This account is in use.")|force_escape }}">
<i class="fas fa-trash"></i>
{{ _("Delete")|force_escape }}
</button>
{% else %}
<button class="btn btn-danger" type="button" data-toggle="modal" data-target="#del-modal">
<i class="fas fa-trash"></i>
{{ _("Delete")|force_escape }}
</button>
{% endif %}
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Parent Account:")|force_escape }}</div>
<div class="col-sm-10">
{% if account.parent %}
{{ account.parent }}
{% else %}
{{ _("Topmost")|force_escape }}
{% endif %}
</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Code:")|force_escape }}</div>
<div class="col-sm-10">{{ account.code }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Title:")|force_escape }}</div>
<div class="col-sm-10">{{ account.title }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Child Accounts:")|force_escape }}</div>
<div class="col-sm-10">
{% for child in account.child_set.all %}
<a class="btn btn-primary" type="role" href="{% url "accounting:accounts.detail" child as url %}{% url_with_return url %}">
{{ child }}
</a>
{% empty %}
{{ _("This account is an end-point account.")|force_escape }}
{% endfor %}
</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Created at:")|force_escape }}</div>
<div class="col-sm-10">{{ account.created_at }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Created by:")|force_escape }}</div>
<div class="col-sm-10">{{ account.created_by }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Updated at:")|force_escape }}</div>
<div class="col-sm-10">{{ account.updated_at }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Updated by:")|force_escape }}</div>
<div class="col-sm-10">{{ account.updated_by }}</div>
</div>
{% endblock %}

View File

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
account_detail.html: The template for the account form
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/8/8
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% if form.account %}
{% setvar "title" form.account %}
{% else %}
{% setvar "title" _("Add a New Account") %}
{% endif %}
{% static "accounting/js/account-form.js" as file %}{% add_js file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if account %}{% url "accounting:accounts.detail" form.account %}{% else %}{% url "accounting:accounts" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{{ _("Back")|force_escape }}
</a>
</div>
<form id="account-form" action="{{ request.get_full_path }}" method="post">
{% csrf_token %}
<input id="all-account-url" type="hidden" value="{% url "accounting:api.accounts" %}" />
<input id="account-code-original" type="hidden" value="{% if form.account %}{{ form.account.code }}{% endif %}" />
<div class="row form-group">
<label class="col-sm-2" for="account-parent">{{ _("Parent Account:")|force_escape }}</label>
<div id="account-parent" class="col-sm-10">
{% if form.parent %}
{{ form.parent }}
{% else %}
{{ _("Topmost")|force_escape }}
{% endif %}
</div>
</div>
<div class="row form-group">
<label class="col-sm-2 col-form-label" for="account-code">{{ _("Code:")|force_escape }}</label>
<div class="col-sm-10">
<input id="account-code" class="form-control {% if form.code.errors %} is-invalid {% endif %}" type="text" name="code" value="{{ form.code.value|default:"" }}" maxlength="5" required="required" />
<div id="account-code-error" class="invalid-feedback">{% if form.code.errors %}{{ form.code.errors.0 }}{% endif %}</div>
</div>
</div>
<div class="row form-group">
<label class="col-sm-2 col-form-label" for="account-title">{{ _("Title:")|force_escape }}</label>
<div class="col-sm-10">
<input id="account-title" class="form-control {% if form.title.errors %} is-invalid {% endif %}" type="text" name="title" value="{{ form.title.value|default:"" }}" required="required" />
<div id="account-title-error" class="invalid-feedback">{% if form.title.errors %}{{ form.title.errors.0 }}{% endif %}</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-12">
<button class="btn btn-primary" type="submit">
<i class="fas fa-save"></i>
{{ _("Save")|force_escape }}
</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,83 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
account_list.html: The template for the account list
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/8/7
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% trans "Accounts" context "Accounting" as text %}
{% setvar "title" text %}
{% add_lib "bootstrap4-datatables" %}
{% static "accounting/js/account-list.js" as file %}{% add_js file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% url "accounting:accounts.create" %}">
<i class="fas fa-plus"></i>
{{ _("New")|force_escape }}
</a>
{% trans "Accounts" context "Accounting" as text %}
{% with current_report_icon="fas fa-list-ol" current_report_title=text %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
</div>
{% if account_list %}
<table id="accounts" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">{{ _("Code")|force_escape }}</th>
<th scope="col">{{ _("Title")|force_escape }}</th>
<th class="actions" scope="col">{{ _("View")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for account in account_list %}
<tr class="{% if account.is_parent_and_in_use %} table-danger {% endif %}">
<td>{{ account.code }}</td>
<td>
{{ account.title|title_case }}
{% if account.is_parent_and_in_use %}
<span class="badge badge-danger badge-pill">
{{ _("Parent Account In Use")|force_escape }}
</span>
{% endif %}
</td>
<td class="actions">
<a href="{% url "accounting:accounts.detail" account %}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
<span class="d-none d-sm-inline">{{ _("View")|force_escape }}</span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>{{ _("There is currently no data.")|force_escape }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,78 @@
{% comment %}
The Mia Accounting Application
form-record-non-transfer.html: The template for a record in the non-transfer transaction form
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/8/5
{% endcomment %}
{% load accounting %}
<li id="{{ record_type }}-{{ no }}" class="list-group-item {% if record.non_field_errors %} list-group-item-danger {% endif %} draggable-record {{ record_type }}-record" data-no="{{ no }}">
<div id="{{ record_type }}-{{ no }}-error">{% if record.non_field_errors %}{{ record.non_field_errors.0 }}{% endif %}</div>
<div class="d-flex justify-content-between">
<div class="row">
{% if record.id.value %}
<input type="hidden" name="{{ record_type }}-{{ no }}-id" value="{{ record.id.value }}" />
{% endif %}
<input id="{{ record_type }}-{{ no }}-ord" class="{{ record_type }}-ord" type="hidden" name="{{ record_type }}-{{ no }}-ord" value="{{ order }}" />
<div class="col-lg-6">
<div class="row">
<div class="col-sm-8">
<label for="{{ record_type }}-{{ no }}-summary" class="record-label">{{ _("Summary:")|force_escape }}</label>
<input id="{{ record_type }}-{{ no }}-summary" class="form-control record-summary {% if record.summary.errors %} is-invalid {% endif %}" type="text" name="{{ record_type }}-{{ no }}-summary" value="{{ record.summary.value|default:"" }}" maxlength="128" data-toggle="modal" data-target="#summary-modal" data-type="{{ record_type }}" data-no="{{ no }}" />
<div id="{{ record_type }}-{{ no }}-summary-error" class="invalid-feedback">{{ record.summary.errors.0|default:"" }}</div>
</div>
<div class="col-sm-4">
<label for="{{ record_type }}-{{ no }}-amount" class="record-label">{{ _("Amount:")|force_escape }}</label>
<input id="{{ record_type }}-{{ no }}-amount" class="form-control record-amount {{ record_type }}-to-sum {% if record.amount.errors %} is-invalid {% endif %}" type="number" step="0.01" min="0.01" name="{{ record_type }}-{{ no }}-amount" value="{{ record.amount.value|short_value }}" required="required" data-type="{{ record_type }}" />
<div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div>
</div>
</div>
</div>
<div class="col-lg-6">
<label for="{{ record_type }}-{{ no }}-account" class="record-label">{{ _("Account:")|force_escape }}</label>
<select id="{{ record_type }}-{{ no }}-account" class="form-control record-account {{ record_type }}-account {% if record.account.errors %} is-invalid {% endif %}" name="{{ record_type }}-{{ no }}-account" data-type="{{ record_type }}">
{% if record.account is not None %}
<option value="{{ record.account.value|default:"" }}" selected="selected">{{ record.account.value|default:"" }} {{ record.account_title|default:"" }}</option>
{% else %}
<option value=""></option>
{% endif %}
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
</select>
<div id="{{ record_type }}-{{ no }}-account-error" class="invalid-feedback">{{ record.account.errors.0|default:"" }}</div>
</div>
</div>
<div>
<div class="btn-group d-none d-lg-flex btn-actions-{{ record_type }}">
<button class="btn btn-outline-secondary btn-sort-{{ record_type }}" type="button">
<i class="fas fa-sort"></i>
</button>
<button id="{{ record_type }}-{{ no }}-delete" type="button" class="btn btn-danger btn-del-record btn-del-{{ record_type }}" data-type="{{ record_type }}" data-no="{{ no }}">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="btn-group-vertical d-lg-none btn-actions-{{ record_type }}">
<button class="btn btn-outline-secondary btn-sort-{{ record_type }}" type="button">
<i class="fas fa-sort"></i>
</button>
<button id="{{ record_type }}-{{ no }}-m-delete" type="button" class="btn btn-danger btn-del-record btn-del-{{ record_type }}" data-type="{{ record_type }}" data-no="{{ no }}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</li>

View File

@ -0,0 +1,70 @@
{% comment %}
The Mia Accounting Application
form-record-transfer.html: The template for a record in the transfer transaction form
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/8/5
{% endcomment %}
{% load accounting %}
<li id="{{ record_type }}-{{ no }}" class="list-group-item {% if record.non_field_errors %} list-group-item-danger {% endif %} draggable-record {{ record_type }}-record" data-no="{{ no }}">
<div id="{{ record_type }}-{{ no }}-error">{% if record.non_field_errors %}{{ record.non_field_errors.0 }}{% endif %}</div>
<div class="d-flex">
<div>
{% if record.id.value %}
<input type="hidden" name="{{ record_type }}-{{ no }}-id" value="{{ record.id.value }}" />
{% endif %}
<input id="{{ record_type }}-{{ no }}-ord" class="{{ record_type }}-ord" type="hidden" name="{{ record_type }}-{{ no }}-ord" value="{{ order }}" />
<div class="row">
<div class="col-lg-8">
<label for="{{ record_type }}-{{ no }}-summary" class="record-label">{{ _("Summary:")|force_escape }}</label>
<input id="{{ record_type }}-{{ no }}-summary" class="form-control record-summary {% if record.summary.errors %} is-invalid {% endif %}" type="text" name="{{ record_type }}-{{ no }}-summary" value="{{ record.summary.value|default:"" }}" maxlength="128" data-toggle="modal" data-target="#summary-modal" data-type="{{ record_type }}" data-no="{{ no }}" />
<div id="{{ record_type }}-{{ no }}-summary-error" class="invalid-feedback">{{ record.summary.errors.0|default:"" }}</div>
</div>
<div class="col-lg-4">
<label for="{{ record_type }}-{{ no }}-amount" class="record-label">{{ _("Amount:")|force_escape }}</label>
<input id="{{ record_type }}-{{ no }}-amount" class="form-control record-amount {{ record_type }}-to-sum {% if record.amount.errors %} is-invalid {% endif %}" type="number" step="0.01" min="0.01" name="{{ record_type }}-{{ no }}-amount" value="{{ record.amount.value|short_value }}" required="required" data-type="{{ record_type }}" />
<div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<label for="{{ record_type }}-{{ no }}-account" class="record-label">{{ _("Account:")|force_escape }}</label>
<select id="{{ record_type }}-{{ no }}-account" class="form-control record-account {{ record_type }}-account {% if record.account.errors %} is-invalid {% endif %}" name="{{ record_type }}-{{ no }}-account" data-type="{{ record_type }}">
{% if record.account is not None %}
<option value="{{ record.account.value|default:"" }}" selected="selected">{{ record.account.value|default:"" }} {{ record.account_title|default:"" }}</option>
{% else %}
<option value=""></option>
{% endif %}
<option value="">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</option>
</select>
<div id="{{ record_type }}-{{ no }}-account-error" class="invalid-feedback">{{ record.account.errors.0|default:"" }}</div>
</div>
</div>
</div>
<div>
<div class="btn-group-vertical btn-actions-{{ record_type }}">
<button class="btn btn-outline-secondary btn-sort-{{ record_type }}" type="button">
<i class="fas fa-sort"></i>
</button>
<button id="{{ record_type }}-{{ no }}-m-delete" type="button" class="btn btn-danger btn-del-record btn-del-{{ record_type }}" data-type="{{ record_type }}" data-no="{{ no }}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</li>

View File

@ -0,0 +1,108 @@
{% comment %}
The Mia Accounting Application
cash.html: The template for the accounting cash reports
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/9
{% endcomment %}
{% load i18n %}
{% load accounting %}
<!-- the accounting record search dialog -->
<form action="{% url "accounting:search" %}" method="GET">
<!-- The Modal -->
<div class="modal fade" id="accounting-search-modal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">{{ _("Search Accounting Records")|force_escape }}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<!-- Modal body -->
<div class="modal-body">
<label for="accounting-query">{{ _("Search:")|force_escape }}</label>
<input id="accounting-query" type="text" name="q" value="{% if request.resolver_match.url_name == "search" %}{{ request.GET.q }}{% endif %}" placeholder="{{ _("e.g. Coffee")|force_escape }}" required="required" />
</div>
<!-- Modal footer -->
<div class="modal-footer">
<button class="btn btn-primary" type="submit">
<i class="fas fa-search"></i>
{{ _("Search")|force_escape }}
</button>
</div>
</div>
</div>
</div>
</form>
<!-- the report chooser button -->
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<span class="d-none d-md-inline">
<i class="{{ current_report_icon }}"></i>
{{ current_report_title|force_escape }}
</span>
<span class="d-md-none">{{ _("Book")|force_escape }}</span>
</button>
{% report_url cash_account=cash_account ledger_account=ledger_account period=period as report_url %}
<div class="dropdown-menu account-picker">
<a class="dropdown-item {% if request.resolver_match.url_name == "cash" %} active {% endif %}" href="{{ report_url.cash }}">
<i class="fas fa-money-bill-wave"></i>
{{ _("Cash Account")|force_escape }}
</a>
<a class="dropdown-item {% if request.resolver_match.url_name == "cash-summary" %} active {% endif %}" href="{{ report_url.cash_summary }}">
<i class="fas fa-money-bill-wave"></i>
{{ _("Cash Summary")|force_escape }}
</a>
<a class="dropdown-item {% if request.resolver_match.url_name == "ledger" %} active {% endif %}" href="{{ report_url.ledger }}">
<i class="fas fa-file-invoice-dollar"></i>
{{ _("Ledger")|force_escape }}
</a>
<a class="dropdown-item {% if request.resolver_match.url_name == "ledger-summary" %} active {% endif %}" href="{{ report_url.ledger_summary }}">
<i class="fas fa-file-invoice-dollar"></i>
{{ _("Ledger Summary")|force_escape }}
</a>
<a class="dropdown-item {% if request.resolver_match.url_name == "journal" %} active {% endif %}" href="{{ report_url.journal }}">
<i class="fas fa-book"></i>
{{ _("Journal")|force_escape }}
</a>
<a class="dropdown-item {% if request.resolver_match.url_name == "trial-balance" %} active {% endif %}" href="{{ report_url.trial_balance }}">
<i class="fas fa-balance-scale-right"></i>
{{ _("Trial Balance")|force_escape }}
</a>
<a class="dropdown-item {% if request.resolver_match.url_name == "income-statement" %} active {% endif %}" href="{{ report_url.income_statement }}">
<i class="fas fa-file-invoice"></i>
{{ _("Income Statement")|force_escape }}
</a>
<a class="dropdown-item {% if request.resolver_match.url_name == "balance-sheet" %} active {% endif %}" href="{{ report_url.balance_sheet }}">
<i class="fas fa-balance-scale"></i>
{{ _("Balance Sheet")|force_escape }}
</a>
<span class="dropdown-item dropdown-search {% if request.resolver_match.url_name == "search" %} active {% endif %}" data-toggle="modal" data-target="#accounting-search-modal">
<i class="fas fa-search"></i>
{{ _("Search")|force_escape }}
</span>
<a class="dropdown-item {% if request.resolver_match.url_name == "accounts" %} active {% endif %}" href="{% url "accounting:accounts" %}">
<i class="fas fa-list-ol"></i>
{% trans "Accounts" context "Accounting" as text %}{{ text|force_escape }}
</a>
</div>
</div>

View File

@ -0,0 +1,164 @@
{% comment %}
The Mia Accounting Application
summary-helper.html: The view of the summary-helper dialog
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/4/3
{% endcomment %}
{% load i18n %}
<!-- the summary helper dialog -->
<!-- The Modal -->
<form id="summary-helper-form" action="" method="get">
<input id="summary-record" type="hidden" value="" />
<div class="modal fade" id="summary-modal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">
<label for="summary-summary">
<i class="fas fa-edit"></i>
{{ _("Summary")|force_escape }}
</label>
</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<!-- Modal body -->
<div class="modal-body">
<div class="summary-container">
<input id="summary-summary" class="form-control" value="" />
</div>
<ul class="nav nav-tabs">
<li class="nav-item">
<span id="summary-tab-category" class="summary-tab nav-link active" data-tab="category">{{ _("General")|force_escape }}</span>
</li>
<li class="nav-item">
<span id="summary-tab-travel" class="summary-tab nav-link" data-tab="travel">{{ _("Travel")|force_escape }}</span>
</li>
<li class="nav-item">
<span id="summary-tab-bus" class="summary-tab nav-link" data-tab="bus">{{ _("Bus")|force_escape }}</span>
</li>
<li class="nav-item">
<span id="summary-tab-regular" class="summary-tab nav-link" data-tab="regular">{{ _("Regular")|force_escape }}</span>
</li>
<li class="nav-item">
<span id="summary-tab-count" class="summary-tab nav-link" data-tab="count">{{ _("Count")|force_escape }}</span>
</li>
</ul>
<!-- A general category -->
<div id="summary-tab-content-category" class="summary-tab-content">
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-general-category">{{ _("Category:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-general-category" class="form-control summary-helper-input" type="text" value="" />
<div id="summary-general-categories-known" class="summary-categories-known"></div>
</div>
</div>
</div>
<!-- A general travel route -->
<div id="summary-tab-content-travel" class="summary-tab-content d-none">
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-travel-category">{{ _("Category:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-travel-category" class="form-control summary-helper-input summary-travel-part" type="text" value="" />
<div id="summary-travel-categories-known" class="summary-categories-known"></div>
</div>
</div>
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-travel-from">{{ _("From:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-travel-from" class="form-control summary-helper-input summary-travel-part" type="text" value="" />
</div>
</div>
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-travel-direction">{{ _("Direction:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-travel-direction" class="summary-helper-input" type="hidden" value="" />
<span id="btn-summary-one-way" class="btn btn-outline-primary btn-summary-helper btn-summary-travel-direction"></span>
<span class="btn btn-outline-primary btn-summary-helper btn-summary-travel-direction"></span>
</div>
</div>
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-travel-to">{{ _("To:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-travel-to" class="form-control summary-helper-input summary-travel-part" type="text" value="" />
</div>
</div>
</div>
<!-- A bus route -->
<div id="summary-tab-content-bus" class="summary-tab-content d-none">
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-bus-category">{{ _("Category:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-bus-category" class="form-control summary-helper-input summary-bus-part" type="text" value="" />
<div id="summary-bus-categories-known" class="summary-categories-known"></div>
</div>
</div>
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-bus-route">{{ _("Route:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-bus-route" class="form-control summary-helper-input summary-bus-part" type="text" value="" />
</div>
</div>
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-bus-from">{{ _("From:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-bus-from" class="form-control summary-helper-input summary-bus-part" type="text" value="" />
</div>
</div>
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-bus-to">{{ _("To:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-bus-to" class="form-control summary-helper-input summary-bus-part" type="text" value="" />
</div>
</div>
</div>
<!-- Regular accounts -->
<div id="summary-tab-content-regular" class="summary-tab-content d-none">
<div class="row">
<div class="col-sm-12">
<div id="summary-regular-accounts" class="summary-categories-known"></div>
</div>
</div>
</div>
<div id="summary-tab-content-count" class="summary-tab-content d-none">
<div class="row">
<label class="col-sm-2 col-form-label" for="summary-count">{{ _("Count:")|force_escape }}</label>
<div class="col-sm-10">
<input id="summary-count" class="form-control summary-helper-input" type="number" min="1" value="" />
</div>
</div>
</div>
</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
<button id="summary-confirm" class="btn btn-danger" type="submit" data-dismiss="modal">{{ _("Confirm")|force_escape }}</button>
</div>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,322 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
report-balance-sheet.html: The template for the balance sheets
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/20
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with prep_period=request.resolver_match.kwargs.period.prep_desc %}Balance Sheet {{ prep_period }}{% endblocktrans %}
{% setvar "title" title %}
{% add_lib "period-chooser" %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-edit"></i>
{% trans "New" context "Accounting" as text %}{{ text|force_escape }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url "accounting:transactions.create" "expense" as url %}{% url_with_return url %}">
{{ _("Cash Expense")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "income" as url %}{% url_with_return url %}">
{{ _("Cash Income")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "transfer" as url %}{% url_with_return url %}">
{{ _("Transfer")|force_escape }}
</a>
</div>
</div>
{% with current_report_icon="fas fa-balance-scale" current_report_title=_("Balance Sheet") period=request.resolver_match.kwargs.period %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#period-modal">
<i class="far fa-calendar-alt"></i>
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.period.description }}</span>
<span class="d-md-none">{{ _("Period")|force_escape }}</span>
</button>
</div>
{% with period=request.resolver_match.kwargs.period %}
{% include "mia_core/include/period-chooser.html" %}
{% endwith %}
{# The table for large screens #}
<div class="d-none d-lg-block report-block report-block-lg">
<div class="row justify-content-center">
<h2>{{ title|title_case }}</h2>
</div>
<div class="row">
<div class="col-sm-6">
<table class="table table-borderless table-hover table-sm balance-sheet-table">
<thead>
<tr>
<th colspan="3" scope="col">{{ assets.title|title_case }}</th>
</tr>
</thead>
<tbody>
{% for group in assets.groups %}
<tr class="group-title">
<td><div>{{ group.title|title_case }}</div></td>
<td class="amount"></td>
<td class="actions"></td>
</tr>
{% for account in group.details %}
<tr>
<td><div class="account">{{ account.title|title_case }}</div></td>
<td class="amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_amount }}</td>
<td class="actions">
<a href="{{ account.url }}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
{{ _("View")|force_escape }}
</a>
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
<div class="col-sm-6">
<table class="table table-borderless table-hover table-sm balance-sheet-table">
<thead>
<tr>
<th colspan="3" scope="col">{{ liabilities.title|title_case }}</th>
</tr>
</thead>
<tbody>
{% for group in liabilities.groups %}
<tr class="group-title">
<td><div>{{ group.title|title_case }}</div></td>
<td class="amount"></td>
<td class="actions"></td>
</tr>
{% for account in group.details %}
<tr>
<td><div class="account">{{ account.title|title_case }}</div></td>
<td class="amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_amount }}</td>
<td class="actions">
<a href="{{ account.url }}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
{{ _("View")|force_escape }}
</a>
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
<tfoot>
<tr class="total">
<td>{{ _("Total")|force_escape }}</td>
<td class="amount {% if liabilities.amount < 0 %} text-danger {% endif %}">
{{ liabilities.amount|accounting_amount }}
</td>
</tr>
</tfoot>
</table>
<table class="table table-borderless table-hover table-sm balance-sheet-table">
<thead>
<tr>
<th colspan="3" scope="col">{{ owners_equity.title|title_case }}</th>
</tr>
</thead>
<tbody>
{% for group in owners_equity.groups %}
<tr class="group-title">
<td><div>{{ group.title|title_case }}</div></td>
<td class="amount"></td>
<td class="actions"></td>
</tr>
{% for account in group.details %}
<tr>
<td><div class="account">{{ account.title|title_case }}</div></td>
<td class="amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_amount }}</td>
<td class="actions">
<a href="{{ account.url }}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
{{ _("View")|force_escape }}
</a>
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
<tfoot>
<tr class="total">
<td>{{ _("Total")|force_escape }}</td>
<td class="amount {% if owners_equity.amount < 0 %} text-danger {% endif %}">
{{ owners_equity.amount|accounting_amount }}
</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="row">
<div class="col-sm-6 assets-total">
<table class="table table-borderless table-hover table-sm balance-sheet-total-table">
<tfoot>
<tr class="total">
<td class="align-middle">{{ _("Total")|force_escape }}</td>
<td class="text-right align-middle font-italic {% if assets.amount < 0 %} text-danger {% endif %}">
{{ assets.amount|accounting_amount }}
</td>
</tr>
</tfoot>
</table>
</div>
<div class="col-sm-6 liabilities-total">
<table class="table table-borderless table-hover table-sm balance-sheet-total-table">
<tfoot>
<tr class="total">
<td class="align-middle">{{ _("Total")|force_escape }}</td>
<td class="text-right align-middle font-italic {% if liabilities.amount|add:owners_equity.amount < 0 %} text-danger {% endif %}">
{{ liabilities.amount|add:owners_equity.amount|accounting_amount }}
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
{# The list for small screens #}
<div class="d-lg-none report-block report-block-sm">
<div class="row justify-content-center">
<h2>{{ title|escape }}</h2>
</div>
<div class="row">
<div class="col-sm-6">
<ul class="list-group balance-sheet-list">
<li class="list-group-item section-title">
{{ assets.title|title_case }}
</li>
{% for group in assets.groups %}
<li class="list-group-item d-flex justify-content-between align-items-center group-title">
{{ group.title|title_case }}
</li>
{% for account in group.details %}
<li class="list-group-item d-flex justify-content-between align-items-center account">
<a class="list-group-item-action" href="{{ account.url }}">
{{ account.title|title_case }}
<div class="float-right">
<span class="badge {% if account.amount < 0 %} badge-warning {% else %} badge-secondary {% endif %} badge-pill">
{{ account.amount|accounting_amount }}
</span>
</div>
</a>
</li>
{% endfor %}
{% endfor %}
<li class="list-group-item d-flex justify-content-between align-items-center grand-total">
{{ _("Total")|force_escape }}
<span class="badge {% if assets.amount < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ assets.amount|accounting_amount }}
</span>
</li>
</ul>
</div>
<div class="col-sm-6">
<ul class="list-group balance-sheet-list">
<li class="list-group-item section-title">
{{ liabilities.title|title_case }}
</li>
{% for group in liabilities.groups %}
<li class="list-group-item d-flex justify-content-between align-items-center group-title">
{{ group.title|title_case }}
</li>
{% for account in group.details %}
<li class="list-group-item d-flex justify-content-between align-items-center account">
<a class="list-group-item-action" href="{{ account.url }}">
{{ account.title|title_case }}
<div class="float-right">
<span class="badge {% if account.amount < 0 %} badge-warning {% else %} badge-secondary {% endif %} badge-pill">
{{ account.amount|accounting_amount }}
</span>
</div>
</a>
</li>
{% endfor %}
{% endfor %}
<li class="list-group-item d-flex justify-content-between align-items-center total">
{{ _("Total")|force_escape }}
<span class="badge {% if liabilities.amount < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ liabilities.amount|accounting_amount }}
</span>
</li>
</ul>
<ul class="list-group balance-sheet-list">
<li class="list-group-item section-title">
{{ owners_equity.title|title_case }}
</li>
{% for group in owners_equity.groups %}
<li class="list-group-item d-flex justify-content-between align-items-center group-title">
{{ group.title|title_case }}
</li>
{% for account in group.details %}
<li class="list-group-item d-flex justify-content-between align-items-center account">
<a class="list-group-item-action" href="{{ account.url }}">
{{ account.title|title_case }}
<div class="float-right">
<span class="badge {% if account.amount < 0 %} badge-warning {% else %} badge-secondary {% endif %} badge-pill">
{{ account.amount|accounting_amount }}
</span>
</div>
</a>
</li>
{% endfor %}
{% endfor %}
<li class="list-group-item d-flex justify-content-between align-items-center total">
{{ _("Total")|force_escape }}
<span class="badge {% if owners_equity.amount < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ owners_equity.amount|accounting_amount }}
</span>
</li>
</ul>
<ul class="list-group balance-sheet-list">
<li class="list-group-item d-flex justify-content-between align-items-center grand-total">
{{ _("Total")|force_escape }}
<span class="badge {% if liabilities.amount|add:owners_equity.amount < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ liabilities.amount|add:owners_equity.amount|accounting_amount }}
</span>
</li>
</ul>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,163 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
report-cash-summary.html: The template for the cash account summaries
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/15
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with account=request.resolver_match.kwargs.account.title %}Cash Summary for {{ account }}{% endblocktrans %}
{% setvar "title" title %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-edit"></i>
{% trans "New" context "Accounting" as text %}{{ text|force_escape }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url "accounting:transactions.create" "expense" as url %}{% url_with_return url %}">
{{ _("Cash Expense")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "income" as url %}{% url_with_return url %}">
{{ _("Cash Income")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "transfer" as url %}{% url_with_return url %}">
{{ _("Transfer")|force_escape }}
</a>
</div>
</div>
{% with current_report_icon="fas fa-money-bill-wave" current_report_title=_("Cash Summary") cash_account=request.resolver_match.kwargs.account %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.account.title|title_case }}</span>
<span class="d-md-none">{{ _("Account")|force_escape }}</span>
</button>
<div class="dropdown-menu account-picker">
<div class="dropdown-header">{{ _("Shortcuts")|force_escape }}</div>
{% for account in shortcut_accounts %}
<a class="dropdown-item {% if account.code == request.resolver_match.kwargs.account.code %} active {% endif %}>" href="{% url "accounting:cash-summary" account %}">
{{ account.title|title_case }}
</a>
{% endfor %}
<div class="dropdown-header">{{ _("All")|force_escape }}</div>
{% for account in all_accounts %}
<a class="dropdown-item {% if account.code == request.resolver_match.kwargs.account.code %} active {% endif %}>" href="{% url "accounting:cash-summary" account %}">
{{ account.code }} {{ account.title|title_case }}
</a>
{% endfor %}
</div>
</div>
</div>
{% if month_list %}
{% include "mia_core/include/pagination.html" %}
{# The table for large screens #}
<table class="table table-striped table-hover d-none d-sm-table general-journal-table">
<thead>
<tr>
<th scope="col">{{ _("Month")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Income")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Expense")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Balance")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Cumulative Balance")|force_escape }}</th>
<th class="actions" scope="col">{{ _("View")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for month in month_list %}
<tr class="{% if month.balance < 0 %} table-danger {% endif %}">
<td>{{ month.label }}</td>
<td class="amount">{{ month.credit|accounting_amount }}</td>
<td class="amount">{{ month.debit|accounting_amount }}</td>
<td class="amount {% if month.balance < 0 %} text-danger {% endif %}">{{ month.balance|accounting_amount }}</td>
<td class="amount {% if month.cumulative_balance < 0 %} text-danger {% endif %}">{{ month.cumulative_balance|accounting_amount }}</td>
<td class="actions">
{% if month.month is not None %}
<a class="btn btn-info" role="button" href="{% url "accounting:cash" request.resolver_match.kwargs.account month.month|date:"Y-m" %}">
<i class="fas fa-eye"></i>
<span class="d-none d-lg-inline">{{ _("View")|force_escape }}</span>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{# The list for small screens #}
<ul class="list-group d-sm-none">
{% for month in month_list %}
<li class="list-group-item {% if month.balance < 0 %} list-group-item-danger {% endif %}">
{% if month.month is not None %}
<a class="list-group-item-action d-flex justify-content-between align-items-center" href="{% url "accounting:cash" request.resolver_match.kwargs.account month.month|date:"Y-m" %}">
{{ month.label }}
<div>
<span class="badge badge-success badge-pill">
{{ month.credit|short_amount }}
</span>
<span class="badge badge-warning badge-pill">
{{ month.debit|short_amount }}
</span>
<span class="badge {% if month.balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ month.balance|short_amount }}
</span>
<span class="badge {% if month.cumulative_balance < 0 %} badge-danger {% else %} badge-primary {% endif %} badge-pill">
{{ month.cumulative_balance|short_amount }}
</span>
</div>
</a>
{% else %}
<div class="d-flex justify-content-between align-items-center">
{{ month.label }}
<div>
<span class="badge badge-success badge-pill">
{{ month.credit|short_amount }}
</span>
<span class="badge badge-warning badge-pill">
{{ month.debit|short_amount }}
</span>
<span class="badge {% if month.balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ month.balance|short_amount }}
</span>
<span class="badge {% if month.cumulative_balance < 0 %} badge-danger {% else %} badge-primary {% endif %} badge-pill">
{{ month.cumulative_balance|short_amount }}
</span>
</div>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ _("There is currently no data.")|force_escape }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,205 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
report-cash.html: The template for the cash accounts
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/1
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with account=request.resolver_match.kwargs.account.title prep_period=request.resolver_match.kwargs.period.prep_desc %}Cash Account for {{ account }} {{ prep_period }}{% endblocktrans %}
{% setvar "title" title %}
{% add_lib "period-chooser" %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-edit"></i>
{% trans "New" context "Accounting" as text %}{{ text|force_escape }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url "accounting:transactions.create" "expense" as url %}{% url_with_return url %}">
{{ _("Cash Expense")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "income" as url %}{% url_with_return url %}">
{{ _("Cash Income")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "transfer" as url %}{% url_with_return url %}">
{{ _("Transfer")|force_escape }}
</a>
</div>
</div>
{% with current_report_icon="fas fa-money-bill-wave" current_report_title=_("Cash Account") cash_account=request.resolver_match.kwargs.account period=request.resolver_match.kwargs.period %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.account.title|title_case }}</span>
<span class="d-md-none">{{ _("Account")|force_escape }}</span>
</button>
<div class="dropdown-menu account-picker">
<div class="dropdown-header">{{ _("Shortcuts")|force_escape }}</div>
{% for account in shortcut_accounts %}
<a class="dropdown-item {% if account.code == request.resolver_match.kwargs.account.code %} active {% endif %}>" href="{% url "accounting:cash" account request.resolver_match.kwargs.period %}">
{{ account.title|title_case }}
</a>
{% endfor %}
<div class="dropdown-header">{{ _("All")|force_escape }}</div>
{% for account in all_accounts %}
<a class="dropdown-item {% if account.code == request.resolver_match.kwargs.account.code %} active {% endif %}>" href="{% url "accounting:cash" account request.resolver_match.kwargs.period %}">
{{ account.code }} {{ account.title|title_case }}
</a>
{% endfor %}
</div>
</div>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#period-modal">
<i class="far fa-calendar-alt"></i>
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.period.description }}</span>
<span class="d-md-none">{{ _("Period")|force_escape }}</span>
</button>
</div>
{% with period=request.resolver_match.kwargs.period %}
{% include "mia_core/include/period-chooser.html" %}
{% endwith %}
{% if record_list %}
{% include "mia_core/include/pagination.html" %}
{# The table for large screens #}
<table class="table table-striped table-hover d-none d-md-table general-journal-table">
<thead>
<tr>
<th scope="col">{{ _("Date")|force_escape }}</th>
<th scope="col">{{ _("Account")|force_escape }}</th>
<th scope="col">{{ _("Summary")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Income")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Expense")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Balance")|force_escape }}</th>
<th class="actions" scope="col">{{ _("View")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for record in record_list %}
<tr class="{% if not record.is_balanced or record.has_order_hole %} table-danger {% endif %}">
<td>{{ record.transaction.date|smart_date }}</td>
<td>{{ record.account.title|title_case }}</td>
<td>{{ record.summary|default:"" }}{% if not record.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}{% if record.has_order_hole %}
<span class="badge badge-danger badge-pill">
{{ _("Need Reorder")|force_escape }}
</span>
{% endif %}</td>
<td class="amount">{{ record.credit_amount|accounting_amount }}</td>
<td class="amount">{{ record.debit_amount|accounting_amount }}</td>
<td class="amount {% if record.balance < 0 %} text-danger {% endif %}">{{ record.balance|accounting_amount }}</td>
<td class="actions">
{% if record.pk is not None %}
<a href="{% url "accounting:transactions.detail" record.transaction.type record.transaction as url %}{% url_with_return url %}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
<span class="d-none d-lg-inline">{{ _("View")|force_escape }}</span>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{# The list for small screens #}
<ul class="list-group d-md-none">
{% for record in record_list %}
<li class="list-group-item {% if not record.is_balanced or record.has_order_hole %} list-group-item-danger {% endif %}">
{% if record.pk is not None %}
<a class="list-group-item-action" href="{% url "accounting:transactions.detail" record.transaction.type record.transaction as url %}{% url_with_return url %}">
<div class="date-account-line d-flex justify-content-between align-items-center">
{{ record.transaction.date|smart_date }} {{ record.account.title|title_case }}
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
{{ record.summary|default:"" }}
{% if not record.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}
{% if record.has_order_hole %}
<span class="badge badge-danger badge-pill">
{{ _("Need Reorder")|force_escape }}
</span>
{% endif %}
</div>
</div>
<div>
{% if record.credit_amount is not None %}
<span class="badge badge-success badge-pill">
{{ record.credit_amount|short_amount }}
</span>
{% endif %}
{% if record.debit_amount is not None %}
<span class="badge badge-warning badge-pill">
-{{ record.debit_amount|short_amount }}
</span>
{% endif %}
<span class="badge {% if record.balance < 0 %} badge-danger {% else %} badge-primary {% endif %} badge-pill">
{{ record.balance|short_amount }}
</span>
</div>
</a>
{% else %}
<div class="date-account-line d-flex justify-content-between align-items-center">
{{ record.transaction.date|smart_date }} {{ record.account.title }}
</div>
<div class="d-flex justify-content-between align-items-center">
<div>{{ record.summary|default:"" }}</div>
</div>
<div>
{% if record.credit_amount is not None %}
<span class="badge badge-success badge-pill">
{{ record.credit_amount|short_amount }}
</span>
{% endif %}
{% if record.debit_amount is not None %}
<span class="badge badge-warning badge-pill">
-{{ record.debit_amount|short_amount }}
</span>
{% endif %}
<span class="badge {% if record.balance < 0 %} badge-danger {% else %} badge-primary {% endif %} badge-pill">
{{ record.balance|short_amount }}
</span>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ _("There is currently no data.")|force_escape }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,213 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
report-income-statement.html: The template for the income statements
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/19
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with prep_period=request.resolver_match.kwargs.period.prep_desc %}Income Statement {{ prep_period }}{% endblocktrans %}
{% setvar "title" title %}
{% add_lib "period-chooser" %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-edit"></i>
{% trans "New" context "Accounting" as text %}{{ text|force_escape }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url "accounting:transactions.create" "expense" as url %}{% url_with_return url %}">
{{ _("Cash Expense")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "income" as url %}{% url_with_return url %}">
{{ _("Cash Income")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "transfer" as url %}{% url_with_return url %}">
{{ _("Transfer")|force_escape }}
</a>
</div>
</div>
{% with current_report_icon="fas fa-file-invoice" current_report_title=_("Income Statement") period=request.resolver_match.kwargs.period %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#period-modal">
<i class="far fa-calendar-alt"></i>
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.period.description }}</span>
<span class="d-md-none">{{ _("Period")|force_escape }}</span>
</button>
</div>
{% with period=request.resolver_match.kwargs.period %}
{% include "mia_core/include/period-chooser.html" %}
{% endwith %}
{# The table for large screens #}
<div class="d-none d-sm-block report-block report-block-lg">
<div class="row justify-content-center">
<h2>{{ title|title_case }}</h2>
</div>
<div class="row">
<div class="col-sm-12">
<table class="table table-borderless table-hover table-sm income-statement-table">
<thead>
<tr>
<th scope="col"></th>
<th class="amount" colspan="2" scope="col">{{ _("Amount")|force_escape }}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for section in section_list %}
<tr class="section-title">
<td><div>{{ section.title|title_case }}</div></td>
<td class="amount"></td>
<td class="amount"></td>
<td class="actions"></td>
</tr>
{% if section.groups %}
{% for group in section.groups %}
<tr class="group-title">
<td><div class="group-title">{{ group.title|title_case }}</div></td>
<td class="amount"></td>
<td class="amount"></td>
<td class="actions"></td>
</tr>
{% for account in group.details %}
<tr>
<td><div class="account">{{ account.title|title_case }}</div></td>
<td class="amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_amount }}</td>
<td class="amount"></td>
<td class="actions">
<a href="{% url "accounting:ledger" account request.resolver_match.kwargs.period %}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
<span class="d-none d-lg-inline">{{ _("View")|force_escape }}</span>
</a>
</td>
</tr>
{% endfor %}
<tr class="total">
<td><div>{{ _("Total")|force_escape }}</div></td>
<td class="amount"></td>
<td class="amount {% if group.amount < 0 %} text-danger {% endif %}">{{ group.amount|accounting_amount }}</td>
<td class="actions"></td>
</tr>
{% endfor %}
{% else %}
<tr class="total">
<td><div>{{ _("Total")|force_escape }}</div></td>
<td class="amount"></td>
<td class="amount">-</td>
<td class="actions"></td>
</tr>
{% endif %}
{% if section.cumulative_total is not None %}
<tr class="cumulative-total">
<td><div>{{ section.cumulative_total.title|title_case }}</div></td>
<td class="amount"></td>
<td class="amount {% if section.cumulative_total.amount < 0 %} text-danger {% endif %}">{{ section.cumulative_total.amount|accounting_amount }}</td>
<td class="actions"></td>
</tr>
{% endif %}
{% if section.has_next %}
<tr><td colspan="4"></td></tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# The list for small screens #}
<div class="d-sm-none report-block report-block-sm">
<div class="row justify-content-center">
<h2>{{ title }}</h2>
</div>
<div class="row">
<div class="col-sm-12">
<ul class="list-group income-statement-list">
{% for section in section_list %}
<li class="list-group-item d-flex justify-content-between align-items-center section-title">
{{ section.title|title_case }}
</li>
{% if section.groups %}
{% for group in section.groups %}
<li class="list-group-item d-flex justify-content-between align-items-center group-title">
{{ group.title|title_case }}
</li>
{% for account in group.details %}
<li class="list-group-item d-flex justify-content-between align-items-center account">
<a class="list-group-item-action" href="{% url "accounting:ledger" account request.resolver_match.kwargs.period %}">
{{ account.title|title_case }}
<div class="float-right">
<span class="badge {% if account.amount < 0 %} badge-warning {% else %} badge-secondary {% endif %} badge-pill">
{{ account.amount|short_amount }}
</span>
</div>
</a>
</li>
{% endfor %}
<li class="list-group-item d-flex justify-content-between align-items-center total">
{{ _("Total")|force_escape }}
<div class="float-right">
<span class="badge {% if group.amount < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ group.amount|short_amount }}
</span>
</div>
</li>
{% endfor %}
{% else %}
<li class="list-group-item d-flex justify-content-between align-items-center total">
{{ _("Total")|force_escape }}
<div class="float-right">
<span class="badge {% if group.amount < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">-</span>
</div>
</li>
{% endif %}
{% if section.cumulative_total is not None %}
<li class="list-group-item d-flex justify-content-between align-items-center cumulative-total">
{{ section.cumulative_total.title|title_case }}
<div class="float-right">
<span class="badge {% if section.cumulative_total.amount < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ section.cumulative_total.amount|short_amount }}
</span>
</div>
</li>
{% endif %}
{% if section.has_next %}
<li class="list-group-item d-flex justify-content-between align-items-center"></li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,192 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
report-journal.html: The template for the accounting journals
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/17
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with prep_period=request.resolver_match.kwargs.period.prep_desc %}Journal {{ prep_period }}{% endblocktrans %}
{% setvar "title" title %}
{% add_lib "period-chooser" %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-edit"></i>
{% trans "New" context "Accounting" as text %}{{ text|force_escape }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url "accounting:transactions.create" "expense" as url %}{% url_with_return url %}">
{{ _("Cash Expense")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "income" as url %}{% url_with_return url %}">
{{ _("Cash Income")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "transfer" as url %}{% url_with_return url %}">
{{ _("Transfer")|force_escape }}
</a>
</div>
</div>
{% with current_report_icon="fas fa-book" current_report_title=_("Journal") period=request.resolver_match.kwargs.period %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#period-modal">
<i class="far fa-calendar-alt"></i>
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.period.description }}</span>
<span class="d-md-none">{{ _("Period")|force_escape }}</span>
</button>
</div>
{% with period=request.resolver_match.kwargs.period %}
{% include "mia_core/include/period-chooser.html" %}
{% endwith %}
{% if record_list %}
{% include "mia_core/include/pagination.html" %}
{# The table for large screens #}
<table class="table table-striped table-hover d-none d-lg-table general-journal-table">
<thead>
<tr>
<th scope="col">{{ _("Date")|force_escape }}</th>
<th scope="col">{{ _("Account")|force_escape }}</th>
<th scope="col">{{ _("Summary")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Debit")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Credit")|force_escape }}</th>
<th scope="col">{{ _("Notes")|force_escape }}</th>
<th class="actions" scope="col">{{ _("View")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for record in record_list %}
<tr class="{% if not record.is_balanced or record.has_order_hole %} table-danger {% endif %}">
<td>{{ record.transaction.date|smart_date }}</td>
<td>{{ record.account.title|title_case }}</td>
<td><div class="{% if record.is_credit %} journal-credit {% else %} journal-debit {% endif %}">{{ record.summary|default:"" }}{% if not record.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}{% if record.has_order_hole %}
<span class="badge badge-danger badge-pill">
{{ _("Need Reorder")|force_escape }}
</span>
{% endif %}</div></td>
<td class="amount">{{ record.debit_amount|accounting_amount }}</td>
<td class="amount">{{ record.credit_amount|accounting_amount }}</td>
<td>{{ record.transaction.note|default:"" }}</td>
<td class="actions">
{% if record.pk is not None %}
<a href="{% url "accounting:transactions.detail" record.transaction.type record.transaction as url %}{% url_with_return url %}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
{{ _("View")|force_escape }}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{# The list for small screens #}
<ul class="list-group d-lg-none">
{% for record in record_list %}
<li class="list-group-item {% if not record.is_balanced or record.has_order_hole %} list-group-item-danger {% endif %}">
{% if record.pk is not None %}
<a class="list-group-item-action" href="{% url "accounting:transactions.detail" record.transaction.type record.transaction as url %}{% url_with_return url %}">
<div class="{% if record.is_credit %} journal-credit {% else %} journal-debit {% endif %}">
<div class="date-account-line">
{{ record.transaction.date|smart_date }} {{ record.account.title|title_case }}
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
{{ record.summary|default:"" }}
{% if not record.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}
{% if record.has_order_hole %}
<span class="badge badge-danger badge-pill">
{{ _("Need Reorder")|force_escape }}
</span>
{% endif %}
</div>
<div>
{% if record.debit_amount is not None %}
<span class="badge badge-success badge-pill">
{{ record.debit_amount|short_amount }}
</span>
{% endif %}
{% if record.credit_amount is not None %}
<span class="badge badge-warning badge-pill">
{{ record.credit_amount|short_amount }}
</span>
{% endif %}
</div>
</div>
<div>{{ record.transaction.note|default:"" }}</div>
</div>
</a>
{% else %}
<div class="{% if record.is_credit %} journal-credit {% else %} journal-debit {% endif %}">
<div class="date-account-line">
{{ record.transaction.date|smart_date }} {{ record.account.title|title_case }}
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
{{ record.summary|default:"" }}
{% if not record.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}
</div>
<div>
{% if record.debit_amount is not None %}
<span class="badge badge-success badge-pill">
{{ record.debit_amount|short_amount }}
</span>
{% endif %}
{% if record.credit_amount is not None %}
<span class="badge badge-warning badge-pill">
{{ record.credit_amount|short_amount }}
</span>
{% endif %}
</div>
</div>
<div>{{ record.transaction.note|default:"" }}</div>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ _("There is currently no data.")|force_escape }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,162 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
report-ledger-summary.html: The template for the ledger summaries
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/16
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with account=request.resolver_match.kwargs.account.title %}Ledger Summary for {{ account }}{% endblocktrans %}
{% setvar "title" title %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-edit"></i>
{% trans "New" context "Accounting" as text %}{{ text|force_escape }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url "accounting:transactions.create" "expense" as url %}{% url_with_return url %}">
{{ _("Cash Expense")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "income" as url %}{% url_with_return url %}">
{{ _("Cash Income")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "transfer" as url %}{% url_with_return url %}">
{{ _("Transfer")|force_escape }}
</a>
</div>
</div>
{% with current_report_icon="fas fa-file-invoice-dollar" current_report_title=_("Ledger Summary") ledger_account=request.resolver_match.kwargs.account %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.account.title|title_case }}</span>
<span class="d-md-none">{{ _("Account")|force_escape }}</span>
</button>
<div class="dropdown-menu account-picker">
{% for account in accounts %}
<a class="dropdown-item {% if account.code == request.resolver_match.kwargs.account.code %} active {% endif %}>" href="{% url "accounting:ledger-summary" account %}">
{{ account.title|title_case }}
</a>
{% endfor %}
</div>
</div>
</div>
{% if month_list %}
{% include "mia_core/include/pagination.html" %}
{# The table for large screens #}
<table class="table table-striped table-hover d-none d-sm-table general-journal-table">
<thead>
<tr>
<th scope="col">{{ _("Month")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Debit")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Credit")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Balance")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Cumulative Balance")|force_escape }}</th>
<th class="actions" scope="col">{{ _("View")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for month in month_list %}
<tr class="{% if request.resolver_match.kwargs.account.code|first in "12" and month.balance < 0 %} table-danger {% endif %}">
<td>{{ month.label }}</td>
<td class="amount">{{ month.debit|accounting_amount }}</td>
<td class="amount">{{ month.credit|accounting_amount }}</td>
<td class="amount {% if month.balance < 0 %} text-danger {% endif %}">{{ month.balance|accounting_amount }}</td>
<td class="amount {% if month.cumulative_balance < 0 %} text-danger {% endif %}">{{ month.cumulative_balance|accounting_amount }}</td>
<td class="actions">
{% if month.month is not None %}
<a class="btn btn-info" role="button" href="{% url "accounting:ledger" request.resolver_match.kwargs.account month.month|date:"Y-m" %}">
<i class="fas fa-eye"></i>
<span class="d-none d-lg-inline">{{ _("View")|force_escape }}</span>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{# The list for small screens #}
<ul class="list-group d-sm-none">
{% for month in month_list %}
<li class="list-group-item {% if request.resolver_match.kwargs.account.code|first in "12" and month.balance < 0 %} list-group-item-danger {% endif %}">
{% if month.month is not None %}
<a class="list-group-item-action d-flex justify-content-between align-items-center" href="{% url "accounting:ledger" request.resolver_match.kwargs.account month.month|date:"Y-m" %}">
{{ month.label }}
<div>
<span class="badge badge-success badge-pill">
{{ month.debit|short_amount }}
</span>
<span class="badge badge-warning badge-pill">
{{ month.credit|short_amount }}
</span>
<span class="badge {% if month.balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ month.balance|short_amount }}
</span>
<span class="badge {% if month.cumulative_balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ month.cumulative_balance|short_amount }}
</span>
<span class="badge {% if month.balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ month.balance|short_amount }}
</span>
<span class="badge {% if month.cumulative_balance < 0 %} badge-danger {% else %} badge-primary {% endif %} badge-pill">
{{ month.cumulative_balance|short_amount }}
</span>
</div>
</a>
{% else %}
<div class="d-flex justify-content-between align-items-center">
{{ month.label }}
<div>
<span class="badge badge-success badge-pill">
{{ month.debit|short_amount }}
</span>
<span class="badge badge-warning badge-pill">
{{ month.credit|short_amount }}
</span>
<span class="badge {% if month.balance < 0 %} badge-danger {% else %} badge-info {% endif %} badge-pill">
{{ month.balance|short_amount }}
</span>
<span class="badge {% if month.cumulative_balance < 0 %} badge-danger {% else %} badge-primary {% endif %} badge-pill">
{{ month.cumulative_balance|short_amount }}
</span>
</div>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ _("There is currently no data.")|force_escape }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,218 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
report-ledger.html: The template for the ledgers
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/16
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with account=request.resolver_match.kwargs.account.title prep_period=request.resolver_match.kwargs.period.prep_desc %}Ledger for {{ account }} {{ prep_period }}{% endblocktrans %}
{% setvar "title" title %}
{% add_lib "period-chooser" %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-edit"></i>
{% trans "New" context "Accounting" as text %}{{ text|force_escape }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url "accounting:transactions.create" "expense" as url %}{% url_with_return url %}">
{{ _("Cash Expense")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "income" as url %}{% url_with_return url %}">
{{ _("Cash Income")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "transfer" as url %}{% url_with_return url %}">
{{ _("Transfer")|force_escape }}
</a>
</div>
</div>
{% with current_report_icon="fas fa-file-invoice-dollar" current_report_title=_("Ledger") ledger_account=request.resolver_match.kwargs.account period=request.resolver_match.kwargs.period %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.account.title|title_case }}</span>
<span class="d-md-none">{{ _("Account")|force_escape }}</span>
</button>
<div class="dropdown-menu account-picker">
{% for account in accounts %}
<a class="dropdown-item {% if account.code == request.resolver_match.kwargs.account.code %} active {% endif %}" href="{% url "accounting:ledger" account request.resolver_match.kwargs.period %}">
{{ account.code }} {{ account.title|title_case }}
</a>
{% endfor %}
</div>
</div>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#period-modal">
<i class="far fa-calendar-alt"></i>
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.period.description }}</span>
<span class="d-md-none">{{ _("Period")|force_escape }}</span>
</button>
</div>
{% with period=request.resolver_match.kwargs.period %}
{% include "mia_core/include/period-chooser.html" %}
{% endwith %}
{% if record_list %}
{% include "mia_core/include/pagination.html" %}
{# The table for large screens #}
<table class="table table-striped table-hover d-none d-md-table general-journal-table">
<thead>
<tr>
<th scope="col">{{ _("Date")|force_escape }}</th>
<th scope="col">{{ _("Account")|force_escape }}</th>
<th scope="col">{{ _("Summary")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Debit")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Credit")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Balance")|force_escape }}</th>
<th class="actions" scope="col">{{ _("View")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for record in record_list %}
<tr class="{% if not record.is_balanced or record.has_order_hole or record.is_payable %} table-danger {% endif %}{% if record.is_existing_equipment %} table-info {% endif %}">
<td>{{ record.transaction.date|smart_date }}</td>
<td>{{ record.account.title|title_case }}</td>
<td>{{ record.summary|default:"" }}{% if not record.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}{% if record.has_order_hole %}
<span class="badge badge-danger badge-pill">
{{ _("Need Reorder")|force_escape }}
</span>
{% endif %}{% if record.is_payable %}
<span class="badge badge-danger badge-pill">
{{ _("Payable")|force_escape }}
</span>
{% endif %}{% if record.is_existing_equipment %}
<span class="badge badge-info badge-pill">
{{ _("Existing")|force_escape }}
</span>
{% endif %}</td>
<td class="amount">{{ record.debit_amount|accounting_amount }}</td>
<td class="amount">{{ record.credit_amount|accounting_amount }}</td>
<td class="amount {% if record.balance < 0 %} text-danger {% endif %}">{{ record.balance|accounting_amount }}</td>
<td class="actions">
{% if record.pk is not None %}
<a href="{% url "accounting:transactions.detail" record.transaction.type record.transaction as url %}{% url_with_return url %}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
<span class="d-none d-lg-inline">{{ _("View")|force_escape }}</span>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{# The list for small screens #}
<ul class="list-group d-md-none">
{% for record in record_list %}
<li class="list-group-item {% if not record.is_balanced or record.has_order_hole or record.is_payable %} list-group-item-danger {% endif %}{% if record.is_existing_equipment %} list-group-item-info {% endif %}">
{% if record.pk is not None %}
<a class="list-group-item-action" href="{% url "accounting:transactions.detail" record.transaction.type record.transaction as url %}{% url_with_return url %}">
<div class="date-account-line">
{{ record.transaction.date|smart_date }} {{ record.account.title|title_case }}
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
{{ record.summary|default:"" }}
{% if not record.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}
{% if record.has_order_hole %}
<span class="badge badge-danger badge-pill">
{{ _("Need Reorder")|force_escape }}
</span>
{% endif %}
{% if record.is_payable %}
<span class="badge badge-danger badge-pill">
{{ _("Payable")|force_escape }}
</span>
{% endif %}
{% if record.is_existing_equipment %}
<span class="badge badge-info badge-pill">
{{ _("Existing")|force_escape }}
</span>
{% endif %}
</div>
<div>
{% if record.debit_amount is not None %}
<span class="badge badge-success badge-pill">
{{ record.debit_amount|short_amount }}
</span>
{% endif %}
{% if record.credit_amount is not None %}
<span class="badge badge-warning badge-pill">
{{ record.credit_amount|short_amount }}
</span>
{% endif %}
<span class="badge {% if record.balance < 0 %} badge-danger {% else %} badge-primary {% endif %} badge-pill">
{{ record.balance|short_amount }}
</span>
</div>
</div>
</a>
{% else %}
<div class="date-account-line">
{{ record.transaction.date|smart_date }} {{ record.account.title|title_case }}
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
{{ record.summary|default:"" }}
</div>
<div>
{% if record.debit_amount is not None %}
<span class="badge badge-success badge-pill">
{{ record.debit_amount|short_amount }}
</span>
{% endif %}
{% if record.credit_amount is not None %}
<span class="badge badge-warning badge-pill">
{{ record.credit_amount|short_amount }}
</span>
{% endif %}
<span class="badge {% if record.balance < 0 %} badge-danger {% else %} badge-primary {% endif %} badge-pill">
{{ record.balance|short_amount }}
</span>
</div>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ _("There is currently no data.")|force_escape }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,164 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
report-trial-balance.html: The template for the trial balances
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/19
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with prep_period=request.resolver_match.kwargs.period.prep_desc %}Trial Balance {{ prep_period }}{% endblocktrans %}
{% setvar "title" title %}
{% add_lib "period-chooser" %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-edit"></i>
{% trans "New" context "Accounting" as text %}{{ text|force_escape }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url "accounting:transactions.create" "expense" as url %}{% url_with_return url %}">
{{ _("Cash Expense")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "income" as url %}{% url_with_return url %}">
{{ _("Cash Income")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "transfer" as url %}{% url_with_return url %}">
{{ _("Transfer")|force_escape }}
</a>
</div>
</div>
{% with current_report_icon="fas fa-balance-scale-right" current_report_title=_("Trial Balance") period=request.resolver_match.kwargs.period %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#period-modal">
<i class="far fa-calendar-alt"></i>
<span class="d-none d-md-inline">{{ request.resolver_match.kwargs.period.description }}</span>
<span class="d-md-none">{{ _("Period")|force_escape }}</span>
</button>
</div>
{% with period=request.resolver_match.kwargs.period %}
{% include "mia_core/include/period-chooser.html" %}
{% endwith %}
{% if account_list %}
{% include "mia_core/include/pagination.html" %}
{# The table for large screens #}
<div class="d-none d-sm-block report-block report-block-lg">
<div class="row justify-content-center">
<h2>{{ title|title_case }}</h2>
</div>
<div class="row">
<div class="col-sm-12">
<table class="table table-borderless table-hover trial-balance-table">
<thead>
<tr>
<th scope="col">{{ _("Account")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Debit")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Credit")|force_escape }}</th>
<th class="actions" scope="col">{{ _("View")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for account in account_list %}
<tr>
<td>{{ account.title|title_case }}</td>
<td class="amount">{{ account.debit_amount|accounting_amount }}</td>
<td class="amount">{{ account.credit_amount|accounting_amount }}</td>
<td class="actions">
<a href="{% url "accounting:ledger" account request.resolver_match.kwargs.period %}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
<span class="d-none d-lg-inline">{{ _("View")|force_escape }}</span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td>{{ _("Total")|force_escape }}</td>
<td class="amount">{{ total_item.debit_amount|accounting_amount }}</td>
<td class="amount">{{ total_item.credit_amount|accounting_amount }}</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
{# The list for mobile browsers #}
<div class="d-sm-none report-block report-block-sm">
<div class="row justify-content-center">
<h2>{{ title|force_escape }}</h2>
</div>
<div class="row">
<div class="col-sm-12">
<ul class="list-group d-lg-none trial-balance-list">
{% for account in account_list %}
<li class="list-group-item">
<a class="list-group-item-action d-flex justify-content-between align-items-center" href="{% url "accounting:ledger" account request.resolver_match.kwargs.period %}">
{{ account.title|title_case }}
<div>
{% if account.debit_amount is not None %}
<span class="badge badge-success badge-pill">
{{ account.debit_amount|short_amount }}
</span>
{% endif %}
{% if account.credit_amount is not None %}
<span class="badge badge-warning badge-pill">
{{ account.credit_amount|short_amount }}
</span>
{% endif %}
</div>
</a>
</li>
{% endfor %}
<li class="list-group-item d-flex justify-content-between align-items-center total">
{{ _("Total")|force_escape }}
<div>
<span class="badge badge-success badge-pill">
{{ total_item.debit_amount|short_amount }}
</span>
<span class="badge badge-warning badge-pill">
{{ total_item.credit_amount|short_amount }}
</span>
</div>
</li>
</ul>
</div>
</div>
</div>
{% else %}
<p>{{ _("There is currently no data.")|force_escape }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,158 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
search.html: The template for the search results
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/21
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with query=request.GET.q %}Search Result for “{{ query }}”{% endblocktrans %}
{% setvar "title" title %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-edit"></i>
{% trans "New" context "Accounting" as text %}{{ text|force_escape }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url "accounting:transactions.create" "expense" as url %}{% url_with_return url %}">
{{ _("Cash Expense")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "income" as url %}{% url_with_return url %}">
{{ _("Cash Income")|force_escape }}
</a>
<a class="dropdown-item" href="{% url "accounting:transactions.create" "transfer" as url %}{% url_with_return url %}">
{{ _("Transfer")|force_escape }}
</a>
</div>
</div>
{% with current_report_icon="fas fa-search" current_report_title=_("Search") %}
{% include "accounting/include/report-chooser.html" %}
{% endwith %}
<form class="btn btn-primary input-group" action="{% url "accounting:search" %}" method="get">
<input id="search-input" class="form-control form-control-sm search-input" type="text" name="q" value="{{ request.GET.q }}" />
<label for="search-input" class="search-label">
<button type="submit">
<i class="fas fa-search"></i>
{{ _("Search")|force_escape }}
</button>
</label>
</form>
</div>
{% if record_list %}
{% include "mia_core/include/pagination.html" %}
{# The table for large screens #}
<table class="table table-striped table-hover d-none d-md-table general-journal-table">
<thead>
<tr>
<th scope="col">{{ _("Date")|force_escape }}</th>
<th scope="col">{{ _("Account")|force_escape }}</th>
<th scope="col">{{ _("Summary")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Debit")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Credit")|force_escape }}</th>
<th scope="col">{{ _("Notes")|force_escape }}</th>
<th class="actions" scope="col">{{ _("View")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for record in record_list %}
<tr class="{% if not record.is_balanced or record.has_order_hole %} table-danger {% endif %}">
<td>{{ record.transaction.date|smart_date }}</td>
<td>{{ record.account.title|title_case }}</td>
<td><div class="{% if record.is_credit %} journal-credit {% else %} journal-debit {% endif %}">{{ record.summary|default:"" }}{% if not record.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}{% if record.has_order_hole %}
<span class="badge badge-danger badge-pill">
{{ _("Need Reorder")|force_escape }}
</span>
{% endif %}</div></td>
<td class="amount">{{ record.debit_amount|accounting_amount }}</td>
<td class="amount">{{ record.credit_amount|accounting_amount }}</td>
<td>{{ record.transaction.notes|default:"" }}</td>
<td class="actions">
<a href="{% url "accounting:transactions.detail" record.transaction.type record.transaction as url %}{% url_with_return url %}" class="btn btn-info" role="button">
<i class="fas fa-eye"></i>
{{ _("View")|force_escape }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{# The list for small screens #}
<ul class="list-group d-md-none">
{% for record in record_list %}
<li class="list-group-item {% if not record.is_balanced or record.has_order_hole %} list-group-item-danger {% endif %}">
<a class="list-group-item-action" href="{% url "accounting:transactions.detail" record.transaction.type record.transaction as url %}{% url_with_return url %}">
<div class="{% if record.is_credit %} journal-credit {% else %} journal-debit {% endif %}">
<div class="date-account-line">
{{ record.transaction.date|smart_date }} {{ record.account.title|title_case }}
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
{{ record.summary|default:"" }}
{% if not record.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}
{% if record.has_order_hole %}
<span class="badge badge-danger badge-pill">
{{ _("Need Reorder")|force_escape }}
</span>
{% endif %}
</div>
<div>
{% if record.debit_amount is not None %}
<span class="badge badge-success badge-pill">
{{ record.debit_amount|short_amount }}
</span>
{% endif %}
{% if record.credit_amount is not None %}
<span class="badge badge-warning badge-pill">
{{ record.credit_amount|short_amount }}
</span>
{% endif %}
</div>
</div>
<div>{{ record.transaction.note|default:"" }}</div>
</div>
</a>
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ _("There is currently no data.")|force_escape }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,197 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
transaction_detail-expense.html: The template for the detail of the
cash-expense transactions
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/23
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% setvar "title" _("Cash Expense Transaction") %}
{% static "accounting/css/transactions.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
{% if txn.has_order_hole %}
<div class="alert alert-danger alert-dismissible fade show">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>{{ _("Error:") }}</strong> {{ _("The transactions on this day are not well-ordered. Please reorder them.")|force_escape }}
</div>
{% endif %}
<!-- the delete confirmation dialog -->
<form action="{% url "accounting:transactions.delete" txn as url %}{% url_keep_return url %}" method="post">
{% csrf_token %}
<!-- The Modal -->
<div class="modal fade" id="del-modal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">{{ _("Cash Expense Transaction Deletion Confirmation")|force_escape }}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<!-- Modal body -->
<div class="modal-body">{{ _("Do you really want to delete this cash expense transaction?")|force_escape }}</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
</div>
</div>
</div>
</div>
</form>
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if "r" in request.GET %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{{ _("Back")|force_escape }}
</a>
<a class="btn btn-primary" role="button" href="{% url "accounting:transactions.update" "expense" txn as url %}{% url_keep_return url %}">
<i class="fas fa-edit"></i>
{{ _("Edit")|force_escape }}
</a>
{% if not txn.has_many_same_day %}
<button type="button" class="btn btn-secondary d-none d-sm-inline" disabled="disabled" title="{{ _("There is no other transaction at the same day.")|force_escape }}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</button>
{% else %}
<a class="btn btn-primary d-none d-sm-inline" role="button" href="{% url "accounting:transactions.sort" txn.date as url %}{% url_with_return url %}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</a>
{% endif %}
<a class="btn btn-primary d-none d-sm-inline" href="{% url "accounting:transactions.detail" "transfer" txn as url %}{% url_keep_return url %}">
<i class="fas fa-exchange-alt"></i>
{{ _("To Transfer")|force_escape }}
</a>
<div class="btn-group d-sm-none">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
<i class="fas fa-bars"></i>
</button>
<div class="dropdown-menu">
{% if not txn.has_many_same_day %}
<span class="dropdown-item disabled" title="{{ _("There is no other transaction at the same day.")|force_escape }}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</span>
{% else %}
<a class="dropdown-item" href="{% url "accounting:transactions.sort" txn.date as url %}{% url_with_return url %}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</a>
{% endif %}
<a class="dropdown-item" href="{% url "accounting:transactions.detail" "transfer" txn as url %}{% url_keep_return url %}">
<i class="fas fa-exchange-alt"></i>
{{ _("To Transfer")|force_escape }}
</a>
</div>
</div>
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#del-modal">
<i class="fas fa-trash"></i>
{{ _("Delete")|force_escape }}
</button>
</div>
<div class="row">
<div class="col-sm-2">{{ _("Date:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.date|smart_date }}</div>
</div>
<table class="table table-striped table-hover d-none d-sm-table">
<thead>
<tr>
<th scope="col">{{ _("Account")|force_escape }}</th>
<th scope="col">{{ _("Summary")|force_escape }}</th>
<th class="amount" scope="col">{{ _("$")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for x in txn.debit_records %}
<tr>
<td>{{ x.account.title|title_case }}</td>
<td>{{ x.summary|default:"" }}</td>
<td class="amount">{{ x.amount|accounting_amount }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="2">{{ _("Total")|force_escape }}</td>
<td class="amount">{{ txn.debit_total|accounting_amount }}</td>
</tr>
</tfoot>
</table>
<ul class="list-group d-sm-none">
{% for x in txn.debit_records %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center account-line">
{{ x.account.title|title_case }}
<span class="badge badge-info">{{ x.amount|accounting_amount }}</span>
</div>
<div>{{ x.summary|default:"" }}</div>
</li>
{% endfor %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center account-line">
{{ _("Total")|force_escape }}
<span class="badge badge-info">{{ txn.debit_total|accounting_amount }}</span>
</div>
</li>
</ul>
{% if txn.notes %}
<div class="row">
<div class="col-sm-2">{{ _("Notes:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.notes }}</div>
</div>
{% endif %}
<div class="row form-group">
<div class="col-sm-2">{{ _("Created at:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.created_at }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Created by:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.created_by }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Updated at:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.updated_at }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Updated by:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.updated_by }}</div>
</div>
{% endblock %}

View File

@ -0,0 +1,122 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
transaction_form-expense.html: The template for the form of the
cash-expense transactions
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/23
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% setvar "title" _("Cash Expense Transaction") %}
{% add_lib "jquery-ui" "decimal-js" %}
{% static "accounting/css/transactions.css" as file %}{% add_css file %}
{% static "accounting/css/summary-helper.css" as file %}{% add_css file %}
{% static "accounting/js/transaction-form.js" as file %}{% add_js file %}
{% static "accounting/js/summary-helper.js" as file %}{% add_js file %}
{% endblock %}
{% block content %}
{% include "accounting/include/summary-helper.html" %}
{% for message in form.non_field_errors %}
<div class="alert alert-danger alert-dismissible fade show">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>{{ _("Error:")|force_escape }}</strong> {{ message }}
</div>
{% endfor %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if form.transaction %}{% url "accounting:transactions.detail" "expense" form.transaction as url %}{% url_keep_return url %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{{ _("Back")|force_escape }}
</a>
</div>
<input id="account-option-url" type="hidden" value="{% url "accounting:api.accounts.options" %}" />
<input id="summary-categories" type="hidden" value="{{ summary_categories }}" />
<input id="regular-accounts" type="hidden" value="{{ regular_accounts }}" />
<input id="new-record-template" type="hidden" value="{{ new_record_template }}" />
<form id="txn-form" action="{% if form.transaction %}{% url "accounting:transactions.update" "expense" form.transaction as url %}{% url_keep_return url %}{% else %}{% url "accounting:transactions.create" "expense" as url %}{% url_keep_return url %}{% endif %}" method="post">
{% csrf_token %}
<div class="row form-group">
<div class="col-sm-2">
<label for="txn-date">{{ _("Date:")|force_escape }}</label>
</div>
<div class="col-sm-10">
{% now "Y-m-d" as today %}
<input id="txn-date" class="form-control {% if form.date.errors %} is-invalid {% endif %}" type="date" name="date" value="{% if form.is_bound %}{{ form.date.value }}{% else %}{% now "Y-m-d" %}{% endif %}" required="required" />
<div id="txn-date-error" class="invalid-feedback">{{ form.date.errors.0|default:"" }}</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-12">
<ul id="debit-records" class="list-group">
{% for record in form.debit_records %}
{% with record_type="debit" no=forloop.counter order=forloop.counter %}
{% include "accounting/include/record_form-non-transfer.html" %}
{% endwith %}
{% empty %}
{% with record_type="debit" no=1 order=1 %}
{% include "accounting/include/record_form-non-transfer.html" %}
{% endwith %}
{% endfor %}
</ul>
<ul class="list-group">
<li class="list-group-item">
<button class="btn btn-primary btn-new" type="button" data-type="debit">
<i class="fas fa-plus"></i>
</button>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
{{ _("Total")|force_escape }}
<span id="debit-total" class="amount">{{ form.debit_total|short_amount }}</span>
</div>
</li>
</ul>
</div>
</div>
<div class="row form-group">
<div class="col-sm-2">
<label for="txn-note">{{ _("Notes:")|force_escape }}</label>
</div>
<div class="col-sm-10">
<textarea id="txn-note" class="form-control {% if form.notes.errors %} is-invalid {% endif %}" name="notes">{{ form.notes.value|default:"" }}</textarea>
<div id="txn-note-error" class="invalid-feedback">{{ form.notes.errors.0|default:"" }}</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-12">
<button class="btn btn-primary" type="submit">
<i class="fas fa-save"></i>
{{ _("Save")|force_escape }}
</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,199 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
transaction_detail-income.html: The template for the detail of the
cash-income transactions
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/23
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% setvar "title" _("Cash Income Transaction") %}
{% static "accounting/css/transactions.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
{% if txn.has_order_hole %}
<div class="alert alert-danger alert-dismissible fade show">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>{{ _("Error:") }}</strong> {{ _("The transactions on this day are not well-ordered. Please reorder them.")|force_escape }}
</div>
{% endif %}
<!-- the delete confirmation dialog -->
<form action="{% url "accounting:transactions.delete" txn as url %}{% url_keep_return url %}" method="post">
{% csrf_token %}
<!-- The Modal -->
<div class="modal fade" id="del-modal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">{{ _("Cash Income Transaction Deletion Confirmation")|force_escape }}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<!-- Modal body -->
<div class="modal-body">{{ _("Do you really want to delete this cash income transaction?")|force_escape }}</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
</div>
</div>
</div>
</div>
</form>
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if "r" in request.GET %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{{ _("Back")|force_escape }}
</a>
<a class="btn btn-primary" role="button" href="{% url "accounting:transactions.update" "income" txn as url %}{% url_keep_return url %}">
<i class="fas fa-edit"></i>
{{ _("Edit")|force_escape }}
</a>
{% if not txn.has_many_same_day %}
<button type="button" class="btn btn-secondary d-none d-sm-inline" disabled="disabled" title="{{ _("There is no other transaction at the same day.")|force_escape }}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</button>
{% else %}
<a class="btn btn-primary d-none d-sm-inline" role="button" href="{% url "accounting:transactions.sort" txn.date as url %}{% url_with_return url %}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</a>
{% endif %}
<a class="btn btn-primary d-none d-sm-inline" href="{% url "accounting:transactions.detail" "transfer" txn as url %}{% url_keep_return url %}">
<i class="fas fa-exchange-alt"></i>
{{ _("To Transfer")|force_escape }}
</a>
<div class="btn-group d-sm-none">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
<i class="fas fa-bars"></i>
</button>
<div class="dropdown-menu">
{% if not txn.has_many_same_day %}
<span class="dropdown-item disabled" title="{{ _("There is no other transaction at the same day.")|force_escape }}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</span>
{% else %}
<a class="dropdown-item" href="{% url "accounting:transactions.sort" txn.date as url %}{% url_with_return url %}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</a>
{% endif %}
<a class="dropdown-item" href="{% url "accounting:transactions.detail" "transfer" txn as url %}{% url_keep_return url %}">
<i class="fas fa-exchange-alt"></i>
{{ _("To Transfer")|force_escape }}
</a>
</div>
</div>
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#del-modal">
<i class="fas fa-trash"></i>
{{ _("Delete")|force_escape }}
</button>
</div>
<div class="row">
<div class="col-sm-2">{{ _("Date:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.date|smart_date }}</div>
</div>
<table class="table table-striped table-hover d-none d-sm-table">
<thead>
<tr>
<th scope="col">{{ _("Account")|force_escape }}</th>
<th scope="col">{{ _("Summary")|force_escape }}</th>
<th class="amount" scope="col">{{ _("$")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for x in txn.credit_records %}
<tr>
<td>{{ x.account.title|title_case }}</td>
<td>{{ x.summary|default:"" }}</td>
<td class="amount">{{ x.amount|accounting_amount }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="2">{{ _("Total")|force_escape }}</td>
<td class="amount">{{ txn.credit_total|accounting_amount }}</td>
</tr>
</tfoot>
</table>
<ul class="list-group d-sm-none">
{% for x in txn.credit_records %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center account-line">
{{ x.account.title|title_case }}
<span class="badge badge-info">{{ x.amount|accounting_amount }}</span>
</div>
<div>{{ x.summary|default:"" }}</div>
</li>
{% endfor %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center account-line">
{{ _("Total")|force_escape }}
<span class="badge badge-info">{{ txn.credit_total|accounting_amount }}</span>
</div>
</li>
</ul>
{% if txn.notes %}
<div class="row">
<div class="col-sm-2">{{ _("Notes:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.notes }}</div>
</div>
{% endif %}
<div class="row form-group">
<div class="col-sm-2">{{ _("Created at:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.created_at }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Created by:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.created_by }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Updated at:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.updated_at }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Updated by:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.updated_by }}</div>
</div>
{% endblock %}

View File

@ -0,0 +1,121 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
transaction_form-income.html: The template for the form of the
cash-income transactions
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/23
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% setvar "title" _("Cash Income Transaction") %}
{% add_lib "jquery-ui" "decimal-js" %}
{% static "accounting/css/transactions.css" as file %}{% add_css file %}
{% static "accounting/css/summary-helper.css" as file %}{% add_css file %}
{% static "accounting/js/transaction-form.js" as file %}{% add_js file %}
{% static "accounting/js/summary-helper.js" as file %}{% add_js file %}
{% endblock %}
{% block content %}
{% include "accounting/include/summary-helper.html" %}
{% for message in form.non_field_errors %}
<div class="alert alert-danger alert-dismissible fade show">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>{{ _("Error:")|force_escape }}</strong> {{ message }}
</div>
{% endfor %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if form.transaction %}{% url "accounting:transactions.detail" "income" form.transaction as url %}{% url_keep_return url %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{{ _("Back")|force_escape }}
</a>
</div>
<input id="account-option-url" type="hidden" value="{% url "accounting:api.accounts.options" %}" />
<input id="summary-categories" type="hidden" value="{{ summary_categories }}" />
<input id="regular-accounts" type="hidden" value="{{ regular_accounts }}" />
<input id="new-record-template" type="hidden" value="{{ new_record_template }}" />
<form id="txn-form" action="{% if form.transaction %}{% url "accounting:transactions.update" "income" form.transaction as url %}{% url_keep_return url %}{% else %}{% url "accounting:transactions.create" "income" as url %}{% url_keep_return url %}{% endif %}" method="post">
{% csrf_token %}
<div class="row form-group">
<div class="col-sm-2">
<label for="txn-date">{{ _("Date:")|force_escape }}</label>
</div>
<div class="col-sm-10">
<input id="txn-date" class="form-control {% if form.date.errors %} is-invalid {% endif %}" type="date" name="date" value="{% if form.is_bound %}{{ form.date.value }}{% else %}{% now "Y-m-d" %}{% endif %}" required="required" />
<div id="txn-date-error" class="invalid-feedback">{{ form.date.errors.0|default:"" }}</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-12">
<ul id="credit-records" class="list-group">
{% for record in form.credit_records %}
{% with record_type="credit" no=forloop.counter order=forloop.counter %}
{% include "accounting/include/record_form-non-transfer.html" %}
{% endwith %}
{% empty %}
{% with record_type="credit" no=1 order=1 %}
{% include "accounting/include/record_form-non-transfer.html" %}
{% endwith %}
{% endfor %}
</ul>
<ul class="list-group">
<li class="list-group-item">
<button class="btn btn-primary btn-new" type="button" data-type="credit">
<i class="fas fa-plus"></i>
</button>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
{{ _("Total")|force_escape }}
<span id="credit-total" class="amount">{{ form.credit_total|short_amount }}</span>
</div>
</li>
</ul>
</div>
</div>
<div class="row form-group">
<div class="col-sm-2">
<label for="txn-note">{{ _("Notes:")|force_escape }}</label>
</div>
<div class="col-sm-10">
<textarea id="txn-note" class="form-control {% if form.notes.errors %} is-invalid {% endif %}" name="notes">{{ form.notes.value|default:"" }}</textarea>
<div id="txn-note-error" class="invalid-feedback">{{ form.notes.errors.0|default:"" }}</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-12">
<button class="btn btn-primary" type="submit">
<i class="fas fa-save"></i>
{{ _("Save")|force_escape }}
</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,143 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
transaction-sort.html: The template to sort transactions in a same day
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/8/6
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% blocktrans asvar title with date=form.date|smart_date %}Reorder the Transactions in {{ date }}{% endblocktrans %}
{% setvar "title" title %}
{% add_lib "jquery-ui" %}
{% static "accounting/css/report.css" as file %}{% add_css file %}
{% static "accounting/css/transactions-sort.css" as file %}{% add_css file %}
{% static "accounting/js/transaction-sort.js" as file %}{% add_js file %}
{% endblock %}
{% block content %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if "r" in request.GET %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{{ _("Back")|force_escape }}
</a>
</div>
<div class="form-group row">
<div class="col-sm-2">
<label for="txn-date">{{ _("Date:")|force_escape }}</label>
</div>
<div id="txn-date" class="col-sm-10">
{{ form.date|smart_date }}
</div>
</div>
{% if form.txn_list|length > 1 %}
<form action="{% url "accounting:transactions.sort" form.date as url %}{% url_keep_return url %}" method="post">
{% csrf_token %}
<table class="table general-journal-table">
<thead>
<tr>
<th class="actions" scope="col"></th>
<th scope="col">{{ _("Type")|force_escape }}</th>
<th scope="col">{{ _("Content")|force_escape }}</th>
<th class="amount" scope="col">{{ _("Amount")|force_escape }}</th>
<th scope="col">{{ _("Notes")|force_escape }}</th>
</tr>
</thead>
<tbody id="transactions">
{% for txn in form.txn_list %}
<tr id="transaction-{{ txn.pk }}" class="transaction {% if not txn.is_balanced %} table-danger {% endif %}">
<td class="actions">
<div class="btn-group">
<button class="btn btn-outline-secondary" type="button">
<i class="fas fa-sort"></i>
</button>
<a class="btn btn-primary" role="button" href="{% url "accounting:transactions.detail" txn.type txn as url %}{% url_with_return url %}">
<i class="fas fa-eye"></i>
</a>
</div>
</td>
<td>
{% if txn.is_cash_expense %}
{{ _("Cash Expense")|force_escape }}
{% elif txn.is_cash_income %}
{{ _("Cash Income")|force_escape }}
{% else %}
{{ _("Transfer")|force_escape }}
{% endif %}
</td>
<td>
<input id="transaction-{{ txn.pk }}-ord" type="hidden" name="transaction-{{ txn.pk }}-ord" value="{{ forloop.counter }}" />
{% if txn.is_cash_expense %}
<ul class="txn-content-expense">
{% for summary in txn.debit_summaries %}
<li>{{ summary }}</li>
{% endfor %}
</ul>
{% elif txn.is_cash_income %}
<ul class="txn-content-income">
{% for summary in txn.credit_summaries %}
<li>{{ summary }}</li>
{% endfor %}
</ul>
{% else %}
<ul class="txn-content-expense">
{% for summary in txn.debit_summaries %}
<li>{{ summary }}</li>
{% endfor %}
</ul>
<ul class="txn-content-income">
{% for summary in txn.credit_summaries %}
<li>{{ summary }}</li>
{% endfor %}
</ul>
{% endif %}
{% if not txn.is_balanced %}
<span class="badge badge-danger badge-pill">
{{ _("Unbalanced")|force_escape }}
</span>
{% endif %}
</td>
<td class="amount">
{{ txn.amount|accounting_amount }}
</td>
<td>{{ txn.notes|default:"" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="form-group row">
<div class="col-sm-12">
<button class="btn btn-primary" type="submit">
<i class="fas fa-save"></i>
{{ _("Save")|force_escape }}
</button>
</div>
</div>
</form>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,242 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
transaction_detail-transfer.html: The template for the detail of the
transfer transactions
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/23
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% setvar "title" _("Transfer Transaction") %}
{% static "accounting/css/transactions.css" as file %}{% add_css file %}
{% endblock %}
{% block content %}
{% if txn.has_order_hole %}
<div class="alert alert-danger alert-dismissible fade show">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>{{ _("Error:") }}</strong> {{ _("The transactions on this day are not well-ordered. Please reorder them.")|force_escape }}
</div>
{% endif %}
<!-- the delete confirmation dialog -->
<form action="{% url "accounting:transactions.delete" txn as url %}{% url_keep_return url %}" method="post">
{% csrf_token %}
<!-- The Modal -->
<div class="modal fade" id="del-modal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">{{ _("Transfer Transaction Deletion Confirmation")|force_escape }}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<!-- Modal body -->
<div class="modal-body">{{ _("Do you really want to delete this transfer transaction?")|force_escape }}</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Cancel")|force_escape }}</button>
<button class="btn btn-danger" type="submit" name="del-confirm">{{ _("Confirm")|force_escape }}</button>
</div>
</div>
</div>
</div>
</form>
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if "r" in request.GET %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{{ _("Back")|force_escape }}
</a>
<a class="btn btn-primary" role="button" href="{% url "accounting:transactions.update" "transfer" txn as url %}{% url_keep_return url %}">
<i class="fas fa-edit"></i>
{{ _("Edit")|force_escape }}
</a>
{% if not txn.has_many_same_day %}
<button type="button" class="btn btn-secondary d-none d-sm-inline" disabled="disabled" title="{{ _("There is no other transaction at the same day.")|force_escape }}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</button>
{% else %}
<a class="btn btn-primary d-none d-sm-inline" role="button" href="{% url "accounting:transactions.sort" txn.date as url %}{% url_with_return url %}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</a>
{% endif %}
<div class="btn-group d-sm-none">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
<i class="fas fa-bars"></i>
</button>
<div class="dropdown-menu">
{% if not txn.has_many_same_day %}
<span class="dropdown-item disabled" title="{{ _("There is no other transaction at the same day.")|force_escape }}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</span>
{% else %}
<a class="dropdown-item" href="{% url "accounting:transactions.sort" txn.date as url %}{% url_with_return url %}">
<i class="fas fa-sort"></i>
{{ _("Sort")|force_escape }}
</a>
{% endif %}
</div>
</div>
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#del-modal">
<i class="fas fa-trash"></i>
{{ _("Delete")|force_escape }}
</button>
</div>
<div class="row">
<div class="col-sm-2">{{ _("Date:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.date|smart_date }}</div>
</div>
<div class="row">
<div class="col-sm-6">
<h2>{{ _("Debit")|force_escape }}</h2>
<table class="table table-striped table-hover d-none d-lg-table">
<thead>
<tr>
<th scope="col">{{ _("Account")|force_escape }}</th>
<th scope="col">{{ _("Summary")|force_escape }}</th>
<th class="amount" scope="col">{{ _("$")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for x in txn.debit_records %}
<tr>
<td>{{ x.account.title|title_case }}</td>
<td>{{ x.summary|default:"" }}</td>
<td class="amount">{{ x.amount|accounting_amount }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="2">{{ _("Total")|force_escape }}</td>
<td class="amount">{{ txn.debit_total|accounting_amount }}</td>
</tr>
</tfoot>
</table>
<ul class="list-group d-lg-none">
{% for x in txn.debit_records %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center account-line">
{{ x.account.title|title_case }}
<span class="badge badge-info">{{ x.amount|accounting_amount }}</span>
</div>
<div>{{ x.summary|default:"" }}</div>
</li>
{% endfor %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center account-line">
{{ _("Total")|force_escape }}
<span class="badge badge-info">{{ txn.debit_total|accounting_amount }}</span>
</div>
</li>
</ul>
</div>
<div class="col-sm-6">
<h2>{{ _("Credit")|force_escape }}</h2>
<table class="table table-striped table-hover d-none d-lg-table">
<thead>
<tr>
<th scope="col">{{ _("Account")|force_escape }}</th>
<th scope="col">{{ _("Summary")|force_escape }}</th>
<th class="amount" scope="col">{{ _("$")|force_escape }}</th>
</tr>
</thead>
<tbody>
{% for x in txn.credit_records %}
<tr>
<td>{{ x.account.title|title_case }}</td>
<td>{{ x.summary|default:"" }}</td>
<td class="amount">{{ x.amount|accounting_amount }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="2">{{ _("Total")|force_escape }}</td>
<td class="amount">{{ txn.credit_total|accounting_amount }}</td>
</tr>
</tfoot>
</table>
<ul class="list-group d-lg-none">
{% for x in txn.credit_records %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center account-line">
{{ x.account.title|title_case }}
<span class="badge badge-info">{{ x.amount|accounting_amount }}</span>
</div>
<div>{{ x.summary|default:"" }}</div>
</li>
{% endfor %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center account-line">
{{ _("Total")|force_escape }}
<span class="badge badge-info">{{ txn.credit_total|accounting_amount }}</span>
</div>
</li>
</ul>
</div>
</div>
{% if txn.notes %}
<div class="row">
<div class="col-sm-2">{{ _("Notes:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.notes }}</div>
</div>
{% endif %}
<div class="row form-group">
<div class="col-sm-2">{{ _("Created at:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.created_at }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Created by:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.created_by }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Updated at:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.updated_at }}</div>
</div>
<div class="row form-group">
<div class="col-sm-2">{{ _("Updated by:")|force_escape }}</div>
<div class="col-sm-10">{{ txn.updated_by }}</div>
</div>
{% endblock %}

View File

@ -0,0 +1,154 @@
{% extends "base.html" %}
{% comment %}
The Mia Accounting Application
transaction_form-transfer.html: The template for the form of the
transfer transactions
Copyright (c) 2020 imacat.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2020/7/23
{% endcomment %}
{% load static %}
{% load i18n %}
{% load mia_core %}
{% load accounting %}
{% block settings %}
{% setvar "title" _("Transfer Transaction") %}
{% add_lib "jquery-ui" "decimal-js" %}
{% static "accounting/css/transactions.css" as file %}{% add_css file %}
{% static "accounting/css/summary-helper.css" as file %}{% add_css file %}
{% static "accounting/js/transaction-form.js" as file %}{% add_js file %}
{% static "accounting/js/summary-helper.js" as file %}{% add_js file %}
{% endblock %}
{% block content %}
{% include "accounting/include/summary-helper.html" %}
{% for message in form.non_field_errors %}
<div class="alert alert-danger alert-dismissible fade show">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<strong>{{ _("Error:")|force_escape }}</strong> {{ message }}
</div>
{% endfor %}
<div class="btn-group btn-actions">
<a class="btn btn-primary" role="button" href="{% if form.transaction %}{% url "accounting:transactions.detail" "transfer" form.transaction as url %}{% url_keep_return url %}{% elif request.GET.r %}{{ request.GET.r }}{% else %}{% url "accounting:home" %}{% endif %}">
<i class="fas fa-chevron-circle-left"></i>
{{ _("Back")|force_escape }}
</a>
</div>
<input id="account-option-url" type="hidden" value="{% url "accounting:api.accounts.options" %}" />
<input id="summary-categories" type="hidden" value="{{ summary_categories }}" />
<input id="regular-accounts" type="hidden" value="{{ regular_accounts }}" />
<input id="new-record-template" type="hidden" value="{{ new_record_template }}" />
<form id="txn-form" action="{% if form.transaction %}{% url "accounting:transactions.update" "transfer" form.transaction as url %}{% url_keep_return url %}{% else %}{% url "accounting:transactions.create" "transfer" as url %}{% url_keep_return url %}{% endif %}" method="post">
{% csrf_token %}
<div class="row form-group">
<div class="col-sm-2">
<label for="txn-date">{{ _("Date:")|force_escape }}</label>
</div>
<div class="col-sm-10">
<input id="txn-date" class="form-control {% if form.date.errors %} is-invalid {% endif %}" type="date" name="date" value="{% if form.is_bound %}{{ form.date.value }}{% else %}{% now "Y-m-d" %}{% endif %}" required="required" />
<div id="txn-date-error" class="invalid-feedback">{{ form.date.errors.0|default:"" }}</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-6">
<h2>{{ _("Debit")|force_escape }}</h2>
<ul id="debit-records" class="list-group">
{% for record in form.debit_records %}
{% with record_type="debit" no=forloop.counter order=forloop.counter %}
{% include "accounting/include/record_form-transfer.html" %}
{% endwith %}
{% empty %}
{% with record_type="debit" no=1 order=1 %}
{% include "accounting/include/record_form-transfer.html" %}
{% endwith %}
{% endfor %}
</ul>
<ul class="list-group">
<li class="list-group-item">
<button class="btn btn-primary btn-new" type="button" data-type="debit">
<i class="fas fa-plus"></i>
</button>
</li>
<li class="list-group-item">
<div id="debit-total-row" class="d-flex justify-content-between align-items-center form-control {% if form.balance_error %} is-invalid {% endif %} balance-row">
{{ _("Total")|force_escape }}
<span id="debit-total" class="amount">{{ form.debit_total|short_amount }}</span>
</div>
<div id="debit-total-error" class="invalid-feedback balance-error">{{ form.balance_error|default:"" }}</div>
</li>
</ul>
</div>
<div class="col-sm-6">
<h2>{{ _("Credit")|force_escape }}</h2>
<ul id="credit-records" class="list-group">
{% for record in form.credit_records %}
{% with record_type="credit" no=forloop.counter order=forloop.counter %}
{% include "accounting/include/record_form-transfer.html" %}
{% endwith %}
{% empty %}
{% with record_type="credit" no=1 order=1 %}
{% include "accounting/include/record_form-transfer.html" %}
{% endwith %}
{% endfor %}
</ul>
<ul class="list-group">
<li class="list-group-item">
<button class="btn btn-primary btn-new" type="button" data-type="credit">
<i class="fas fa-plus"></i>
</button>
</li>
<li class="list-group-item">
<div id="credit-total-row" class="d-flex justify-content-between align-items-center form-control {% if form.balance_error %} is-invalid {% endif %} balance-row">
{{ _("Total")|force_escape }}
<span id="credit-total" class="amount">{{ form.credit_total|short_amount }}</span>
</div>
<div id="credit-total-error" class="invalid-feedback balance-error">{{ form.balance_error|default:"" }}</div>
</li>
</ul>
</div>
</div>
<div class="row form-group">
<div class="col-sm-2">
<label for="txn-note">{{ _("Notes:")|force_escape }}</label>
</div>
<div class="col-sm-10">
<textarea id="txn-note" class="form-control {% if form.notes.errors %} is-invalid {% endif %}" name="notes">{{ form.notes.value|default:"" }}</textarea>
<div id="txn-note-error" class="invalid-feedback">{{ form.notes.errors.0|default:"" }}</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-12">
<button class="btn btn-primary" type="submit">
<i class="fas fa-save"></i>
{{ _("Save")|force_escape }}
</button>
</div>
</div>
</form>
{% endblock %}

View File

View File

@ -0,0 +1,144 @@
# The accounting application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/7/13
# 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 template tags and filters of the accounting application.
"""
import re
from decimal import Decimal
from typing import Optional
from django import template
from django.template import RequestContext
from accounting.models import Account
from accounting.utils import ReportUrl
from mia_core.period import Period
register = template.Library()
def _strip_decimal_zeros(value: Decimal) -> str:
"""Formats a decimal value, stripping excess decimal zeros.
Args:
value: The value.
Returns:
str: The value with excess decimal zeros stripped.
"""
s = str(value)
s = re.sub(r"^(.*\.[0-9]*?)0+$", r"\1", s)
s = re.sub(r"^(.*)\.$", r"\1", s)
return s
def _format_positive_amount(value: Decimal) -> str:
"""Formats a positive amount, groups every 3 digits by commas.
Args:
value: The amount.
Returns:
str: The amount in the desired format.
"""
s = _strip_decimal_zeros(value)
while True:
m = re.match("^([1-9][0-9]*)([0-9]{3}.*)", s)
if m is None:
break
s = m.group(1) + "," + m.group(2)
return s
@register.filter
def accounting_amount(value: Optional[Decimal]) -> str:
"""Formats an amount with the accounting notation, grouping every 3 digits
by commas, and marking negative numbers with brackets instead of signs.
Args:
value: The amount.
Returns:
str: The amount in the desired format.
"""
if value is None:
return ""
if value == 0:
return "-"
s = _format_positive_amount(abs(value))
if value < 0:
s = F"({s})"
return s
@register.filter
def short_amount(value: Optional[Decimal]) -> str:
"""Formats an amount, groups every 3 digits by commas.
Args:
value: The amount.
Returns:
str: The amount in the desired format.
"""
if value is None:
return ""
if value == 0:
return "-"
s = _format_positive_amount(abs(value))
if value < 0:
s = "-" + s
return s
@register.filter
def short_value(value: Optional[Decimal]) -> str:
"""Formats a decimal value, stripping excess decimal zeros.
Args:
value: The value.
Returns:
str: The value with excess decimal zeroes stripped.
"""
if value is None:
return ""
return _strip_decimal_zeros(value)
@register.simple_tag(takes_context=True)
def report_url(context: RequestContext,
cash_account: Optional[Account],
ledger_account: Optional[Account],
period: Optional[Period]) -> ReportUrl:
"""Returns accounting report URL helper.
Args:
context: The request context.
cash_account: The current cash account.
ledger_account: The current ledger account.
period: The period.
Returns:
ReportUrl: The accounting report URL helper.
"""
return ReportUrl(
namespace=context.request.resolver_match.namespace,
cash=cash_account or None,
ledger=ledger_account or None,
period=period or None)

75
src/accounting/tests.py Normal file
View File

@ -0,0 +1,75 @@
# The accounting application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/8/2
# 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 test cases of the accounting application.
"""
from django.test import TestCase
from .forms import TransactionForm
class SortTransactionPostTestCase(TestCase):
"""Tests the sort_post_txn_records() utility."""
def test_sort(self):
"""Tests the sort_post_txn_records() utility."""
post = {
"date": "2020-07-15",
"notes": "",
"debit-2-account": "4144",
"debit-2-ord": "4",
"debit-2-summary": "",
"debit-2-amount": "262",
"debit-3-id": "714703431",
"debit-3-account": "2715",
"debit-3-ord": "4",
"debit-3-summary": "lunch",
"debit-3-amount": "477",
"debit-16-id": "541574719",
"debit-16-account": "6634",
"debit-16-ord": "2",
"debit-16-summary": "dinner",
"debit-16-amount": "525",
"credit-7-id": "747725334",
"credit-7-account": "1211",
"credit-7-ord": "3",
"credit-7-summary": "",
"credit-7-amount": "667",
}
TransactionForm._sort_post_txn_records(post)
self.assertEqual(post.get("date"), "2020-07-15")
self.assertEqual(post.get("notes"), "")
self.assertEqual(post.get("debit-1-ord"), "1")
self.assertEqual(post.get("debit-1-id"), "541574719")
self.assertEqual(post.get("debit-1-account"), "6634")
self.assertEqual(post.get("debit-1-summary"), "dinner")
self.assertEqual(post.get("debit-1-amount"), "525")
self.assertEqual(post.get("debit-2-ord"), "2")
self.assertEqual(post.get("debit-2-account"), "4144")
self.assertEqual(post.get("debit-2-summary"), "")
self.assertEqual(post.get("debit-2-amount"), "262")
self.assertEqual(post.get("debit-3-ord"), "3")
self.assertEqual(post.get("debit-3-id"), "714703431")
self.assertEqual(post.get("debit-3-account"), "2715")
self.assertEqual(post.get("debit-3-summary"), "lunch")
self.assertEqual(post.get("debit-3-amount"), "477")
self.assertEqual(post.get("credit-1-ord"), "1")
self.assertEqual(post.get("credit-1-id"), "747725334")
self.assertEqual(post.get("credit-1-account"), "1211")
self.assertEqual(post.get("credit-1-summary"), "")
self.assertEqual(post.get("credit-1-amount"), "667")

101
src/accounting/urls.py Normal file
View File

@ -0,0 +1,101 @@
# The accounting application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/6/30
# 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 route settings of the accounting application.
"""
from django.urls import path, register_converter
from django.views.decorators.http import require_GET
from mia_core.views import RedirectView
from . import converters, views
register_converter(converters.PeriodConverter, "period")
register_converter(converters.AccountConverter, "account")
register_converter(converters.CashAccountConverter, "cash-account")
register_converter(converters.LedgerAccountConverter, "ledger-account")
register_converter(converters.TransactionTypeConverter, "txn-type")
register_converter(converters.TransactionConverter, "txn")
register_converter(converters.DateConverter, "date")
app_name = "accounting"
urlpatterns = [
path("", require_GET(RedirectView.as_view(
query_string=True,
pattern_name="accounting:cash.home",
)), name="home"),
path("cash",
views.CashDefaultView.as_view(), name="cash.home"),
path("cash/<cash-account:account>/<period:period>",
views.cash, name="cash"),
path("cash-summary",
views.CashSummaryDefaultView.as_view(), name="cash-summary.home"),
path("cash-summary/<cash-account:account>",
views.cash_summary, name="cash-summary"),
path("ledger",
views.LedgerDefaultView.as_view(), name="ledger.home"),
path("ledger/<ledger-account:account>/<period:period>",
views.ledger, name="ledger"),
path("ledger-summary",
views.LedgerSummaryDefaultView.as_view(), name="ledger-summary.home"),
path("ledger-summary/<ledger-account:account>",
views.ledger_summary, name="ledger-summary"),
path("journal",
views.JournalDefaultView.as_view(), name="journal.home"),
path("journal/<period:period>",
views.journal, name="journal"),
path("trial-balance",
views.TrialBalanceDefaultView.as_view(), name="trial-balance.home"),
path("trial-balance/<period:period>",
views.trial_balance, name="trial-balance"),
path("income-statement",
views.IncomeStatementDefaultView.as_view(),
name="income-statement.home"),
path("income-statement/<period:period>",
views.income_statement, name="income-statement"),
path("balance-sheet",
views.BalanceSheetDefaultView.as_view(), name="balance-sheet.home"),
path("balance-sheet/<period:period>",
views.balance_sheet, name="balance-sheet"),
path("search",
views.SearchListView.as_view(), name="search"),
path("transactions/<txn-type:txn_type>/create",
views.TransactionFormView.as_view(), name="transactions.create"),
path("transactions/<txn-type:txn_type>/<txn:txn>",
views.TransactionView.as_view(), name="transactions.detail"),
path("transactions/<txn-type:txn_type>/<txn:txn>/update",
views.TransactionFormView.as_view(), name="transactions.update"),
path("transactions/<txn:txn>/delete",
views.TransactionDeleteView.as_view(), name="transactions.delete"),
path("transactions/sort/<date:date>",
views.TransactionSortFormView.as_view(), name="transactions.sort"),
path("accounts",
views.AccountListView.as_view(), name="accounts"),
path("accounts/create",
views.AccountFormView.as_view(), name="accounts.create"),
path("accounts/<account:account>",
views.AccountView.as_view(), name="accounts.detail"),
path("accounts/<account:account>/update",
views.AccountFormView.as_view(), name="accounts.update"),
path("accounts/<account:account>/delete",
views.account_delete, name="accounts.delete"),
path("api/accounts",
views.api_account_list, name="api.accounts"),
path("api/accounts/options",
views.api_account_options, name="api.accounts.options"),
]

464
src/accounting/utils.py Normal file
View File

@ -0,0 +1,464 @@
# The accounting application of the Mia project.
# by imacat <imacat@mail.imacat.idv.tw>, 2020/7/13
# 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 utilities of the accounting application.
"""
import datetime
from typing import Union, Tuple, List, Optional, Iterable
from django.conf import settings
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 gettext as _
from mia_core.period import Period
from mia_core.templatetags.mia_core import smart_month
from mia_core.utils import new_pk
from .models import Account, Transaction, Record
AccountData = Tuple[Union[str, int], str, str, str]
RecordData = Tuple[Union[str, int], Optional[str], float]
DEFAULT_CASH_ACCOUNT = "1111"
CASH_SHORTCUT_ACCOUNTS = ["0", "1111"]
DEFAULT_LEDGER_ACCOUNT = "1111"
PAYABLE_ACCOUNTS = ["2141", "21413"]
EQUIPMENT_ACCOUNTS = ["1441"],
class MonthlySummary:
"""A summary record.
Args:
month: The month.
label: The text label.
credit: The credit amount.
debit: The debit amount.
balance: The balance.
cumulative_balance: The cumulative balance.
Attributes:
month (datetime.date): The month.
label (str): The text label.
credit (int): The credit amount.
debit (int): The debit amount.
balance (int): The balance.
cumulative_balance (int): The cumulative balance.
"""
def __init__(self, month: datetime.date = None, label: str = None,
credit: int = None, debit: int = None, balance: int = None,
cumulative_balance: int = None):
self.month = month
self.label = label
self.credit = credit
self.debit = debit
self.balance = balance
self.cumulative_balance = cumulative_balance
if self.label is None and self.month is not None:
self.label = smart_month(self.month)
class ReportUrl:
"""The URL of the accounting reports.
Args:
namespace: The namespace of the current application.
cash: The currently-specified account of the
cash account or cash summary.
ledger: The currently-specified account of the
ledger or leger summary.
period: The currently-specified period.
"""
def __init__(self, namespace: str, cash: Account = None,
ledger: Account = None, period: Period = None,):
self._period = Period() if period is None else period
self._cash = get_default_cash_account() if cash is None else cash
self._ledger = get_default_ledger_account()\
if ledger is None else ledger
self._namespace = namespace
def cash(self) -> str:
return reverse("accounting:cash", args=[self._cash, self._period],
current_app=self._namespace)
def cash_summary(self) -> str:
return reverse("accounting:cash-summary", args=[self._cash],
current_app=self._namespace)
def ledger(self) -> str:
return reverse("accounting:ledger", args=[self._ledger, self._period],
current_app=self._namespace)
def ledger_summary(self) -> str:
return reverse("accounting:ledger-summary", args=[self._ledger],
current_app=self._namespace)
def journal(self) -> str:
return reverse("accounting:journal", args=[self._period],
current_app=self._namespace)
def trial_balance(self) -> str:
return reverse("accounting:trial-balance", args=[self._period],
current_app=self._namespace)
def income_statement(self) -> str:
return reverse("accounting:income-statement", args=[self._period],
current_app=self._namespace)
def balance_sheet(self) -> str:
return reverse("accounting:balance-sheet", args=[self._period],
current_app=self._namespace)
class DataFiller:
"""The helper to populate the accounting data.
Args:
user: The user in action.
Attributes:
user (User): The user in action.
"""
def __init__(self, user):
self.user = user
def add_accounts(self, accounts: List[AccountData]) -> None:
"""Adds accounts.
Args:
accounts (tuple[tuple[any]]): Tuples of
(code, English, Traditional Chinese, Simplified Chinese)
of the accounts.
"""
for data in accounts:
code = data[0]
if isinstance(code, int):
code = str(code)
parent = None if len(code) == 1\
else Account.objects.get(code=code[:-1])
account = Account(parent=parent, code=code, current_user=self.user)
account.set_l10n_in("title", "en", data[1])
account.set_l10n_in("title", "zh-hant", data[2])
account.set_l10n_in("title", "zh-hans", data[3])
account.save()
def add_transfer_transaction(self, date: Union[datetime.date, int],
debit: List[RecordData],
credit: List[RecordData]) -> None:
"""Adds a transfer transaction.
Args:
date: The date, or the number of days from
today.
debit: Tuples of (account, summary, amount) of the debit records.
credit: Tuples of (account, summary, amount) of the credit records.
"""
if isinstance(date, int):
date = timezone.localdate() + timezone.timedelta(days=date)
order = Transaction.objects.filter(date=date).count() + 1
transaction = Transaction(pk=new_pk(Transaction), date=date, ord=order,
current_user=self.user)
transaction.save()
order = 1
for data in debit:
account = data[0]
if isinstance(account, str):
account = Account.objects.get(code=account)
elif isinstance(account, int):
account = Account.objects.get(code=str(account))
transaction.record_set.create(pk=new_pk(Record), is_credit=False,
ord=order, account=account,
summary=data[1], amount=data[2],
current_user=self.user)
order = order + 1
order = 1
for data in credit:
account = data[0]
if isinstance(account, str):
account = Account.objects.get(code=account)
elif isinstance(account, int):
account = Account.objects.get(code=str(account))
transaction.record_set.create(pk=new_pk(Record), is_credit=True,
ord=order, account=account,
summary=data[1], amount=data[2],
current_user=self.user)
order = order + 1
def add_income_transaction(self, date: Union[datetime.date, int],
credit: List[RecordData]) -> None:
"""Adds a cash income transaction.
Args:
date: The date, or the number of days from today.
credit: Tuples of (account, summary, amount) of the credit records.
"""
amount = sum([x[2] for x in credit])
self.add_transfer_transaction(
date, [(Account.CASH, None, amount)], credit)
def add_expense_transaction(self, date: Union[datetime.date, int],
debit: List[RecordData]) -> None:
"""Adds a cash income transaction.
Args:
date: The date, or the number of days from today.
debit: Tuples of (account, summary, amount) of the debit records.
"""
amount = sum([x[2] for x in debit])
self.add_transfer_transaction(
date, debit, [(Account.CASH, None, amount)])
def get_cash_accounts() -> List[Account]:
"""Returns the cash accounts.
Returns:
The cash accounts.
"""
accounts = list(
Account.objects
.filter(
code__in=Record.objects
.filter(
Q(account__code__startswith="11")
| Q(account__code__startswith="12")
| Q(account__code__startswith="21")
| Q(account__code__startswith="22"))
.values("account__code"))
.order_by("code"))
accounts.insert(0, Account(
code="0",
title=_("current assets and liabilities"),
))
return accounts
def get_default_cash_account() -> Account:
"""Returns the default cash account.
Returns:
The default cash account.
"""
try:
code = settings.ACCOUNTING["DEFAULT_CASH_ACCOUNT"]
except AttributeError:
code = DEFAULT_CASH_ACCOUNT
except TypeError:
code = DEFAULT_CASH_ACCOUNT
except KeyError:
code = DEFAULT_CASH_ACCOUNT
if code == "0":
return Account(code="0", title=_("current assets and liabilities"))
try:
return Account.objects.get(code=code)
except Account.DoesNotExist:
pass
try:
return Account.objects.get(code=DEFAULT_CASH_ACCOUNT)
except Account.DoesNotExist:
pass
return Account(code="0", title=_("current assets and liabilities"))
def get_cash_shortcut_accounts() -> List[str]:
"""Returns the codes of the shortcut cash accounts.
Returns:
The codes of the shortcut cash accounts.
"""
try:
accounts = settings.ACCOUNTING["CASH_SHORTCUT_ACCOUNTS"]
except AttributeError:
return CASH_SHORTCUT_ACCOUNTS
except TypeError:
return CASH_SHORTCUT_ACCOUNTS
except KeyError:
return CASH_SHORTCUT_ACCOUNTS
if not isinstance(accounts, list):
return CASH_SHORTCUT_ACCOUNTS
return accounts
def get_ledger_accounts() -> List[Account]:
"""Returns the accounts for the ledger.
Returns:
The accounts for the ledger.
"""
"""
For SQL one-liner:
SELECT s.*
FROM accounting_accounts AS s
WHERE s.code IN (SELECT s.code
FROM accounting_accounts AS s
INNER JOIN (SELECT s.code
FROM accounting_accounts AS s
INNER JOIN accounting_records AS r ON r.account_id = s.id
GROUP BY s.code) AS u
ON u.code LIKE s.code || '%%'
GROUP BY s.code)
ORDER BY s.code
"""
codes = {}
for code in [x.code for x in Account.objects
.annotate(Count("record"))
.filter(record__count__gt=0)]:
while len(code) > 0:
codes[code] = True
code = code[:-1]
return Account.objects.filter(code__in=codes).order_by("code")
def get_default_ledger_account() -> Optional[Account]:
"""Returns the default ledger account.
Returns:
The default ledger account.
"""
try:
code = settings.ACCOUNTING["DEFAULT_CASH_ACCOUNT"]
except AttributeError:
code = DEFAULT_CASH_ACCOUNT
except TypeError:
code = DEFAULT_CASH_ACCOUNT
except KeyError:
code = DEFAULT_CASH_ACCOUNT
try:
return Account.objects.get(code=code)
except Account.DoesNotExist:
pass
try:
return Account.objects.get(code=DEFAULT_LEDGER_ACCOUNT)
except Account.DoesNotExist:
pass
return None
def find_imbalanced(records: Iterable[Record]) -> None:
""""Finds the records with imbalanced transactions, and sets their
is_balanced attribute.
Args:
records: The accounting records.
"""
imbalanced = [x.pk for x in Transaction.objects
.annotate(
balance=Sum(Case(
When(record__is_credit=True, then=-1),
default=1) * F("record__amount")))
.filter(~Q(balance=0))]
for record in records:
record.is_balanced = record.transaction.pk not in imbalanced
def find_order_holes(records: Iterable[Record]) -> None:
""""Finds whether the order of the transactions on this day is not
1, 2, 3, 4, 5..., and should be reordered, and sets their
has_order_holes attributes.
Args:
records: The accounting records.
"""
holes = [x["date"] for x in Transaction.objects
.values("date")
.annotate(count=Count("ord"),
max=Max("ord"),
min=Min("ord"))
.filter(~(Q(max=F("count")) & Q(min=1)))] +\
[x["date"] for x in Transaction.objects
.values("date", "ord")
.annotate(count=Count("pk"))
.filter(~Q(count=1))]
for record in records:
record.has_order_hole = record.pk is not None\
and record.transaction.date in holes
def find_payable_records(account: Account, records: Iterable[Record]) -> None:
"""Finds and sets the whether the payable record is paid.
Args:
account: The current ledger account.
records: The accounting records.
"""
try:
payable_accounts = settings.ACCOUNTING["PAYABLE_ACCOUNTS"]
except AttributeError:
return
except TypeError:
return
except KeyError:
return
if not isinstance(payable_accounts, list):
return
if account.code not in payable_accounts:
return
rows = Record.objects\
.filter(
account__code__in=payable_accounts,
summary__isnull=False)\
.values("account__code", "summary")\
.annotate(
balance=Sum(Case(When(is_credit=True, then=1), default=-1)
* F("amount")))\
.filter(~Q(balance=0))
keys = ["%s-%s" % (x["account__code"], x["summary"]) for x in rows]
for x in [x for x in records
if x.pk is not None
and F"{x.account.code}-{x.summary}" in keys]:
x.is_payable = True
def find_existing_equipments(account: Account,
records: Iterable[Record]) -> None:
"""Finds and sets the equipments that still exist.
Args:
account: The current ledger account.
records: The accounting records.
"""
try:
equipment_accounts = settings.ACCOUNTING["EQUIPMENT_ACCOUNTS"]
except AttributeError:
return
except TypeError:
return
except KeyError:
return
if not isinstance(equipment_accounts, list):
return
if account.code not in equipment_accounts:
return
rows = Record.objects\
.filter(
account__code__in=equipment_accounts,
summary__isnull=False)\
.values("account__code", "summary")\
.annotate(
balance=Sum(Case(When(is_credit=True, then=1), default=-1)
* F("amount")))\
.filter(~Q(balance=0))
keys = ["%s-%s" % (x["account__code"], x["summary"]) for x in rows]
for x in [x for x in records
if x.pk is not None
and F"{x.account.code}-{x.summary}" in keys]:
x.is_existing_equipment = True

View File

@ -0,0 +1,64 @@
# 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 gettext as _
from .models import Account, Record
def validate_record_id(value: str) -> None:
"""Validates the record ID.
Args:
value: The record ID.
Raises:
ValidationError: When the validation fails.
"""
try:
Record.objects.get(pk=value)
except Record.DoesNotExist:
raise ValidationError(_("This accounting record does not exists."),
code="not_exist")
def validate_record_account_code(value: str) -> None:
"""Validates an account code.
Args:
value: The account code.
Raises:
ValidationError: When the validation fails.
"""
try:
Account.objects.get(code=value)
except Account.DoesNotExist:
raise ValidationError(_("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(_("You cannot select a parent account."),
code="parent_account")

1225
src/accounting/views.py Normal file

File diff suppressed because it is too large Load Diff