Moved the source files to the "src" subdirectory.
This commit is contained in:
0
src/accounting/__init__.py
Normal file
0
src/accounting/__init__.py
Normal file
5
src/accounting/apps.py
Normal file
5
src/accounting/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountingConfig(AppConfig):
|
||||
name = 'accounting'
|
248
src/accounting/converters.py
Normal file
248
src/accounting/converters.py
Normal 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
632
src/accounting/forms.py
Normal 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
|
1058
src/accounting/locale/zh_Hant/LC_MESSAGES/django.po
Normal file
1058
src/accounting/locale/zh_Hant/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
126
src/accounting/locale/zh_Hant/LC_MESSAGES/djangojs.po
Normal file
126
src/accounting/locale/zh_Hant/LC_MESSAGES/djangojs.po
Normal 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個字)。"
|
889
src/accounting/management/commands/accounting_accounts.py
Normal file
889
src/accounting/management/commands/accounting_accounts.py
Normal 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)
|
206
src/accounting/management/commands/accounting_sample.py
Normal file
206
src/accounting/management/commands/accounting_sample.py
Normal 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)])
|
0
src/accounting/migrations/__init__.py
Normal file
0
src/accounting/migrations/__init__.py
Normal file
506
src/accounting/models.py
Normal file
506
src/accounting/models.py
Normal 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
|
223
src/accounting/static/accounting/css/report.css
Normal file
223
src/accounting/static/accounting/css/report.css
Normal 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;
|
||||
}
|
36
src/accounting/static/accounting/css/summary-helper.css
Normal file
36
src/accounting/static/accounting/css/summary-helper.css
Normal 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;
|
||||
}
|
43
src/accounting/static/accounting/css/transactions-sort.css
Normal file
43
src/accounting/static/accounting/css/transactions-sort.css
Normal 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;
|
||||
}
|
40
src/accounting/static/accounting/css/transactions.css
Normal file
40
src/accounting/static/accounting/css/transactions.css
Normal 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;
|
||||
}
|
189
src/accounting/static/accounting/js/account-form.js
Normal file
189
src/accounting/static/accounting/js/account-form.js
Normal file
@ -0,0 +1,189 @@
|
||||
/* The Mia Website
|
||||
* account-form.js: The JavaScript to edit an account
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2019-2020 imacat.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
* First written: 2020/3/23
|
||||
*/
|
||||
|
||||
// Initializes the page JavaScript.
|
||||
$(function () {
|
||||
getAllAccounts();
|
||||
$("#account-code").on("blur", function () {
|
||||
updateParent(this);
|
||||
validateCode();
|
||||
});
|
||||
$("#account-title").on("blur", function () {
|
||||
validateTitle();
|
||||
});
|
||||
$("#account-form").on("submit", function () {
|
||||
return validateForm();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* All the accounts
|
||||
* @type {Array.}
|
||||
* @private
|
||||
*/
|
||||
let accounts;
|
||||
|
||||
/**
|
||||
* Obtains all the accounts.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function getAllAccounts() {
|
||||
const request = new XMLHttpRequest();
|
||||
request.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;
|
||||
}
|
29
src/accounting/static/accounting/js/account-list.js
Normal file
29
src/accounting/static/accounting/js/account-list.js
Normal 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,
|
||||
});
|
||||
});
|
601
src/accounting/static/accounting/js/summary-helper.js
Normal file
601
src/accounting/static/accounting/js/summary-helper.js
Normal 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);
|
||||
}
|
509
src/accounting/static/accounting/js/transaction-form.js
Normal file
509
src/accounting/static/accounting/js/transaction-form.js
Normal 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;
|
||||
}
|
47
src/accounting/static/accounting/js/transaction-sort.js
Normal file
47
src/accounting/static/accounting/js/transaction-sort.js
Normal 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;
|
||||
}
|
||||
}
|
173
src/accounting/templates/accounting/account_detail.html
Normal file
173
src/accounting/templates/accounting/account_detail.html
Normal 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">×</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">×</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 %}
|
87
src/accounting/templates/accounting/account_form.html
Normal file
87
src/accounting/templates/accounting/account_form.html
Normal 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 %}
|
83
src/accounting/templates/accounting/account_list.html
Normal file
83
src/accounting/templates/accounting/account_list.html
Normal 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 %}
|
@ -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>
|
@ -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>
|
108
src/accounting/templates/accounting/include/report-chooser.html
Normal file
108
src/accounting/templates/accounting/include/report-chooser.html
Normal 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">×</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>
|
164
src/accounting/templates/accounting/include/summary-helper.html
Normal file
164
src/accounting/templates/accounting/include/summary-helper.html
Normal 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">×</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>
|
322
src/accounting/templates/accounting/report-balance-sheet.html
Normal file
322
src/accounting/templates/accounting/report-balance-sheet.html
Normal 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 %}
|
163
src/accounting/templates/accounting/report-cash-summary.html
Normal file
163
src/accounting/templates/accounting/report-cash-summary.html
Normal 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 %}
|
205
src/accounting/templates/accounting/report-cash.html
Normal file
205
src/accounting/templates/accounting/report-cash.html
Normal 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 %}
|
213
src/accounting/templates/accounting/report-income-statement.html
Normal file
213
src/accounting/templates/accounting/report-income-statement.html
Normal 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 %}
|
192
src/accounting/templates/accounting/report-journal.html
Normal file
192
src/accounting/templates/accounting/report-journal.html
Normal 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 %}
|
162
src/accounting/templates/accounting/report-ledger-summary.html
Normal file
162
src/accounting/templates/accounting/report-ledger-summary.html
Normal 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 %}
|
218
src/accounting/templates/accounting/report-ledger.html
Normal file
218
src/accounting/templates/accounting/report-ledger.html
Normal 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 %}
|
164
src/accounting/templates/accounting/report-trial-balance.html
Normal file
164
src/accounting/templates/accounting/report-trial-balance.html
Normal 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 %}
|
158
src/accounting/templates/accounting/search.html
Normal file
158
src/accounting/templates/accounting/search.html
Normal 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 %}
|
@ -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">×</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">×</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 %}
|
@ -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">×</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 %}
|
@ -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">×</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">×</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 %}
|
||||
|
||||
|
121
src/accounting/templates/accounting/transaction_income_form.html
Normal file
121
src/accounting/templates/accounting/transaction_income_form.html
Normal 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">×</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 %}
|
143
src/accounting/templates/accounting/transaction_sort_form.html
Normal file
143
src/accounting/templates/accounting/transaction_sort_form.html
Normal 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 %}
|
@ -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">×</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">×</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 %}
|
@ -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">×</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 %}
|
0
src/accounting/templatetags/__init__.py
Normal file
0
src/accounting/templatetags/__init__.py
Normal file
144
src/accounting/templatetags/accounting.py
Normal file
144
src/accounting/templatetags/accounting.py
Normal 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
75
src/accounting/tests.py
Normal 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
101
src/accounting/urls.py
Normal 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
464
src/accounting/utils.py
Normal 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
|
64
src/accounting/validators.py
Normal file
64
src/accounting/validators.py
Normal 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
1225
src/accounting/views.py
Normal file
File diff suppressed because it is too large
Load Diff
0
src/mia_core/__init__.py
Normal file
0
src/mia_core/__init__.py
Normal file
5
src/mia_core/apps.py
Normal file
5
src/mia_core/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MiaCoreConfig(AppConfig):
|
||||
name = 'mia_core'
|
135
src/mia_core/locale/zh_Hant/LC_MESSAGES/django.po
Normal file
135
src/mia_core/locale/zh_Hant/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,135 @@
|
||||
# Traditional Chinese PO file for the Mia core application
|
||||
# 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-core 3.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-01-17 00:24+0800\n"
|
||||
"PO-Revision-Date: 2021-01-17 00:29+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"
|
||||
|
||||
#: mia_core/period.py:452 mia_core/period.py:487 mia_core/period.py:505
|
||||
#: mia_core/period.py:518 mia_core/period.py:564
|
||||
#, python-format
|
||||
msgid "In %s"
|
||||
msgstr "%s"
|
||||
|
||||
#: mia_core/period.py:462
|
||||
#, python-format
|
||||
msgid "Since %s"
|
||||
msgstr "%s至今"
|
||||
|
||||
#: mia_core/period.py:475 mia_core/period.py:496 mia_core/period.py:575
|
||||
#, python-format
|
||||
msgid "Until %s"
|
||||
msgstr "至%s前"
|
||||
|
||||
#: mia_core/period.py:504
|
||||
msgid "All Time"
|
||||
msgstr "全部"
|
||||
|
||||
#: mia_core/period.py:588 mia_core/period.py:621
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:60
|
||||
#: mia_core/templatetags/mia_core.py:219
|
||||
msgid "This Month"
|
||||
msgstr "這個月"
|
||||
|
||||
#: mia_core/period.py:629
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:63
|
||||
#: mia_core/templatetags/mia_core.py:226
|
||||
msgid "Last Month"
|
||||
msgstr "上個月"
|
||||
|
||||
#: mia_core/period.py:644
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:76
|
||||
msgid "This Year"
|
||||
msgstr "今年"
|
||||
|
||||
#: mia_core/period.py:646
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:79
|
||||
msgid "Last Year"
|
||||
msgstr "去年"
|
||||
|
||||
#: mia_core/period.py:661
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:95
|
||||
#: mia_core/templatetags/mia_core.py:187
|
||||
msgid "Today"
|
||||
msgstr "今天"
|
||||
|
||||
#: mia_core/period.py:663
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:98
|
||||
#: mia_core/templatetags/mia_core.py:190
|
||||
msgid "Yesterday"
|
||||
msgstr "昨天"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:37
|
||||
msgid "Choosing Your Period"
|
||||
msgstr "選擇時間區間"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:45
|
||||
msgid "Month"
|
||||
msgstr "月"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:48
|
||||
msgid "Year"
|
||||
msgstr "年"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:51
|
||||
msgid "Day"
|
||||
msgstr "日"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:54
|
||||
msgid "Custom"
|
||||
msgstr "自訂"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:66
|
||||
msgid "Since Last Month"
|
||||
msgstr "上個月至今"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:103
|
||||
msgid "Date:"
|
||||
msgstr "日期:"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:107
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:125
|
||||
msgid "Confirm"
|
||||
msgstr "確定"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:113
|
||||
msgid "All"
|
||||
msgstr "全部"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:117
|
||||
msgid "From:"
|
||||
msgstr "從:"
|
||||
|
||||
#: mia_core/templates/mia_core/include/period-chooser.html:121
|
||||
msgid "To:"
|
||||
msgstr "到:"
|
||||
|
||||
#: mia_core/templatetags/mia_core.py:192
|
||||
msgid "Tomorrow"
|
||||
msgstr "明天"
|
||||
|
||||
#: mia_core/utils.py:342
|
||||
msgctxt "Pagination|"
|
||||
msgid "Previous"
|
||||
msgstr "上一頁"
|
||||
|
||||
#: mia_core/utils.py:370 mia_core/utils.py:391
|
||||
msgctxt "Pagination|"
|
||||
msgid "..."
|
||||
msgstr "…"
|
||||
|
||||
#: mia_core/utils.py:410
|
||||
msgctxt "Pagination|"
|
||||
msgid "Next"
|
||||
msgstr "下一頁"
|
130
src/mia_core/management/commands/make_trans.py
Normal file
130
src/mia_core/management/commands/make_trans.py
Normal file
@ -0,0 +1,130 @@
|
||||
# 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 os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import BaseCommand, CommandParser, CommandError, \
|
||||
call_command
|
||||
from django.utils import timezone
|
||||
from opencc import OpenCC
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Updates the revision date, converts the Traditional Chinese
|
||||
translation into Simplified Chinese, and then calls the
|
||||
compilemessages command.
|
||||
"""
|
||||
help = ("Updates the revision date, converts the Traditional Chinese"
|
||||
" translation into Simplified Chinese, and then calls the"
|
||||
" compilemessages command.")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._cc: OpenCC = OpenCC("tw2sp")
|
||||
self._now: str = timezone.localtime().strftime("%Y-%m-%d %H:%M%z")
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Adds command line arguments to the parser.
|
||||
|
||||
Args:
|
||||
parser (CommandParser): The command line argument parser.
|
||||
"""
|
||||
parser.add_argument("app_dir", nargs="+",
|
||||
help=("One or more application directories that"
|
||||
" contains their locale subdirectories"))
|
||||
parser.add_argument("--domain", "-d", action="append",
|
||||
choices=["django", "djangojs"], required=True,
|
||||
help="The domain, either django or djangojs")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Runs the command.
|
||||
|
||||
Args:
|
||||
*args (list[str]): The command line arguments.
|
||||
**options (dict[str,str]): The command line switches.
|
||||
"""
|
||||
locale_dirs = [os.path.join(settings.BASE_DIR, x, "locale")
|
||||
for x in options["app_dir"]]
|
||||
missing = [x for x in locale_dirs if not os.path.isdir(x)]
|
||||
if len(missing) > 0:
|
||||
error = "Directories not exist: " + ", ".join(missing)
|
||||
raise CommandError(error, returncode=1)
|
||||
domains = [x for x in ["django", "djangojs"] if x in options["domain"]]
|
||||
for locale_dir in locale_dirs:
|
||||
for domain in domains:
|
||||
self._handle_po(locale_dir, domain)
|
||||
call_command("compilemessages")
|
||||
|
||||
def _handle_po(self, locale_dir: str, domain: str) -> None:
|
||||
"""Updates a PO file in a specific directory
|
||||
|
||||
Args:
|
||||
locale_dir: the locale directory that contains the PO file
|
||||
domain: The domain, either django or djangojs.
|
||||
"""
|
||||
zh_hant = os.path.join(
|
||||
locale_dir, "zh_Hant", "LC_MESSAGES", F"{domain}.po")
|
||||
zh_hans = os.path.join(
|
||||
locale_dir, "zh_Hans", "LC_MESSAGES", F"{domain}.po")
|
||||
self._update_rev_date(zh_hant)
|
||||
self._convert_chinese(zh_hant, zh_hans)
|
||||
|
||||
def _update_rev_date(self, file: str) -> None:
|
||||
"""Updates the revision date of the PO file.
|
||||
|
||||
Args:
|
||||
file: the PO file as its full path.
|
||||
"""
|
||||
size = Path(file).stat().st_size
|
||||
with open(file, "r+") as f:
|
||||
content = f.read(size)
|
||||
content = re.sub("\n\"PO-Revision-Date: [^\n]*\"\n",
|
||||
F"\n\"PO-Revision-Date: {self._now}\\\\n\"\n",
|
||||
content)
|
||||
f.seek(0)
|
||||
f.write(content)
|
||||
|
||||
def _convert_chinese(self, zh_hant: str, zh_hans: str) -> None:
|
||||
"""Creates the Simplified Chinese PO file from the Traditional
|
||||
Chinese PO file.
|
||||
|
||||
Args:
|
||||
zh_hant: the Traditional Chinese PO file as its full path.
|
||||
zh_hans: the Simplified Chinese PO file as its full path.
|
||||
"""
|
||||
size = Path(zh_hant).stat().st_size
|
||||
with open(zh_hant, "r") as f:
|
||||
content = f.read(size)
|
||||
content = self._cc.convert(content)
|
||||
content = re.sub("^# Traditional Chinese PO file for the ",
|
||||
"# Simplified Chinese PO file for the ", content)
|
||||
content = re.sub("\n\"PO-Revision-Date: [^\n]*\"\n",
|
||||
F"\n\"PO-Revision-Date: {self._now}\\\\n\"\n",
|
||||
content)
|
||||
content = re.sub("\n\"Language-Team: Traditional Chinese",
|
||||
"\n\"Language-Team: Simplified Chinese", content)
|
||||
content = re.sub("\n\"Language: [^\n]*\"\n",
|
||||
"\n\"Language: Simplified Chinese\\\\n\"\n",
|
||||
content)
|
||||
with open(zh_hans, "w") as f:
|
||||
f.write(content)
|
236
src/mia_core/models.py
Normal file
236
src/mia_core/models.py
Normal file
@ -0,0 +1,236 @@
|
||||
# The core 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 Mia core application.
|
||||
|
||||
"""
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from dirtyfields import DirtyFieldsMixin
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
|
||||
from mia_core.utils import new_pk, Language
|
||||
|
||||
|
||||
class RandomPkModel(models.Model):
|
||||
"""The abstract data model that uses 9-digit random primary keys."""
|
||||
id = models.PositiveIntegerField(primary_key=True)
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None,
|
||||
update_fields=None):
|
||||
if self.pk is None:
|
||||
self.pk = new_pk(self.__class__)
|
||||
super().save(force_insert=force_insert, force_update=force_update,
|
||||
using=using, update_fields=update_fields)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class StampedModel(models.Model):
|
||||
"""The abstract base model that has created_at, created_by, updated_at, and
|
||||
updated_by."""
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
||||
related_name="created_%(app_label)s_%(class)s")
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
||||
related_name="updated_%(app_label)s_%(class)s")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.current_user = None
|
||||
if "current_user" in kwargs:
|
||||
self.current_user = kwargs["current_user"]
|
||||
del kwargs["current_user"]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None,
|
||||
update_fields=None):
|
||||
if self.current_user is None:
|
||||
raise AttributeError(
|
||||
F"Missing current_user in {self.__class__.__name__}")
|
||||
try:
|
||||
self.created_by
|
||||
except ObjectDoesNotExist as e:
|
||||
self.created_by = self.current_user
|
||||
self.updated_by = self.current_user
|
||||
super().save(force_insert=force_insert, force_update=force_update,
|
||||
using=using, update_fields=update_fields)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class LocalizedModel(models.Model):
|
||||
"""An abstract localized model."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if getattr(self, "_l10n", None) is None:
|
||||
self._l10n: Dict[str, Dict[str, Any]] = {}
|
||||
# Amends the is_dirty() method in DirtyFieldsMixin
|
||||
if isinstance(self, DirtyFieldsMixin):
|
||||
old_is_dirty = getattr(self, "is_dirty", None)
|
||||
|
||||
def new_is_dirty(check_relationship=False, check_m2m=None) -> bool:
|
||||
"""Returns whether the current data model is changed."""
|
||||
if old_is_dirty(check_relationship=check_relationship,
|
||||
check_m2m=check_m2m):
|
||||
return True
|
||||
default_language = self._get_default_language()
|
||||
for name in self._l10n:
|
||||
for language in self._l10n[name]:
|
||||
new_value = self._l10n[name][language]
|
||||
if language == default_language:
|
||||
if getattr(self, name + "_l10n") != new_value:
|
||||
return True
|
||||
else:
|
||||
l10n_rec = self._get_l10n_set() \
|
||||
.filter(name=name, language=language) \
|
||||
.first()
|
||||
if l10n_rec is None or l10n_rec.value != new_value:
|
||||
return True
|
||||
return False
|
||||
self.is_dirty = new_is_dirty
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None,
|
||||
update_fields=None):
|
||||
"""Saves the data model, along with the localized contents."""
|
||||
default_language = self._get_default_language()
|
||||
l10n_to_save: List[models.Model] = []
|
||||
if getattr(self, "_l10n", None) is not None:
|
||||
for name in self._l10n:
|
||||
for language in self._l10n[name]:
|
||||
new_value = self._l10n[name][language]
|
||||
if language == default_language:
|
||||
setattr(self, name + "_l10n", new_value)
|
||||
else:
|
||||
current_value = getattr(self, name + "_l10n")
|
||||
if current_value is None or current_value == "":
|
||||
setattr(self, name + "_l10n", new_value)
|
||||
if self.pk is None:
|
||||
l10n_rec = None
|
||||
else:
|
||||
l10n_rec = self._get_l10n_set()\
|
||||
.filter(name=name, language=language)\
|
||||
.first()
|
||||
if l10n_rec is None:
|
||||
l10n_to_save.append(self._get_l10n_set().model(
|
||||
master=self, name=name,
|
||||
language=language,
|
||||
value=self._l10n[name][language]))
|
||||
elif l10n_rec.value != new_value:
|
||||
if getattr(self, name + "_l10n") == l10n_rec.value:
|
||||
setattr(self, name + "_l10n", new_value)
|
||||
l10n_rec.value = new_value
|
||||
l10n_to_save.append(l10n_rec)
|
||||
super().save(force_insert=force_insert, force_update=force_update,
|
||||
using=using, update_fields=update_fields)
|
||||
for l10n_rec in l10n_to_save:
|
||||
if isinstance(self, StampedModel)\
|
||||
and isinstance(l10n_rec, StampedModel):
|
||||
l10n_rec.current_user = self.current_user
|
||||
l10n_rec.save(force_insert=force_insert, force_update=force_update,
|
||||
using=using, update_fields=update_fields)
|
||||
|
||||
def _get_l10n_set(self):
|
||||
"""Returns the related localization data model."""
|
||||
l10n_set = getattr(self, "l10n_set", None)
|
||||
if l10n_set is None:
|
||||
raise AttributeError("Please define the localization data model.")
|
||||
return l10n_set
|
||||
|
||||
def _get_default_language(self) -> str:
|
||||
"""Returns the default language."""
|
||||
default = getattr(self.__class__, "DEFAULT_LANGUAGE", None)
|
||||
return Language.default().id if default is None else default
|
||||
|
||||
def get_l10n(self, name: str) -> Any:
|
||||
"""Returns the value of a localized field in the current language.
|
||||
|
||||
Args:
|
||||
name: The field name.
|
||||
|
||||
Returns:
|
||||
The value of this field in the current language.
|
||||
"""
|
||||
return self.get_l10n_in(name, Language.current().id)
|
||||
|
||||
def get_l10n_in(self, name: str, language: str) -> Any:
|
||||
"""Returns the value of a localized field in a specific language.
|
||||
|
||||
Args:
|
||||
name: The field name.
|
||||
language: The language ID.
|
||||
|
||||
Returns:
|
||||
The value of this field in this language.
|
||||
"""
|
||||
if getattr(self, "_l10n", None) is None:
|
||||
self._l10n: Dict[str, Dict[str, Any]] = {}
|
||||
if name not in self._l10n:
|
||||
self._l10n[name]: Dict[str, Any] = {}
|
||||
if language not in self._l10n[name]:
|
||||
if language != self._get_default_language():
|
||||
l10n_rec = self._get_l10n_set() \
|
||||
.filter(name=name, language=language) \
|
||||
.first()
|
||||
self._l10n[name][language] = getattr(self, name + "_l10n") \
|
||||
if l10n_rec is None else l10n_rec.value
|
||||
else:
|
||||
self._l10n[name][language] = getattr(self, name + "_l10n")
|
||||
return self._l10n[name][language]
|
||||
|
||||
def set_l10n(self, name: str, value: Any) -> None:
|
||||
"""Sets the value of a localized field in the current language.
|
||||
|
||||
Args:
|
||||
name: The field name.
|
||||
value: The value.
|
||||
"""
|
||||
self.set_l10n_in(name, Language.current().id, value)
|
||||
|
||||
def set_l10n_in(self, name: str, language: str, value: Any) -> None:
|
||||
"""Sets the value of a localized field in a specific language.
|
||||
|
||||
Args:
|
||||
name: The field name.
|
||||
language: The language ID.
|
||||
value: The value.
|
||||
"""
|
||||
if getattr(self, "_l10n", None) is None:
|
||||
self._l10n: Dict[str, Dict[str, Any]] = {}
|
||||
if name not in self._l10n:
|
||||
self._l10n[name]: Dict[str, Any] = {}
|
||||
self._l10n[name][language] = value
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class L10nModel(models.Model):
|
||||
"""The abstract base localization model."""
|
||||
name = models.CharField(max_length=128)
|
||||
language = models.CharField(max_length=7)
|
||||
value = models.CharField(max_length=65535)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
667
src/mia_core/period.py
Normal file
667
src/mia_core/period.py
Normal file
@ -0,0 +1,667 @@
|
||||
# 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 period chooser utilities of the Mia core application.
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
from typing import Optional, List
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.template import defaultfilters
|
||||
from django.utils import dateformat, timezone
|
||||
from django.utils.translation import gettext
|
||||
|
||||
from mia_core.utils import Language
|
||||
|
||||
|
||||
class Period:
|
||||
"""The template helper for the period chooser.
|
||||
|
||||
Args:
|
||||
spec: The current period specification
|
||||
data_start: The available first day of the data.
|
||||
data_end: The available last day of the data.
|
||||
|
||||
Raises:
|
||||
ValueError: When the period specification is invalid.
|
||||
"""
|
||||
def __init__(self, spec: str = None, data_start: datetime.date = None,
|
||||
data_end: datetime.date = None):
|
||||
# Raises ValueError
|
||||
self._period = self.Parser(spec)
|
||||
self._data_start = data_start
|
||||
self._data_end = data_end
|
||||
|
||||
@property
|
||||
def spec(self) -> str:
|
||||
"""Returns the period specification.
|
||||
|
||||
Returns:
|
||||
The period specification.
|
||||
"""
|
||||
return self._period.spec
|
||||
|
||||
@property
|
||||
def start(self) -> datetime.date:
|
||||
"""Returns the start day of the currently-specified period.
|
||||
|
||||
Returns:
|
||||
The start day of the currently-specified period.
|
||||
"""
|
||||
return self._period.start
|
||||
|
||||
@property
|
||||
def end(self) -> datetime.date:
|
||||
"""Returns the end day of the currently-specified period.
|
||||
|
||||
Returns:
|
||||
The end day of the currently-specified period.
|
||||
"""
|
||||
return self._period.end
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""Returns the text description of the currently-specified period.
|
||||
|
||||
Returns:
|
||||
The text description of the currently-specified period
|
||||
"""
|
||||
return self._period.description
|
||||
|
||||
@property
|
||||
def prep_desc(self) -> str:
|
||||
"""Returns the text description with preposition of the
|
||||
currently-specified period.
|
||||
|
||||
Returns:
|
||||
The text description with preposition of the currently-specified
|
||||
period.
|
||||
"""
|
||||
return self._period.prep_desc
|
||||
|
||||
@staticmethod
|
||||
def _get_last_month_start() -> datetime.date:
|
||||
"""Returns the first day of the last month.
|
||||
|
||||
Returns:
|
||||
The first day of the last month.
|
||||
"""
|
||||
today = timezone.localdate()
|
||||
month = today.month - 1
|
||||
year = today.year
|
||||
if month < 1:
|
||||
month = 12
|
||||
year = year - 1
|
||||
return datetime.date(year, month, 1)
|
||||
|
||||
@staticmethod
|
||||
def _get_next_month_start() -> datetime.date:
|
||||
"""Returns the first day of the next month.
|
||||
|
||||
Returns:
|
||||
The first day of the next month.
|
||||
"""
|
||||
today = timezone.localdate()
|
||||
month = today.month + 1
|
||||
year = today.year
|
||||
if month > 12:
|
||||
month = 1
|
||||
year = year + 1
|
||||
return datetime.date(year, month, 1)
|
||||
|
||||
def this_month(self) -> Optional[str]:
|
||||
"""Returns the specification of this month.
|
||||
|
||||
Returns:
|
||||
The specification of this month, or None if there is no data in or
|
||||
before this month.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
today = timezone.localdate()
|
||||
first_month_start = datetime.date(
|
||||
self._data_start.year, self._data_start.month, 1)
|
||||
if today < first_month_start:
|
||||
return None
|
||||
return dateformat.format(today, "Y-m")
|
||||
|
||||
def last_month(self) -> Optional[str]:
|
||||
"""Returns the specification of last month.
|
||||
|
||||
Returns:
|
||||
The specification of last month, or None if there is no data in or
|
||||
before last month.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
last_month_start = self._get_last_month_start()
|
||||
first_month_start = datetime.date(
|
||||
self._data_start.year, self._data_start.month, 1)
|
||||
if last_month_start < first_month_start:
|
||||
return None
|
||||
return dateformat.format(last_month_start, "Y-m")
|
||||
|
||||
def since_last_month(self) -> Optional[str]:
|
||||
"""Returns the specification since last month.
|
||||
|
||||
Returns:
|
||||
The specification since last month, or None if there is no data in
|
||||
or before last month.
|
||||
"""
|
||||
last_month = self.last_month()
|
||||
if last_month is None:
|
||||
return None
|
||||
return last_month + "-"
|
||||
|
||||
def has_months_to_choose(self) -> bool:
|
||||
"""Returns whether there are months to choose besides this month and
|
||||
last month.
|
||||
|
||||
Returns:
|
||||
True if there are months to choose besides this month and last
|
||||
month, or False otherwise.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return False
|
||||
if self._data_start < self._get_last_month_start():
|
||||
return True
|
||||
if self._data_end >= self._get_next_month_start():
|
||||
return True
|
||||
return False
|
||||
|
||||
def chosen_month(self) -> Optional[str]:
|
||||
"""Returns the specification of the chosen month, or None if the
|
||||
current period is not a month or is out of available data range.
|
||||
|
||||
Returns:
|
||||
The specification of the chosen month, or None if the current
|
||||
period is not a month or is out of available data range.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
m = re.match("^[0-9]{4}-[0-2]{2}", self._period.spec)
|
||||
if m is None:
|
||||
return None
|
||||
if self._period.end < self._data_start:
|
||||
return None
|
||||
if self._period.start > self._data_end:
|
||||
return None
|
||||
return self._period.spec
|
||||
|
||||
def this_year(self) -> Optional[str]:
|
||||
"""Returns the specification of this year.
|
||||
|
||||
Returns:
|
||||
The specification of this year, or None if there is no data in or
|
||||
before this year.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
this_year = timezone.localdate().year
|
||||
if this_year < self._data_start.year:
|
||||
return None
|
||||
return str(this_year)
|
||||
|
||||
def last_year(self) -> Optional[str]:
|
||||
"""Returns the specification of last year.
|
||||
|
||||
Returns:
|
||||
The specification of last year, or None if there is no data in or
|
||||
before last year.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
last_year = timezone.localdate().year - 1
|
||||
if last_year < self._data_start.year:
|
||||
return None
|
||||
return str(last_year)
|
||||
|
||||
def has_years_to_choose(self) -> bool:
|
||||
"""Returns whether there are years to choose besides this year and
|
||||
last year.
|
||||
|
||||
Returns:
|
||||
True if there are years to choose besides this year and last year,
|
||||
or False otherwise.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return False
|
||||
this_year = timezone.localdate().year
|
||||
if self._data_start.year < this_year - 1:
|
||||
return True
|
||||
if self._data_end.year > this_year:
|
||||
return True
|
||||
return False
|
||||
|
||||
def years_to_choose(self) -> Optional[List[str]]:
|
||||
"""Returns the years to choose besides this year and last year.
|
||||
|
||||
Returns:
|
||||
The years to choose besides this year and last year, or None if
|
||||
there is no data.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
this_year = timezone.localdate().year
|
||||
before = [str(x) for x in range(
|
||||
self._data_start.year, this_year - 1)]
|
||||
after = [str(x) for x in range(
|
||||
self._data_end.year, this_year, -1)]
|
||||
return after + before[::-1]
|
||||
|
||||
def today(self) -> Optional[None]:
|
||||
"""Returns the specification of today.
|
||||
|
||||
Returns:
|
||||
The specification of today, or None if there is no data in or
|
||||
before today.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
today = timezone.localdate()
|
||||
if today < self._data_start or today > self._data_end:
|
||||
return None
|
||||
return dateformat.format(today, "Y-m-d")
|
||||
|
||||
def yesterday(self) -> Optional[str]:
|
||||
"""Returns the specification of yesterday.
|
||||
|
||||
Returns:
|
||||
The specification of yesterday, or None if there is no data in or
|
||||
before yesterday.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
yesterday = timezone.localdate() - datetime.timedelta(days=1)
|
||||
if yesterday < self._data_start or yesterday > self._data_end:
|
||||
return None
|
||||
return dateformat.format(yesterday, "Y-m-d")
|
||||
|
||||
def chosen_day(self) -> str:
|
||||
"""Returns the specification of the chosen day.
|
||||
|
||||
Returns:
|
||||
The specification of the chosen day, or the start day of the period
|
||||
if the current period is not a day.
|
||||
"""
|
||||
return dateformat.format(self._period.start, "Y-m-d")
|
||||
|
||||
def has_days_to_choose(self) -> bool:
|
||||
"""Returns whether there are more than one day to choose from.
|
||||
|
||||
Returns:
|
||||
True if there are more than one day to choose from, or False
|
||||
otherwise.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return False
|
||||
if self._data_start == self._data_end:
|
||||
return False
|
||||
return True
|
||||
|
||||
def first_day(self) -> Optional[str]:
|
||||
"""Returns the specification of the available first day.
|
||||
|
||||
Returns:
|
||||
The specification of the available first day, or None if there is
|
||||
no data.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
return dateformat.format(self._data_start, "Y-m-d")
|
||||
|
||||
def last_day(self) -> Optional[str]:
|
||||
"""Returns the specification of the available last day.
|
||||
|
||||
Returns:
|
||||
The specification of the available last day, or None if there is no
|
||||
data.
|
||||
"""
|
||||
if self._data_end is None:
|
||||
return None
|
||||
return dateformat.format(self._data_end, "Y-m-d")
|
||||
|
||||
def chosen_start(self) -> Optional[str]:
|
||||
"""Returns the specification of of the first day of the
|
||||
specified period.
|
||||
|
||||
Returns:
|
||||
The specification of of the first day of the specified period, or
|
||||
None if there is no data.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
day = self._period.start \
|
||||
if self._period.start >= self._data_start \
|
||||
else self._data_start
|
||||
return dateformat.format(day, "Y-m-d")
|
||||
|
||||
def chosen_end(self) -> Optional[str]:
|
||||
"""Returns the specification of of the last day of the
|
||||
specified period.
|
||||
|
||||
Returns:
|
||||
The specification of of the last day of the specified period, or
|
||||
None if there is data.
|
||||
"""
|
||||
if self._data_end is None:
|
||||
return None
|
||||
day = self._period.end \
|
||||
if self._period.end <= self._data_end \
|
||||
else self._data_end
|
||||
return dateformat.format(day, "Y-m-d")
|
||||
|
||||
def period_before(self) -> Optional[str]:
|
||||
"""Returns the specification of the period before the current period.
|
||||
|
||||
Returns:
|
||||
The specification of the period before the current period, or None
|
||||
if there is no data before the current period.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
if self.start <= self._data_start:
|
||||
return None
|
||||
previous_day = self.start - datetime.timedelta(days=1)
|
||||
if re.match("^[0-9]{4}$", self.spec):
|
||||
return "-" + str(previous_day.year)
|
||||
if re.match("^[0-9]{4}-[0-9]{2}$", self.spec):
|
||||
return dateformat.format(previous_day, "-Y-m")
|
||||
return dateformat.format(previous_day, "-Y-m-d")
|
||||
|
||||
def month_picker_params(self) -> Optional[str]:
|
||||
"""Returns the parameters for the month-picker, as a JSON text string.
|
||||
|
||||
Returns:
|
||||
The parameters for the month-picker, as a JSON text string, or None
|
||||
if there is no data.
|
||||
"""
|
||||
if self._data_start is None:
|
||||
return None
|
||||
start = datetime.date(self._data_start.year, self._data_start.month, 1)
|
||||
return DjangoJSONEncoder().encode({
|
||||
"locale": Language.current().locale,
|
||||
"minDate": start,
|
||||
"maxDate": self._data_end,
|
||||
"defaultDate": self.chosen_month(),
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def default_spec() -> str:
|
||||
"""Returns the specification for the default period.
|
||||
|
||||
Returns:
|
||||
str: The specification for the default period
|
||||
"""
|
||||
return dateformat.format(timezone.localdate(), "Y-m")
|
||||
|
||||
class Parser:
|
||||
"""The period parser.
|
||||
|
||||
Args:
|
||||
spec (str|None): The period specification.
|
||||
|
||||
Raises:
|
||||
ValueError: When the period specification is invalid.
|
||||
|
||||
Attributes:
|
||||
spec (str): The currently-using period specification.
|
||||
start (datetime.date): The start of the period.
|
||||
end (datetime.date): The end of the period.
|
||||
description (str): The text description of the period.
|
||||
prep_desc (str): The text description with preposition.
|
||||
"""
|
||||
VERY_START: datetime.date = datetime.date(1990, 1, 1)
|
||||
|
||||
def __init__(self, spec: str):
|
||||
self.spec = None
|
||||
self.start = None
|
||||
self.end = None
|
||||
self.description = None
|
||||
self.prep_desc = None
|
||||
|
||||
if spec is None:
|
||||
self._set_this_month()
|
||||
return
|
||||
self.spec = spec
|
||||
# A specific month
|
||||
m = re.match("^([0-9]{4})-([0-9]{2})$", spec)
|
||||
if m is not None:
|
||||
year = int(m.group(1))
|
||||
month = int(m.group(2))
|
||||
# Raises ValueError
|
||||
self.start = datetime.date(year, month, 1)
|
||||
self.end = self._month_last_day(self.start)
|
||||
self.description = self._month_text(year, month)
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# From a specific month
|
||||
m = re.match("^([0-9]{4})-([0-9]{2})-$", spec)
|
||||
if m is not None:
|
||||
year = int(m.group(1))
|
||||
month = int(m.group(2))
|
||||
# Raises ValueError
|
||||
self.start = datetime.date(year, month, 1)
|
||||
self.end = self._month_last_day(timezone.localdate())
|
||||
self.description = gettext("Since %s")\
|
||||
% self._month_text(year, month)
|
||||
self.prep_desc = self.description
|
||||
return
|
||||
# Until a specific month
|
||||
m = re.match("^-([0-9]{4})-([0-9]{2})$", spec)
|
||||
if m is not None:
|
||||
year = int(m.group(1))
|
||||
month = int(m.group(2))
|
||||
# Raises ValueError
|
||||
until_month = datetime.date(year, month, 1)
|
||||
self.start = Period.Parser.VERY_START
|
||||
self.end = self._month_last_day(until_month)
|
||||
self.description = gettext("Until %s")\
|
||||
% self._month_text(year, month)
|
||||
self.prep_desc = self.description
|
||||
return
|
||||
# A specific year
|
||||
m = re.match("^([0-9]{4})$", spec)
|
||||
if m is not None:
|
||||
year = int(m.group(1))
|
||||
# Raises ValueError
|
||||
self.start = datetime.date(year, 1, 1)
|
||||
self.end = datetime.date(year, 12, 31)
|
||||
self.description = self._year_text(year)
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# Until a specific year
|
||||
m = re.match("^-([0-9]{4})$", spec)
|
||||
if m is not None:
|
||||
year = int(m.group(1))
|
||||
# Raises ValueError
|
||||
self.end = datetime.date(year, 12, 31)
|
||||
self.start = Period.Parser.VERY_START
|
||||
self.description = gettext("Until %s")\
|
||||
% self._year_text(year)
|
||||
self.prep_desc = self.description
|
||||
return
|
||||
# All time
|
||||
if spec == "-":
|
||||
self.start = Period.Parser.VERY_START
|
||||
self.end = self._month_last_day(timezone.localdate())
|
||||
self.description = gettext("All Time")
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# A specific date
|
||||
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$",
|
||||
spec)
|
||||
if m is not None:
|
||||
# Raises ValueError
|
||||
self.start = datetime.date(
|
||||
int(m.group(1)),
|
||||
int(m.group(2)),
|
||||
int(m.group(3)))
|
||||
self.end = self.start
|
||||
self.description = self._date_text(self.start)
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# A specific date period
|
||||
m = re.match(("^([0-9]{4})-([0-9]{2})-([0-9]{2})"
|
||||
"-([0-9]{4})-([0-9]{2})-([0-9]{2})$"),
|
||||
spec)
|
||||
if m is not None:
|
||||
# Raises ValueError
|
||||
self.start = datetime.date(
|
||||
int(m.group(1)),
|
||||
int(m.group(2)),
|
||||
int(m.group(3)))
|
||||
self.end = datetime.date(
|
||||
int(m.group(4)),
|
||||
int(m.group(5)),
|
||||
int(m.group(6)))
|
||||
today = timezone.localdate()
|
||||
# Spans several years
|
||||
if self.start.year != self.end.year:
|
||||
self.description = "%s-%s" % (
|
||||
defaultfilters.date(self.start, "Y/n/j"),
|
||||
defaultfilters.date(self.end, "Y/n/j"))
|
||||
# Spans several months
|
||||
elif self.start.month != self.end.month:
|
||||
if self.start.year != today.year:
|
||||
self.description = "%s-%s" % (
|
||||
defaultfilters.date(self.start, "Y/n/j"),
|
||||
defaultfilters.date(self.end, "n/j"))
|
||||
else:
|
||||
self.description = "%s-%s" % (
|
||||
defaultfilters.date(self.start, "n/j"),
|
||||
defaultfilters.date(self.end, "n/j"))
|
||||
# Spans several days
|
||||
elif self.start.day != self.end.day:
|
||||
if self.start.year != today.year:
|
||||
self.description = "%s-%s" % (
|
||||
defaultfilters.date(self.start, "Y/n/j"),
|
||||
defaultfilters.date(self.end, "j"))
|
||||
else:
|
||||
self.description = "%s-%s" % (
|
||||
defaultfilters.date(self.start, "n/j"),
|
||||
defaultfilters.date(self.end, "j"))
|
||||
# At the same day
|
||||
else:
|
||||
self.spec = dateformat.format(self.start, "Y-m-d")
|
||||
self.description = self._date_text(self.start)
|
||||
self.prep_desc = gettext("In %s") % self.description
|
||||
return
|
||||
# Until a specific day
|
||||
m = re.match("^-([0-9]{4})-([0-9]{2})-([0-9]{2})$", spec)
|
||||
if m is not None:
|
||||
# Raises ValueError
|
||||
self.end = datetime.date(
|
||||
int(m.group(1)),
|
||||
int(m.group(2)),
|
||||
int(m.group(3)))
|
||||
self.start = Period.Parser.VERY_START
|
||||
self.description = gettext("Until %s")\
|
||||
% self._date_text(self.end)
|
||||
self.prep_desc = self.description
|
||||
return
|
||||
# Wrong period format
|
||||
raise ValueError
|
||||
|
||||
def _set_this_month(self) -> None:
|
||||
"""Sets the period to this month."""
|
||||
today = timezone.localdate()
|
||||
self.spec = dateformat.format(today, "Y-m")
|
||||
self.start = datetime.date(today.year, today.month, 1)
|
||||
self.end = self._month_last_day(self.start)
|
||||
self.description = gettext("This Month")
|
||||
|
||||
@staticmethod
|
||||
def _month_last_day(day: datetime.date) -> datetime.date:
|
||||
"""Calculates and returns the last day of a month.
|
||||
|
||||
Args:
|
||||
day: A day in the month.
|
||||
|
||||
Returns:
|
||||
The last day in the month
|
||||
"""
|
||||
next_month = day.month + 1
|
||||
next_year = day.year
|
||||
if next_month > 12:
|
||||
next_month = 1
|
||||
next_year = next_year + 1
|
||||
return datetime.date(
|
||||
next_year, next_month, 1) - datetime.timedelta(days=1)
|
||||
|
||||
@staticmethod
|
||||
def _month_text(year: int, month: int) -> str:
|
||||
"""Returns the text description of a month.
|
||||
|
||||
Args:
|
||||
year: The year.
|
||||
month: The month.
|
||||
|
||||
Returns:
|
||||
The description of the month.
|
||||
"""
|
||||
today = timezone.localdate()
|
||||
if year == today.year and month == today.month:
|
||||
return gettext("This Month")
|
||||
prev_month = today.month - 1
|
||||
prev_year = today.year
|
||||
if prev_month < 1:
|
||||
prev_month = 12
|
||||
prev_year = prev_year - 1
|
||||
prev = datetime.date(prev_year, prev_month, 1)
|
||||
if year == prev.year and month == prev.month:
|
||||
return gettext("Last Month")
|
||||
return "%d/%d" % (year, month)
|
||||
|
||||
@staticmethod
|
||||
def _year_text(year: int) -> str:
|
||||
"""Returns the text description of a year.
|
||||
|
||||
Args:
|
||||
year: The year.
|
||||
|
||||
Returns:
|
||||
The description of the year.
|
||||
"""
|
||||
this_year = timezone.localdate().year
|
||||
if year == this_year:
|
||||
return gettext("This Year")
|
||||
if year == this_year - 1:
|
||||
return gettext("Last Year")
|
||||
return str(year)
|
||||
|
||||
@staticmethod
|
||||
def _date_text(day: datetime.date) -> str:
|
||||
"""Returns the text description of a day.
|
||||
|
||||
Args:
|
||||
day: The date.
|
||||
|
||||
Returns:
|
||||
The description of the day.
|
||||
"""
|
||||
today = timezone.localdate()
|
||||
if day == today:
|
||||
return gettext("Today")
|
||||
elif day == today - datetime.timedelta(days=1):
|
||||
return gettext("Yesterday")
|
||||
elif day.year != today.year:
|
||||
return defaultfilters.date(day, "Y/n/j")
|
||||
else:
|
||||
return defaultfilters.date(day, "n/j")
|
26
src/mia_core/static/mia_core/css/period-chooser.css
Normal file
26
src/mia_core/static/mia_core/css/period-chooser.css
Normal file
@ -0,0 +1,26 @@
|
||||
/* The Mia Website
|
||||
* period-chooser.css: The style sheet for the period chooser
|
||||
*/
|
||||
|
||||
/* 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
|
||||
*/
|
||||
|
||||
.period-shortcuts {
|
||||
margin-bottom: 0.2em;
|
||||
}
|
98
src/mia_core/static/mia_core/js/period-chooser.js
Normal file
98
src/mia_core/static/mia_core/js/period-chooser.js
Normal file
@ -0,0 +1,98 @@
|
||||
/* The Mia Website
|
||||
* period-chooser.js: The JavaScript for the period chooser
|
||||
*/
|
||||
|
||||
/* 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/14
|
||||
*/
|
||||
|
||||
// Initializes the period chooser JavaScript.
|
||||
$(function () {
|
||||
$(".period-tab")
|
||||
.on("click", function () {
|
||||
switchPeriodTab($(this));
|
||||
});
|
||||
$("#button-period-day")
|
||||
.on("click", function () {
|
||||
window.location = $("#period-url").val()
|
||||
.replace("0000-00-00", $("#day-picker").val());
|
||||
});
|
||||
$("#period-start")
|
||||
.on("change", function () {
|
||||
$("#period-end")[0].min = this.value;
|
||||
});
|
||||
$("#period-end")
|
||||
.on("change", function () {
|
||||
$("#period-start")[0].max = this.value;
|
||||
});
|
||||
$("#button-period-custom")
|
||||
.on("click", function () {
|
||||
window.location = $("#period-url").val().replace(
|
||||
"0000-00-00",
|
||||
$("#period-start").val() + "-" + $("#period-end").val());
|
||||
});
|
||||
|
||||
const monthPickerParams = JSON.parse($("#period-month-picker-params").val());
|
||||
const monthPicker = $("#month-picker");
|
||||
monthPicker.datetimepicker({
|
||||
locale: monthPickerParams.locale,
|
||||
inline: true,
|
||||
format: "YYYY-MM",
|
||||
minDate: monthPickerParams.minDate,
|
||||
maxDate: monthPickerParams.maxDate,
|
||||
useCurrent: false,
|
||||
defaultDate: monthPickerParams.defaultDate,
|
||||
});
|
||||
monthPicker.on("change.datetimepicker", function (e) {
|
||||
monthPickerChanged(e.date);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Turns to the page to view the records of a month when the month is
|
||||
* selected.
|
||||
*
|
||||
* @param {moment} newDate the date with the selected new month
|
||||
* @private
|
||||
*/
|
||||
function monthPickerChanged(newDate) {
|
||||
const year = newDate.year();
|
||||
const month = newDate.month() + 1;
|
||||
let periodSpec;
|
||||
if (month < 10) {
|
||||
periodSpec = year + "-0" + month;
|
||||
} else {
|
||||
periodSpec = year + "-" + month;
|
||||
}
|
||||
window.location = $("#period-url").val()
|
||||
.replace("0000-00-00", periodSpec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the period chooser to tab.
|
||||
*
|
||||
* @param {jQuery} tab the navigation tab corresponding to a type
|
||||
* of period
|
||||
* @private
|
||||
*/
|
||||
function switchPeriodTab(tab) {
|
||||
$(".period-content").addClass("d-none");
|
||||
$("#period-content-" + tab.data("tab")).removeClass("d-none");
|
||||
$(".period-tab").removeClass("active");
|
||||
tab.addClass("active");
|
||||
}
|
48
src/mia_core/templates/mia_core/include/pagination.html
Normal file
48
src/mia_core/templates/mia_core/include/pagination.html
Normal file
@ -0,0 +1,48 @@
|
||||
{% comment %}
|
||||
The core application of the Mia project
|
||||
pagination.html: The side-wide layout template
|
||||
|
||||
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 %}
|
||||
|
||||
{# The pagination, if any #}
|
||||
{% if pagination.is_paged %}
|
||||
<ul class="pagination">
|
||||
{% for link in pagination.links %}
|
||||
{% if link.url is not None %}
|
||||
<li class="page-item {% if link.is_active %} active {% endif %}{% if not link.is_small_screen %} d-none d-md-inline {% endif %}">
|
||||
<a class="page-link" href="{{ link.url }}">{{ link.title }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled {% if link.is_active %} active {% endif %}{% if not link.is_small_screen %} d-none d-md-inline {% endif %}">
|
||||
<span class="page-link">{{ link.title }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li class="page-item active d-none d-md-inline">
|
||||
<div class="page-link dropdown-toggle" data-toggle="dropdown">
|
||||
{{ pagination.page_size }}
|
||||
</div>
|
||||
<div class="dropdown-menu">
|
||||
{% for option in pagination.page_size_options %}
|
||||
<a class="dropdown-item {% if pagination.page_size == option.size %} active {% endif %}" href="{{ option.url }}">{{ option.size }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
131
src/mia_core/templates/mia_core/include/period-chooser.html
Normal file
131
src/mia_core/templates/mia_core/include/period-chooser.html
Normal file
@ -0,0 +1,131 @@
|
||||
{% comment %}
|
||||
The core application of the Mia project
|
||||
period-chooser.html: The side-wide layout template
|
||||
|
||||
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/10
|
||||
{% endcomment %}
|
||||
{% load i18n %}
|
||||
{% load mia_core %}
|
||||
|
||||
<!-- the period chooser dialog -->
|
||||
<!-- The Modal -->
|
||||
<input id="period-url" type="hidden" value="{% url_period "0000-00-00" %}" />
|
||||
<input id="period-month-picker-params" type="hidden" value="{{ period.month_picker_params }}" />
|
||||
<div class="modal fade" id="period-modal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<i class="far fa-calendar-alt"></i>
|
||||
{{ _("Choosing Your Period")|force_escape }}
|
||||
</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal body -->
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<span class="period-tab nav-link active" data-tab="month">{{ _("Month")|force_escape }}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span class="period-tab nav-link" data-tab="year">{{ _("Year")|force_escape }}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span class="period-tab nav-link" data-tab="day">{{ _("Day")|force_escape }}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span class="period-tab nav-link" data-tab="custom">{{ _("Custom")|force_escape }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="period-content-month" class="period-content modal-body">
|
||||
<div class="period-shortcuts">
|
||||
{% if period.this_month is not None %}
|
||||
<a class="btn btn-primary" role="button" href="{% url_period period.this_month %}">{{ _("This Month")|force_escape }}</a>
|
||||
{% endif %}
|
||||
{% if period.last_month is not None %}
|
||||
<a class="btn btn-primary" role="button" href="{% url_period period.last_month %}">{{ _("Last Month")|force_escape }}</a>
|
||||
{% endif %}
|
||||
{% if period.since_last_month is not None %}
|
||||
<a class="btn btn-primary" role="button" href="{% url_period period.since_last_month %}">{{ _("Since Last Month")|force_escape }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if period.has_months_to_choose %}
|
||||
<div id="month-picker" class="col-sm-7"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="period-content-year" class="period-content modal-body d-none">
|
||||
<div class="period-shortcuts">
|
||||
{% if period.this_year is not None %}
|
||||
<a class="btn btn-primary" role="button" href="{% url_period period.this_year %}">{{ _("This Year")|force_escape }}</a>
|
||||
{% endif %}
|
||||
{% if period.last_year is not None %}
|
||||
<a class="btn btn-primary" role="button" href="{% url_period period.last_year %}">{{ _("Last Year")|force_escape }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if period.has_years_to_choose %}
|
||||
<ul class="nav nav-pills">
|
||||
{% for year in period.years_to_choose %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if period.spec == year %} active {% endif %}" href="{% url_period year %}">{{ year }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="period-content-day" class="period-content modal-body d-none">
|
||||
<div class="period-shortcuts">
|
||||
{% if period.today is not None %}
|
||||
<a class="btn btn-primary" role="button" href="{% url_period period.today %}">{{ _("Today")|force_escape }}</a>
|
||||
{% endif %}
|
||||
{% if period.yesterday is not None %}
|
||||
<a class="btn btn-primary" role="button" href="{% url_period period.yesterday %}">{{ _("Yesterday")|force_escape }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if period.has_days_to_choose %}
|
||||
<div>
|
||||
<label for="day-picker">{{ _("Date:")|force_escape }}</label>
|
||||
<input id="day-picker" type="date" value="{{ period.chosen_day }}" min="{{ period.data_start }}" max="{{ period.data_end }}" required="required" />
|
||||
</div>
|
||||
<div>
|
||||
<button id="button-period-day" class="btn btn-primary" type="submit">{{ _("Confirm")|force_escape }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="period-content-custom" class="period-content modal-body d-none">
|
||||
<div class="period-shortcuts">
|
||||
<a class="btn btn-primary" role="button" href="{% url_period "-" %}">{{ _("All")|force_escape }}</a>
|
||||
</div>
|
||||
{% if period.has_days_to_choose %}
|
||||
<div>
|
||||
<label for="period-start">{{ _("From:")|force_escape }}</label>
|
||||
<input id="period-start" type="date" value="{{ period.chosen_start }}" min="{{ period.data_start }}" max="{{ period.chosen_end }}" required="required" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="period-end">{{ _("To:")|force_escape }}</label>
|
||||
<input id="period-end" type="date" value="{{ period.chosen_end }}" min="{{ period.chosen_start }}" max="{{ period.data_end }}" required="required" />
|
||||
</div>
|
||||
<div>
|
||||
<button id="button-period-custom" class="btn btn-primary" type="submit">{{ _("Confirm")|force_escape }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
0
src/mia_core/templatetags/__init__.py
Normal file
0
src/mia_core/templatetags/__init__.py
Normal file
278
src/mia_core/templatetags/mia_core.py
Normal file
278
src/mia_core/templatetags/mia_core.py
Normal file
@ -0,0 +1,278 @@
|
||||
# The core application of the Mia project.
|
||||
# by imacat <imacat@mail.imacat.idv.tw>, 2020/7/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 template tags and filters of the Mia core application.
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
|
||||
import titlecase
|
||||
from django import template
|
||||
from django.http import HttpRequest
|
||||
from django.template import defaultfilters, RequestContext
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.safestring import SafeString
|
||||
from django.utils.translation import gettext, get_language
|
||||
|
||||
from mia_core.utils import UrlBuilder, CssAndJavaScriptLibraries
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def setvar(context: RequestContext, key: str, value: Any) -> str:
|
||||
"""Sets a variable in the template.
|
||||
|
||||
Args:
|
||||
context: the context
|
||||
key: The variable name
|
||||
value: The variable value
|
||||
|
||||
Returns:
|
||||
An empty string.
|
||||
"""
|
||||
context.dicts[0][key] = value
|
||||
return ""
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def url_period(context: RequestContext, period_spec: str) -> str:
|
||||
"""Returns the current URL with a new period.
|
||||
|
||||
Args:
|
||||
context: The request context.
|
||||
period_spec: The period specification.
|
||||
|
||||
Returns:
|
||||
The current URL with the new period.
|
||||
"""
|
||||
view_name = "%s:%s" % (
|
||||
context.request.resolver_match.app_name,
|
||||
context.request.resolver_match.url_name)
|
||||
kwargs = context.request.resolver_match.kwargs.copy()
|
||||
kwargs["period"] = period_spec
|
||||
namespace = context.request.resolver_match.namespace
|
||||
return reverse(view_name, kwargs=kwargs, current_app=namespace)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def url_with_return(context: RequestContext, url: str) -> str:
|
||||
"""Returns the URL with the current page added as the "r" query parameter,
|
||||
so that returning to this page is possible.
|
||||
|
||||
Args:
|
||||
context: The request context.
|
||||
url: The URL.
|
||||
|
||||
Returns:
|
||||
The URL with the current page added as the "r" query parameter.
|
||||
"""
|
||||
return str(UrlBuilder(url).query(
|
||||
r=str(UrlBuilder(context.request.get_full_path()).remove("s"))))
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def url_keep_return(context: RequestContext, url: str) -> str:
|
||||
"""Returns the URL with the current "r" query parameter set, so that the
|
||||
next processor can still return to the same page.
|
||||
|
||||
Args:
|
||||
context: The request context.
|
||||
url: The URL.
|
||||
|
||||
Returns:
|
||||
The URL with the current "r" query parameter set.
|
||||
"""
|
||||
return str(UrlBuilder(url).query(r=context.request.GET.get("r")))
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def init_libs(context: RequestContext) -> str:
|
||||
"""Initializes the static libraries.
|
||||
|
||||
Args:
|
||||
context: The request context.
|
||||
|
||||
Returns:
|
||||
An empty string.
|
||||
"""
|
||||
if "libs" not in context.dicts[0]:
|
||||
context.dicts[0]["libs"] = CssAndJavaScriptLibraries()
|
||||
return ""
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def add_lib(context: RequestContext, *args) -> str:
|
||||
"""Adds CSS and JavaScript libraries.
|
||||
|
||||
Args:
|
||||
context: The request context.
|
||||
args: The names of the CSS and JavaScript libraries.
|
||||
|
||||
Returns:
|
||||
An empty string.
|
||||
"""
|
||||
if "libs" not in context.dicts[0]:
|
||||
context.dicts[0]["libs"] = CssAndJavaScriptLibraries(args)
|
||||
else:
|
||||
context.dicts[0]["libs"].use(args)
|
||||
return ""
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def add_css(context: RequestContext, url: str) -> str:
|
||||
"""Adds a local CSS file. The file is added to the "css" template
|
||||
list variable.
|
||||
|
||||
Args:
|
||||
context: The request context.
|
||||
url: The URL or path of the CSS file.
|
||||
|
||||
Returns:
|
||||
An empty string.
|
||||
"""
|
||||
if "libs" not in context.dicts[0]:
|
||||
context.dicts[0]["libs"] = CssAndJavaScriptLibraries()
|
||||
context.dicts[0]["libs"].add_css(url)
|
||||
return ""
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def add_js(context: RequestContext, url: str) -> str:
|
||||
"""Adds a local JavaScript file. The file is added to the "js" template
|
||||
list variable.
|
||||
|
||||
Args:
|
||||
context: The request context.
|
||||
url: The URL or path of the JavaScript file.
|
||||
|
||||
Returns:
|
||||
An empty string.
|
||||
"""
|
||||
if "libs" not in context.dicts[0]:
|
||||
context.dicts[0]["libs"] = CssAndJavaScriptLibraries()
|
||||
context.dicts[0]["libs"].add_js(url)
|
||||
return ""
|
||||
|
||||
|
||||
@register.filter
|
||||
def smart_date(value: datetime.date) -> str:
|
||||
"""Formats the date for human friendliness.
|
||||
|
||||
Args:
|
||||
value: The date.
|
||||
|
||||
Returns:
|
||||
The human-friendly format of the date.
|
||||
"""
|
||||
if value == date.today():
|
||||
return gettext("Today")
|
||||
prev_days = (value - date.today()).days
|
||||
if prev_days == -1:
|
||||
return gettext("Yesterday")
|
||||
if prev_days == 1:
|
||||
return gettext("Tomorrow")
|
||||
if get_language() == "zh-hant":
|
||||
if prev_days == -2:
|
||||
return "前天"
|
||||
if prev_days == -3:
|
||||
return "大前天"
|
||||
if prev_days == 2:
|
||||
return "後天"
|
||||
if prev_days == 3:
|
||||
return "大後天"
|
||||
if date.today().year == value.year:
|
||||
return defaultfilters.date(value, "n/j(D)").replace("星期", "")
|
||||
return defaultfilters.date(value, "Y/n/j(D)").replace("星期", "")
|
||||
|
||||
|
||||
@register.filter
|
||||
def smart_month(value: datetime.date) -> str:
|
||||
"""Formats the month for human friendliness.
|
||||
|
||||
Args:
|
||||
value: The month.
|
||||
|
||||
Returns:
|
||||
The human-friendly format of the month.
|
||||
"""
|
||||
today = timezone.localdate()
|
||||
if value.year == today.year and value.month == today.month:
|
||||
return gettext("This Month")
|
||||
month = today.month - 1
|
||||
year = today.year
|
||||
if month < 1:
|
||||
month = 12
|
||||
year = year - 1
|
||||
if value.year == year and value.month == month:
|
||||
return gettext("Last Month")
|
||||
return defaultfilters.date(value, "Y/n")
|
||||
|
||||
|
||||
@register.filter
|
||||
def title_case(value: str) -> str:
|
||||
"""Formats the title in a proper American-English case.
|
||||
|
||||
Args:
|
||||
value: The title.
|
||||
|
||||
Returns:
|
||||
The title in a proper American-English case.
|
||||
"""
|
||||
value = str(value)
|
||||
if isinstance(value, SafeString):
|
||||
value = value + ""
|
||||
return titlecase.titlecase(value)
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_in_section(request: HttpRequest, section_name: str) -> bool:
|
||||
"""Returns whether the request is currently in a section.
|
||||
|
||||
Args:
|
||||
request: The request.
|
||||
section_name: The view name of this section.
|
||||
|
||||
Returns:
|
||||
True if the request is currently in this section, or False otherwise.
|
||||
"""
|
||||
if request is None:
|
||||
return False
|
||||
if request.resolver_match is None:
|
||||
return False
|
||||
view_name = request.resolver_match.view_name
|
||||
return view_name == section_name\
|
||||
or view_name.startswith(section_name + ".")
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_static_url(target: str) -> bool:
|
||||
"""Returns whether the target URL is a static path
|
||||
|
||||
Args:
|
||||
target: The target, either a static path that need to be passed to
|
||||
the static template tag, or an HTTP, HTTPS URL or absolute path
|
||||
that should be displayed directly.
|
||||
|
||||
Returns:
|
||||
True if the target URL is a static path, or False otherwise.
|
||||
"""
|
||||
return not (re.match("^https?://", target) or target.startswith("/"))
|
3
src/mia_core/tests.py
Normal file
3
src/mia_core/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
664
src/mia_core/utils.py
Normal file
664
src/mia_core/utils.py
Normal file
@ -0,0 +1,664 @@
|
||||
# The core application of the Mia project.
|
||||
# by imacat <imacat@mail.imacat.idv.tw>, 2020/7/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 utilities of the Mia core application.
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import random
|
||||
import urllib.parse
|
||||
from typing import Dict, List, Any, Type, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import pgettext, get_language
|
||||
|
||||
|
||||
def new_pk(cls: Type[Model]) -> int:
|
||||
"""Finds a random ID that does not conflict with the existing data records.
|
||||
|
||||
Args:
|
||||
cls: The Django model class.
|
||||
|
||||
Returns:
|
||||
The new random ID.
|
||||
"""
|
||||
while True:
|
||||
pk = random.randint(100000000, 999999999)
|
||||
try:
|
||||
cls.objects.get(pk=pk)
|
||||
except cls.DoesNotExist:
|
||||
return pk
|
||||
|
||||
|
||||
def strip_post(post: Dict[str, str]) -> None:
|
||||
"""Strips the values of the POSTed data. Empty strings are removed.
|
||||
|
||||
Args:
|
||||
post (dict[str]): The POSTed data.
|
||||
"""
|
||||
for key in list(post.keys()):
|
||||
post[key] = post[key].strip()
|
||||
if post[key] == "":
|
||||
del post[key]
|
||||
|
||||
|
||||
STORAGE_KEY: str = "stored_post"
|
||||
|
||||
|
||||
def store_post(request: HttpRequest, post: Dict[str, str]):
|
||||
"""Stores the POST data into the session.
|
||||
|
||||
Args:
|
||||
request: The request.
|
||||
post: The POST data.
|
||||
"""
|
||||
request.session[STORAGE_KEY] = post
|
||||
|
||||
|
||||
def retrieve_store(request: HttpRequest) -> Optional[Dict[str, str]]:
|
||||
"""Retrieves the POST data from the storage.
|
||||
|
||||
Args:
|
||||
request: The request.
|
||||
|
||||
Returns:
|
||||
The POST data, or None if the previously-stored data does not exist.
|
||||
"""
|
||||
if STORAGE_KEY not in request.session:
|
||||
return None
|
||||
post = request.session[STORAGE_KEY]
|
||||
del request.session[STORAGE_KEY]
|
||||
return post
|
||||
|
||||
|
||||
def parse_date(s: str):
|
||||
"""Parses a string for a date. The date can be either YYYY-MM-DD,
|
||||
Y/M/D, or M/D/Y.
|
||||
|
||||
Args:
|
||||
s: The string.
|
||||
|
||||
Returns:
|
||||
The date.
|
||||
|
||||
Raises:
|
||||
ValueError: When the string is not in a valid format.
|
||||
"""
|
||||
for f in ["%Y-%m-%d", "%m/%d/%Y", "%Y/%m/%d"]:
|
||||
try:
|
||||
return datetime.datetime.strptime(s, f)
|
||||
except ValueError:
|
||||
pass
|
||||
raise ValueError(F"not a recognized date {s}")
|
||||
|
||||
|
||||
class Language:
|
||||
"""A language.
|
||||
|
||||
Args:
|
||||
language: The Django language code.
|
||||
|
||||
Attributes:
|
||||
id (str): The language ID
|
||||
db (str): The database column suffix of this language.
|
||||
locale (str); The locale name of this language.
|
||||
is_default (bool): Whether this is the default language.
|
||||
"""
|
||||
def __init__(self, language: str):
|
||||
self.id = language
|
||||
self.db = "_" + language.lower().replace("-", "_")
|
||||
if language == "zh-hant":
|
||||
self.locale = "zh-TW"
|
||||
elif language == "zh-hans":
|
||||
self.locale = "zh-CN"
|
||||
else:
|
||||
self.locale = language
|
||||
self.is_default = (language == settings.LANGUAGE_CODE)
|
||||
|
||||
@staticmethod
|
||||
def default():
|
||||
return Language(settings.LANGUAGE_CODE)
|
||||
|
||||
@staticmethod
|
||||
def current():
|
||||
return Language(get_language())
|
||||
|
||||
|
||||
class UrlBuilder:
|
||||
"""The URL builder.
|
||||
|
||||
Attributes:
|
||||
path (str): the base path
|
||||
params (list[Param]): The query parameters
|
||||
"""
|
||||
def __init__(self, start_url: str):
|
||||
"""Constructs a new URL builder.
|
||||
|
||||
Args:
|
||||
start_url (str): The URL to start with
|
||||
"""
|
||||
pos = start_url.find("?")
|
||||
if pos == -1:
|
||||
self.path = start_url
|
||||
self.params = []
|
||||
return
|
||||
self.path = start_url[:pos]
|
||||
self.params = []
|
||||
for piece in start_url[pos + 1:].split("&"):
|
||||
pos = piece.find("=")
|
||||
name = urllib.parse.unquote_plus(piece[:pos])
|
||||
value = urllib.parse.unquote_plus(piece[pos + 1:])
|
||||
self.params.append(self.Param(name, value))
|
||||
|
||||
def add(self, name, value):
|
||||
"""Adds a query parameter.
|
||||
|
||||
Args:
|
||||
name (str): The parameter name
|
||||
value (str): The parameter value
|
||||
|
||||
Returns:
|
||||
UrlBuilder: The URL builder itself, with the parameter
|
||||
modified.
|
||||
"""
|
||||
if value is not None:
|
||||
self.params.append(self.Param(name, value))
|
||||
return self
|
||||
|
||||
def remove(self, name):
|
||||
"""Removes a query parameter.
|
||||
|
||||
Args:
|
||||
name (str): The parameter name
|
||||
|
||||
Returns:
|
||||
UrlBuilder: The URL builder itself, with the parameter
|
||||
modified.
|
||||
"""
|
||||
self.params = [x for x in self.params if x.name != name]
|
||||
return self
|
||||
|
||||
def query(self, **kwargs):
|
||||
"""A keyword-styled query parameter setter. The existing values are
|
||||
always replaced. Multiple-values are added when the value is a list or
|
||||
tuple. The existing values are dropped when the value is None.
|
||||
"""
|
||||
for key in kwargs:
|
||||
self.remove(key)
|
||||
if isinstance(kwargs[key], list) or isinstance(kwargs[key], tuple):
|
||||
for value in kwargs[key]:
|
||||
self.add(key, value)
|
||||
elif kwargs[key] is None:
|
||||
pass
|
||||
else:
|
||||
self.add(key, kwargs[key])
|
||||
return self
|
||||
|
||||
def clone(self):
|
||||
"""Returns a copy of this URL builder.
|
||||
|
||||
Returns:
|
||||
UrlBuilder: A copy of this URL builder.
|
||||
"""
|
||||
another = UrlBuilder(self.path)
|
||||
another.params = [
|
||||
self.Param(x.name, x.value) for x in self.params]
|
||||
return another
|
||||
|
||||
def __str__(self) -> str:
|
||||
if len(self.params) == 0:
|
||||
return self.path
|
||||
return self.path + "?" + "&".join([
|
||||
str(x) for x in self.params])
|
||||
|
||||
class Param:
|
||||
"""A query parameter.
|
||||
|
||||
Attributes:
|
||||
name: The parameter name
|
||||
value: The parameter value
|
||||
"""
|
||||
def __init__(self, name: str, value: str):
|
||||
"""Constructs a new query parameter
|
||||
|
||||
Args:
|
||||
name (str): The parameter name
|
||||
value (str): The parameter value
|
||||
"""
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns the string representation of this query
|
||||
parameter.
|
||||
|
||||
Returns:
|
||||
str: The string representation of this query
|
||||
parameter
|
||||
"""
|
||||
return "%s=%s" % (
|
||||
urllib.parse.quote(self.name),
|
||||
urllib.parse.quote(self.value))
|
||||
|
||||
|
||||
class Pagination:
|
||||
"""The pagination.
|
||||
|
||||
Args:
|
||||
request: The request.
|
||||
items: All the items.
|
||||
is_reversed: Whether we should display the last page first.
|
||||
|
||||
Raises:
|
||||
PaginationException: With invalid pagination parameters
|
||||
|
||||
Attributes:
|
||||
current_url (UrlBuilder): The current request URL.
|
||||
is_reversed (bool): Whether we should display the last page first.
|
||||
page_size (int): The page size.
|
||||
total_pages (int): The total number of pages available.
|
||||
is_paged (bool): Whether there are more than one page.
|
||||
page_no (int): The current page number.
|
||||
items (list[Model]): The items in the current page.
|
||||
"""
|
||||
DEFAULT_PAGE_SIZE = 10
|
||||
|
||||
def __init__(self, request: HttpRequest, items: List[Any],
|
||||
is_reversed: bool = False):
|
||||
self.current_url = UrlBuilder(request.get_full_path())
|
||||
self.is_reversed = is_reversed
|
||||
self.page_size = self.DEFAULT_PAGE_SIZE
|
||||
self.total_pages = None
|
||||
self.is_paged = None
|
||||
self.page_no = 1
|
||||
self.items = []
|
||||
|
||||
# The page size
|
||||
try:
|
||||
self.page_size = int(request.GET["page-size"])
|
||||
if self.page_size == self.DEFAULT_PAGE_SIZE:
|
||||
raise PaginationException(self.current_url.remove("page-size"))
|
||||
if self.page_size < 1:
|
||||
raise PaginationException(self.current_url.remove("page-size"))
|
||||
except KeyError:
|
||||
self.page_size = self.DEFAULT_PAGE_SIZE
|
||||
except ValueError:
|
||||
raise PaginationException(self.current_url.remove("page-size"))
|
||||
self.total_pages = int(
|
||||
(len(items) - 1) / self.page_size) + 1
|
||||
default_page_no = 1 if not is_reversed else self.total_pages
|
||||
self.is_paged = self.total_pages > 1
|
||||
|
||||
# The page number
|
||||
try:
|
||||
self.page_no = int(request.GET["page"])
|
||||
if not self.is_paged:
|
||||
raise PaginationException(self.current_url.remove("page"))
|
||||
if self.page_no == default_page_no:
|
||||
raise PaginationException(self.current_url.remove("page"))
|
||||
if self.page_no < 1:
|
||||
raise PaginationException(self.current_url.remove("page"))
|
||||
if self.page_no > self.total_pages:
|
||||
raise PaginationException(self.current_url.remove("page"))
|
||||
except KeyError:
|
||||
self.page_no = default_page_no
|
||||
except ValueError:
|
||||
raise PaginationException(self.current_url.remove("page"))
|
||||
|
||||
if not self.is_paged:
|
||||
self.page_no = 1
|
||||
self.items = items
|
||||
return
|
||||
start_no = self.page_size * (self.page_no - 1)
|
||||
self.items = items[start_no:start_no + self.page_size]
|
||||
|
||||
def links(self):
|
||||
"""Returns the navigation links of the pagination bar.
|
||||
|
||||
Returns:
|
||||
List[Link]: The navigation links of the pagination bar.
|
||||
"""
|
||||
base_url = self.current_url.clone().remove("page").remove("s")
|
||||
links = []
|
||||
# The previous page
|
||||
link = self.Link()
|
||||
link.title = pgettext("Pagination|", "Previous")
|
||||
if self.page_no > 1:
|
||||
if self.page_no - 1 == 1:
|
||||
if not self.is_reversed:
|
||||
link.url = str(base_url)
|
||||
else:
|
||||
link.url = str(base_url.clone().add(
|
||||
"page", "1"))
|
||||
else:
|
||||
link.url = str(base_url.clone().add(
|
||||
"page", str(self.page_no - 1)))
|
||||
link.is_small_screen = True
|
||||
links.append(link)
|
||||
# The first page
|
||||
link = self.Link()
|
||||
link.title = "1"
|
||||
if not self.is_reversed:
|
||||
link.url = str(base_url)
|
||||
else:
|
||||
link.url = str(base_url.clone().add(
|
||||
"page", "1"))
|
||||
if self.page_no == 1:
|
||||
link.is_active = True
|
||||
links.append(link)
|
||||
# The previous ellipsis
|
||||
if self.page_no > 4:
|
||||
link = self.Link()
|
||||
if self.page_no > 5:
|
||||
link.title = pgettext("Pagination|", "...")
|
||||
else:
|
||||
link.title = "2"
|
||||
link.url = str(base_url.clone().add(
|
||||
"page", "2"))
|
||||
links.append(link)
|
||||
# The nearby pages
|
||||
for no in range(self.page_no - 2, self.page_no + 3):
|
||||
if no <= 1 or no >= self.total_pages:
|
||||
continue
|
||||
link = self.Link()
|
||||
link.title = str(no)
|
||||
link.url = str(base_url.clone().add(
|
||||
"page", str(no)))
|
||||
if no == self.page_no:
|
||||
link.is_active = True
|
||||
links.append(link)
|
||||
# The next ellipsis
|
||||
if self.page_no + 3 < self.total_pages:
|
||||
link = self.Link()
|
||||
if self.page_no + 4 < self.total_pages:
|
||||
link.title = pgettext("Pagination|", "...")
|
||||
else:
|
||||
link.title = str(self.total_pages - 1)
|
||||
link.url = str(base_url.clone().add(
|
||||
"page", str(self.total_pages - 1)))
|
||||
links.append(link)
|
||||
# The last page
|
||||
link = self.Link()
|
||||
link.title = str(self.total_pages)
|
||||
if self.is_reversed:
|
||||
link.url = str(base_url)
|
||||
else:
|
||||
link.url = str(base_url.clone().add(
|
||||
"page", str(self.total_pages)))
|
||||
if self.page_no == self.total_pages:
|
||||
link.is_active = True
|
||||
links.append(link)
|
||||
# The next page
|
||||
link = self.Link()
|
||||
link.title = pgettext("Pagination|", "Next")
|
||||
if self.page_no < self.total_pages:
|
||||
if self.page_no + 1 == self.total_pages:
|
||||
if self.is_reversed:
|
||||
link.url = str(base_url)
|
||||
else:
|
||||
link.url = str(base_url.clone().add(
|
||||
"page", str(self.total_pages)))
|
||||
else:
|
||||
link.url = str(base_url.clone().add(
|
||||
"page", str(self.page_no + 1)))
|
||||
link.is_small_screen = True
|
||||
links.append(link)
|
||||
return links
|
||||
|
||||
class Link:
|
||||
"""A navigation link in the pagination bar.
|
||||
|
||||
Attributes:
|
||||
url (str): The link URL, or for a non-link slot.
|
||||
title (str): The title of the link.
|
||||
is_active (bool): Whether this link is currently active.
|
||||
is_small_screen (bool): Whether this link is for small
|
||||
screens
|
||||
"""
|
||||
def __int__(self):
|
||||
self.url = None
|
||||
self.title = None
|
||||
self.is_active = False
|
||||
self.is_small_screen = False
|
||||
|
||||
def page_size_options(self):
|
||||
"""Returns the page size options.
|
||||
|
||||
Returns:
|
||||
List[PageSizeOption]: The page size options.
|
||||
"""
|
||||
base_url = self.current_url.remove("page").remove("page-size")
|
||||
return [self.PageSizeOption(x, self._page_size_url(base_url, x))
|
||||
for x in [10, 100, 200]]
|
||||
|
||||
@staticmethod
|
||||
def _page_size_url(base_url: UrlBuilder, size: int) -> str:
|
||||
"""Returns the URL for a new page size.
|
||||
|
||||
Args:
|
||||
base_url (UrlBuilder): The base URL builder.
|
||||
size (int): The new page size.
|
||||
|
||||
Returns:
|
||||
str: The URL for the new page size.
|
||||
"""
|
||||
if size == Pagination.DEFAULT_PAGE_SIZE:
|
||||
return str(base_url)
|
||||
return str(base_url.clone().add("page-size", str(size)))
|
||||
|
||||
class PageSizeOption:
|
||||
"""A page size option.
|
||||
|
||||
Args:
|
||||
size: The page size.
|
||||
url: The URL of this page size.
|
||||
|
||||
Attributes:
|
||||
size (int): The page size.
|
||||
url (str): The URL for this page size.
|
||||
"""
|
||||
def __init__(self, size: int, url: str):
|
||||
self.size = size
|
||||
self.url = url
|
||||
|
||||
|
||||
class PaginationException(Exception):
|
||||
"""The exception thrown with invalid pagination parameters.
|
||||
|
||||
Args:
|
||||
url_builder: The canonical URL to redirect to.
|
||||
|
||||
Attributes:
|
||||
url (str): The canonical URL to redirect to.
|
||||
"""
|
||||
def __init__(self, url_builder: UrlBuilder):
|
||||
self.url = str(url_builder)
|
||||
|
||||
|
||||
CDN_LIBRARIES = {
|
||||
"jquery": {"css": [],
|
||||
"js": ["https://code.jquery.com/jquery-3.5.1.min.js"]},
|
||||
"bootstrap4": {
|
||||
"css": [("https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/"
|
||||
"bootstrap.min.css")],
|
||||
"js": [("https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/"
|
||||
"popper.min.js"),
|
||||
("https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/"
|
||||
"bootstrap.min.js")]},
|
||||
"font-awesome-5": {
|
||||
"css": ["https://use.fontawesome.com/releases/v5.14.0/css/all.css"],
|
||||
"js": []},
|
||||
"bootstrap4-datatables": {
|
||||
"css": [("https://cdn.datatables.net/1.10.21/css/"
|
||||
"jquery.dataTables.min.css"),
|
||||
("https://cdn.datatables.net/1.10.21/css/"
|
||||
"dataTables.bootstrap4.min.css")],
|
||||
"js": [("https://cdn.datatables.net/1.10.21/js/"
|
||||
"jquery.dataTables.min.js"),
|
||||
("https://cdn.datatables.net/1.10.21/js/"
|
||||
"dataTables.bootstrap4.min.js")]},
|
||||
"jquery-ui": {"css": [("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/"
|
||||
"1.12.1/jquery-ui.min.css")],
|
||||
"js": [("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/"
|
||||
"1.12.1/jquery-ui.min.js")]},
|
||||
"bootstrap4-tempusdominus": {
|
||||
"css": [("https://cdnjs.cloudflare.com/ajax/libs/"
|
||||
"tempusdominus-bootstrap-4/5.1.2/css/"
|
||||
"tempusdominus-bootstrap-4.min.css")],
|
||||
"js": [("https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/"
|
||||
"moment-with-locales.min.js"),
|
||||
("https://cdnjs.cloudflare.com/ajax/libs/"
|
||||
"tempusdominus-bootstrap-4/5.1.2/js/"
|
||||
"tempusdominus-bootstrap-4.js")]},
|
||||
"decimal-js": {"css": [],
|
||||
"js": [("https://cdnjs.cloudflare.com/ajax/libs/decimal.js/"
|
||||
"10.2.0/decimal.min.js")]},
|
||||
"period-chooser": {"css": ["mia_core/css/period-chooser.css"],
|
||||
"js": ["mia_core/js/period-chooser.js"]}
|
||||
}
|
||||
DEFAULT_LIBS = []
|
||||
|
||||
|
||||
class CssAndJavaScriptLibraries:
|
||||
"""The CSS and JavaScript library resolver."""
|
||||
AVAILABLE_LIBS: List[str] = ["jquery", "bootstrap4", "font-awesome-5",
|
||||
"bootstrap4-datatables", "jquery-ui",
|
||||
"bootstrap4-tempusdominus", "decimal-js",
|
||||
"i18n", "period-chooser"]
|
||||
|
||||
def __init__(self, *args):
|
||||
self._use: Dict[str, bool] = {x: False for x in self.AVAILABLE_LIBS}
|
||||
self._add_default_libs()
|
||||
# The specified libraries
|
||||
if len(args) > 0:
|
||||
libs = args[0]
|
||||
invalid = [x for x in libs if x not in self.AVAILABLE_LIBS]
|
||||
if len(invalid) > 0:
|
||||
raise NameError("library %s invalid" % ", ".join(invalid))
|
||||
for lib in libs:
|
||||
self._use[lib] = True
|
||||
self._css = []
|
||||
try:
|
||||
self._css = self._css + settings.DEFAULT_CSS
|
||||
except AttributeError:
|
||||
pass
|
||||
self._js = []
|
||||
try:
|
||||
self._css = self._css + settings.DEFAULT_JS
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def _add_default_libs(self):
|
||||
"""Adds the default libraries."""
|
||||
invalid = [x for x in DEFAULT_LIBS if x not in self.AVAILABLE_LIBS]
|
||||
if len(invalid) > 0:
|
||||
raise NameError("library %s invalid" % ", ".join(invalid))
|
||||
for lib in DEFAULT_LIBS:
|
||||
self._use[lib] = True
|
||||
|
||||
def use(self, *args) -> None:
|
||||
"""Use the specific libraries.
|
||||
|
||||
Args:
|
||||
args: The libraries.
|
||||
"""
|
||||
if len(args) == 0:
|
||||
return
|
||||
libs = args[0]
|
||||
invalid = [x for x in libs if x not in self.AVAILABLE_LIBS]
|
||||
if len(invalid) > 0:
|
||||
raise NameError("library %s invalid" % ", ".join(invalid))
|
||||
for lib in libs:
|
||||
self._use[lib] = True
|
||||
|
||||
def add_css(self, css) -> None:
|
||||
"""Adds a custom CSS file."""
|
||||
self._css.append(css)
|
||||
|
||||
def add_js(self, js) -> None:
|
||||
"""Adds a custom JavaScript file."""
|
||||
self._js.append(js)
|
||||
|
||||
def css(self) -> List[str]:
|
||||
"""Returns the stylesheet files to use."""
|
||||
use: Dict[str, bool] = self._solve_use_dependencies()
|
||||
css = []
|
||||
for lib in [x for x in self.AVAILABLE_LIBS if use[x]]:
|
||||
if lib == "i18n":
|
||||
continue
|
||||
try:
|
||||
css = css + settings.STATIC_LIBS[lib]["css"]
|
||||
except AttributeError:
|
||||
css = css + CDN_LIBRARIES[lib]["css"]
|
||||
except TypeError:
|
||||
css = css + CDN_LIBRARIES[lib]["css"]
|
||||
except KeyError:
|
||||
css = css + CDN_LIBRARIES[lib]["css"]
|
||||
return css + self._css
|
||||
|
||||
def js(self) -> List[str]:
|
||||
"""Returns the JavaScript files to use."""
|
||||
use: Dict[str, bool] = self._solve_use_dependencies()
|
||||
js = []
|
||||
for lib in [x for x in self.AVAILABLE_LIBS if use[x]]:
|
||||
if lib == "i18n":
|
||||
js.append(reverse("javascript-catalog"))
|
||||
continue
|
||||
try:
|
||||
js = js + settings.STATIC_LIBS[lib]["js"]
|
||||
except AttributeError:
|
||||
js = js + CDN_LIBRARIES[lib]["js"]
|
||||
except TypeError:
|
||||
js = js + CDN_LIBRARIES[lib]["js"]
|
||||
except KeyError:
|
||||
js = js + CDN_LIBRARIES[lib]["js"]
|
||||
return js + self._js
|
||||
|
||||
def _solve_use_dependencies(self) -> Dict[str, bool]:
|
||||
"""Solves and returns the library dependencies."""
|
||||
use: Dict[str, bool] = {x: self._use[x] for x in self._use}
|
||||
if use["period-chooser"]:
|
||||
use["bootstrap4-tempusdominus"] = True
|
||||
if use["bootstrap4-tempusdominus"]:
|
||||
use["bootstrap4"] = True
|
||||
if use["bootstrap4-datatables"]:
|
||||
use["bootstrap4"] = True
|
||||
if use["jquery-ui"]:
|
||||
use["jquery"] = True
|
||||
if use["bootstrap4"]:
|
||||
use["jquery"] = True
|
||||
return use
|
||||
|
||||
|
||||
def add_default_libs(*args) -> None:
|
||||
"""Adds the specified libraries to the default CSS and JavaScript
|
||||
libraries.
|
||||
|
||||
Args:
|
||||
args: The libraries to be added to the default libraries
|
||||
"""
|
||||
libs = args
|
||||
invalid = [x for x in libs
|
||||
if x not in CssAndJavaScriptLibraries.AVAILABLE_LIBS]
|
||||
if len(invalid) > 0:
|
||||
raise NameError("library %s invalid" % ", ".join(invalid))
|
||||
for lib in libs:
|
||||
if lib not in DEFAULT_LIBS:
|
||||
DEFAULT_LIBS.append(lib)
|
225
src/mia_core/views.py
Normal file
225
src/mia_core/views.py
Normal file
@ -0,0 +1,225 @@
|
||||
# The core application of the Mia project.
|
||||
# by imacat <imacat@mail.imacat.idv.tw>, 2020/7/4
|
||||
|
||||
# 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 views of the Mia core application.
|
||||
|
||||
"""
|
||||
from typing import Dict, Type, Optional, Any
|
||||
|
||||
from dirtyfields import DirtyFieldsMixin
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db import transaction
|
||||
from django.db.models import Model
|
||||
from django.http import HttpResponse, HttpRequest, \
|
||||
HttpResponseRedirect, Http404
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.generic import DeleteView as CoreDeleteView, \
|
||||
RedirectView as CoreRedirectView
|
||||
from django.views.generic.base import View
|
||||
|
||||
from . import utils
|
||||
from .models import StampedModel
|
||||
from .utils import UrlBuilder
|
||||
|
||||
|
||||
class RedirectView(CoreRedirectView):
|
||||
"""The redirect view, with current_app at the current namespace."""
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
url = reverse(self.pattern_name, kwargs=kwargs,
|
||||
current_app=self.request.resolver_match.namespace)
|
||||
if self.query_string and self.request.META["QUERY_STRING"] != "":
|
||||
url = url + "?" + self.request.META["QUERY_STRING"]
|
||||
return url
|
||||
|
||||
|
||||
class FormView(View):
|
||||
"""The base form view."""
|
||||
model: Type[Model] = None
|
||||
form_class: Type[forms.Form] = None
|
||||
template_name: str = None
|
||||
context_object_name: str = "form"
|
||||
success_url: str = None
|
||||
error_url: str = None
|
||||
not_modified_message: str = None
|
||||
success_message: str = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.object: Optional[Model] = None
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""The view to store an accounting transaction."""
|
||||
self.object = self.get_object()
|
||||
if self.request.method == "POST":
|
||||
return self.post(request, *args, **kwargs)
|
||||
else:
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Handles the GET requests."""
|
||||
return render(self.request, self.get_template_name(),
|
||||
self.get_context_data(**kwargs))
|
||||
|
||||
def post(self, request: HttpRequest, *args,
|
||||
**kwargs) -> HttpResponseRedirect:
|
||||
"""Handles the POST requests."""
|
||||
form = self.get_form(**kwargs)
|
||||
if not form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
return self.form_valid(form)
|
||||
|
||||
def get_form_class(self) -> Type[forms.Form]:
|
||||
"""Returns the form class."""
|
||||
if self.form_class is None:
|
||||
raise AttributeError("Please defined the form_class property.")
|
||||
return self.form_class
|
||||
|
||||
@property
|
||||
def _model(self):
|
||||
if self.model is None:
|
||||
raise AttributeError("Please defined the model property.")
|
||||
return self.model
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
"""Returns the context data for the template."""
|
||||
return {self.context_object_name: self.get_form()}
|
||||
|
||||
def get_form(self, **kwargs) -> forms.Form:
|
||||
"""Returns the form for the template."""
|
||||
if self.request.method == "POST":
|
||||
post = self.request.POST.dict()
|
||||
utils.strip_post(post)
|
||||
return self.make_form_from_post(post)
|
||||
else:
|
||||
previous_post = utils.retrieve_store(self.request)
|
||||
if previous_post is not None:
|
||||
return self.make_form_from_post(previous_post)
|
||||
if self.object is not None:
|
||||
return self.make_form_from_model(self.object)
|
||||
return self.get_form_class()()
|
||||
|
||||
def get_template_name(self) -> str:
|
||||
"""Returns the name of the template."""
|
||||
if self.template_name is not None:
|
||||
return self.template_name
|
||||
if self.model is not None:
|
||||
app_name = self.request.resolver_match.app_name
|
||||
model_name = self.model.__name__.lower()
|
||||
return F"{app_name}/{model_name}_form.html"
|
||||
raise AttributeError(
|
||||
"Please either define the template_name or the model property.")
|
||||
|
||||
def make_form_from_post(self, post: Dict[str, str]) -> forms.Form:
|
||||
"""Creates and returns the form from the POST data."""
|
||||
return self.get_form_class()(post)
|
||||
|
||||
def make_form_from_model(self, obj: Model) -> forms.Form:
|
||||
"""Creates and returns the form from a data model."""
|
||||
form_class = self.get_form_class()
|
||||
return form_class({x: getattr(obj, x, None)
|
||||
for x in form_class.base_fields})
|
||||
|
||||
def fill_model_from_form(self, obj: Model, form: forms.Form) -> None:
|
||||
"""Fills in the data model from the form."""
|
||||
for name in form.fields:
|
||||
setattr(obj, name, form[name].value())
|
||||
if isinstance(obj, StampedModel):
|
||||
obj.current_user = self.request.user
|
||||
|
||||
def form_invalid(self, form: forms.Form) -> HttpResponseRedirect:
|
||||
"""Handles the action when the POST form is invalid."""
|
||||
utils.store_post(self.request, form.data)
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
def form_valid(self, form: forms.Form) -> HttpResponseRedirect:
|
||||
"""Handles the action when the POST form is valid."""
|
||||
if self.object is None:
|
||||
self.object = self._model()
|
||||
self.fill_model_from_form(self.object, form)
|
||||
if isinstance(self.object, DirtyFieldsMixin)\
|
||||
and not self.object.is_dirty(check_relationship=True):
|
||||
message = self.get_not_modified_message(form.cleaned_data)
|
||||
else:
|
||||
with transaction.atomic():
|
||||
self.object.save()
|
||||
message = self.get_success_message(form.cleaned_data)
|
||||
messages.success(self.request, message)
|
||||
return redirect(str(UrlBuilder(self.get_success_url())
|
||||
.query(r=self.request.GET.get("r"))))
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
"""Returns the URL on success."""
|
||||
if self.success_url is not None:
|
||||
return self.success_url
|
||||
get_absolute_url = getattr(self.object, "get_absolute_url", None)
|
||||
if get_absolute_url is not None:
|
||||
return get_absolute_url()
|
||||
raise AttributeError(
|
||||
"Please define either the success_url property,"
|
||||
" the get_absolute_url method on the data model,"
|
||||
" or the get_success_url method.")
|
||||
|
||||
def get_error_url(self) -> str:
|
||||
"""Returns the URL on error"""
|
||||
if self.error_url is not None:
|
||||
return self.error_url
|
||||
return self.request.get_full_path()
|
||||
|
||||
def get_not_modified_message(self, cleaned_data: Dict[str, str]) -> str:
|
||||
"""Returns the message when the data was not modified.
|
||||
|
||||
Args:
|
||||
cleaned_data: The cleaned data of the form.
|
||||
|
||||
Returns:
|
||||
The message when the data was not modified.
|
||||
"""
|
||||
return self.not_modified_message % cleaned_data
|
||||
|
||||
def get_success_message(self, cleaned_data: Dict[str, str]) -> str:
|
||||
"""Returns the success message.
|
||||
|
||||
Args:
|
||||
cleaned_data: The cleaned data of the form.
|
||||
|
||||
Returns:
|
||||
The message when the data was not modified.
|
||||
"""
|
||||
return self.success_message % cleaned_data
|
||||
|
||||
def get_object(self) -> Optional[Model]:
|
||||
"""Finds and returns the current object, or None on a create form."""
|
||||
if "pk" in self.kwargs:
|
||||
pk = self.kwargs["pk"]
|
||||
try:
|
||||
return self._model.objects.get(pk=pk)
|
||||
except self._model.DoesNotExist:
|
||||
raise Http404
|
||||
return None
|
||||
|
||||
|
||||
class DeleteView(SuccessMessageMixin, CoreDeleteView):
|
||||
"""The delete form view, with SuccessMessageMixin."""
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
response = super(DeleteView, self).delete(request, *args, **kwargs)
|
||||
messages.success(request, self.get_success_message({}))
|
||||
return response
|
Reference in New Issue
Block a user