Revised to allow amounts in decimal numbers in the accounting application.

This commit is contained in:
依瑪貓 2020-08-21 10:00:59 +08:00
parent 50bc6fb0b1
commit 567a610e90
11 changed files with 42 additions and 34 deletions

View File

@ -20,6 +20,7 @@
""" """
import datetime import datetime
import re import re
from decimal import Decimal
from typing import Optional, List, Dict from typing import Optional, List, Dict
from django import forms from django import forms
@ -57,12 +58,14 @@ class RecordForm(forms.Form):
error_messages={ error_messages={
"max_length": _("This summary is too long (max. 128 characters)."), "max_length": _("This summary is too long (max. 128 characters)."),
}) })
amount = forms.IntegerField( amount = forms.DecimalField(
min_value=1, max_digits=18,
decimal_places=2,
min_value=0.01,
error_messages={ error_messages={
"required": _("Please fill in the amount."), "required": _("Please fill in the amount."),
"invalid": _("Please fill in a number."), "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): def __init__(self, *args, **kwargs):
@ -436,22 +439,22 @@ class TransactionForm(forms.Form):
return errors[0].message return errors[0].message
return None 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 debit records.
Returns: Returns:
The total amount of the credit records. 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]) 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.
Returns: Returns:
The total amount of the credit records. 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]) if "amount" in x.data and "amount" not in x.errors])

View File

@ -157,7 +157,7 @@ class Command(BaseCommand):
self._filler.add_expense_transaction( self._filler.add_expense_transaction(
-13, -13,
[(6273, "Bus—2623—Uptown→City Park", 30)]) [(6273, "Bus—2623—Uptown→City Park", 477543627.4775)])
self._filler.add_expense_transaction( self._filler.add_expense_transaction(
-2, -2,

View File

@ -20,6 +20,7 @@
""" """
import datetime import datetime
import re import re
from decimal import Decimal
from typing import Dict, List, Optional, Mapping from typing import Dict, List, Optional, Mapping
from dirtyfields import DirtyFieldsMixin from dirtyfields import DirtyFieldsMixin
@ -236,7 +237,7 @@ class Transaction(DirtyFieldsMixin, BaseModel):
record.summary = post[F"{record_type}-{no}-summary"] record.summary = post[F"{record_type}-{no}-summary"]
else: else:
record.summary = None record.summary = None
record.amount = int(post[F"{record_type}-{no}-amount"]) record.amount = Decimal(post[F"{record_type}-{no}-amount"])
records.append(record) records.append(record)
if txn_type != "transfer": if txn_type != "transfer":
if txn_type == "expense": if txn_type == "expense":
@ -314,10 +315,10 @@ class Transaction(DirtyFieldsMixin, BaseModel):
""" """
return [x for x in self.records if not x.is_credit] 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.""" """The total amount of the debit records."""
return sum([x.amount for x in self.debit_records return sum([x.amount for x in self.debit_records
if isinstance(x.amount, int)]) if isinstance(x.amount, Decimal)])
@property @property
def debit_summaries(self) -> List[str]: 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] 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.""" """The total amount of the credit records."""
return sum([x.amount for x in self.credit_records return sum([x.amount for x in self.credit_records
if isinstance(x.amount, int)]) if isinstance(x.amount, Decimal)])
@property @property
def credit_summaries(self) -> List[str]: def credit_summaries(self) -> List[str]:
@ -346,7 +347,7 @@ class Transaction(DirtyFieldsMixin, BaseModel):
for x in self.credit_records] for x in self.credit_records]
@property @property
def amount(self) -> int: def amount(self) -> Decimal:
"""The amount of this transaction.""" """The amount of this transaction."""
return self.debit_total() return self.debit_total()
@ -428,13 +429,13 @@ class Record(DirtyFieldsMixin, BaseModel):
account = models.ForeignKey( account = models.ForeignKey(
Account, on_delete=models.PROTECT) Account, on_delete=models.PROTECT)
summary = models.CharField(max_length=128, blank=True, null=True) 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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._debit_amount = None self._debit_amount: Optional[Decimal] = None
self._credit_amount = None self._credit_amount: Optional[Decimal] = None
self.balance = None self.balance: Optional[Decimal] = None
self._is_balanced = None self._is_balanced = None
self._has_order_hole = None self._has_order_hole = None
self._is_payable = None self._is_payable = None
@ -452,25 +453,25 @@ class Record(DirtyFieldsMixin, BaseModel):
self.amount) self.amount)
@property @property
def debit_amount(self) -> Optional[int]: def debit_amount(self) -> Optional[Decimal]:
"""The debit amount of this accounting record.""" """The debit amount of this accounting record."""
if self._debit_amount is None: if self._debit_amount is None:
self._debit_amount = self.amount if not self.is_credit else None self._debit_amount = self.amount if not self.is_credit else None
return self._debit_amount return self._debit_amount
@debit_amount.setter @debit_amount.setter
def debit_amount(self, value: Optional[int]) -> None: def debit_amount(self, value: Optional[Decimal]) -> None:
self._debit_amount = value self._debit_amount = value
@property @property
def credit_amount(self) -> Optional[int]: def credit_amount(self) -> Optional[Decimal]:
"""The credit amount of this accounting record.""" """The credit amount of this accounting record."""
if self._credit_amount is None: if self._credit_amount is None:
self._credit_amount = self.amount if self.is_credit else None self._credit_amount = self.amount if self.is_credit else None
return self._credit_amount return self._credit_amount
@credit_amount.setter @credit_amount.setter
def credit_amount(self, value: Optional[int]): def credit_amount(self, value: Optional[Decimal]):
self._credit_amount = value self._credit_amount = value
@property @property

View File

@ -177,10 +177,10 @@ function removeBlankOption(select) {
*/ */
function updateTotalAmount(element) { function updateTotalAmount(element) {
const type = element.data("type") const type = element.data("type")
let total = 0; let total = new Decimal("0");
$("." + type + "-to-sum").each(function () { $("." + type + "-to-sum").each(function () {
if (this.value !== "") { if (this.value !== "") {
total += parseInt(this.value); total = total.plus(new Decimal(this.value));
} }
}); });
total = String(total); total = String(total);
@ -465,19 +465,19 @@ function validateAmount(amount) {
function validateBalance() { function validateBalance() {
const balanceRows = $(".balance-row"); const balanceRows = $(".balance-row");
const errorMessages = $(".balance-error"); const errorMessages = $(".balance-error");
let debitTotal = 0; let debitTotal = new Decimal("0");
$(".debit-to-sum").each(function () { $(".debit-to-sum").each(function () {
if (this.value !== "") { 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 () { $(".credit-to-sum").each(function () {
if (this.value !== "") { 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"); balanceRows.addClass("is-invalid");
errorMessages.text(gettext("The total amount of debit and credit records are inconsistent.")) errorMessages.text(gettext("The total amount of debit and credit records are inconsistent."))
return false; return false;

View File

@ -37,7 +37,7 @@ First written: 2020/8/5
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<label for="{{ record_type }}-{{ no }}-amount" class="record-label">{{ _("Amount:")|force_escape }}</label> <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" min="1" name="{{ record_type }}-{{ no }}-amount" value="{{ record.amount.value|default:"" }}" required="required" data-type="{{ record_type }}" /> <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|default:"" }}" required="required" data-type="{{ record_type }}" />
<div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div> <div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div>
</div> </div>
</div> </div>

View File

@ -36,7 +36,7 @@ First written: 2020/8/5
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<label for="{{ record_type }}-{{ no }}-amount" class="record-label">{{ _("Amount:")|force_escape }}</label> <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" min="1" name="{{ record_type }}-{{ no }}-amount" value="{{ record.amount.value|default:"" }}" required="required" data-type="{{ record_type }}" /> <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|default:"" }}" required="required" data-type="{{ record_type }}" />
<div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div> <div id="{{ record_type }}-{{ no }}-amount-error" class="invalid-feedback">{{ record.amount.errors.0|default:"" }}</div>
</div> </div>
</div> </div>

View File

@ -28,6 +28,7 @@ First written: 2020/7/23
{% block settings %} {% block settings %}
{% setvar "title" _("Cash Expense Transaction") %} {% setvar "title" _("Cash Expense Transaction") %}
{% setvar "use_jqueryui" True %} {% 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/transactions.css" as file %}{% add_css file %}
{% static "accounting/css/summary-helper.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/transaction-form.js" as file %}{% add_js file %}

View File

@ -28,6 +28,7 @@ First written: 2020/7/23
{% block settings %} {% block settings %}
{% setvar "title" _("Cash Income Transaction") %} {% setvar "title" _("Cash Income Transaction") %}
{% setvar "use_jqueryui" True %} {% 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/transactions.css" as file %}{% add_css file %}
{% static "accounting/css/summary-helper.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/transaction-form.js" as file %}{% add_js file %}

View File

@ -28,6 +28,7 @@ First written: 2020/7/23
{% block settings %} {% block settings %}
{% setvar "title" _("Transfer Transaction") %} {% setvar "title" _("Transfer Transaction") %}
{% setvar "use_jqueryui" True %} {% 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/transactions.css" as file %}{% add_css file %}
{% static "accounting/css/summary-helper.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/transaction-form.js" as file %}{% add_js file %}

View File

@ -19,6 +19,7 @@
""" """
import re import re
from decimal import Decimal
from typing import Union, Optional from typing import Union, Optional
from django import template from django import template
@ -32,14 +33,14 @@ register = template.Library()
@register.filter @register.filter
def accounting_amount(value: Union[str, int]) -> str: def accounting_amount(value: Union[str, Decimal]) -> str:
if value is None: if value is None:
return "" return ""
if value == 0: if value == 0:
return "-" return "-"
s = str(abs(value)) s = str(abs(value))
while True: 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: if m is None:
break break
s = m.group(1) + "," + m.group(2) s = m.group(1) + "," + m.group(2)

View File

@ -33,7 +33,7 @@ from mia_core.utils import new_pk
from .models import Account, Transaction, Record from .models import Account, Transaction, Record
AccountData = Tuple[Union[str, int], str, str, str] 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" DEFAULT_CASH_ACCOUNT = "1111"
CASH_SHORTCUT_ACCOUNTS = ["0", "1111"] CASH_SHORTCUT_ACCOUNTS = ["0", "1111"]