diff --git a/accounting/forms.py b/accounting/forms.py index 722e40e..b84e4be 100644 --- a/accounting/forms.py +++ b/accounting/forms.py @@ -20,6 +20,7 @@ """ import datetime import re +from decimal import Decimal from typing import Optional, List, Dict from django import forms @@ -57,12 +58,14 @@ class RecordForm(forms.Form): error_messages={ "max_length": _("This summary is too long (max. 128 characters)."), }) - amount = forms.IntegerField( - min_value=1, + 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 at least 1."), + "min_value": _("The amount must be more than 0."), }) def __init__(self, *args, **kwargs): @@ -436,22 +439,22 @@ class TransactionForm(forms.Form): return errors[0].message return None - def debit_total(self) -> int: + def debit_total(self) -> Decimal: """Returns the total amount of the debit records. Returns: The total amount of the credit records. """ - return sum([int(x.data["amount"]) for x in self.debit_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) -> int: + def credit_total(self) -> Decimal: """Returns the total amount of the credit records. Returns: The total amount of the credit records. """ - return sum([int(x.data["amount"]) for x in self.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]) diff --git a/accounting/management/commands/accounting_sample.py b/accounting/management/commands/accounting_sample.py index 8d6aa42..11c9243 100644 --- a/accounting/management/commands/accounting_sample.py +++ b/accounting/management/commands/accounting_sample.py @@ -157,7 +157,7 @@ class Command(BaseCommand): self._filler.add_expense_transaction( -13, - [(6273, "Bus—2623—Uptown→City Park", 30)]) + [(6273, "Bus—2623—Uptown→City Park", 477543627.4775)]) self._filler.add_expense_transaction( -2, diff --git a/accounting/models.py b/accounting/models.py index b394513..f23981b 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -20,6 +20,7 @@ """ import datetime import re +from decimal import Decimal from typing import Dict, List, Optional, Mapping from dirtyfields import DirtyFieldsMixin @@ -236,7 +237,7 @@ class Transaction(DirtyFieldsMixin, BaseModel): record.summary = post[F"{record_type}-{no}-summary"] else: record.summary = None - record.amount = int(post[F"{record_type}-{no}-amount"]) + record.amount = Decimal(post[F"{record_type}-{no}-amount"]) records.append(record) if txn_type != "transfer": if txn_type == "expense": @@ -314,10 +315,10 @@ class Transaction(DirtyFieldsMixin, BaseModel): """ return [x for x in self.records if not x.is_credit] - def debit_total(self) -> int: + 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, int)]) + if isinstance(x.amount, Decimal)]) @property def debit_summaries(self) -> List[str]: @@ -334,10 +335,10 @@ class Transaction(DirtyFieldsMixin, BaseModel): """ return [x for x in self.records if x.is_credit] - def credit_total(self) -> int: + 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, int)]) + if isinstance(x.amount, Decimal)]) @property def credit_summaries(self) -> List[str]: @@ -346,7 +347,7 @@ class Transaction(DirtyFieldsMixin, BaseModel): for x in self.credit_records] @property - def amount(self) -> int: + def amount(self) -> Decimal: """The amount of this transaction.""" return self.debit_total() @@ -428,13 +429,13 @@ class Record(DirtyFieldsMixin, BaseModel): account = models.ForeignKey( Account, on_delete=models.PROTECT) summary = models.CharField(max_length=128, blank=True, null=True) - amount = models.PositiveIntegerField() + amount = models.DecimalField(max_digits=18, decimal_places=2) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._debit_amount = None - self._credit_amount = None - self.balance = None + 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 @@ -452,25 +453,25 @@ class Record(DirtyFieldsMixin, BaseModel): self.amount) @property - def debit_amount(self) -> Optional[int]: + 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[int]) -> None: + def debit_amount(self, value: Optional[Decimal]) -> None: self._debit_amount = value @property - def credit_amount(self) -> Optional[int]: + 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[int]): + def credit_amount(self, value: Optional[Decimal]): self._credit_amount = value @property diff --git a/accounting/static/accounting/js/transaction-form.js b/accounting/static/accounting/js/transaction-form.js index ab9085f..d6b43fb 100644 --- a/accounting/static/accounting/js/transaction-form.js +++ b/accounting/static/accounting/js/transaction-form.js @@ -177,10 +177,10 @@ function removeBlankOption(select) { */ function updateTotalAmount(element) { const type = element.data("type") - let total = 0; + let total = new Decimal("0"); $("." + type + "-to-sum").each(function () { if (this.value !== "") { - total += parseInt(this.value); + total = total.plus(new Decimal(this.value)); } }); total = String(total); @@ -465,19 +465,19 @@ function validateAmount(amount) { function validateBalance() { const balanceRows = $(".balance-row"); const errorMessages = $(".balance-error"); - let debitTotal = 0; + let debitTotal = new Decimal("0"); $(".debit-to-sum").each(function () { if (this.value !== "") { - debitTotal += parseInt(this.value); + debitTotal = debitTotal.plus(new Decimal(this.value)); } }); - let creditTotal = 0; + let creditTotal = new Decimal("0"); $(".credit-to-sum").each(function () { if (this.value !== "") { - creditTotal += parseInt(this.value); + creditTotal = creditTotal.plus(new Decimal(this.value)); } }); - if (debitTotal !== creditTotal) { + if (!debitTotal.equals(creditTotal)) { balanceRows.addClass("is-invalid"); errorMessages.text(gettext("The total amount of debit and credit records are inconsistent.")) return false; diff --git a/accounting/templates/accounting/include/record_form-non-transfer.html b/accounting/templates/accounting/include/record_form-non-transfer.html index b879772..94adfc9 100644 --- a/accounting/templates/accounting/include/record_form-non-transfer.html +++ b/accounting/templates/accounting/include/record_form-non-transfer.html @@ -37,7 +37,7 @@ First written: 2020/8/5
- +
{{ record.amount.errors.0|default:"" }}
diff --git a/accounting/templates/accounting/include/record_form-transfer.html b/accounting/templates/accounting/include/record_form-transfer.html index 02ffa3c..478a229 100644 --- a/accounting/templates/accounting/include/record_form-transfer.html +++ b/accounting/templates/accounting/include/record_form-transfer.html @@ -36,7 +36,7 @@ First written: 2020/8/5
- +
{{ record.amount.errors.0|default:"" }}
diff --git a/accounting/templates/accounting/transaction_expense_form.html b/accounting/templates/accounting/transaction_expense_form.html index 03080fb..9b1ad0e 100644 --- a/accounting/templates/accounting/transaction_expense_form.html +++ b/accounting/templates/accounting/transaction_expense_form.html @@ -28,6 +28,7 @@ First written: 2020/7/23 {% block settings %} {% setvar "title" _("Cash Expense Transaction") %} {% setvar "use_jqueryui" True %} + {% static "ext-libs/decimal/decimal.min.js" as file %}{% add_js file %} {% 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 %} diff --git a/accounting/templates/accounting/transaction_income_form.html b/accounting/templates/accounting/transaction_income_form.html index 74c2e91..a7b9b3a 100644 --- a/accounting/templates/accounting/transaction_income_form.html +++ b/accounting/templates/accounting/transaction_income_form.html @@ -28,6 +28,7 @@ First written: 2020/7/23 {% block settings %} {% setvar "title" _("Cash Income Transaction") %} {% setvar "use_jqueryui" True %} + {% static "ext-libs/decimal/decimal.min.js" as file %}{% add_js file %} {% 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 %} diff --git a/accounting/templates/accounting/transaction_transfer_form.html b/accounting/templates/accounting/transaction_transfer_form.html index bb29aa6..9376800 100644 --- a/accounting/templates/accounting/transaction_transfer_form.html +++ b/accounting/templates/accounting/transaction_transfer_form.html @@ -28,6 +28,7 @@ First written: 2020/7/23 {% block settings %} {% setvar "title" _("Transfer Transaction") %} {% setvar "use_jqueryui" True %} + {% static "ext-libs/decimal/decimal.min.js" as file %}{% add_js file %} {% 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 %} diff --git a/accounting/templatetags/accounting.py b/accounting/templatetags/accounting.py index 091f362..8251a1d 100644 --- a/accounting/templatetags/accounting.py +++ b/accounting/templatetags/accounting.py @@ -19,6 +19,7 @@ """ import re +from decimal import Decimal from typing import Union, Optional from django import template @@ -32,14 +33,14 @@ register = template.Library() @register.filter -def accounting_amount(value: Union[str, int]) -> str: +def accounting_amount(value: Union[str, Decimal]) -> str: if value is None: return "" if value == 0: return "-" s = str(abs(value)) while True: - m = re.match("^([1-9][0-9]*)([0-9]{3})", s) + m = re.match("^([1-9][0-9]*)([0-9]{3}.*)", s) if m is None: break s = m.group(1) + "," + m.group(2) diff --git a/accounting/utils.py b/accounting/utils.py index 752b69d..049c8a4 100644 --- a/accounting/utils.py +++ b/accounting/utils.py @@ -33,7 +33,7 @@ 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], int] +RecordData = Tuple[Union[str, int], Optional[str], float] DEFAULT_CASH_ACCOUNT = "1111" CASH_SHORTCUT_ACCOUNTS = ["0", "1111"]