Renamed "transaction" to "voucher", "cash expense transaction" to "cash disbursement voucher", and "cash income transaction" to "cash receipt voucher".
This commit is contained in:
parent
1e286fbeba
commit
5db13393cc
@ -80,8 +80,8 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
|
||||
from . import currency
|
||||
currency.init_app(app, bp)
|
||||
|
||||
from . import transaction
|
||||
transaction.init_app(app, bp)
|
||||
from . import voucher
|
||||
voucher.init_app(app, bp)
|
||||
|
||||
from . import report
|
||||
report.init_app(app, bp)
|
||||
|
@ -447,12 +447,12 @@ class CurrencyL10n(db.Model):
|
||||
"""The localized name."""
|
||||
|
||||
|
||||
class TransactionCurrency:
|
||||
"""A currency in a transaction."""
|
||||
class VoucherCurrency:
|
||||
"""A currency in a voucher."""
|
||||
|
||||
def __init__(self, code: str, debit: list[JournalEntry],
|
||||
credit: list[JournalEntry]):
|
||||
"""Constructs the currency in the transaction.
|
||||
"""Constructs the currency in the voucher.
|
||||
|
||||
:param code: The currency code.
|
||||
:param debit: The debit entries.
|
||||
@ -490,13 +490,13 @@ class TransactionCurrency:
|
||||
return sum([x.amount for x in self.credit])
|
||||
|
||||
|
||||
class Transaction(db.Model):
|
||||
"""A transaction."""
|
||||
__tablename__ = "accounting_transactions"
|
||||
class Voucher(db.Model):
|
||||
"""A voucher."""
|
||||
__tablename__ = "accounting_vouchers"
|
||||
"""The table name."""
|
||||
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
||||
autoincrement=False)
|
||||
"""The transaction ID."""
|
||||
"""The voucher ID."""
|
||||
date = db.Column(db.Date, nullable=False)
|
||||
"""The date."""
|
||||
no = db.Column(db.Integer, nullable=False, default=text("1"))
|
||||
@ -523,22 +523,22 @@ class Transaction(db.Model):
|
||||
"""The ID of the updator."""
|
||||
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
||||
"""The updator."""
|
||||
entries = db.relationship("JournalEntry", back_populates="transaction")
|
||||
entries = db.relationship("JournalEntry", back_populates="voucher")
|
||||
"""The journal entries."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns the string representation of this transaction.
|
||||
"""Returns the string representation of this voucher.
|
||||
|
||||
:return: The string representation of this transaction.
|
||||
:return: The string representation of this voucher.
|
||||
"""
|
||||
if self.is_cash_expense:
|
||||
return gettext("Cash Expense Transaction#%(id)s", id=self.id)
|
||||
if self.is_cash_income:
|
||||
return gettext("Cash Income Transaction#%(id)s", id=self.id)
|
||||
return gettext("Transfer Transaction#%(id)s", id=self.id)
|
||||
if self.is_cash_disbursement:
|
||||
return gettext("Cash Disbursement Voucher#%(id)s", id=self.id)
|
||||
if self.is_cash_receipt:
|
||||
return gettext("Cash Receipt Voucher#%(id)s", id=self.id)
|
||||
return gettext("Transfer Voucher#%(id)s", id=self.id)
|
||||
|
||||
@property
|
||||
def currencies(self) -> list[TransactionCurrency]:
|
||||
def currencies(self) -> list[VoucherCurrency]:
|
||||
"""Returns the journal entries categorized by their currencies.
|
||||
|
||||
:return: The currency categories.
|
||||
@ -551,7 +551,7 @@ class Transaction(db.Model):
|
||||
codes.append(entry.currency_code)
|
||||
by_currency[entry.currency_code] = []
|
||||
by_currency[entry.currency_code].append(entry)
|
||||
return [TransactionCurrency(code=x,
|
||||
return [VoucherCurrency(code=x,
|
||||
debit=[y for y in by_currency[x]
|
||||
if y.is_debit],
|
||||
credit=[y for y in by_currency[x]
|
||||
@ -559,10 +559,10 @@ class Transaction(db.Model):
|
||||
for x in codes]
|
||||
|
||||
@property
|
||||
def is_cash_income(self) -> bool:
|
||||
"""Returns whether this is a cash income transaction.
|
||||
def is_cash_receipt(self) -> bool:
|
||||
"""Returns whether this is a cash receipt voucher.
|
||||
|
||||
:return: True if this is a cash income transaction, or False otherwise.
|
||||
:return: True if this is a cash receipt voucher, or False otherwise.
|
||||
"""
|
||||
for currency in self.currencies:
|
||||
if len(currency.debit) > 1:
|
||||
@ -572,10 +572,10 @@ class Transaction(db.Model):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_cash_expense(self) -> bool:
|
||||
"""Returns whether this is a cash expense transaction.
|
||||
def is_cash_disbursement(self) -> bool:
|
||||
"""Returns whether this is a cash disbursement voucher.
|
||||
|
||||
:return: True if this is a cash expense transaction, or False
|
||||
:return: True if this is a cash disbursement voucher, or False
|
||||
otherwise.
|
||||
"""
|
||||
for currency in self.currencies:
|
||||
@ -587,9 +587,9 @@ class Transaction(db.Model):
|
||||
|
||||
@property
|
||||
def can_delete(self) -> bool:
|
||||
"""Returns whether the transaction can be deleted.
|
||||
"""Returns whether the voucher can be deleted.
|
||||
|
||||
:return: True if the transaction can be deleted, or False otherwise.
|
||||
:return: True if the voucher can be deleted, or False otherwise.
|
||||
"""
|
||||
if not hasattr(self, "__can_delete"):
|
||||
def has_offset() -> bool:
|
||||
@ -601,12 +601,12 @@ class Transaction(db.Model):
|
||||
return getattr(self, "__can_delete")
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Deletes the transaction.
|
||||
"""Deletes the voucher.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
JournalEntry.query\
|
||||
.filter(JournalEntry.transaction_id == self.id).delete()
|
||||
.filter(JournalEntry.voucher_id == self.id).delete()
|
||||
db.session.delete(self)
|
||||
|
||||
|
||||
@ -617,18 +617,17 @@ class JournalEntry(db.Model):
|
||||
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
||||
autoincrement=False)
|
||||
"""The entry ID."""
|
||||
transaction_id = db.Column(db.Integer,
|
||||
db.ForeignKey(Transaction.id,
|
||||
onupdate="CASCADE",
|
||||
voucher_id = db.Column(db.Integer,
|
||||
db.ForeignKey(Voucher.id, onupdate="CASCADE",
|
||||
ondelete="CASCADE"),
|
||||
nullable=False)
|
||||
"""The transaction ID."""
|
||||
transaction = db.relationship(Transaction, back_populates="entries")
|
||||
"""The transaction."""
|
||||
"""The voucher ID."""
|
||||
voucher = db.relationship(Voucher, back_populates="entries")
|
||||
"""The voucher."""
|
||||
is_debit = db.Column(db.Boolean, nullable=False)
|
||||
"""True for a debit entry, or False for a credit entry."""
|
||||
no = db.Column(db.Integer, nullable=False)
|
||||
"""The entry number under the transaction and debit or credit."""
|
||||
"""The entry number under the voucher and debit or credit."""
|
||||
original_entry_id = db.Column(db.Integer,
|
||||
db.ForeignKey(id, onupdate="CASCADE"),
|
||||
nullable=True)
|
||||
@ -665,7 +664,7 @@ class JournalEntry(db.Model):
|
||||
from accounting.template_filters import format_date, format_amount
|
||||
setattr(self, "__str",
|
||||
gettext("%(date)s %(summary)s %(amount)s",
|
||||
date=format_date(self.transaction.date),
|
||||
date=format_date(self.voucher.date),
|
||||
summary="" if self.summary is None
|
||||
else self.summary,
|
||||
amount=format_amount(self.amount)))
|
||||
@ -750,12 +749,13 @@ class JournalEntry(db.Model):
|
||||
frac: Decimal = (value - whole).normalize()
|
||||
return str(whole) + str(abs(frac))[1:]
|
||||
|
||||
txn_day: date = self.transaction.date
|
||||
voucher_day: date = self.voucher.date
|
||||
summary: str = "" if self.summary is None else self.summary
|
||||
return ([summary],
|
||||
[str(txn_day.year),
|
||||
"{}/{}".format(txn_day.year, txn_day.month),
|
||||
"{}/{}".format(txn_day.month, txn_day.day),
|
||||
"{}/{}/{}".format(txn_day.year, txn_day.month, txn_day.day),
|
||||
[str(voucher_day.year),
|
||||
"{}/{}".format(voucher_day.year, voucher_day.month),
|
||||
"{}/{}".format(voucher_day.month, voucher_day.day),
|
||||
"{}/{}/{}".format(voucher_day.year, voucher_day.month,
|
||||
voucher_day.day),
|
||||
format_amount(self.amount),
|
||||
format_amount(self.net_balance)])
|
||||
|
@ -23,7 +23,7 @@ This file is largely taken from the NanoParma ERP project, first written in
|
||||
import typing as t
|
||||
from datetime import date
|
||||
|
||||
from accounting.models import Transaction
|
||||
from accounting.models import Voucher
|
||||
from .period import Period
|
||||
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
||||
LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
|
||||
@ -61,8 +61,8 @@ class PeriodChooser:
|
||||
self.url_template: str = get_url(TemplatePeriod())
|
||||
"""The URL template."""
|
||||
|
||||
first: Transaction | None \
|
||||
= Transaction.query.order_by(Transaction.date).first()
|
||||
first: Voucher | None \
|
||||
= Voucher.query.order_by(Voucher.date).first()
|
||||
start: date | None = None if first is None else first.date
|
||||
|
||||
# Attributes
|
||||
|
@ -24,7 +24,7 @@ from flask import render_template, Response
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, BaseAccount, Account, Transaction, \
|
||||
from accounting.models import Currency, BaseAccount, Account, Voucher, \
|
||||
JournalEntry
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
@ -127,14 +127,14 @@ class AccountCollector:
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
sa.or_(*sub_conditions)]
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Transaction.date <= self.__period.end)
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount)).label("balance")
|
||||
select_balance: sa.Select \
|
||||
= sa.select(Account.id, Account.base_code, Account.no,
|
||||
balance_func)\
|
||||
.join(Transaction).join(Account)\
|
||||
.join(Voucher).join(Account)\
|
||||
.filter(*conditions)\
|
||||
.group_by(Account.id, Account.base_code, Account.no)\
|
||||
.order_by(Account.base_code, Account.no)
|
||||
@ -179,7 +179,7 @@ class AccountCollector:
|
||||
return None
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
Transaction.date < self.__period.start]
|
||||
Voucher.date < self.__period.start]
|
||||
return self.__query_balance(conditions)
|
||||
|
||||
def __add_current_period(self) -> None:
|
||||
@ -199,9 +199,9 @@ class AccountCollector:
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Transaction.date >= self.__period.start)
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Transaction.date <= self.__period.end)
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
return self.__query_balance(conditions)
|
||||
|
||||
@staticmethod
|
||||
@ -218,7 +218,7 @@ class AccountCollector:
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount))
|
||||
select_balance: sa.Select = sa.select(balance_func)\
|
||||
.join(Transaction).join(Account).filter(*conditions)
|
||||
.join(Voucher).join(Account).filter(*conditions)
|
||||
return db.session.scalar(select_balance)
|
||||
|
||||
def __add_owner_s_equity(self, code: str, amount: Decimal | None,
|
||||
|
@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, Account, Transaction, JournalEntry
|
||||
from accounting.models import Currency, Account, Voucher, JournalEntry
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -70,14 +70,14 @@ class ReportEntry:
|
||||
self.url: str | None = None
|
||||
"""The URL to the journal entry."""
|
||||
if entry is not None:
|
||||
self.date = entry.transaction.date
|
||||
self.date = entry.voucher.date
|
||||
self.account = entry.account
|
||||
self.summary = entry.summary
|
||||
self.income = None if entry.is_debit else entry.amount
|
||||
self.expense = entry.amount if entry.is_debit else None
|
||||
self.note = entry.transaction.note
|
||||
self.url = url_for("accounting.transaction.detail",
|
||||
txn=entry.transaction)
|
||||
self.note = entry.voucher.note
|
||||
self.url = url_for("accounting.voucher.detail",
|
||||
voucher=entry.voucher)
|
||||
|
||||
|
||||
class EntryCollector:
|
||||
@ -120,10 +120,10 @@ class EntryCollector:
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount))
|
||||
select: sa.Select = sa.Select(balance_func)\
|
||||
.join(Transaction).join(Account)\
|
||||
.join(Voucher).join(Account)\
|
||||
.filter(be(JournalEntry.currency_code == self.__currency.code),
|
||||
self.__account_condition,
|
||||
Transaction.date < self.__period.start)
|
||||
Voucher.date < self.__period.start)
|
||||
balance: int | None = db.session.scalar(select)
|
||||
if balance is None:
|
||||
return None
|
||||
@ -148,23 +148,23 @@ class EntryCollector:
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
self.__account_condition]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Transaction.date >= self.__period.start)
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Transaction.date <= self.__period.end)
|
||||
txn_with_account: sa.Select = sa.Select(Transaction.id).\
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
voucher_with_account: sa.Select = sa.Select(Voucher.id).\
|
||||
join(JournalEntry).join(Account).filter(*conditions)
|
||||
|
||||
return [ReportEntry(x)
|
||||
for x in JournalEntry.query.join(Transaction).join(Account)
|
||||
.filter(JournalEntry.transaction_id.in_(txn_with_account),
|
||||
for x in JournalEntry.query.join(Voucher).join(Account)
|
||||
.filter(JournalEntry.voucher_id.in_(voucher_with_account),
|
||||
JournalEntry.currency_code == self.__currency.code,
|
||||
sa.not_(self.__account_condition))
|
||||
.order_by(Transaction.date,
|
||||
Transaction.no,
|
||||
.order_by(Voucher.date,
|
||||
Voucher.no,
|
||||
JournalEntry.is_debit,
|
||||
JournalEntry.no)
|
||||
.options(selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.transaction))]
|
||||
selectinload(JournalEntry.voucher))]
|
||||
|
||||
@property
|
||||
def __account_condition(self) -> sa.BinaryExpression:
|
||||
@ -212,7 +212,7 @@ class EntryCollector:
|
||||
class CSVRow(BaseCSVRow):
|
||||
"""A row in the CSV."""
|
||||
|
||||
def __init__(self, txn_date: date | str | None,
|
||||
def __init__(self, voucher_date: date | str | None,
|
||||
account: str | None,
|
||||
summary: str | None,
|
||||
income: str | Decimal | None,
|
||||
@ -221,7 +221,7 @@ class CSVRow(BaseCSVRow):
|
||||
note: str | None):
|
||||
"""Constructs a row in the CSV.
|
||||
|
||||
:param txn_date: The transaction date.
|
||||
:param voucher_date: The voucher date.
|
||||
:param account: The account.
|
||||
:param summary: The summary.
|
||||
:param income: The income.
|
||||
@ -229,7 +229,7 @@ class CSVRow(BaseCSVRow):
|
||||
:param balance: The balance.
|
||||
:param note: The note.
|
||||
"""
|
||||
self.date: date | str | None = txn_date
|
||||
self.date: date | str | None = voucher_date
|
||||
"""The date."""
|
||||
self.account: str | None = account
|
||||
"""The account."""
|
||||
|
@ -24,7 +24,7 @@ from flask import render_template, Response
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, BaseAccount, Account, Transaction, \
|
||||
from accounting.models import Currency, BaseAccount, Account, Voucher, \
|
||||
JournalEntry
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
@ -259,14 +259,14 @@ class IncomeStatement(BaseReport):
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
sa.or_(*sub_conditions)]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Transaction.date >= self.__period.start)
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Transaction.date <= self.__period.end)
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, -JournalEntry.amount),
|
||||
else_=JournalEntry.amount)).label("balance")
|
||||
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
||||
.join(Transaction).join(Account)\
|
||||
.join(Voucher).join(Account)\
|
||||
.filter(*conditions)\
|
||||
.group_by(Account.id)\
|
||||
.order_by(Account.base_code, Account.no)
|
||||
|
@ -25,7 +25,7 @@ from flask import render_template, Response
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, Account, Transaction, JournalEntry
|
||||
from accounting.models import Currency, Account, Voucher, JournalEntry
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -47,8 +47,8 @@ class ReportEntry:
|
||||
"""
|
||||
self.entry: JournalEntry = entry
|
||||
"""The journal entry."""
|
||||
self.transaction: Transaction = entry.transaction
|
||||
"""The transaction."""
|
||||
self.voucher: Voucher = entry.voucher
|
||||
"""The voucher."""
|
||||
self.currency: Currency = entry.currency
|
||||
"""The account."""
|
||||
self.account: Account = entry.account
|
||||
@ -66,7 +66,7 @@ class ReportEntry:
|
||||
class CSVRow(BaseCSVRow):
|
||||
"""A row in the CSV."""
|
||||
|
||||
def __init__(self, txn_date: str | date,
|
||||
def __init__(self, voucher_date: str | date,
|
||||
currency: str,
|
||||
account: str,
|
||||
summary: str | None,
|
||||
@ -75,13 +75,13 @@ class CSVRow(BaseCSVRow):
|
||||
note: str | None):
|
||||
"""Constructs a row in the CSV.
|
||||
|
||||
:param txn_date: The transaction date.
|
||||
:param voucher_date: The voucher date.
|
||||
:param summary: The summary.
|
||||
:param debit: The debit amount.
|
||||
:param credit: The credit amount.
|
||||
:param note: The note.
|
||||
"""
|
||||
self.date: str | date = txn_date
|
||||
self.date: str | date = voucher_date
|
||||
"""The date."""
|
||||
self.currency: str = currency
|
||||
"""The currency."""
|
||||
@ -155,9 +155,9 @@ def get_csv_rows(entries: list[JournalEntry]) -> list[CSVRow]:
|
||||
gettext("Account"), gettext("Summary"),
|
||||
gettext("Debit"), gettext("Credit"),
|
||||
gettext("Note"))]
|
||||
rows.extend([CSVRow(x.transaction.date, x.currency.code,
|
||||
rows.extend([CSVRow(x.voucher.date, x.currency.code,
|
||||
str(x.account).title(), x.summary,
|
||||
x.debit, x.credit, x.transaction.note)
|
||||
x.debit, x.credit, x.voucher.note)
|
||||
for x in entries])
|
||||
return rows
|
||||
|
||||
@ -182,18 +182,18 @@ class Journal(BaseReport):
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] = []
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Transaction.date >= self.__period.start)
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Transaction.date <= self.__period.end)
|
||||
return JournalEntry.query.join(Transaction)\
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
return JournalEntry.query.join(Voucher)\
|
||||
.filter(*conditions)\
|
||||
.order_by(Transaction.date,
|
||||
Transaction.no,
|
||||
.order_by(Voucher.date,
|
||||
Voucher.no,
|
||||
JournalEntry.is_debit.desc(),
|
||||
JournalEntry.no)\
|
||||
.options(selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.currency),
|
||||
selectinload(JournalEntry.transaction)).all()
|
||||
selectinload(JournalEntry.voucher)).all()
|
||||
|
||||
def csv(self) -> Response:
|
||||
"""Returns the report as CSV for download.
|
||||
|
@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, Account, Transaction, JournalEntry
|
||||
from accounting.models import Currency, Account, Voucher, JournalEntry
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -67,13 +67,13 @@ class ReportEntry:
|
||||
self.url: str | None = None
|
||||
"""The URL to the journal entry."""
|
||||
if entry is not None:
|
||||
self.date = entry.transaction.date
|
||||
self.date = entry.voucher.date
|
||||
self.summary = entry.summary
|
||||
self.debit = entry.amount if entry.is_debit else None
|
||||
self.credit = None if entry.is_debit else entry.amount
|
||||
self.note = entry.transaction.note
|
||||
self.url = url_for("accounting.transaction.detail",
|
||||
txn=entry.transaction)
|
||||
self.note = entry.voucher.note
|
||||
self.url = url_for("accounting.voucher.detail",
|
||||
voucher=entry.voucher)
|
||||
|
||||
|
||||
class EntryCollector:
|
||||
@ -116,10 +116,10 @@ class EntryCollector:
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount))
|
||||
select: sa.Select = sa.Select(balance_func).join(Transaction)\
|
||||
select: sa.Select = sa.Select(balance_func).join(Voucher)\
|
||||
.filter(be(JournalEntry.currency_code == self.__currency.code),
|
||||
be(JournalEntry.account_id == self.__account.id),
|
||||
Transaction.date < self.__period.start)
|
||||
Voucher.date < self.__period.start)
|
||||
balance: int | None = db.session.scalar(select)
|
||||
if balance is None:
|
||||
return None
|
||||
@ -143,16 +143,16 @@ class EntryCollector:
|
||||
= [JournalEntry.currency_code == self.__currency.code,
|
||||
JournalEntry.account_id == self.__account.id]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Transaction.date >= self.__period.start)
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Transaction.date <= self.__period.end)
|
||||
return [ReportEntry(x) for x in JournalEntry.query.join(Transaction)
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
return [ReportEntry(x) for x in JournalEntry.query.join(Voucher)
|
||||
.filter(*conditions)
|
||||
.order_by(Transaction.date,
|
||||
Transaction.no,
|
||||
.order_by(Voucher.date,
|
||||
Voucher.no,
|
||||
JournalEntry.is_debit.desc(),
|
||||
JournalEntry.no)
|
||||
.options(selectinload(JournalEntry.transaction)).all()]
|
||||
.options(selectinload(JournalEntry.voucher)).all()]
|
||||
|
||||
def __get_total_entry(self) -> ReportEntry | None:
|
||||
"""Composes the total entry.
|
||||
@ -193,7 +193,7 @@ class EntryCollector:
|
||||
class CSVRow(BaseCSVRow):
|
||||
"""A row in the CSV."""
|
||||
|
||||
def __init__(self, txn_date: date | str | None,
|
||||
def __init__(self, voucher_date: date | str | None,
|
||||
summary: str | None,
|
||||
debit: str | Decimal | None,
|
||||
credit: str | Decimal | None,
|
||||
@ -201,14 +201,14 @@ class CSVRow(BaseCSVRow):
|
||||
note: str | None):
|
||||
"""Constructs a row in the CSV.
|
||||
|
||||
:param txn_date: The transaction date.
|
||||
:param voucher_date: The voucher date.
|
||||
:param summary: The summary.
|
||||
:param debit: The debit amount.
|
||||
:param credit: The credit amount.
|
||||
:param balance: The balance.
|
||||
:param note: The note.
|
||||
"""
|
||||
self.date: date | str | None = txn_date
|
||||
self.date: date | str | None = voucher_date
|
||||
"""The date."""
|
||||
self.summary: str | None = summary
|
||||
"""The summary."""
|
||||
|
@ -26,7 +26,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
|
||||
Transaction, JournalEntry
|
||||
Voucher, JournalEntry
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
from accounting.report.utils.csv_export import csv_download
|
||||
@ -62,21 +62,21 @@ class EntryCollector:
|
||||
self.__get_account_condition(k)),
|
||||
JournalEntry.currency_code.in_(
|
||||
self.__get_currency_condition(k)),
|
||||
JournalEntry.transaction_id.in_(
|
||||
self.__get_transaction_condition(k))]
|
||||
JournalEntry.voucher_id.in_(
|
||||
self.__get_voucher_condition(k))]
|
||||
try:
|
||||
sub_conditions.append(JournalEntry.amount == Decimal(k))
|
||||
except ArithmeticError:
|
||||
pass
|
||||
conditions.append(sa.or_(*sub_conditions))
|
||||
return JournalEntry.query.join(Transaction).filter(*conditions)\
|
||||
.order_by(Transaction.date,
|
||||
Transaction.no,
|
||||
return JournalEntry.query.join(Voucher).filter(*conditions)\
|
||||
.order_by(Voucher.date,
|
||||
Voucher.no,
|
||||
JournalEntry.is_debit,
|
||||
JournalEntry.no)\
|
||||
.options(selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.currency),
|
||||
selectinload(JournalEntry.transaction)).all()
|
||||
selectinload(JournalEntry.voucher)).all()
|
||||
|
||||
@staticmethod
|
||||
def __get_account_condition(k: str) -> sa.Select:
|
||||
@ -115,35 +115,35 @@ class EntryCollector:
|
||||
Currency.code.in_(select_l10n)))
|
||||
|
||||
@staticmethod
|
||||
def __get_transaction_condition(k: str) -> sa.Select:
|
||||
"""Composes and returns the condition to filter the transaction.
|
||||
def __get_voucher_condition(k: str) -> sa.Select:
|
||||
"""Composes and returns the condition to filter the voucher.
|
||||
|
||||
:param k: The keyword.
|
||||
:return: The condition to filter the transaction.
|
||||
:return: The condition to filter the voucher.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] = [Transaction.note.contains(k)]
|
||||
txn_date: datetime
|
||||
conditions: list[sa.BinaryExpression] = [Voucher.note.contains(k)]
|
||||
voucher_date: datetime
|
||||
try:
|
||||
txn_date = datetime.strptime(k, "%Y")
|
||||
voucher_date = datetime.strptime(k, "%Y")
|
||||
conditions.append(
|
||||
be(sa.extract("year", Transaction.date) == txn_date.year))
|
||||
be(sa.extract("year", Voucher.date) == voucher_date.year))
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
txn_date = datetime.strptime(k, "%Y/%m")
|
||||
voucher_date = datetime.strptime(k, "%Y/%m")
|
||||
conditions.append(sa.and_(
|
||||
sa.extract("year", Transaction.date) == txn_date.year,
|
||||
sa.extract("month", Transaction.date) == txn_date.month))
|
||||
sa.extract("year", Voucher.date) == voucher_date.year,
|
||||
sa.extract("month", Voucher.date) == voucher_date.month))
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
txn_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
|
||||
voucher_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
|
||||
conditions.append(sa.and_(
|
||||
sa.extract("month", Transaction.date) == txn_date.month,
|
||||
sa.extract("day", Transaction.date) == txn_date.day))
|
||||
sa.extract("month", Voucher.date) == voucher_date.month,
|
||||
sa.extract("day", Voucher.date) == voucher_date.day))
|
||||
except ValueError:
|
||||
pass
|
||||
return sa.select(Transaction.id).filter(sa.or_(*conditions))
|
||||
return sa.select(Voucher.id).filter(sa.or_(*conditions))
|
||||
|
||||
|
||||
class PageParams(BasePageParams):
|
||||
|
@ -24,7 +24,7 @@ from flask import Response, render_template
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, Account, Transaction, JournalEntry
|
||||
from accounting.models import Currency, Account, Voucher, JournalEntry
|
||||
from accounting.report.period import Period, PeriodChooser
|
||||
from accounting.report.utils.base_page_params import BasePageParams
|
||||
from accounting.report.utils.base_report import BaseReport
|
||||
@ -180,14 +180,14 @@ class TrialBalance(BaseReport):
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [JournalEntry.currency_code == self.__currency.code]
|
||||
if self.__period.start is not None:
|
||||
conditions.append(Transaction.date >= self.__period.start)
|
||||
conditions.append(Voucher.date >= self.__period.start)
|
||||
if self.__period.end is not None:
|
||||
conditions.append(Transaction.date <= self.__period.end)
|
||||
conditions.append(Voucher.date <= self.__period.end)
|
||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||
(JournalEntry.is_debit, JournalEntry.amount),
|
||||
else_=-JournalEntry.amount)).label("balance")
|
||||
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
||||
.join(Transaction).join(Account)\
|
||||
.join(Voucher).join(Account)\
|
||||
.filter(*conditions)\
|
||||
.group_by(Account.id)\
|
||||
.order_by(Account.base_code, Account.no)
|
||||
|
@ -27,7 +27,7 @@ from flask import request
|
||||
|
||||
from accounting import db
|
||||
from accounting.models import Currency, JournalEntry
|
||||
from accounting.utils.txn_types import TransactionType
|
||||
from accounting.utils.voucher_types import VoucherType
|
||||
from .option_link import OptionLink
|
||||
from .report_chooser import ReportChooser
|
||||
|
||||
@ -52,12 +52,12 @@ class BasePageParams(ABC):
|
||||
"""
|
||||
|
||||
@property
|
||||
def txn_types(self) -> t.Type[TransactionType]:
|
||||
"""Returns the transaction types.
|
||||
def voucher_types(self) -> t.Type[VoucherType]:
|
||||
"""Returns the voucher types.
|
||||
|
||||
:return: The transaction types.
|
||||
:return: The voucher types.
|
||||
"""
|
||||
return TransactionType
|
||||
return VoucherType
|
||||
|
||||
@property
|
||||
def csv_uri(self) -> str:
|
||||
|
@ -149,7 +149,7 @@
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/** The transaction management */
|
||||
/** The voucher management */
|
||||
.accounting-currency-control {
|
||||
background-color: transparent;
|
||||
}
|
||||
@ -169,14 +169,14 @@
|
||||
.accounting-list-group-hover .list-group-item:hover {
|
||||
background-color: #ececec;
|
||||
}
|
||||
.accounting-transaction-entry {
|
||||
.accounting-voucher-entry {
|
||||
border: none;
|
||||
}
|
||||
.accounting-transaction-entry-header {
|
||||
.accounting-voucher-entry-header {
|
||||
font-weight: bolder;
|
||||
border-bottom: thick double slategray;
|
||||
}
|
||||
.list-group-item.accounting-transaction-entry-total {
|
||||
.list-group-item.accounting-voucher-entry-total {
|
||||
font-weight: bolder;
|
||||
border-top: thick double slategray;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
* transaction-transfer-form.js: The JavaScript for the transfer transaction form
|
||||
* account-selector.js: The JavaScript for the account selector
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2023 imacat.
|
||||
|
@ -29,8 +29,8 @@
|
||||
class JournalEntryEditor {
|
||||
|
||||
/**
|
||||
* The transaction form
|
||||
* @type {TransactionForm}
|
||||
* The voucher form
|
||||
* @type {VoucherForm}
|
||||
*/
|
||||
form;
|
||||
|
||||
@ -217,7 +217,7 @@ class JournalEntryEditor {
|
||||
/**
|
||||
* Constructs a new journal entry editor.
|
||||
*
|
||||
* @param form {TransactionForm} the transaction form
|
||||
* @param form {VoucherForm} the voucher form
|
||||
*/
|
||||
constructor(form) {
|
||||
this.form = form;
|
||||
|
@ -105,7 +105,7 @@ class OriginalEntrySelector {
|
||||
* Returns the net balance for an original entry.
|
||||
*
|
||||
* @param currentEntry {JournalEntrySubForm} the journal entry sub-form that is currently editing
|
||||
* @param form {TransactionForm} the transaction form
|
||||
* @param form {VoucherForm} the voucher form
|
||||
* @param originalEntryId {string} the ID of the original entry
|
||||
* @return {Decimal} the net balance of the original entry
|
||||
*/
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
* transaction-form.js: The JavaScript for the transaction form
|
||||
* voucher-form.js: The JavaScript for the voucher form
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2023 imacat.
|
||||
@ -23,14 +23,14 @@
|
||||
"use strict";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
TransactionForm.initialize();
|
||||
VoucherForm.initialize();
|
||||
});
|
||||
|
||||
/**
|
||||
* The transaction form
|
||||
* The voucher form
|
||||
*
|
||||
*/
|
||||
class TransactionForm {
|
||||
class VoucherForm {
|
||||
|
||||
/**
|
||||
* The form element
|
||||
@ -105,7 +105,7 @@ class TransactionForm {
|
||||
entryEditor;
|
||||
|
||||
/**
|
||||
* Constructs the transaction form.
|
||||
* Constructs the voucher form.
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
@ -325,17 +325,17 @@ class TransactionForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* The transaction form
|
||||
* @type {TransactionForm}
|
||||
* The voucher form
|
||||
* @type {VoucherForm}
|
||||
*/
|
||||
static #form;
|
||||
|
||||
/**
|
||||
* Initializes the transaction form.
|
||||
* Initializes the voucher form.
|
||||
*
|
||||
*/
|
||||
static initialize() {
|
||||
this.#form = new TransactionForm()
|
||||
this.#form = new VoucherForm()
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,8 +352,8 @@ class CurrencySubForm {
|
||||
element;
|
||||
|
||||
/**
|
||||
* The transaction form
|
||||
* @type {TransactionForm}
|
||||
* The voucher form
|
||||
* @type {VoucherForm}
|
||||
*/
|
||||
form;
|
||||
|
||||
@ -420,7 +420,7 @@ class CurrencySubForm {
|
||||
/**
|
||||
* Constructs a currency sub-form
|
||||
*
|
||||
* @param form {TransactionForm} the transaction form
|
||||
* @param form {VoucherForm} the voucher form
|
||||
* @param element {HTMLDivElement} the currency sub-form element
|
||||
*/
|
||||
constructor(form, element) {
|
@ -1,5 +1,5 @@
|
||||
/* The Mia! Accounting Flask Project
|
||||
* transaction-order.js: The JavaScript for the transaction order
|
||||
* voucher-order.js: The JavaScript for the voucher order
|
||||
*/
|
||||
|
||||
/* Copyright (c) 2023 imacat.
|
@ -14,7 +14,7 @@
|
||||
# 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 globals for the transaction management.
|
||||
"""The template globals.
|
||||
|
||||
"""
|
||||
from flask import current_app
|
||||
|
@ -37,7 +37,7 @@ First written: 2023/3/7
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
|
||||
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
add-txn-material-fab.html: The material floating action buttons to add a new transaction
|
||||
add-voucher-material-fab.html: The material floating action buttons to add a new voucher
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -22,13 +22,13 @@ First written: 2023/2/25
|
||||
{% if accounting_can_edit() %}
|
||||
<div id="accounting-material-fab-speed-dial" class="d-md-none accounting-material-fab">
|
||||
<div id="accounting-material-fab-speed-dial-actions" class="d-md-none accounting-material-fab-speed-dial-group">
|
||||
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||
<a class="btn rounded-pill" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_DISBURSEMENT)|accounting_append_next }}">
|
||||
{{ A_("Cash expense") }}
|
||||
</a>
|
||||
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||
<a class="btn rounded-pill" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_RECEIPT)|accounting_append_next }}">
|
||||
{{ A_("Cash income") }}
|
||||
</a>
|
||||
<a class="btn rounded-pill" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||
<a class="btn rounded-pill" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.TRANSFER)|accounting_append_next }}">
|
||||
{{ A_("Transfer") }}
|
||||
</a>
|
||||
</div>
|
@ -27,17 +27,17 @@ First written: 2023/3/8
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_DISBURSEMENT)|accounting_append_next }}">
|
||||
{{ A_("Cash Expense") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.CASH_RECEIPT)|accounting_append_next }}">
|
||||
{{ A_("Cash Income") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.voucher.create", voucher_type=report.voucher_types.TRANSFER)|accounting_append_next }}">
|
||||
{{ A_("Transfer") }}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -38,7 +38,7 @@ First written: 2023/3/5
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
|
||||
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
|
||||
|
@ -37,7 +37,7 @@ First written: 2023/3/7
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
|
||||
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
|
||||
|
@ -36,7 +36,7 @@ First written: 2023/3/4
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
|
||||
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
|
||||
@ -60,8 +60,8 @@ First written: 2023/3/4
|
||||
</div>
|
||||
<div class="accounting-report-table-body">
|
||||
{% for entry in report.entries %}
|
||||
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||
<div>{{ entry.transaction.date|accounting_format_date }}</div>
|
||||
<a class="accounting-report-table-row" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
|
||||
<div>{{ entry.voucher.date|accounting_format_date }}</div>
|
||||
<div>{{ entry.currency.name }}</div>
|
||||
<div>
|
||||
<span class="d-none d-md-inline">{{ entry.account.code }}</span>
|
||||
@ -77,11 +77,11 @@ First written: 2023/3/4
|
||||
|
||||
<div class="list-group d-md-none">
|
||||
{% for entry in report.entries %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div {% if not entry.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
||||
<div class="text-muted small">
|
||||
{{ entry.transaction.date|accounting_format_date }}
|
||||
{{ entry.voucher.date|accounting_format_date }}
|
||||
{{ entry.account.title|title }}
|
||||
{% if entry.currency.code != accounting_default_currency_code() %}
|
||||
<span class="badge rounded-pill bg-info">{{ entry.currency.code }}</span>
|
||||
|
@ -38,7 +38,7 @@ First written: 2023/3/5
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
|
||||
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
|
||||
|
@ -35,7 +35,7 @@ First written: 2023/3/8
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
|
||||
|
||||
{% include "accounting/report/include/search-modal.html" %}
|
||||
|
||||
@ -57,8 +57,8 @@ First written: 2023/3/8
|
||||
</div>
|
||||
<div class="accounting-report-table-body">
|
||||
{% for entry in report.entries %}
|
||||
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||
<div>{{ entry.transaction.date|accounting_format_date }}</div>
|
||||
<a class="accounting-report-table-row" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
|
||||
<div>{{ entry.voucher.date|accounting_format_date }}</div>
|
||||
<div>{{ entry.currency.name }}</div>
|
||||
<div>
|
||||
<span class="d-none d-md-inline">{{ entry.account.code }}</span>
|
||||
@ -74,11 +74,11 @@ First written: 2023/3/8
|
||||
|
||||
<div class="list-group d-md-none">
|
||||
{% for entry in report.entries %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.voucher.detail", voucher=entry.voucher)|accounting_append_next }}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div {% if not entry.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
||||
<div class="text-muted small">
|
||||
{{ entry.transaction.date|accounting_format_date }}
|
||||
{{ entry.voucher.date|accounting_format_date }}
|
||||
{{ entry.account.title|title }}
|
||||
{% if entry.currency.code != accounting_default_currency_code() %}
|
||||
<span class="badge rounded-pill bg-info">{{ entry.currency.code }}</span>
|
||||
|
@ -37,7 +37,7 @@ First written: 2023/3/5
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% include "accounting/report/include/add-txn-material-fab.html" %}
|
||||
{% include "accounting/report/include/add-voucher-material-fab.html" %}
|
||||
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
create.html: The cash expense transaction creation form
|
||||
create.html: The cash disbursement voucher creation form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,10 +19,10 @@ create.html: The cash expense transaction creation form
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{% extends "accounting/transaction/expense/include/form.html" %}
|
||||
{% extends "accounting/voucher/disbursement/include/form.html" %}
|
||||
|
||||
{% block header %}{% block title %}{{ A_("Add a New Cash Expense Transaction") }}{% endblock %}{% endblock %}
|
||||
{% block header %}{% block title %}{{ A_("Add a New Cash Disbursement Voucher") }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
|
||||
|
||||
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
|
||||
{% block action_url %}{{ url_for("accounting.voucher.store", voucher_type=voucher_type) }}{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
detail.html: The account detail
|
||||
detail.html: The cash disbursement voucher detail
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,26 +19,26 @@ detail.html: The account detail
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/26
|
||||
#}
|
||||
{% extends "accounting/transaction/include/detail.html" %}
|
||||
{% extends "accounting/voucher/include/detail.html" %}
|
||||
|
||||
{% block to_transfer %}
|
||||
<a class="btn btn-primary" href="{{ url_for("accounting.transaction.edit", txn=obj)|accounting_txn_to_transfer|accounting_inherit_next }}">
|
||||
<a class="btn btn-primary" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_voucher_to_transfer|accounting_inherit_next }}">
|
||||
<i class="fa-solid fa-bars-staggered"></i>
|
||||
{{ A_("To Transfer") }}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block transaction_currencies %}
|
||||
{% block voucher_currencies %}
|
||||
{% for currency in obj.currencies %}
|
||||
<div class="mb-3">
|
||||
<div class="mb-2 fw-bolder">{{ currency.name }}</div>
|
||||
|
||||
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
|
||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-header">{{ A_("Content") }}</li>
|
||||
<li class="list-group-item accounting-voucher-entry accounting-voucher-entry-header">{{ A_("Content") }}</li>
|
||||
{% with entries = currency.debit %}
|
||||
{% include "accounting/transaction/include/detail-entries.html" %}
|
||||
{% include "accounting/voucher/include/detail-entries.html" %}
|
||||
{% endwith %}
|
||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
||||
<li class="list-group-item accounting-voucher-entry accounting-voucher-entry-total">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
edit.html: The cash income transaction edit form
|
||||
edit.html: The cash disbursement voucher edit form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,10 +19,10 @@ edit.html: The cash income transaction edit form
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{% extends "accounting/transaction/income/include/form.html" %}
|
||||
{% extends "accounting/voucher/disbursement/include/form.html" %}
|
||||
|
||||
{% block header %}{% block title %}{{ A_("Editing %(txn)s", txn=txn) }}{% endblock %}{% endblock %}
|
||||
{% block header %}{% block title %}{{ A_("Editing %(voucher)s", voucher=voucher) }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block back_url %}{{ url_for("accounting.transaction.detail", txn=txn)|accounting_inherit_next }}{% endblock %}
|
||||
{% block back_url %}{{ url_for("accounting.voucher.detail", voucher=voucher)|accounting_inherit_next }}{% endblock %}
|
||||
|
||||
{% block action_url %}{{ url_for("accounting.transaction.update", txn=txn)|accounting_txn_with_type }}{% endblock %}
|
||||
{% block action_url %}{{ url_for("accounting.voucher.update", voucher=voucher)|accounting_voucher_with_type }}{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
currency-sub-form.html: The currency sub-form in the cash expense transaction form
|
||||
currency-sub-form.html: The currency sub-form in the cash disbursement voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -64,11 +64,11 @@ First written: 2023/2/25
|
||||
offset_total = entry_form.offset_total|accounting_default("0"),
|
||||
net_balance_data = entry_form.net_balance,
|
||||
net_balance_text = entry_form.net_balance|accounting_format_amount,
|
||||
amount_data = entry_form.amount.data|accounting_txn_format_amount_input,
|
||||
amount_data = entry_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = entry_form.amount.errors,
|
||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
||||
entry_errors = entry_form.all_errors %}
|
||||
{% include "accounting/transaction/include/form-entry-item.html" %}
|
||||
{% include "accounting/voucher/include/form-entry-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
form.html: The cash expense transaction form
|
||||
form.html: The cash disbursement voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,7 +19,7 @@ form.html: The cash expense transaction form
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{% extends "accounting/transaction/include/form.html" %}
|
||||
{% extends "accounting/voucher/include/form.html" %}
|
||||
|
||||
{% block currency_sub_forms %}
|
||||
{% if form.currencies %}
|
||||
@ -33,7 +33,7 @@ First written: 2023/2/25
|
||||
debit_forms = currency_form.debit,
|
||||
debit_errors = currency_form.debit_errors,
|
||||
debit_total = currency_form.form.debit_total|accounting_format_amount %}
|
||||
{% include "accounting/transaction/expense/include/form-currency-item.html" %}
|
||||
{% include "accounting/voucher/disbursement/include/form-currency-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
@ -41,17 +41,17 @@ First written: 2023/2/25
|
||||
only_one_currency_form = True,
|
||||
currency_code_data = accounting_default_currency_code(),
|
||||
debit_total = "-" %}
|
||||
{% include "accounting/transaction/expense/include/form-currency-item.html" %}
|
||||
{% include "accounting/voucher/disbursement/include/form-currency-item.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_modals %}
|
||||
{% with summary_editor = form.summary_editor.debit %}
|
||||
{% include "accounting/transaction/include/summary-editor-modal.html" %}
|
||||
{% include "accounting/voucher/include/summary-editor-modal.html" %}
|
||||
{% endwith %}
|
||||
{% with entry_type = "debit",
|
||||
account_options = form.debit_account_options %}
|
||||
{% include "accounting/transaction/include/account-selector-modal.html" %}
|
||||
{% include "accounting/voucher/include/account-selector-modal.html" %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
detail-entries-item: The journal entries in the transaction detail
|
||||
detail-entries-item: The journal entries in the voucher detail
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -21,7 +21,7 @@ First written: 2023/3/14
|
||||
#}
|
||||
{# <ul> For SonarQube not to complain about incorrect HTML #}
|
||||
{% for entry in entries %}
|
||||
<li class="list-group-item accounting-transaction-entry">
|
||||
<li class="list-group-item accounting-voucher-entry">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<div class="small">{{ entry.account }}</div>
|
||||
@ -30,7 +30,7 @@ First written: 2023/3/14
|
||||
{% endif %}
|
||||
{% if entry.original_entry %}
|
||||
<div class="fst-italic small accounting-original-entry">
|
||||
<a href="{{ url_for("accounting.transaction.detail", txn=entry.original_entry.transaction)|accounting_append_next }}">
|
||||
<a href="{{ url_for("accounting.voucher.detail", voucher=entry.original_entry.voucher)|accounting_append_next }}">
|
||||
{{ A_("Offset %(entry)s", entry=entry.original_entry) }}
|
||||
</a>
|
||||
</div>
|
||||
@ -43,8 +43,8 @@ First written: 2023/3/14
|
||||
<ul class="ms-2 ps-0">
|
||||
{% for offset in entry.offsets %}
|
||||
<li>
|
||||
<a href="{{ url_for("accounting.transaction.detail", txn=offset.transaction)|accounting_append_next }}">
|
||||
{{ offset.transaction.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
|
||||
<a href="{{ url_for("accounting.voucher.detail", voucher=offset.voucher)|accounting_append_next }}">
|
||||
{{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
@ -31,12 +31,12 @@ First written: 2023/2/26
|
||||
{{ A_("Back") }}
|
||||
</a>
|
||||
{% if accounting_can_edit() %}
|
||||
<a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.transaction.edit", txn=obj)|accounting_inherit_next }}">
|
||||
<a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_inherit_next }}">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
{{ A_("Settings") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-primary" href="{{ url_for("accounting.transaction.order", txn_date=obj.date)|accounting_append_next }}">
|
||||
<a class="btn btn-primary" href="{{ url_for("accounting.voucher.order", voucher_date=obj.date)|accounting_append_next }}">
|
||||
<i class="fa-solid fa-bars-staggered"></i>
|
||||
{{ A_("Order") }}
|
||||
</a>
|
||||
@ -58,14 +58,14 @@ First written: 2023/2/26
|
||||
|
||||
{% if accounting_can_edit() %}
|
||||
<div class="d-md-none accounting-material-fab">
|
||||
<a class="btn btn-primary" href="{{ url_for("accounting.transaction.edit", txn=obj)|accounting_inherit_next }}">
|
||||
<a class="btn btn-primary" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_inherit_next }}">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if accounting_can_edit() and obj.can_delete %}
|
||||
<form action="{{ url_for("accounting.transaction.delete", txn=obj) }}" method="post">
|
||||
<form action="{{ url_for("accounting.voucher.delete", voucher=obj) }}" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if request.args.next %}
|
||||
<input type="hidden" name="next" value="{{ request.args.next }}">
|
||||
@ -74,11 +74,11 @@ First written: 2023/2/26
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="accounting-delete-modal-label">{{ A_("Delete Transaction Confirmation") }}</h1>
|
||||
<h1 class="modal-title fs-5" id="accounting-delete-modal-label">{{ A_("Delete Voucher Confirmation") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ A_("Do you really want to delete this transaction?") }}
|
||||
{{ A_("Do you really want to delete this voucher?") }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
|
||||
@ -99,13 +99,13 @@ First written: 2023/2/26
|
||||
{{ obj.date|accounting_format_date }}
|
||||
</div>
|
||||
|
||||
{% block transaction_currencies %}{% endblock %}
|
||||
{% block voucher_currencies %}{% endblock %}
|
||||
|
||||
{% if obj.note %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
{{ obj.note|accounting_txn_text2html|safe }}
|
||||
{{ obj.note|accounting_voucher_text2html|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
entry-sub-form.html: The journal entry sub-form in the transaction form
|
||||
entry-sub-form.html: The journal entry sub-form in the voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -43,7 +43,7 @@ First written: 2023/2/25
|
||||
<div>{{ A_("Offsets") }}</div>
|
||||
<ul class="ms-2 ps-0">
|
||||
{% for offset in offset_entries %}
|
||||
<li>{{ offset.transaction.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}</li>
|
||||
<li>{{ offset.voucher.date|accounting_format_date }} {{ offset.amount|accounting_format_amount }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
form.html: The transfer transaction form
|
||||
form.html: The base voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -23,7 +23,7 @@ First written: 2023/2/26
|
||||
|
||||
{% block accounting_scripts %}
|
||||
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/transaction-form.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/voucher-form.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-editor.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/account-selector.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/original-entry-selector.js") }}"></script>
|
||||
@ -88,8 +88,8 @@ First written: 2023/2/26
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% include "accounting/transaction/include/journal-entry-editor-modal.html" %}
|
||||
{% include "accounting/voucher/include/journal-entry-editor-modal.html" %}
|
||||
{% block form_modals %}{% endblock %}
|
||||
{% include "accounting/transaction/include/original-entry-selector-modal.html" %}
|
||||
{% include "accounting/voucher/include/original-entry-selector-modal.html" %}
|
||||
|
||||
{% endblock %}
|
@ -37,8 +37,8 @@ First written: 2023/2/25
|
||||
|
||||
<ul id="accounting-original-entry-selector-option-list" class="list-group accounting-selector-list">
|
||||
{% for entry in form.original_entry_options %}
|
||||
<li id="accounting-original-entry-selector-option-{{ entry.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-entry-selector-option" data-id="{{ entry.id }}" data-date="{{ entry.transaction.date }}" data-entry-type="{{ "debit" if entry.is_debit else "credit" }}" data-currency-code="{{ entry.currency.code }}" data-account-code="{{ entry.account_code }}" data-account-text="{{ entry.account }}" data-summary="{{ entry.summary|accounting_default }}" data-net-balance="{{ entry.net_balance|accounting_txn_format_amount_input }}" data-text="{{ entry }}" data-query-values="{{ entry.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">
|
||||
<div>{{ entry.transaction.date|accounting_format_date }} {{ entry.summary|accounting_default }}</div>
|
||||
<li id="accounting-original-entry-selector-option-{{ entry.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-entry-selector-option" data-id="{{ entry.id }}" data-date="{{ entry.voucher.date }}" data-entry-type="{{ "debit" if entry.is_debit else "credit" }}" data-currency-code="{{ entry.currency.code }}" data-account-code="{{ entry.account_code }}" data-account-text="{{ entry.account }}" data-summary="{{ entry.summary|accounting_default }}" data-net-balance="{{ entry.net_balance|accounting_voucher_format_amount_input }}" data-text="{{ entry }}" data-query-values="{{ entry.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-entry-editor-modal">
|
||||
<div>{{ entry.voucher.date|accounting_format_date }} {{ entry.summary|accounting_default }}</div>
|
||||
<div>
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<span id="accounting-original-entry-selector-option-{{ entry.id }}-net-balance">{{ entry.net_balance|accounting_format_amount }}</span>
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
order.html: The order of the transactions in a same day
|
||||
order.html: The order of the vouchers in a same day
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -23,10 +23,10 @@ First written: 2023/2/26
|
||||
|
||||
{% block accounting_scripts %}
|
||||
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/transaction-order.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/voucher-order.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}{% block title %}{{ A_("Transactions on %(date)s", date=date) }}{% endblock %}{% endblock %}
|
||||
{% block header %}{% block title %}{{ A_("Vouchers on %(date)s", date=date) }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -38,7 +38,7 @@ First written: 2023/2/26
|
||||
</div>
|
||||
|
||||
{% if list|length > 1 and accounting_can_edit() %}
|
||||
<form action="{{ url_for("accounting.transaction.sort", txn_date=date) }}" method="post">
|
||||
<form action="{{ url_for("accounting.voucher.sort", voucher_date=date) }}" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if request.args.next %}
|
||||
<input type="hidden" name="next" value="{{ request.args.next }}">
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
create.html: The transfer transaction creation form
|
||||
create.html: The cash receipt voucher creation form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,10 +19,10 @@ create.html: The transfer transaction creation form
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{% extends "accounting/transaction/transfer/include/form.html" %}
|
||||
{% extends "accounting/voucher/receipt/include/form.html" %}
|
||||
|
||||
{% block header %}{% block title %}{{ A_("Add a New Transfer Transaction") }}{% endblock %}{% endblock %}
|
||||
{% block header %}{% block title %}{{ A_("Add a New Cash Receipt Voucher") }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
|
||||
|
||||
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
|
||||
{% block action_url %}{{ url_for("accounting.voucher.store", voucher_type=voucher_type) }}{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
detail.html: The account detail
|
||||
detail.html: The cash receipt voucher detail
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,26 +19,26 @@ detail.html: The account detail
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/26
|
||||
#}
|
||||
{% extends "accounting/transaction/include/detail.html" %}
|
||||
{% extends "accounting/voucher/include/detail.html" %}
|
||||
|
||||
{% block to_transfer %}
|
||||
<a class="btn btn-primary" href="{{ url_for("accounting.transaction.edit", txn=obj)|accounting_txn_to_transfer|accounting_inherit_next }}">
|
||||
<a class="btn btn-primary" href="{{ url_for("accounting.voucher.edit", voucher=obj)|accounting_voucher_to_transfer|accounting_inherit_next }}">
|
||||
<i class="fa-solid fa-bars-staggered"></i>
|
||||
{{ A_("To Transfer") }}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block transaction_currencies %}
|
||||
{% block voucher_currencies %}
|
||||
{% for currency in obj.currencies %}
|
||||
<div class="mb-3">
|
||||
<div class="mb-2 fw-bolder">{{ currency.name }}</div>
|
||||
|
||||
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
|
||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-header">{{ A_("Content") }}</li>
|
||||
<li class="list-group-item accounting-voucher-entry accounting-voucher-entry-header">{{ A_("Content") }}</li>
|
||||
{% with entries = currency.credit %}
|
||||
{% include "accounting/transaction/include/detail-entries.html" %}
|
||||
{% include "accounting/voucher/include/detail-entries.html" %}
|
||||
{% endwith %}
|
||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
||||
<li class="list-group-item accounting-voucher-entry accounting-voucher-entry-total">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
edit.html: The cash expense transaction edit form
|
||||
edit.html: The cash receipt voucher edit form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,10 +19,10 @@ edit.html: The cash expense transaction edit form
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{% extends "accounting/transaction/expense/include/form.html" %}
|
||||
{% extends "accounting/voucher/receipt/include/form.html" %}
|
||||
|
||||
{% block header %}{% block title %}{{ A_("Editing %(txn)s", txn=txn) }}{% endblock %}{% endblock %}
|
||||
{% block header %}{% block title %}{{ A_("Editing %(voucher)s", voucher=voucher) }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block back_url %}{{ url_for("accounting.transaction.detail", txn=txn)|accounting_inherit_next }}{% endblock %}
|
||||
{% block back_url %}{{ url_for("accounting.voucher.detail", voucher=voucher)|accounting_inherit_next }}{% endblock %}
|
||||
|
||||
{% block action_url %}{{ url_for("accounting.transaction.update", txn=txn)|accounting_txn_with_type }}{% endblock %}
|
||||
{% block action_url %}{{ url_for("accounting.voucher.update", voucher=voucher)|accounting_voucher_with_type }}{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
currency-sub-form.html: The currency sub-form in the cash income transaction form
|
||||
currency-sub-form.html: The currency sub-form in the cash receipt voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -64,11 +64,11 @@ First written: 2023/2/25
|
||||
offset_total = entry_form.offset_total|accounting_default("0"),
|
||||
net_balance_data = entry_form.net_balance,
|
||||
net_balance_text = entry_form.net_balance|accounting_format_amount,
|
||||
amount_data = entry_form.amount.data|accounting_txn_format_amount_input,
|
||||
amount_data = entry_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = entry_form.amount.errors,
|
||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
||||
entry_errors = entry_form.all_errors %}
|
||||
{% include "accounting/transaction/include/form-entry-item.html" %}
|
||||
{% include "accounting/voucher/include/form-entry-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
form.html: The cash income transaction form
|
||||
form.html: The cash receipt voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,7 +19,7 @@ form.html: The cash income transaction form
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{% extends "accounting/transaction/include/form.html" %}
|
||||
{% extends "accounting/voucher/include/form.html" %}
|
||||
|
||||
{% block currency_sub_forms %}
|
||||
{% if form.currencies %}
|
||||
@ -33,7 +33,7 @@ First written: 2023/2/25
|
||||
credit_forms = currency_form.credit,
|
||||
credit_errors = currency_form.credit_errors,
|
||||
credit_total = currency_form.form.credit_total|accounting_format_amount %}
|
||||
{% include "accounting/transaction/income/include/form-currency-item.html" %}
|
||||
{% include "accounting/voucher/receipt/include/form-currency-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
@ -41,17 +41,17 @@ First written: 2023/2/25
|
||||
only_one_currency_form = True,
|
||||
currency_code_data = accounting_default_currency_code(),
|
||||
credit_total = "-" %}
|
||||
{% include "accounting/transaction/income/include/form-currency-item.html" %}
|
||||
{% include "accounting/voucher/receipt/include/form-currency-item.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_modals %}
|
||||
{% with summary_editor = form.summary_editor.credit %}
|
||||
{% include "accounting/transaction/include/summary-editor-modal.html" %}
|
||||
{% include "accounting/voucher/include/summary-editor-modal.html" %}
|
||||
{% endwith %}
|
||||
{% with entry_type = "credit",
|
||||
account_options = form.credit_account_options %}
|
||||
{% include "accounting/transaction/include/account-selector-modal.html" %}
|
||||
{% include "accounting/voucher/include/account-selector-modal.html" %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
create.html: The cash income transaction creation form
|
||||
create.html: The transfer voucher creation form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,10 +19,10 @@ create.html: The cash income transaction creation form
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{% extends "accounting/transaction/income/include/form.html" %}
|
||||
{% extends "accounting/voucher/transfer/include/form.html" %}
|
||||
|
||||
{% block header %}{% block title %}{{ A_("Add a New Cash Income Transaction") }}{% endblock %}{% endblock %}
|
||||
{% block header %}{% block title %}{{ A_("Add a New Transfer Voucher") }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.report.default") }}{% endblock %}
|
||||
|
||||
{% block action_url %}{{ url_for("accounting.transaction.store", txn_type=txn_type) }}{% endblock %}
|
||||
{% block action_url %}{{ url_for("accounting.voucher.store", voucher_type=voucher_type) }}{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
detail.html: The account detail
|
||||
detail.html: The transfer voucher detail
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,9 +19,9 @@ detail.html: The account detail
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/26
|
||||
#}
|
||||
{% extends "accounting/transaction/include/detail.html" %}
|
||||
{% extends "accounting/voucher/include/detail.html" %}
|
||||
|
||||
{% block transaction_currencies %}
|
||||
{% block voucher_currencies %}
|
||||
{% for currency in obj.currencies %}
|
||||
<div class="mb-3">
|
||||
<div class="mb-2 fw-bolder">{{ currency.name }}</div>
|
||||
@ -30,11 +30,11 @@ First written: 2023/2/26
|
||||
{# The debit entries #}
|
||||
<div class="col-sm-6 mb-2">
|
||||
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
|
||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-header">{{ A_("Debit") }}</li>
|
||||
<li class="list-group-item accounting-voucher-entry accounting-voucher-entry-header">{{ A_("Debit") }}</li>
|
||||
{% with entries = currency.debit %}
|
||||
{% include "accounting/transaction/include/detail-entries.html" %}
|
||||
{% include "accounting/voucher/include/detail-entries.html" %}
|
||||
{% endwith %}
|
||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
||||
<li class="list-group-item accounting-voucher-entry accounting-voucher-entry-total">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
||||
@ -46,11 +46,11 @@ First written: 2023/2/26
|
||||
{# The credit entries #}
|
||||
<div class="col-sm-6 mb-2">
|
||||
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover">
|
||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-header">{{ A_("Credit") }}</li>
|
||||
<li class="list-group-item accounting-voucher-entry accounting-voucher-entry-header">{{ A_("Credit") }}</li>
|
||||
{% with entries = currency.credit %}
|
||||
{% include "accounting/transaction/include/detail-entries.html" %}
|
||||
{% include "accounting/voucher/include/detail-entries.html" %}
|
||||
{% endwith %}
|
||||
<li class="list-group-item accounting-transaction-entry accounting-transaction-entry-total">
|
||||
<li class="list-group-item accounting-voucher-entry accounting-voucher-entry-total">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>{{ A_("Total") }}</div>
|
||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
edit.html: The transfer transaction edit form
|
||||
edit.html: The transfer voucher edit form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,10 +19,10 @@ edit.html: The transfer transaction edit form
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{% extends "accounting/transaction/transfer/include/form.html" %}
|
||||
{% extends "accounting/voucher/transfer/include/form.html" %}
|
||||
|
||||
{% block header %}{% block title %}{{ A_("Editing %(txn)s", txn=txn) }}{% endblock %}{% endblock %}
|
||||
{% block header %}{% block title %}{{ A_("Editing %(voucher)s", voucher=voucher) }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block back_url %}{{ url_for("accounting.transaction.detail", txn=txn)|accounting_inherit_next }}{% endblock %}
|
||||
{% block back_url %}{{ url_for("accounting.voucher.detail", voucher=voucher)|accounting_inherit_next }}{% endblock %}
|
||||
|
||||
{% block action_url %}{{ url_for("accounting.transaction.update", txn=txn)|accounting_txn_with_type }}{% endblock %}
|
||||
{% block action_url %}{{ url_for("accounting.voucher.update", voucher=voucher)|accounting_voucher_with_type }}{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
currency-sub-form.html: The currency sub-form in the transfer transaction form
|
||||
currency-sub-form.html: The currency sub-form in the transfer voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -66,11 +66,11 @@ First written: 2023/2/25
|
||||
offset_total = entry_form.offset_total|accounting_default,
|
||||
net_balance_data = entry_form.net_balance,
|
||||
net_balance_text = entry_form.net_balance|accounting_format_amount,
|
||||
amount_data = entry_form.amount.data|accounting_txn_format_amount_input,
|
||||
amount_data = entry_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = entry_form.amount.errors,
|
||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
||||
entry_errors = entry_form.all_errors %}
|
||||
{% include "accounting/transaction/include/form-entry-item.html" %}
|
||||
{% include "accounting/voucher/include/form-entry-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -114,11 +114,11 @@ First written: 2023/2/25
|
||||
offset_total = entry_form.offset_total|accounting_default("0"),
|
||||
net_balance_data = entry_form.net_balance,
|
||||
net_balance_text = entry_form.net_balance|accounting_format_amount,
|
||||
amount_data = entry_form.amount.data|accounting_txn_format_amount_input,
|
||||
amount_data = entry_form.amount.data|accounting_voucher_format_amount_input,
|
||||
amount_errors = entry_form.amount.errors,
|
||||
amount_text = entry_form.amount.data|accounting_format_amount,
|
||||
entry_errors = entry_form.all_errors %}
|
||||
{% include "accounting/transaction/include/form-entry-item.html" %}
|
||||
{% include "accounting/voucher/include/form-entry-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
@ -1,6 +1,6 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
form.html: The transfer transaction form
|
||||
form.html: The transfer voucher form
|
||||
|
||||
Copyright (c) 2023 imacat.
|
||||
|
||||
@ -19,7 +19,7 @@ form.html: The transfer transaction form
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
{% extends "accounting/transaction/include/form.html" %}
|
||||
{% extends "accounting/voucher/include/form.html" %}
|
||||
|
||||
{% block currency_sub_forms %}
|
||||
{% if form.currencies %}
|
||||
@ -36,7 +36,7 @@ First written: 2023/2/25
|
||||
credit_forms = currency_form.credit,
|
||||
credit_errors = currency_form.credit_errors,
|
||||
credit_total = currency_form.form.credit_total|accounting_format_amount %}
|
||||
{% include "accounting/transaction/transfer/include/form-currency-item.html" %}
|
||||
{% include "accounting/voucher/transfer/include/form-currency-item.html" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
@ -45,24 +45,24 @@ First written: 2023/2/25
|
||||
currency_code_data = accounting_default_currency_code(),
|
||||
debit_total = "-",
|
||||
credit_total = "-" %}
|
||||
{% include "accounting/transaction/transfer/include/form-currency-item.html" %}
|
||||
{% include "accounting/voucher/transfer/include/form-currency-item.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_modals %}
|
||||
{% with summary_editor = form.summary_editor.debit %}
|
||||
{% include "accounting/transaction/include/summary-editor-modal.html" %}
|
||||
{% include "accounting/voucher/include/summary-editor-modal.html" %}
|
||||
{% endwith %}
|
||||
{% with summary_editor = form.summary_editor.credit %}
|
||||
{% include "accounting/transaction/include/summary-editor-modal.html" %}
|
||||
{% include "accounting/voucher/include/summary-editor-modal.html" %}
|
||||
{% endwith %}
|
||||
{% with entry_type = "debit",
|
||||
account_options = form.debit_account_options %}
|
||||
{% include "accounting/transaction/include/account-selector-modal.html" %}
|
||||
{% include "accounting/voucher/include/account-selector-modal.html" %}
|
||||
{% endwith %}
|
||||
{% with entry_type = "credit",
|
||||
account_options = form.credit_account_options %}
|
||||
{% include "accounting/transaction/include/account-selector-modal.html" %}
|
||||
{% include "accounting/voucher/include/account-selector-modal.html" %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
@ -1,92 +0,0 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 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 reorder forms for the transaction management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import request
|
||||
|
||||
from accounting import db
|
||||
from accounting.models import Transaction
|
||||
|
||||
|
||||
def sort_transactions_in(txn_date: date, exclude: int | None = None) -> None:
|
||||
"""Sorts the transactions under a date after changing the date or deleting
|
||||
a transaction.
|
||||
|
||||
:param txn_date: The date of the transaction.
|
||||
:param exclude: The transaction ID to exclude.
|
||||
:return: None.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] = [Transaction.date == txn_date]
|
||||
if exclude is not None:
|
||||
conditions.append(Transaction.id != exclude)
|
||||
transactions: list[Transaction] = Transaction.query\
|
||||
.filter(*conditions)\
|
||||
.order_by(Transaction.no).all()
|
||||
for i in range(len(transactions)):
|
||||
if transactions[i].no != i + 1:
|
||||
transactions[i].no = i + 1
|
||||
|
||||
|
||||
class TransactionReorderForm:
|
||||
"""The form to reorder the transactions."""
|
||||
|
||||
def __init__(self, txn_date: date):
|
||||
"""Constructs the form to reorder the transactions in a day.
|
||||
|
||||
:param txn_date: The date.
|
||||
"""
|
||||
self.date: date = txn_date
|
||||
self.is_modified: bool = False
|
||||
|
||||
def save_order(self) -> None:
|
||||
"""Saves the order of the account.
|
||||
|
||||
:return:
|
||||
"""
|
||||
transactions: list[Transaction] = Transaction.query\
|
||||
.filter(Transaction.date == self.date).all()
|
||||
|
||||
# Collects the specified order.
|
||||
orders: dict[Transaction, int] = {}
|
||||
for txn in transactions:
|
||||
if f"{txn.id}-no" in request.form:
|
||||
try:
|
||||
orders[txn] = int(request.form[f"{txn.id}-no"])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Missing and invalid orders are appended to the end.
|
||||
missing: list[Transaction] \
|
||||
= [x for x in transactions if x not in orders]
|
||||
if len(missing) > 0:
|
||||
next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
|
||||
for txn in missing:
|
||||
orders[txn] = next_no
|
||||
|
||||
# Sort by the specified order first, and their original order.
|
||||
transactions.sort(key=lambda x: (orders[x], x.no))
|
||||
|
||||
# Update the orders.
|
||||
with db.session.no_autoflush:
|
||||
for i in range(len(transactions)):
|
||||
if transactions[i].no != i + 1:
|
||||
transactions[i].no = i + 1
|
||||
self.is_modified = True
|
@ -1,326 +0,0 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||
|
||||
# Copyright (c) 2023 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 operators for different transaction types.
|
||||
|
||||
"""
|
||||
import typing as t
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from flask import render_template, request, abort
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
from accounting.models import Transaction
|
||||
from accounting.template_globals import default_currency_code
|
||||
from accounting.utils.txn_types import TransactionType
|
||||
from accounting.transaction.forms import TransactionForm, \
|
||||
IncomeTransactionForm, ExpenseTransactionForm, TransferTransactionForm
|
||||
|
||||
|
||||
class TransactionOperator(ABC):
|
||||
"""The base transaction operator."""
|
||||
CHECK_ORDER: int = -1
|
||||
"""The order when checking the transaction operator."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def form(self) -> t.Type[TransactionForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_create_template(self, form: FlaskForm) -> str:
|
||||
"""Renders the template for the form to create a transaction.
|
||||
|
||||
:param form: The transaction form.
|
||||
:return: the form to create a transaction.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_detail_template(self, txn: Transaction) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: the detail page.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_edit_template(self, txn: Transaction, form: FlaskForm) -> str:
|
||||
"""Renders the template for the form to edit a transaction.
|
||||
|
||||
:param txn: The transaction.
|
||||
:param form: The form.
|
||||
:return: the form to edit a transaction.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def is_my_type(self, txn: Transaction) -> bool:
|
||||
"""Checks and returns whether the transaction belongs to the type.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: True if the transaction belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _entry_template(self) -> str:
|
||||
"""Renders and returns the template for the journal entry sub-form.
|
||||
|
||||
:return: The template for the journal entry sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/transaction/include/form-entry-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
entry_type="ENTRY_TYPE",
|
||||
entry_index="ENTRY_INDEX")
|
||||
|
||||
|
||||
class IncomeTransaction(TransactionOperator):
|
||||
"""An income transaction."""
|
||||
CHECK_ORDER: int = 2
|
||||
"""The order when checking the transaction operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[TransactionForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return IncomeTransactionForm
|
||||
|
||||
def render_create_template(self, form: IncomeTransactionForm) -> str:
|
||||
"""Renders the template for the form to create a transaction.
|
||||
|
||||
:param form: The transaction form.
|
||||
:return: the form to create a transaction.
|
||||
"""
|
||||
return render_template("accounting/transaction/income/create.html",
|
||||
form=form,
|
||||
txn_type=TransactionType.CASH_INCOME,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def render_detail_template(self, txn: Transaction) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/transaction/income/detail.html",
|
||||
obj=txn)
|
||||
|
||||
def render_edit_template(self, txn: Transaction,
|
||||
form: IncomeTransactionForm) -> str:
|
||||
"""Renders the template for the form to edit a transaction.
|
||||
|
||||
:param txn: The transaction.
|
||||
:param form: The form.
|
||||
:return: the form to edit a transaction.
|
||||
"""
|
||||
return render_template("accounting/transaction/income/edit.html",
|
||||
txn=txn, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def is_my_type(self, txn: Transaction) -> bool:
|
||||
"""Checks and returns whether the transaction belongs to the type.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: True if the transaction belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return txn.is_cash_income
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/transaction/income/include/form-currency-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
credit_total="-")
|
||||
|
||||
|
||||
class ExpenseTransaction(TransactionOperator):
|
||||
"""An expense transaction."""
|
||||
CHECK_ORDER: int = 1
|
||||
"""The order when checking the transaction operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[TransactionForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return ExpenseTransactionForm
|
||||
|
||||
def render_create_template(self, form: ExpenseTransactionForm) -> str:
|
||||
"""Renders the template for the form to create a transaction.
|
||||
|
||||
:param form: The transaction form.
|
||||
:return: the form to create a transaction.
|
||||
"""
|
||||
return render_template("accounting/transaction/expense/create.html",
|
||||
form=form,
|
||||
txn_type=TransactionType.CASH_EXPENSE,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def render_detail_template(self, txn: Transaction) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/transaction/expense/detail.html",
|
||||
obj=txn)
|
||||
|
||||
def render_edit_template(self, txn: Transaction,
|
||||
form: ExpenseTransactionForm) -> str:
|
||||
"""Renders the template for the form to edit a transaction.
|
||||
|
||||
:param txn: The transaction.
|
||||
:param form: The form.
|
||||
:return: the form to edit a transaction.
|
||||
"""
|
||||
return render_template("accounting/transaction/expense/edit.html",
|
||||
txn=txn, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def is_my_type(self, txn: Transaction) -> bool:
|
||||
"""Checks and returns whether the transaction belongs to the type.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: True if the transaction belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return txn.is_cash_expense
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/transaction/expense/include/form-currency-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
debit_total="-")
|
||||
|
||||
|
||||
class TransferTransaction(TransactionOperator):
|
||||
"""A transfer transaction."""
|
||||
CHECK_ORDER: int = 3
|
||||
"""The order when checking the transaction operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[TransactionForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return TransferTransactionForm
|
||||
|
||||
def render_create_template(self, form: TransferTransactionForm) -> str:
|
||||
"""Renders the template for the form to create a transaction.
|
||||
|
||||
:param form: The transaction form.
|
||||
:return: the form to create a transaction.
|
||||
"""
|
||||
return render_template("accounting/transaction/transfer/create.html",
|
||||
form=form,
|
||||
txn_type=TransactionType.TRANSFER,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def render_detail_template(self, txn: Transaction) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/transaction/transfer/detail.html",
|
||||
obj=txn)
|
||||
|
||||
def render_edit_template(self, txn: Transaction,
|
||||
form: TransferTransactionForm) -> str:
|
||||
"""Renders the template for the form to edit a transaction.
|
||||
|
||||
:param txn: The transaction.
|
||||
:param form: The form.
|
||||
:return: the form to edit a transaction.
|
||||
"""
|
||||
return render_template("accounting/transaction/transfer/edit.html",
|
||||
txn=txn, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def is_my_type(self, txn: Transaction) -> bool:
|
||||
"""Checks and returns whether the transaction belongs to the type.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: True if the transaction belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/transaction/transfer/include/form-currency-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
debit_total="-", credit_total="-")
|
||||
|
||||
|
||||
TXN_TYPE_TO_OP: dict[TransactionType, TransactionOperator] \
|
||||
= {TransactionType.CASH_INCOME: IncomeTransaction(),
|
||||
TransactionType.CASH_EXPENSE: ExpenseTransaction(),
|
||||
TransactionType.TRANSFER: TransferTransaction()}
|
||||
"""The map from the transaction types to their operators."""
|
||||
|
||||
|
||||
def get_txn_op(txn: Transaction, is_check_as: bool = False) \
|
||||
-> TransactionOperator:
|
||||
"""Returns the transaction operator that may be specified in the "as" query
|
||||
parameter. If it is not specified, check the transaction type from the
|
||||
transaction.
|
||||
|
||||
:param txn: The transaction.
|
||||
:param is_check_as: True to check the "as" parameter, or False otherwise.
|
||||
:return: None.
|
||||
"""
|
||||
if is_check_as and "as" in request.args:
|
||||
type_dict: dict[str, TransactionType] \
|
||||
= {x.value: x for x in TransactionType}
|
||||
if request.args["as"] not in type_dict:
|
||||
abort(404)
|
||||
return TXN_TYPE_TO_OP[type_dict[request.args["as"]]]
|
||||
for txn_type in sorted(TXN_TYPE_TO_OP.values(),
|
||||
key=lambda x: x.CHECK_ORDER):
|
||||
if txn_type.is_my_type(txn):
|
||||
return txn_type
|
@ -1,222 +0,0 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
||||
|
||||
# Copyright (c) 2023 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 for the transaction management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
from urllib.parse import parse_qsl, urlencode
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import Blueprint, render_template, session, redirect, request, \
|
||||
flash, url_for
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import lazy_gettext
|
||||
from accounting.models import Transaction
|
||||
from accounting.utils.cast import s
|
||||
from accounting.utils.flash_errors import flash_form_errors
|
||||
from accounting.utils.next_uri import inherit_next, or_next
|
||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
||||
from accounting.utils.txn_types import TransactionType
|
||||
from accounting.utils.user import get_current_user_pk
|
||||
from .forms import sort_transactions_in, TransactionReorderForm
|
||||
from .template_filters import with_type, to_transfer, format_amount_input, \
|
||||
text2html
|
||||
from .utils.operators import TransactionOperator, TXN_TYPE_TO_OP, get_txn_op
|
||||
|
||||
bp: Blueprint = Blueprint("transaction", __name__)
|
||||
"""The view blueprint for the transaction management."""
|
||||
bp.add_app_template_filter(with_type, "accounting_txn_with_type")
|
||||
bp.add_app_template_filter(to_transfer, "accounting_txn_to_transfer")
|
||||
bp.add_app_template_filter(format_amount_input,
|
||||
"accounting_txn_format_amount_input")
|
||||
bp.add_app_template_filter(text2html, "accounting_txn_text2html")
|
||||
|
||||
|
||||
@bp.get("/create/<transactionType:txn_type>", endpoint="create")
|
||||
@has_permission(can_edit)
|
||||
def show_add_transaction_form(txn_type: TransactionType) -> str:
|
||||
"""Shows the form to add a transaction.
|
||||
|
||||
:param txn_type: The transaction type.
|
||||
:return: The form to add a transaction.
|
||||
"""
|
||||
txn_op: TransactionOperator = TXN_TYPE_TO_OP[txn_type]
|
||||
form: txn_op.form
|
||||
if "form" in session:
|
||||
form = txn_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
|
||||
del session["form"]
|
||||
form.validate()
|
||||
else:
|
||||
form = txn_op.form()
|
||||
form.date.data = date.today()
|
||||
return txn_op.render_create_template(form)
|
||||
|
||||
|
||||
@bp.post("/store/<transactionType:txn_type>", endpoint="store")
|
||||
@has_permission(can_edit)
|
||||
def add_transaction(txn_type: TransactionType) -> redirect:
|
||||
"""Adds a transaction.
|
||||
|
||||
:param txn_type: The transaction type.
|
||||
:return: The redirection to the transaction detail on success, or the
|
||||
transaction creation form on error.
|
||||
"""
|
||||
txn_op: TransactionOperator = TXN_TYPE_TO_OP[txn_type]
|
||||
form: txn_op.form = txn_op.form(request.form)
|
||||
if not form.validate():
|
||||
flash_form_errors(form)
|
||||
session["form"] = urlencode(list(request.form.items()))
|
||||
return redirect(inherit_next(with_type(
|
||||
url_for("accounting.transaction.create", txn_type=txn_type))))
|
||||
txn: Transaction = Transaction()
|
||||
form.populate_obj(txn)
|
||||
db.session.add(txn)
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The transaction is added successfully")), "success")
|
||||
return redirect(inherit_next(__get_detail_uri(txn)))
|
||||
|
||||
|
||||
@bp.get("/<transaction:txn>", endpoint="detail")
|
||||
@has_permission(can_view)
|
||||
def show_transaction_detail(txn: Transaction) -> str:
|
||||
"""Shows the transaction detail.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: The detail.
|
||||
"""
|
||||
txn_op: TransactionOperator = get_txn_op(txn)
|
||||
return txn_op.render_detail_template(txn)
|
||||
|
||||
|
||||
@bp.get("/<transaction:txn>/edit", endpoint="edit")
|
||||
@has_permission(can_edit)
|
||||
def show_transaction_edit_form(txn: Transaction) -> str:
|
||||
"""Shows the form to edit a transaction.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: The form to edit the transaction.
|
||||
"""
|
||||
txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True)
|
||||
form: txn_op.form
|
||||
if "form" in session:
|
||||
form = txn_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
|
||||
del session["form"]
|
||||
form.obj = txn
|
||||
form.validate()
|
||||
else:
|
||||
form = txn_op.form(obj=txn)
|
||||
return txn_op.render_edit_template(txn, form)
|
||||
|
||||
|
||||
@bp.post("/<transaction:txn>/update", endpoint="update")
|
||||
@has_permission(can_edit)
|
||||
def update_transaction(txn: Transaction) -> redirect:
|
||||
"""Updates a transaction.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: The redirection to the transaction detail on success, or the
|
||||
transaction edit form on error.
|
||||
"""
|
||||
txn_op: TransactionOperator = get_txn_op(txn, is_check_as=True)
|
||||
form: txn_op.form = txn_op.form(request.form)
|
||||
form.obj = txn
|
||||
if not form.validate():
|
||||
flash_form_errors(form)
|
||||
session["form"] = urlencode(list(request.form.items()))
|
||||
return redirect(inherit_next(with_type(
|
||||
url_for("accounting.transaction.edit", txn=txn))))
|
||||
with db.session.no_autoflush:
|
||||
form.populate_obj(txn)
|
||||
if not form.is_modified:
|
||||
flash(s(lazy_gettext("The transaction was not modified.")), "success")
|
||||
return redirect(inherit_next(__get_detail_uri(txn)))
|
||||
txn.updated_by_id = get_current_user_pk()
|
||||
txn.updated_at = sa.func.now()
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The transaction is updated successfully.")),
|
||||
"success")
|
||||
return redirect(inherit_next(__get_detail_uri(txn)))
|
||||
|
||||
|
||||
@bp.post("/<transaction:txn>/delete", endpoint="delete")
|
||||
@has_permission(can_edit)
|
||||
def delete_transaction(txn: Transaction) -> redirect:
|
||||
"""Deletes a transaction.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: The redirection to the transaction list on success, or the
|
||||
transaction detail on error.
|
||||
"""
|
||||
txn.delete()
|
||||
sort_transactions_in(txn.date, txn.id)
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The transaction is deleted successfully.")),
|
||||
"success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
|
||||
|
||||
@bp.get("/dates/<date:txn_date>", endpoint="order")
|
||||
@has_permission(can_view)
|
||||
def show_transaction_order(txn_date: date) -> str:
|
||||
"""Shows the order of the transactions in a same date.
|
||||
|
||||
:param txn_date: The date.
|
||||
:return: The order of the transactions in the date.
|
||||
"""
|
||||
transactions: list[Transaction] = Transaction.query \
|
||||
.filter(Transaction.date == txn_date) \
|
||||
.order_by(Transaction.no).all()
|
||||
return render_template("accounting/transaction/order.html",
|
||||
date=txn_date, list=transactions)
|
||||
|
||||
|
||||
@bp.post("/dates/<date:txn_date>", endpoint="sort")
|
||||
@has_permission(can_edit)
|
||||
def sort_transactions(txn_date: date) -> redirect:
|
||||
"""Reorders the transactions in a date.
|
||||
|
||||
:param txn_date: The date.
|
||||
:return: The redirection to the incoming account or the account list. The
|
||||
reordering operation does not fail.
|
||||
"""
|
||||
form: TransactionReorderForm = TransactionReorderForm(txn_date)
|
||||
form.save_order()
|
||||
if not form.is_modified:
|
||||
flash(s(lazy_gettext("The order was not modified.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The order is updated successfully.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
|
||||
|
||||
def __get_detail_uri(txn: Transaction) -> str:
|
||||
"""Returns the detail URI of a transaction.
|
||||
|
||||
:param txn: The transaction.
|
||||
:return: The detail URI of the transaction.
|
||||
"""
|
||||
return url_for("accounting.transaction.detail", txn=txn)
|
||||
|
||||
|
||||
def __get_default_page_uri() -> str:
|
||||
"""Returns the URI for the default page.
|
||||
|
||||
:return: The URI for the default page.
|
||||
"""
|
||||
return url_for("accounting.report.default")
|
@ -14,17 +14,17 @@
|
||||
# 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 transaction types.
|
||||
"""The voucher types.
|
||||
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TransactionType(Enum):
|
||||
"""The transaction types."""
|
||||
CASH_INCOME: str = "income"
|
||||
"""The cash income transaction."""
|
||||
CASH_EXPENSE: str = "expense"
|
||||
"""The cash expense transaction."""
|
||||
class VoucherType(Enum):
|
||||
"""The voucher types."""
|
||||
CASH_RECEIPT: str = "receipt"
|
||||
"""The cash receipt voucher."""
|
||||
CASH_DISBURSEMENT: str = "disbursement"
|
||||
"""The cash disbursement voucher."""
|
||||
TRANSFER: str = "transfer"
|
||||
"""The transfer transaction."""
|
||||
"""The transfer voucher."""
|
@ -14,7 +14,7 @@
|
||||
# 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 transaction management.
|
||||
"""The voucher management.
|
||||
|
||||
"""
|
||||
from flask import Flask, Blueprint
|
||||
@ -27,11 +27,11 @@ def init_app(app: Flask, bp: Blueprint) -> None:
|
||||
:param bp: The blueprint of the accounting application.
|
||||
:return: None.
|
||||
"""
|
||||
from .converters import TransactionConverter, TransactionTypeConverter, \
|
||||
from .converters import VoucherConverter, VoucherTypeConverter, \
|
||||
DateConverter
|
||||
app.url_map.converters["transaction"] = TransactionConverter
|
||||
app.url_map.converters["transactionType"] = TransactionTypeConverter
|
||||
app.url_map.converters["voucher"] = VoucherConverter
|
||||
app.url_map.converters["voucherType"] = VoucherTypeConverter
|
||||
app.url_map.converters["date"] = DateConverter
|
||||
|
||||
from .views import bp as transaction_bp
|
||||
bp.register_blueprint(transaction_bp, url_prefix="/transactions")
|
||||
from .views import bp as voucher_bp
|
||||
bp.register_blueprint(voucher_bp, url_prefix="/vouchers")
|
@ -14,7 +14,7 @@
|
||||
# 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 path converters for the transaction management.
|
||||
"""The path converters for the voucher management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
@ -23,61 +23,59 @@ from flask import abort
|
||||
from sqlalchemy.orm import selectinload
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
from accounting.models import Transaction, JournalEntry
|
||||
from accounting.utils.txn_types import TransactionType
|
||||
from accounting.models import Voucher, JournalEntry
|
||||
from accounting.utils.voucher_types import VoucherType
|
||||
|
||||
|
||||
class TransactionConverter(BaseConverter):
|
||||
"""The transaction converter to convert the transaction ID from and to the
|
||||
corresponding transaction in the routes."""
|
||||
class VoucherConverter(BaseConverter):
|
||||
"""The voucher converter to convert the voucher ID from and to the
|
||||
corresponding voucher in the routes."""
|
||||
|
||||
def to_python(self, value: str) -> Transaction:
|
||||
"""Converts a transaction ID to a transaction.
|
||||
def to_python(self, value: str) -> Voucher:
|
||||
"""Converts a voucher ID to a voucher.
|
||||
|
||||
:param value: The transaction ID.
|
||||
:return: The corresponding transaction.
|
||||
:param value: The voucher ID.
|
||||
:return: The corresponding voucher.
|
||||
"""
|
||||
transaction: Transaction | None = Transaction.query\
|
||||
.join(JournalEntry)\
|
||||
.filter(Transaction.id == value)\
|
||||
.options(selectinload(Transaction.entries)
|
||||
voucher: Voucher | None = Voucher.query.join(JournalEntry)\
|
||||
.filter(Voucher.id == value)\
|
||||
.options(selectinload(Voucher.entries)
|
||||
.selectinload(JournalEntry.offsets)
|
||||
.selectinload(JournalEntry.transaction))\
|
||||
.selectinload(JournalEntry.voucher))\
|
||||
.first()
|
||||
if transaction is None:
|
||||
if voucher is None:
|
||||
abort(404)
|
||||
return transaction
|
||||
return voucher
|
||||
|
||||
def to_url(self, value: Transaction) -> str:
|
||||
"""Converts a transaction to its ID.
|
||||
def to_url(self, value: Voucher) -> str:
|
||||
"""Converts a voucher to its ID.
|
||||
|
||||
:param value: The transaction.
|
||||
:param value: The voucher.
|
||||
:return: The ID.
|
||||
"""
|
||||
return str(value.id)
|
||||
|
||||
|
||||
class TransactionTypeConverter(BaseConverter):
|
||||
"""The transaction converter to convert the transaction type ID from and to
|
||||
the corresponding transaction type in the routes."""
|
||||
class VoucherTypeConverter(BaseConverter):
|
||||
"""The voucher converter to convert the voucher type ID from and to the
|
||||
corresponding voucher type in the routes."""
|
||||
|
||||
def to_python(self, value: str) -> TransactionType:
|
||||
"""Converts a transaction ID to a transaction.
|
||||
def to_python(self, value: str) -> VoucherType:
|
||||
"""Converts a voucher ID to a voucher.
|
||||
|
||||
:param value: The transaction ID.
|
||||
:return: The corresponding transaction.
|
||||
:param value: The voucher ID.
|
||||
:return: The corresponding voucher type.
|
||||
"""
|
||||
type_dict: dict[str, TransactionType] \
|
||||
= {x.value: x for x in TransactionType}
|
||||
txn_type: TransactionType | None = type_dict.get(value)
|
||||
if txn_type is None:
|
||||
type_dict: dict[str, VoucherType] = {x.value: x for x in VoucherType}
|
||||
voucher_type: VoucherType | None = type_dict.get(value)
|
||||
if voucher_type is None:
|
||||
abort(404)
|
||||
return txn_type
|
||||
return voucher_type
|
||||
|
||||
def to_url(self, value: TransactionType) -> str:
|
||||
"""Converts a transaction type to its ID.
|
||||
def to_url(self, value: VoucherType) -> str:
|
||||
"""Converts a voucher type to its ID.
|
||||
|
||||
:param value: The transaction type.
|
||||
:param value: The voucher type.
|
||||
:return: The ID.
|
||||
"""
|
||||
return str(value.value)
|
@ -14,9 +14,9 @@
|
||||
# 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 for the transaction management.
|
||||
"""The forms for the voucher management.
|
||||
|
||||
"""
|
||||
from .reorder import sort_transactions_in, TransactionReorderForm
|
||||
from .transaction import TransactionForm, IncomeTransactionForm, \
|
||||
ExpenseTransactionForm, TransferTransactionForm
|
||||
from .reorder import sort_vouchers_in, VoucherReorderForm
|
||||
from .voucher import VoucherForm, CashReceiptVoucherForm, \
|
||||
CashDisbursementVoucherForm, TransferVoucherForm
|
@ -14,7 +14,7 @@
|
||||
# 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 currency sub-forms for the transaction management.
|
||||
"""The currency sub-forms for the voucher management.
|
||||
|
||||
"""
|
||||
from decimal import Decimal
|
||||
@ -29,7 +29,7 @@ from wtforms.validators import DataRequired
|
||||
from accounting import db
|
||||
from accounting.locale import lazy_gettext
|
||||
from accounting.models import Currency, JournalEntry
|
||||
from accounting.transaction.utils.offset_alias import offset_alias
|
||||
from accounting.voucher.utils.offset_alias import offset_alias
|
||||
from accounting.utils.cast import be
|
||||
from accounting.utils.strip_text import strip_text
|
||||
from .journal_entry import JournalEntryForm, CreditEntryForm, DebitEntryForm
|
||||
@ -117,9 +117,9 @@ class IsBalanced:
|
||||
|
||||
|
||||
class CurrencyForm(FlaskForm):
|
||||
"""The form to create or edit a currency in a transaction."""
|
||||
"""The form to create or edit a currency in a voucher."""
|
||||
no = IntegerField()
|
||||
"""The order in the transaction."""
|
||||
"""The order in the voucher."""
|
||||
code = StringField()
|
||||
"""The currency code."""
|
||||
whole_form = BooleanField()
|
||||
@ -132,9 +132,9 @@ class CurrencyForm(FlaskForm):
|
||||
:return: The journal entry sub-forms.
|
||||
"""
|
||||
entry_forms: list[JournalEntryForm] = []
|
||||
if isinstance(self, IncomeCurrencyForm):
|
||||
if isinstance(self, CashReceiptCurrencyForm):
|
||||
entry_forms.extend([x.form for x in self.credit])
|
||||
elif isinstance(self, ExpenseCurrencyForm):
|
||||
elif isinstance(self, CashDisbursementCurrencyForm):
|
||||
entry_forms.extend([x.form for x in self.debit])
|
||||
elif isinstance(self, TransferCurrencyForm):
|
||||
entry_forms.extend([x.form for x in self.debit])
|
||||
@ -161,10 +161,10 @@ class CurrencyForm(FlaskForm):
|
||||
return db.session.scalar(select) > 0
|
||||
|
||||
|
||||
class IncomeCurrencyForm(CurrencyForm):
|
||||
"""The form to create or edit a currency in a cash income transaction."""
|
||||
class CashReceiptCurrencyForm(CurrencyForm):
|
||||
"""The form to create or edit a currency in a cash receipt voucher."""
|
||||
no = IntegerField()
|
||||
"""The order in the transaction."""
|
||||
"""The order in the voucher."""
|
||||
code = StringField(
|
||||
filters=[strip_text],
|
||||
validators=[CURRENCY_REQUIRED,
|
||||
@ -198,10 +198,10 @@ class IncomeCurrencyForm(CurrencyForm):
|
||||
if isinstance(x, str) or isinstance(x, LazyString)]
|
||||
|
||||
|
||||
class ExpenseCurrencyForm(CurrencyForm):
|
||||
"""The form to create or edit a currency in a cash expense transaction."""
|
||||
class CashDisbursementCurrencyForm(CurrencyForm):
|
||||
"""The form to create or edit a currency in a cash disbursement voucher."""
|
||||
no = IntegerField()
|
||||
"""The order in the transaction."""
|
||||
"""The order in the voucher."""
|
||||
code = StringField(
|
||||
filters=[strip_text],
|
||||
validators=[CURRENCY_REQUIRED,
|
||||
@ -236,9 +236,9 @@ class ExpenseCurrencyForm(CurrencyForm):
|
||||
|
||||
|
||||
class TransferCurrencyForm(CurrencyForm):
|
||||
"""The form to create or edit a currency in a transfer transaction."""
|
||||
"""The form to create or edit a currency in a transfer voucher."""
|
||||
no = IntegerField()
|
||||
"""The order in the transaction."""
|
||||
"""The order in the voucher."""
|
||||
code = StringField(
|
||||
filters=[strip_text],
|
||||
validators=[CURRENCY_REQUIRED,
|
@ -14,7 +14,7 @@
|
||||
# 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 journal entry sub-forms for the transaction management.
|
||||
"""The journal entry sub-forms for the voucher management.
|
||||
|
||||
"""
|
||||
import re
|
||||
@ -232,8 +232,8 @@ class NotExceedingOriginalEntryNetBalance:
|
||||
return
|
||||
is_debit: bool = isinstance(form, DebitEntryForm)
|
||||
existing_entry_id: set[int] = set()
|
||||
if form.txn_form.obj is not None:
|
||||
existing_entry_id = {x.id for x in form.txn_form.obj.entries}
|
||||
if form.voucher_form.obj is not None:
|
||||
existing_entry_id = {x.id for x in form.voucher_form.obj.entries}
|
||||
offset_total_func: sa.Function = sa.func.sum(sa.case(
|
||||
(be(JournalEntry.is_debit == is_debit), JournalEntry.amount),
|
||||
else_=-JournalEntry.amount))
|
||||
@ -244,7 +244,7 @@ class NotExceedingOriginalEntryNetBalance:
|
||||
if offset_total_but_form is None:
|
||||
offset_total_but_form = Decimal("0")
|
||||
offset_total_on_form: Decimal = sum(
|
||||
[x.amount.data for x in form.txn_form.entries
|
||||
[x.amount.data for x in form.voucher_form.entries
|
||||
if x.original_entry_id.data == original_entry.id
|
||||
and x.amount != field and x.amount.data is not None])
|
||||
net_balance: Decimal = original_entry.amount - offset_total_but_form \
|
||||
@ -288,15 +288,15 @@ class JournalEntryForm(FlaskForm):
|
||||
"""The amount."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructs a base transaction form.
|
||||
"""Constructs a base journal entry form.
|
||||
|
||||
:param args: The arguments.
|
||||
:param kwargs: The keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
from .transaction import TransactionForm
|
||||
self.txn_form: TransactionForm | None = None
|
||||
"""The source transaction form."""
|
||||
from .voucher import VoucherForm
|
||||
self.voucher_form: VoucherForm | None = None
|
||||
"""The source voucher form."""
|
||||
|
||||
@property
|
||||
def account_text(self) -> str:
|
||||
@ -333,7 +333,7 @@ class JournalEntryForm(FlaskForm):
|
||||
:return: The text representation of the original entry.
|
||||
"""
|
||||
return None if self.__original_entry is None \
|
||||
else self.__original_entry.transaction.date
|
||||
else self.__original_entry.voucher.date
|
||||
|
||||
@property
|
||||
def original_entry_text(self) -> str | None:
|
||||
@ -375,10 +375,10 @@ class JournalEntryForm(FlaskForm):
|
||||
return []
|
||||
return JournalEntry.query\
|
||||
.filter(JournalEntry.original_entry_id == self.eid.data)\
|
||||
.options(selectinload(JournalEntry.transaction),
|
||||
.options(selectinload(JournalEntry.voucher),
|
||||
selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.offsets)
|
||||
.selectinload(JournalEntry.transaction)).all()
|
||||
.selectinload(JournalEntry.voucher)).all()
|
||||
setattr(self, "__offsets", get_offsets())
|
||||
return getattr(self, "__offsets")
|
||||
|
92
src/accounting/voucher/forms/reorder.py
Normal file
92
src/accounting/voucher/forms/reorder.py
Normal file
@ -0,0 +1,92 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||
|
||||
# Copyright (c) 2023 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 reorder forms for the voucher management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import request
|
||||
|
||||
from accounting import db
|
||||
from accounting.models import Voucher
|
||||
|
||||
|
||||
def sort_vouchers_in(voucher_date: date, exclude: int | None = None) -> None:
|
||||
"""Sorts the vouchers under a date after changing the date or deleting
|
||||
a voucher.
|
||||
|
||||
:param voucher_date: The date of the voucher.
|
||||
:param exclude: The voucher ID to exclude.
|
||||
:return: None.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] = [Voucher.date == voucher_date]
|
||||
if exclude is not None:
|
||||
conditions.append(Voucher.id != exclude)
|
||||
vouchers: list[Voucher] = Voucher.query\
|
||||
.filter(*conditions)\
|
||||
.order_by(Voucher.no).all()
|
||||
for i in range(len(vouchers)):
|
||||
if vouchers[i].no != i + 1:
|
||||
vouchers[i].no = i + 1
|
||||
|
||||
|
||||
class VoucherReorderForm:
|
||||
"""The form to reorder the vouchers."""
|
||||
|
||||
def __init__(self, voucher_date: date):
|
||||
"""Constructs the form to reorder the vouchers in a day.
|
||||
|
||||
:param voucher_date: The date.
|
||||
"""
|
||||
self.date: date = voucher_date
|
||||
self.is_modified: bool = False
|
||||
|
||||
def save_order(self) -> None:
|
||||
"""Saves the order of the account.
|
||||
|
||||
:return:
|
||||
"""
|
||||
vouchers: list[Voucher] = Voucher.query\
|
||||
.filter(Voucher.date == self.date).all()
|
||||
|
||||
# Collects the specified order.
|
||||
orders: dict[Voucher, int] = {}
|
||||
for voucher in vouchers:
|
||||
if f"{voucher.id}-no" in request.form:
|
||||
try:
|
||||
orders[voucher] = int(request.form[f"{voucher.id}-no"])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Missing and invalid orders are appended to the end.
|
||||
missing: list[Voucher] \
|
||||
= [x for x in vouchers if x not in orders]
|
||||
if len(missing) > 0:
|
||||
next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
|
||||
for voucher in missing:
|
||||
orders[voucher] = next_no
|
||||
|
||||
# Sort by the specified order first, and their original order.
|
||||
vouchers.sort(key=lambda x: (orders[x], x.no))
|
||||
|
||||
# Update the orders.
|
||||
with db.session.no_autoflush:
|
||||
for i in range(len(vouchers)):
|
||||
if vouchers[i].no != i + 1:
|
||||
vouchers[i].no = i + 1
|
||||
self.is_modified = True
|
@ -14,7 +14,7 @@
|
||||
# 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 transaction forms for the transaction management.
|
||||
"""The voucher forms for the voucher management.
|
||||
|
||||
"""
|
||||
import datetime as dt
|
||||
@ -30,19 +30,19 @@ from wtforms.validators import DataRequired, ValidationError
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import lazy_gettext
|
||||
from accounting.models import Transaction, Account, JournalEntry, \
|
||||
TransactionCurrency
|
||||
from accounting.transaction.utils.account_option import AccountOption
|
||||
from accounting.transaction.utils.original_entries import \
|
||||
from accounting.models import Voucher, Account, JournalEntry, \
|
||||
VoucherCurrency
|
||||
from accounting.voucher.utils.account_option import AccountOption
|
||||
from accounting.voucher.utils.original_entries import \
|
||||
get_selectable_original_entries
|
||||
from accounting.transaction.utils.summary_editor import SummaryEditor
|
||||
from accounting.voucher.utils.summary_editor import SummaryEditor
|
||||
from accounting.utils.random_id import new_id
|
||||
from accounting.utils.strip_text import strip_multiline_text
|
||||
from accounting.utils.user import get_current_user_pk
|
||||
from .currency import CurrencyForm, IncomeCurrencyForm, ExpenseCurrencyForm, \
|
||||
TransferCurrencyForm
|
||||
from .currency import CurrencyForm, CashReceiptCurrencyForm, \
|
||||
CashDisbursementCurrencyForm, TransferCurrencyForm
|
||||
from .journal_entry import JournalEntryForm, DebitEntryForm, CreditEntryForm
|
||||
from .reorder import sort_transactions_in
|
||||
from .reorder import sort_vouchers_in
|
||||
|
||||
DATE_REQUIRED: DataRequired = DataRequired(
|
||||
lazy_gettext("Please fill in the date."))
|
||||
@ -54,7 +54,7 @@ class NotBeforeOriginalEntries:
|
||||
entries."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: DateField) -> None:
|
||||
assert isinstance(form, TransactionForm)
|
||||
assert isinstance(form, VoucherForm)
|
||||
if field.data is None:
|
||||
return
|
||||
min_date: dt.date | None = form.min_date
|
||||
@ -69,7 +69,7 @@ class NotAfterOffsetEntries:
|
||||
"""The validator to check if the date is not after the offset entries."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: DateField) -> None:
|
||||
assert isinstance(form, TransactionForm)
|
||||
assert isinstance(form, VoucherForm)
|
||||
if field.data is None:
|
||||
return
|
||||
max_date: dt.date | None = form.max_date
|
||||
@ -92,7 +92,7 @@ class CannotDeleteOriginalEntriesWithOffset:
|
||||
"""The validator to check the original entries with offset."""
|
||||
|
||||
def __call__(self, form: FlaskForm, field: FieldList) -> None:
|
||||
assert isinstance(form, TransactionForm)
|
||||
assert isinstance(form, VoucherForm)
|
||||
if form.obj is None:
|
||||
return
|
||||
existing_matched_original_entry_id: set[int] \
|
||||
@ -105,8 +105,8 @@ class CannotDeleteOriginalEntriesWithOffset:
|
||||
"Journal entries with offset cannot be deleted."))
|
||||
|
||||
|
||||
class TransactionForm(FlaskForm):
|
||||
"""The base form to create or edit a transaction."""
|
||||
class VoucherForm(FlaskForm):
|
||||
"""The base form to create or edit a voucher."""
|
||||
date = DateField()
|
||||
"""The date."""
|
||||
currencies = FieldList(FormField(CurrencyForm))
|
||||
@ -115,20 +115,20 @@ class TransactionForm(FlaskForm):
|
||||
"""The note."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructs a base transaction form.
|
||||
"""Constructs a base voucher form.
|
||||
|
||||
:param args: The arguments.
|
||||
:param kwargs: The keyword arguments.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_modified: bool = False
|
||||
"""Whether the transaction is modified during populate_obj()."""
|
||||
"""Whether the voucher is modified during populate_obj()."""
|
||||
self.collector: t.Type[JournalEntryCollector] = JournalEntryCollector
|
||||
"""The journal entry collector. The default is the base abstract
|
||||
collector only to provide the correct type. The subclass forms should
|
||||
provide their own collectors."""
|
||||
self.obj: Transaction | None = kwargs.get("obj")
|
||||
"""The transaction, when editing an existing one."""
|
||||
self.obj: Voucher | None = kwargs.get("obj")
|
||||
"""The voucher, when editing an existing one."""
|
||||
self._is_need_payable: bool = False
|
||||
"""Whether we need the payable original entries."""
|
||||
self._is_need_receivable: bool = False
|
||||
@ -139,17 +139,17 @@ class TransactionForm(FlaskForm):
|
||||
"""The original entries whose net balances were exceeded by the
|
||||
amounts in the journal entry sub-forms."""
|
||||
for entry in self.entries:
|
||||
entry.txn_form = self
|
||||
entry.voucher_form = self
|
||||
|
||||
def populate_obj(self, obj: Transaction) -> None:
|
||||
"""Populates the form data into a transaction object.
|
||||
def populate_obj(self, obj: Voucher) -> None:
|
||||
"""Populates the form data into a voucher object.
|
||||
|
||||
:param obj: The transaction object.
|
||||
:param obj: The voucher object.
|
||||
:return: None.
|
||||
"""
|
||||
is_new: bool = obj.id is None
|
||||
if is_new:
|
||||
obj.id = new_id(Transaction)
|
||||
obj.id = new_id(Voucher)
|
||||
self.date: DateField
|
||||
self.__set_date(obj, self.date.data)
|
||||
obj.note = self.note.data
|
||||
@ -183,31 +183,31 @@ class TransactionForm(FlaskForm):
|
||||
entries.extend(currency.entries)
|
||||
return entries
|
||||
|
||||
def __set_date(self, obj: Transaction, new_date: dt.date) -> None:
|
||||
"""Sets the transaction date and number.
|
||||
def __set_date(self, obj: Voucher, new_date: dt.date) -> None:
|
||||
"""Sets the voucher date and number.
|
||||
|
||||
:param obj: The transaction object.
|
||||
:param obj: The voucher object.
|
||||
:param new_date: The new date.
|
||||
:return: None.
|
||||
"""
|
||||
if obj.date is None or obj.date != new_date:
|
||||
if obj.date is not None:
|
||||
sort_transactions_in(obj.date, obj.id)
|
||||
sort_vouchers_in(obj.date, obj.id)
|
||||
if self.max_date is not None and new_date == self.max_date:
|
||||
db_min_no: int | None = db.session.scalar(
|
||||
sa.select(sa.func.min(Transaction.no))
|
||||
.filter(Transaction.date == new_date))
|
||||
sa.select(sa.func.min(Voucher.no))
|
||||
.filter(Voucher.date == new_date))
|
||||
if db_min_no is None:
|
||||
obj.date = new_date
|
||||
obj.no = 1
|
||||
else:
|
||||
obj.date = new_date
|
||||
obj.no = db_min_no - 1
|
||||
sort_transactions_in(new_date)
|
||||
sort_vouchers_in(new_date)
|
||||
else:
|
||||
sort_transactions_in(new_date, obj.id)
|
||||
count: int = Transaction.query\
|
||||
.filter(Transaction.date == new_date).count()
|
||||
sort_vouchers_in(new_date, obj.id)
|
||||
count: int = Voucher.query\
|
||||
.filter(Voucher.date == new_date).count()
|
||||
obj.date = new_date
|
||||
obj.no = count + 1
|
||||
|
||||
@ -285,7 +285,7 @@ class TransactionForm(FlaskForm):
|
||||
if x.original_entry_id.data is not None}
|
||||
if len(original_entry_id) == 0:
|
||||
return None
|
||||
select: sa.Select = sa.select(sa.func.max(Transaction.date))\
|
||||
select: sa.Select = sa.select(sa.func.max(Voucher.date))\
|
||||
.join(JournalEntry).filter(JournalEntry.id.in_(original_entry_id))
|
||||
return db.session.scalar(select)
|
||||
|
||||
@ -297,29 +297,29 @@ class TransactionForm(FlaskForm):
|
||||
"""
|
||||
entry_id: set[int] = {x.eid.data for x in self.entries
|
||||
if x.eid.data is not None}
|
||||
select: sa.Select = sa.select(sa.func.min(Transaction.date))\
|
||||
select: sa.Select = sa.select(sa.func.min(Voucher.date))\
|
||||
.join(JournalEntry)\
|
||||
.filter(JournalEntry.original_entry_id.in_(entry_id))
|
||||
return db.session.scalar(select)
|
||||
|
||||
|
||||
T = t.TypeVar("T", bound=TransactionForm)
|
||||
"""A transaction form variant."""
|
||||
T = t.TypeVar("T", bound=VoucherForm)
|
||||
"""A voucher form variant."""
|
||||
|
||||
|
||||
class JournalEntryCollector(t.Generic[T], ABC):
|
||||
"""The journal entry collector."""
|
||||
|
||||
def __init__(self, form: T, obj: Transaction):
|
||||
def __init__(self, form: T, obj: Voucher):
|
||||
"""Constructs the journal entry collector.
|
||||
|
||||
:param form: The transaction form.
|
||||
:param obj: The transaction.
|
||||
:param form: The voucher form.
|
||||
:param obj: The voucher.
|
||||
"""
|
||||
self.form: T = form
|
||||
"""The transaction form."""
|
||||
self.__obj: Transaction = obj
|
||||
"""The transaction object."""
|
||||
"""The voucher form."""
|
||||
self.__obj: Voucher = obj
|
||||
"""The voucher object."""
|
||||
self.__entries: list[JournalEntry] = list(obj.entries)
|
||||
"""The existing journal entries."""
|
||||
self.__entries_by_id: dict[int, JournalEntry] \
|
||||
@ -327,8 +327,8 @@ class JournalEntryCollector(t.Generic[T], ABC):
|
||||
"""A dictionary from the entry ID to their entries."""
|
||||
self.__no_by_id: dict[int, int] = {x.id: x.no for x in self.__entries}
|
||||
"""A dictionary from the entry number to their entries."""
|
||||
self.__currencies: list[TransactionCurrency] = obj.currencies
|
||||
"""The currencies in the transaction."""
|
||||
self.__currencies: list[VoucherCurrency] = obj.currencies
|
||||
"""The currencies in the voucher."""
|
||||
self._debit_no: int = 1
|
||||
"""The number index for the debit entries."""
|
||||
self._credit_no: int = 1
|
||||
@ -371,11 +371,11 @@ class JournalEntryCollector(t.Generic[T], ABC):
|
||||
def _make_cash_entry(self, forms: list[JournalEntryForm], is_debit: bool,
|
||||
currency_code: str, no: int) -> None:
|
||||
"""Composes the cash journal entry at the other side of the cash
|
||||
transaction.
|
||||
voucher.
|
||||
|
||||
:param forms: The journal entry forms in the same currency.
|
||||
:param is_debit: True for a cash income transaction, or False for a
|
||||
cash expense transaction.
|
||||
:param is_debit: True for a cash receipt voucher, or False for a
|
||||
cash disbursement voucher.
|
||||
:param currency_code: The code of the currency.
|
||||
:param no: The number of the entry.
|
||||
:return: None.
|
||||
@ -441,14 +441,14 @@ class JournalEntryCollector(t.Generic[T], ABC):
|
||||
ord_by_form.get(x)))
|
||||
|
||||
|
||||
class IncomeTransactionForm(TransactionForm):
|
||||
"""The form to create or edit a cash income transaction."""
|
||||
class CashReceiptVoucherForm(VoucherForm):
|
||||
"""The form to create or edit a cash receipt voucher."""
|
||||
date = DateField(
|
||||
validators=[DATE_REQUIRED,
|
||||
NotBeforeOriginalEntries(),
|
||||
NotAfterOffsetEntries()])
|
||||
"""The date."""
|
||||
currencies = FieldList(FormField(IncomeCurrencyForm), name="currency",
|
||||
currencies = FieldList(FormField(CashReceiptCurrencyForm), name="currency",
|
||||
validators=[NeedSomeCurrencies()])
|
||||
"""The journal entries categorized by their currencies."""
|
||||
note = TextAreaField(filters=[strip_multiline_text])
|
||||
@ -461,11 +461,11 @@ class IncomeTransactionForm(TransactionForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._is_need_receivable = True
|
||||
|
||||
class Collector(JournalEntryCollector[IncomeTransactionForm]):
|
||||
"""The journal entry collector for the cash income transactions."""
|
||||
class Collector(JournalEntryCollector[CashReceiptVoucherForm]):
|
||||
"""The journal entry collector for the cash receipt vouchers."""
|
||||
|
||||
def collect(self) -> None:
|
||||
currencies: list[IncomeCurrencyForm] \
|
||||
currencies: list[CashReceiptCurrencyForm] \
|
||||
= [x.form for x in self.form.currencies]
|
||||
self._sort_currency_forms(currencies)
|
||||
for currency in currencies:
|
||||
@ -486,14 +486,15 @@ class IncomeTransactionForm(TransactionForm):
|
||||
self.collector = Collector
|
||||
|
||||
|
||||
class ExpenseTransactionForm(TransactionForm):
|
||||
"""The form to create or edit a cash expense transaction."""
|
||||
class CashDisbursementVoucherForm(VoucherForm):
|
||||
"""The form to create or edit a cash disbursement voucher."""
|
||||
date = DateField(
|
||||
validators=[DATE_REQUIRED,
|
||||
NotBeforeOriginalEntries(),
|
||||
NotAfterOffsetEntries()])
|
||||
"""The date."""
|
||||
currencies = FieldList(FormField(ExpenseCurrencyForm), name="currency",
|
||||
currencies = FieldList(FormField(CashDisbursementCurrencyForm),
|
||||
name="currency",
|
||||
validators=[NeedSomeCurrencies()])
|
||||
"""The journal entries categorized by their currencies."""
|
||||
note = TextAreaField(filters=[strip_multiline_text])
|
||||
@ -506,12 +507,12 @@ class ExpenseTransactionForm(TransactionForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._is_need_payable = True
|
||||
|
||||
class Collector(JournalEntryCollector[ExpenseTransactionForm]):
|
||||
"""The journal entry collector for the cash expense
|
||||
transactions."""
|
||||
class Collector(JournalEntryCollector[CashDisbursementVoucherForm]):
|
||||
"""The journal entry collector for the cash disbursement
|
||||
vouchers."""
|
||||
|
||||
def collect(self) -> None:
|
||||
currencies: list[ExpenseCurrencyForm] \
|
||||
currencies: list[CashDisbursementCurrencyForm] \
|
||||
= [x.form for x in self.form.currencies]
|
||||
self._sort_currency_forms(currencies)
|
||||
for currency in currencies:
|
||||
@ -532,8 +533,8 @@ class ExpenseTransactionForm(TransactionForm):
|
||||
self.collector = Collector
|
||||
|
||||
|
||||
class TransferTransactionForm(TransactionForm):
|
||||
"""The form to create or edit a transfer transaction."""
|
||||
class TransferVoucherForm(VoucherForm):
|
||||
"""The form to create or edit a transfer voucher."""
|
||||
date = DateField(
|
||||
validators=[DATE_REQUIRED,
|
||||
NotBeforeOriginalEntries(),
|
||||
@ -553,8 +554,8 @@ class TransferTransactionForm(TransactionForm):
|
||||
self._is_need_payable = True
|
||||
self._is_need_receivable = True
|
||||
|
||||
class Collector(JournalEntryCollector[TransferTransactionForm]):
|
||||
"""The journal entry collector for the transfer transactions."""
|
||||
class Collector(JournalEntryCollector[TransferVoucherForm]):
|
||||
"""The journal entry collector for the transfer vouchers."""
|
||||
|
||||
def collect(self) -> None:
|
||||
currencies: list[TransferCurrencyForm] \
|
@ -14,7 +14,7 @@
|
||||
# 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 filters for the transaction management.
|
||||
"""The template filters for the voucher management.
|
||||
|
||||
"""
|
||||
from decimal import Decimal
|
||||
@ -26,10 +26,10 @@ from flask import request
|
||||
|
||||
|
||||
def with_type(uri: str) -> str:
|
||||
"""Adds the transaction type to the URI, if it is specified.
|
||||
"""Adds the voucher type to the URI, if it is specified.
|
||||
|
||||
:param uri: The URI.
|
||||
:return: The result URL, optionally with the transaction type added.
|
||||
:return: The result URL, optionally with the voucher type added.
|
||||
"""
|
||||
if "as" not in request.args:
|
||||
return uri
|
||||
@ -43,10 +43,10 @@ def with_type(uri: str) -> str:
|
||||
|
||||
|
||||
def to_transfer(uri: str) -> str:
|
||||
"""Adds the transfer transaction type to the URI.
|
||||
"""Adds the transfer voucher type to the URI.
|
||||
|
||||
:param uri: The URI.
|
||||
:return: The result URL, with the transfer transaction type added.
|
||||
:return: The result URL, with the transfer voucher type added.
|
||||
"""
|
||||
uri_p: ParseResult = urlparse(uri)
|
||||
params: list[tuple[str, str]] = parse_qsl(uri_p.query)
|
@ -14,6 +14,6 @@
|
||||
# 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 for the transaction management.
|
||||
"""The utilities for the voucher management.
|
||||
|
||||
"""
|
@ -14,7 +14,7 @@
|
||||
# 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 account option for the transaction management.
|
||||
"""The account option for the voucher management.
|
||||
|
||||
"""
|
||||
from accounting.models import Account
|
326
src/accounting/voucher/utils/operators.py
Normal file
326
src/accounting/voucher/utils/operators.py
Normal file
@ -0,0 +1,326 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||
|
||||
# Copyright (c) 2023 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 operators for different voucher types.
|
||||
|
||||
"""
|
||||
import typing as t
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from flask import render_template, request, abort
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
from accounting.models import Voucher
|
||||
from accounting.template_globals import default_currency_code
|
||||
from accounting.utils.voucher_types import VoucherType
|
||||
from accounting.voucher.forms import VoucherForm, CashReceiptVoucherForm, \
|
||||
CashDisbursementVoucherForm, TransferVoucherForm
|
||||
|
||||
|
||||
class VoucherOperator(ABC):
|
||||
"""The base voucher operator."""
|
||||
CHECK_ORDER: int = -1
|
||||
"""The order when checking the voucher operator."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def form(self) -> t.Type[VoucherForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_create_template(self, form: FlaskForm) -> str:
|
||||
"""Renders the template for the form to create a voucher.
|
||||
|
||||
:param form: The voucher form.
|
||||
:return: the form to create a voucher.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: the detail page.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def render_edit_template(self, voucher: Voucher, form: FlaskForm) -> str:
|
||||
"""Renders the template for the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param form: The form.
|
||||
:return: the form to edit a voucher.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: True if the voucher belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _entry_template(self) -> str:
|
||||
"""Renders and returns the template for the journal entry sub-form.
|
||||
|
||||
:return: The template for the journal entry sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/voucher/include/form-entry-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
entry_type="ENTRY_TYPE",
|
||||
entry_index="ENTRY_INDEX")
|
||||
|
||||
|
||||
class CashReceiptVoucher(VoucherOperator):
|
||||
"""A cash receipt voucher."""
|
||||
CHECK_ORDER: int = 2
|
||||
"""The order when checking the voucher operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[VoucherForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return CashReceiptVoucherForm
|
||||
|
||||
def render_create_template(self, form: CashReceiptVoucherForm) -> str:
|
||||
"""Renders the template for the form to create a voucher.
|
||||
|
||||
:param form: The voucher form.
|
||||
:return: the form to create a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/receipt/create.html",
|
||||
form=form,
|
||||
voucher_type=VoucherType.CASH_RECEIPT,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/voucher/receipt/detail.html",
|
||||
obj=voucher)
|
||||
|
||||
def render_edit_template(self, voucher: Voucher,
|
||||
form: CashReceiptVoucherForm) -> str:
|
||||
"""Renders the template for the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param form: The form.
|
||||
:return: the form to edit a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/receipt/edit.html",
|
||||
voucher=voucher, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: True if the voucher belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return voucher.is_cash_receipt
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/voucher/receipt/include/form-currency-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
credit_total="-")
|
||||
|
||||
|
||||
class CashDisbursementVoucher(VoucherOperator):
|
||||
"""A cash disbursement voucher."""
|
||||
CHECK_ORDER: int = 1
|
||||
"""The order when checking the voucher operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[VoucherForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return CashDisbursementVoucherForm
|
||||
|
||||
def render_create_template(self, form: CashDisbursementVoucherForm) -> str:
|
||||
"""Renders the template for the form to create a voucher.
|
||||
|
||||
:param form: The voucher form.
|
||||
:return: the form to create a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/disbursement/create.html",
|
||||
form=form,
|
||||
voucher_type=VoucherType.CASH_DISBURSEMENT,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/voucher/disbursement/detail.html",
|
||||
obj=voucher)
|
||||
|
||||
def render_edit_template(self, voucher: Voucher,
|
||||
form: CashDisbursementVoucherForm) -> str:
|
||||
"""Renders the template for the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param form: The form.
|
||||
:return: the form to edit a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/disbursement/edit.html",
|
||||
voucher=voucher, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: True if the voucher belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return voucher.is_cash_disbursement
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/voucher/disbursement/include/form-currency-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
debit_total="-")
|
||||
|
||||
|
||||
class TransferVoucher(VoucherOperator):
|
||||
"""A transfer voucher."""
|
||||
CHECK_ORDER: int = 3
|
||||
"""The order when checking the voucher operator."""
|
||||
|
||||
@property
|
||||
def form(self) -> t.Type[VoucherForm]:
|
||||
"""Returns the form class.
|
||||
|
||||
:return: The form class.
|
||||
"""
|
||||
return TransferVoucherForm
|
||||
|
||||
def render_create_template(self, form: TransferVoucherForm) -> str:
|
||||
"""Renders the template for the form to create a voucher.
|
||||
|
||||
:param form: The voucher form.
|
||||
:return: the form to create a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/transfer/create.html",
|
||||
form=form,
|
||||
voucher_type=VoucherType.TRANSFER,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def render_detail_template(self, voucher: Voucher) -> str:
|
||||
"""Renders the template for the detail page.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: the detail page.
|
||||
"""
|
||||
return render_template("accounting/voucher/transfer/detail.html",
|
||||
obj=voucher)
|
||||
|
||||
def render_edit_template(self, voucher: Voucher,
|
||||
form: TransferVoucherForm) -> str:
|
||||
"""Renders the template for the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param form: The form.
|
||||
:return: the form to edit a voucher.
|
||||
"""
|
||||
return render_template("accounting/voucher/transfer/edit.html",
|
||||
voucher=voucher, form=form,
|
||||
currency_template=self.__currency_template,
|
||||
entry_template=self._entry_template)
|
||||
|
||||
def is_my_type(self, voucher: Voucher) -> bool:
|
||||
"""Checks and returns whether the voucher belongs to the type.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: True if the voucher belongs to the type, or False
|
||||
otherwise.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def __currency_template(self) -> str:
|
||||
"""Renders and returns the template for the currency sub-form.
|
||||
|
||||
:return: The template for the currency sub-form.
|
||||
"""
|
||||
return render_template(
|
||||
"accounting/voucher/transfer/include/form-currency-item.html",
|
||||
currency_index="CURRENCY_INDEX",
|
||||
currency_code_data=default_currency_code(),
|
||||
debit_total="-", credit_total="-")
|
||||
|
||||
|
||||
VOUCHER_TYPE_TO_OP: dict[VoucherType, VoucherOperator] \
|
||||
= {VoucherType.CASH_RECEIPT: CashReceiptVoucher(),
|
||||
VoucherType.CASH_DISBURSEMENT: CashDisbursementVoucher(),
|
||||
VoucherType.TRANSFER: TransferVoucher()}
|
||||
"""The map from the voucher types to their operators."""
|
||||
|
||||
|
||||
def get_voucher_op(voucher: Voucher, is_check_as: bool = False) \
|
||||
-> VoucherOperator:
|
||||
"""Returns the voucher operator that may be specified in the "as" query
|
||||
parameter. If it is not specified, check the voucher type from the
|
||||
voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:param is_check_as: True to check the "as" parameter, or False otherwise.
|
||||
:return: None.
|
||||
"""
|
||||
if is_check_as and "as" in request.args:
|
||||
type_dict: dict[str, VoucherType] \
|
||||
= {x.value: x for x in VoucherType}
|
||||
if request.args["as"] not in type_dict:
|
||||
abort(404)
|
||||
return VOUCHER_TYPE_TO_OP[type_dict[request.args["as"]]]
|
||||
for voucher_type in sorted(VOUCHER_TYPE_TO_OP.values(),
|
||||
key=lambda x: x.CHECK_ORDER):
|
||||
if voucher_type.is_my_type(voucher):
|
||||
return voucher_type
|
@ -25,8 +25,8 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import lazy_gettext
|
||||
from accounting.models import Account, Transaction, JournalEntry
|
||||
from accounting.transaction.forms.journal_entry import JournalEntryForm
|
||||
from accounting.models import Account, Voucher, JournalEntry
|
||||
from accounting.voucher.forms.journal_entry import JournalEntryForm
|
||||
from accounting.utils.cast import be
|
||||
from .offset_alias import offset_alias
|
||||
|
||||
@ -71,11 +71,11 @@ def get_selectable_original_entries(
|
||||
for x in db.session.execute(select_net_balances).all()}
|
||||
entries: list[JournalEntry] = JournalEntry.query\
|
||||
.filter(JournalEntry.id.in_({x for x in net_balances}))\
|
||||
.join(Transaction)\
|
||||
.order_by(Transaction.date, JournalEntry.is_debit, JournalEntry.no)\
|
||||
.join(Voucher)\
|
||||
.order_by(Voucher.date, JournalEntry.is_debit, JournalEntry.no)\
|
||||
.options(selectinload(JournalEntry.currency),
|
||||
selectinload(JournalEntry.account),
|
||||
selectinload(JournalEntry.transaction)).all()
|
||||
selectinload(JournalEntry.voucher)).all()
|
||||
for entry in entries:
|
||||
entry.net_balance = entry.amount if net_balances[entry.id] is None \
|
||||
else net_balances[entry.id]
|
221
src/accounting/voucher/views.py
Normal file
221
src/accounting/voucher/views.py
Normal file
@ -0,0 +1,221 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
||||
|
||||
# Copyright (c) 2023 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 for the voucher management.
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
from urllib.parse import parse_qsl, urlencode
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import Blueprint, render_template, session, redirect, request, \
|
||||
flash, url_for
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from accounting import db
|
||||
from accounting.locale import lazy_gettext
|
||||
from accounting.models import Voucher
|
||||
from accounting.utils.cast import s
|
||||
from accounting.utils.flash_errors import flash_form_errors
|
||||
from accounting.utils.next_uri import inherit_next, or_next
|
||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
||||
from accounting.utils.voucher_types import VoucherType
|
||||
from accounting.utils.user import get_current_user_pk
|
||||
from .forms import sort_vouchers_in, VoucherReorderForm
|
||||
from .template_filters import with_type, to_transfer, format_amount_input, \
|
||||
text2html
|
||||
from .utils.operators import VoucherOperator, VOUCHER_TYPE_TO_OP, \
|
||||
get_voucher_op
|
||||
|
||||
bp: Blueprint = Blueprint("voucher", __name__)
|
||||
"""The view blueprint for the voucher management."""
|
||||
bp.add_app_template_filter(with_type, "accounting_voucher_with_type")
|
||||
bp.add_app_template_filter(to_transfer, "accounting_voucher_to_transfer")
|
||||
bp.add_app_template_filter(format_amount_input,
|
||||
"accounting_voucher_format_amount_input")
|
||||
bp.add_app_template_filter(text2html, "accounting_voucher_text2html")
|
||||
|
||||
|
||||
@bp.get("/create/<voucherType:voucher_type>", endpoint="create")
|
||||
@has_permission(can_edit)
|
||||
def show_add_voucher_form(voucher_type: VoucherType) -> str:
|
||||
"""Shows the form to add a voucher.
|
||||
|
||||
:param voucher_type: The voucher type.
|
||||
:return: The form to add a voucher.
|
||||
"""
|
||||
voucher_op: VoucherOperator = VOUCHER_TYPE_TO_OP[voucher_type]
|
||||
form: voucher_op.form
|
||||
if "form" in session:
|
||||
form = voucher_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
|
||||
del session["form"]
|
||||
form.validate()
|
||||
else:
|
||||
form = voucher_op.form()
|
||||
form.date.data = date.today()
|
||||
return voucher_op.render_create_template(form)
|
||||
|
||||
|
||||
@bp.post("/store/<voucherType:voucher_type>", endpoint="store")
|
||||
@has_permission(can_edit)
|
||||
def add_voucher(voucher_type: VoucherType) -> redirect:
|
||||
"""Adds a voucher.
|
||||
|
||||
:param voucher_type: The voucher type.
|
||||
:return: The redirection to the voucher detail on success, or the
|
||||
voucher creation form on error.
|
||||
"""
|
||||
voucher_op: VoucherOperator = VOUCHER_TYPE_TO_OP[voucher_type]
|
||||
form: voucher_op.form = voucher_op.form(request.form)
|
||||
if not form.validate():
|
||||
flash_form_errors(form)
|
||||
session["form"] = urlencode(list(request.form.items()))
|
||||
return redirect(inherit_next(with_type(
|
||||
url_for("accounting.voucher.create", voucher_type=voucher_type))))
|
||||
voucher: Voucher = Voucher()
|
||||
form.populate_obj(voucher)
|
||||
db.session.add(voucher)
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The voucher is added successfully")), "success")
|
||||
return redirect(inherit_next(__get_detail_uri(voucher)))
|
||||
|
||||
|
||||
@bp.get("/<voucher:voucher>", endpoint="detail")
|
||||
@has_permission(can_view)
|
||||
def show_voucher_detail(voucher: Voucher) -> str:
|
||||
"""Shows the voucher detail.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The detail.
|
||||
"""
|
||||
voucher_op: VoucherOperator = get_voucher_op(voucher)
|
||||
return voucher_op.render_detail_template(voucher)
|
||||
|
||||
|
||||
@bp.get("/<voucher:voucher>/edit", endpoint="edit")
|
||||
@has_permission(can_edit)
|
||||
def show_voucher_edit_form(voucher: Voucher) -> str:
|
||||
"""Shows the form to edit a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The form to edit the voucher.
|
||||
"""
|
||||
voucher_op: VoucherOperator = get_voucher_op(voucher, is_check_as=True)
|
||||
form: voucher_op.form
|
||||
if "form" in session:
|
||||
form = voucher_op.form(ImmutableMultiDict(parse_qsl(session["form"])))
|
||||
del session["form"]
|
||||
form.obj = voucher
|
||||
form.validate()
|
||||
else:
|
||||
form = voucher_op.form(obj=voucher)
|
||||
return voucher_op.render_edit_template(voucher, form)
|
||||
|
||||
|
||||
@bp.post("/<voucher:voucher>/update", endpoint="update")
|
||||
@has_permission(can_edit)
|
||||
def update_voucher(voucher: Voucher) -> redirect:
|
||||
"""Updates a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The redirection to the voucher detail on success, or the voucher
|
||||
edit form on error.
|
||||
"""
|
||||
voucher_op: VoucherOperator = get_voucher_op(voucher, is_check_as=True)
|
||||
form: voucher_op.form = voucher_op.form(request.form)
|
||||
form.obj = voucher
|
||||
if not form.validate():
|
||||
flash_form_errors(form)
|
||||
session["form"] = urlencode(list(request.form.items()))
|
||||
return redirect(inherit_next(with_type(
|
||||
url_for("accounting.voucher.edit", voucher=voucher))))
|
||||
with db.session.no_autoflush:
|
||||
form.populate_obj(voucher)
|
||||
if not form.is_modified:
|
||||
flash(s(lazy_gettext("The voucher was not modified.")), "success")
|
||||
return redirect(inherit_next(__get_detail_uri(voucher)))
|
||||
voucher.updated_by_id = get_current_user_pk()
|
||||
voucher.updated_at = sa.func.now()
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The voucher is updated successfully.")), "success")
|
||||
return redirect(inherit_next(__get_detail_uri(voucher)))
|
||||
|
||||
|
||||
@bp.post("/<voucher:voucher>/delete", endpoint="delete")
|
||||
@has_permission(can_edit)
|
||||
def delete_voucher(voucher: Voucher) -> redirect:
|
||||
"""Deletes a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The redirection to the voucher list on success, or the voucher
|
||||
detail on error.
|
||||
"""
|
||||
voucher.delete()
|
||||
sort_vouchers_in(voucher.date, voucher.id)
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The voucher is deleted successfully.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
|
||||
|
||||
@bp.get("/dates/<date:voucher_date>", endpoint="order")
|
||||
@has_permission(can_view)
|
||||
def show_voucher_order(voucher_date: date) -> str:
|
||||
"""Shows the order of the vouchers in a same date.
|
||||
|
||||
:param voucher_date: The date.
|
||||
:return: The order of the vouchers in the date.
|
||||
"""
|
||||
vouchers: list[Voucher] = Voucher.query \
|
||||
.filter(Voucher.date == voucher_date) \
|
||||
.order_by(Voucher.no).all()
|
||||
return render_template("accounting/voucher/order.html",
|
||||
date=voucher_date, list=vouchers)
|
||||
|
||||
|
||||
@bp.post("/dates/<date:voucher_date>", endpoint="sort")
|
||||
@has_permission(can_edit)
|
||||
def sort_vouchers(voucher_date: date) -> redirect:
|
||||
"""Reorders the vouchers in a date.
|
||||
|
||||
:param voucher_date: The date.
|
||||
:return: The redirection to the incoming account or the account list. The
|
||||
reordering operation does not fail.
|
||||
"""
|
||||
form: VoucherReorderForm = VoucherReorderForm(voucher_date)
|
||||
form.save_order()
|
||||
if not form.is_modified:
|
||||
flash(s(lazy_gettext("The order was not modified.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
db.session.commit()
|
||||
flash(s(lazy_gettext("The order is updated successfully.")), "success")
|
||||
return redirect(or_next(__get_default_page_uri()))
|
||||
|
||||
|
||||
def __get_detail_uri(voucher: Voucher) -> str:
|
||||
"""Returns the detail URI of a voucher.
|
||||
|
||||
:param voucher: The voucher.
|
||||
:return: The detail URI of the voucher.
|
||||
"""
|
||||
return url_for("accounting.voucher.detail", voucher=voucher)
|
||||
|
||||
|
||||
def __get_default_page_uri() -> str:
|
||||
"""Returns the URI for the default page.
|
||||
|
||||
:return: The URI for the default page.
|
||||
"""
|
||||
return url_for("accounting.report.default")
|
@ -27,12 +27,12 @@ from flask.testing import FlaskCliRunner
|
||||
|
||||
from test_site import db
|
||||
from testlib import create_test_app, get_client
|
||||
from testlib_offset import TestData, JournalEntryData, TransactionData, \
|
||||
from testlib_offset import TestData, JournalEntryData, VoucherData, \
|
||||
CurrencyData
|
||||
from testlib_txn import Accounts, match_txn_detail
|
||||
from testlib_voucher import Accounts, match_voucher_detail
|
||||
|
||||
PREFIX: str = "/accounting/transactions"
|
||||
"""The URL prefix for the transaction management."""
|
||||
PREFIX: str = "/accounting/vouchers"
|
||||
"""The URL prefix for the voucher management."""
|
||||
|
||||
|
||||
class OffsetTestCase(unittest.TestCase):
|
||||
@ -48,7 +48,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
|
||||
runner: FlaskCliRunner = self.app.test_cli_runner()
|
||||
with self.app.app_context():
|
||||
from accounting.models import BaseAccount, Transaction, \
|
||||
from accounting.models import BaseAccount, Voucher, \
|
||||
JournalEntry
|
||||
result: Result
|
||||
result = runner.invoke(args="init-db")
|
||||
@ -62,7 +62,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
result = runner.invoke(args=["accounting-init-accounts",
|
||||
"-u", "editor"])
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
Transaction.query.delete()
|
||||
Voucher.query.delete()
|
||||
JournalEntry.query.delete()
|
||||
|
||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||
@ -73,15 +73,15 @@ class OffsetTestCase(unittest.TestCase):
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
from accounting.models import Account, Transaction
|
||||
create_uri: str = f"{PREFIX}/create/income?next=%2F_next"
|
||||
store_uri: str = f"{PREFIX}/store/income"
|
||||
from accounting.models import Account, Voucher
|
||||
create_uri: str = f"{PREFIX}/create/receipt?next=%2F_next"
|
||||
store_uri: str = f"{PREFIX}/store/receipt"
|
||||
form: dict[str, str]
|
||||
old_amount: Decimal
|
||||
response: httpx.Response
|
||||
|
||||
txn_data: TransactionData = TransactionData(
|
||||
self.data.e_r_or3d.txn.days, [CurrencyData(
|
||||
voucher_data: VoucherData = VoucherData(
|
||||
self.data.e_r_or3d.voucher.days, [CurrencyData(
|
||||
"USD",
|
||||
[],
|
||||
[JournalEntryData(Accounts.RECEIVABLE,
|
||||
@ -95,14 +95,14 @@ class OffsetTestCase(unittest.TestCase):
|
||||
original_entry=self.data.e_r_or3d)])])
|
||||
|
||||
# Non-existing original entry ID
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = "9999"
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# The same side
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = self.data.e_p_or1c.id
|
||||
form["currency-1-credit-1-account_code"] = self.data.e_p_or1c.account
|
||||
form["currency-1-credit-1-amount"] = "100"
|
||||
@ -115,8 +115,8 @@ class OffsetTestCase(unittest.TestCase):
|
||||
account = Account.find_by_code(Accounts.RECEIVABLE)
|
||||
account.is_need_offset = False
|
||||
db.session.commit()
|
||||
response = self.client.post(store_uri,
|
||||
data=txn_data.new_form(self.csrf_token))
|
||||
response = self.client.post(
|
||||
store_uri, data=voucher_data.new_form(self.csrf_token))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
with self.app.app_context():
|
||||
@ -125,7 +125,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
db.session.commit()
|
||||
|
||||
# The original entry is also an offset
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = self.data.e_p_of1d.id
|
||||
form["currency-1-credit-1-account_code"] = self.data.e_p_of1d.account
|
||||
response = self.client.post(store_uri, data=form)
|
||||
@ -133,52 +133,54 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not the same currency
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-code"] = "EUR"
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not the same account
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not exceeding net balance - partially offset
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-credit-1-amount"] \
|
||||
= str(txn_data.currencies[0].credit[0].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[0].amount
|
||||
+ Decimal("0.01"))
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not exceeding net balance - unmatched
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-credit-3-amount"] \
|
||||
= str(txn_data.currencies[0].credit[2].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[2].amount
|
||||
+ Decimal("0.01"))
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not before the original entries
|
||||
old_days = txn_data.days
|
||||
txn_data.days = old_days + 1
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
old_days = voucher_data.days
|
||||
voucher_data.days = old_days + 1
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
txn_data.days = old_days
|
||||
voucher_data.days = old_days
|
||||
|
||||
# Success
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
txn_id: int = match_txn_detail(response.headers["Location"])
|
||||
voucher_id: int = match_voucher_detail(response.headers["Location"])
|
||||
with self.app.app_context():
|
||||
txn = db.session.get(Transaction, txn_id)
|
||||
for offset in txn.currencies[0].credit:
|
||||
voucher = db.session.get(Voucher, voucher_id)
|
||||
for offset in voucher.currencies[0].credit:
|
||||
self.assertIsNotNone(offset.original_entry_id)
|
||||
|
||||
def test_edit_receivable_offset(self) -> None:
|
||||
@ -187,27 +189,27 @@ class OffsetTestCase(unittest.TestCase):
|
||||
:return: None.
|
||||
"""
|
||||
from accounting.models import Account
|
||||
txn_data: TransactionData = self.data.t_r_of2
|
||||
edit_uri: str = f"{PREFIX}/{txn_data.id}/edit?next=%2F_next"
|
||||
update_uri: str = f"{PREFIX}/{txn_data.id}/update"
|
||||
voucher_data: VoucherData = self.data.v_r_of2
|
||||
edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
|
||||
update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
|
||||
form: dict[str, str]
|
||||
response: httpx.Response
|
||||
|
||||
txn_data.days = self.data.t_r_or2.days
|
||||
txn_data.currencies[0].debit[0].amount = Decimal("600")
|
||||
txn_data.currencies[0].credit[0].amount = Decimal("600")
|
||||
txn_data.currencies[0].debit[2].amount = Decimal("600")
|
||||
txn_data.currencies[0].credit[2].amount = Decimal("600")
|
||||
voucher_data.days = self.data.v_r_or2.days
|
||||
voucher_data.currencies[0].debit[0].amount = Decimal("600")
|
||||
voucher_data.currencies[0].credit[0].amount = Decimal("600")
|
||||
voucher_data.currencies[0].debit[2].amount = Decimal("600")
|
||||
voucher_data.currencies[0].credit[2].amount = Decimal("600")
|
||||
|
||||
# Non-existing original entry ID
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = "9999"
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# The same side
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = self.data.e_p_or1c.id
|
||||
form["currency-1-credit-1-account_code"] = self.data.e_p_or1c.account
|
||||
form["currency-1-debit-1-amount"] = "100"
|
||||
@ -221,8 +223,8 @@ class OffsetTestCase(unittest.TestCase):
|
||||
account = Account.find_by_code(Accounts.RECEIVABLE)
|
||||
account.is_need_offset = False
|
||||
db.session.commit()
|
||||
response = self.client.post(update_uri,
|
||||
data=txn_data.update_form(self.csrf_token))
|
||||
response = self.client.post(
|
||||
update_uri, data=voucher_data.update_form(self.csrf_token))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
with self.app.app_context():
|
||||
@ -231,7 +233,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
db.session.commit()
|
||||
|
||||
# The original entry is also an offset
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-credit-1-original_entry_id"] = self.data.e_p_of1d.id
|
||||
form["currency-1-credit-1-account_code"] = self.data.e_p_of1d.account
|
||||
response = self.client.post(update_uri, data=form)
|
||||
@ -239,155 +241,159 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not the same currency
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-code"] = "EUR"
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not the same account
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not exceeding net balance - partially offset
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-amount"] \
|
||||
= str(txn_data.currencies[0].debit[0].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[0].amount + Decimal("0.01"))
|
||||
form["currency-1-credit-1-amount"] \
|
||||
= str(txn_data.currencies[0].credit[0].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[0].amount
|
||||
+ Decimal("0.01"))
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not exceeding net balance - unmatched
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-3-amount"] \
|
||||
= str(txn_data.currencies[0].debit[2].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[2].amount + Decimal("0.01"))
|
||||
form["currency-1-credit-3-amount"] \
|
||||
= str(txn_data.currencies[0].credit[2].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[2].amount
|
||||
+ Decimal("0.01"))
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not before the original entries
|
||||
old_days: int = txn_data.days
|
||||
txn_data.days = old_days + 1
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days + 1
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
txn_data.days = old_days
|
||||
voucher_data.days = old_days
|
||||
|
||||
# Success
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"],
|
||||
f"{PREFIX}/{txn_data.id}?next=%2F_next")
|
||||
f"{PREFIX}/{voucher_data.id}?next=%2F_next")
|
||||
|
||||
def test_edit_receivable_original_entry(self) -> None:
|
||||
"""Tests to edit the receivable original entry.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
from accounting.models import Transaction
|
||||
txn_data: TransactionData = self.data.t_r_or1
|
||||
edit_uri: str = f"{PREFIX}/{txn_data.id}/edit?next=%2F_next"
|
||||
update_uri: str = f"{PREFIX}/{txn_data.id}/update"
|
||||
from accounting.models import Voucher
|
||||
voucher_data: VoucherData = self.data.v_r_or1
|
||||
edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
|
||||
update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
|
||||
form: dict[str, str]
|
||||
response: httpx.Response
|
||||
|
||||
txn_data.days = self.data.t_r_of1.days
|
||||
txn_data.currencies[0].debit[0].amount = Decimal("800")
|
||||
txn_data.currencies[0].credit[0].amount = Decimal("800")
|
||||
txn_data.currencies[0].debit[1].amount = Decimal("3.4")
|
||||
txn_data.currencies[0].credit[1].amount = Decimal("3.4")
|
||||
voucher_data.days = self.data.v_r_of1.days
|
||||
voucher_data.currencies[0].debit[0].amount = Decimal("800")
|
||||
voucher_data.currencies[0].credit[0].amount = Decimal("800")
|
||||
voucher_data.currencies[0].debit[1].amount = Decimal("3.4")
|
||||
voucher_data.currencies[0].credit[1].amount = Decimal("3.4")
|
||||
|
||||
# Not the same currency
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-code"] = "EUR"
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not the same account
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not less than offset total - partially offset
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-amount"] \
|
||||
= str(txn_data.currencies[0].debit[0].amount - Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[0].amount - Decimal("0.01"))
|
||||
form["currency-1-credit-1-amount"] \
|
||||
= str(txn_data.currencies[0].credit[0].amount - Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[0].amount
|
||||
- Decimal("0.01"))
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not less than offset total - fully offset
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-2-amount"] \
|
||||
= str(txn_data.currencies[0].debit[1].amount - Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[1].amount - Decimal("0.01"))
|
||||
form["currency-1-credit-2-amount"] \
|
||||
= str(txn_data.currencies[0].credit[1].amount - Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[1].amount
|
||||
- Decimal("0.01"))
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not after the offset entries
|
||||
old_days: int = txn_data.days
|
||||
txn_data.days = old_days - 1
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days - 1
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
txn_data.days = old_days
|
||||
voucher_data.days = old_days
|
||||
|
||||
# Not deleting matched original entries
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
del form["currency-1-debit-1-eid"]
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Success
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"],
|
||||
f"{PREFIX}/{txn_data.id}?next=%2F_next")
|
||||
f"{PREFIX}/{voucher_data.id}?next=%2F_next")
|
||||
|
||||
# The original entry is always before the offset entry, even when they
|
||||
# happen in the same day.
|
||||
with self.app.app_context():
|
||||
txn_or: Transaction | None = db.session.get(
|
||||
Transaction, txn_data.id)
|
||||
self.assertIsNotNone(txn_or)
|
||||
txn_of: Transaction | None = db.session.get(
|
||||
Transaction, self.data.t_r_of1.id)
|
||||
self.assertIsNotNone(txn_of)
|
||||
self.assertEqual(txn_or.date, txn_of.date)
|
||||
self.assertLess(txn_or.no, txn_of.no)
|
||||
voucher_or: Voucher | None = db.session.get(
|
||||
Voucher, voucher_data.id)
|
||||
self.assertIsNotNone(voucher_or)
|
||||
voucher_of: Voucher | None = db.session.get(
|
||||
Voucher, self.data.v_r_of1.id)
|
||||
self.assertIsNotNone(voucher_of)
|
||||
self.assertEqual(voucher_or.date, voucher_of.date)
|
||||
self.assertLess(voucher_or.no, voucher_of.no)
|
||||
|
||||
def test_add_payable_offset(self) -> None:
|
||||
"""Tests to add the payable offset.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
from accounting.models import Account, Transaction
|
||||
create_uri: str = f"{PREFIX}/create/expense?next=%2F_next"
|
||||
store_uri: str = f"{PREFIX}/store/expense"
|
||||
from accounting.models import Account, Voucher
|
||||
create_uri: str = f"{PREFIX}/create/disbursement?next=%2F_next"
|
||||
store_uri: str = f"{PREFIX}/store/disbursement"
|
||||
form: dict[str, str]
|
||||
response: httpx.Response
|
||||
|
||||
txn_data: TransactionData = TransactionData(
|
||||
self.data.e_p_or3c.txn.days, [CurrencyData(
|
||||
voucher_data: VoucherData = VoucherData(
|
||||
self.data.e_p_or3c.voucher.days, [CurrencyData(
|
||||
"USD",
|
||||
[JournalEntryData(Accounts.PAYABLE,
|
||||
self.data.e_p_or1c.summary, "500",
|
||||
@ -401,14 +407,14 @@ class OffsetTestCase(unittest.TestCase):
|
||||
[])])
|
||||
|
||||
# Non-existing original entry ID
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = "9999"
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# The same side
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = self.data.e_r_or1d.id
|
||||
form["currency-1-debit-1-account_code"] = self.data.e_r_or1d.account
|
||||
form["currency-1-debit-1-amount"] = "100"
|
||||
@ -421,8 +427,8 @@ class OffsetTestCase(unittest.TestCase):
|
||||
account = Account.find_by_code(Accounts.PAYABLE)
|
||||
account.is_need_offset = False
|
||||
db.session.commit()
|
||||
response = self.client.post(store_uri,
|
||||
data=txn_data.new_form(self.csrf_token))
|
||||
response = self.client.post(
|
||||
store_uri, data=voucher_data.new_form(self.csrf_token))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
with self.app.app_context():
|
||||
@ -431,7 +437,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
db.session.commit()
|
||||
|
||||
# The original entry is also an offset
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = self.data.e_r_of1c.id
|
||||
form["currency-1-debit-1-account_code"] = self.data.e_r_of1c.account
|
||||
response = self.client.post(store_uri, data=form)
|
||||
@ -439,52 +445,52 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not the same currency
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-code"] = "EUR"
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not the same account
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not exceeding net balance - partially offset
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-debit-1-amount"] \
|
||||
= str(txn_data.currencies[0].debit[0].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[0].amount + Decimal("0.01"))
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not exceeding net balance - unmatched
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
form["currency-1-debit-3-amount"] \
|
||||
= str(txn_data.currencies[0].debit[2].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[2].amount + Decimal("0.01"))
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
|
||||
# Not before the original entries
|
||||
old_days: int = txn_data.days
|
||||
txn_data.days = old_days + 1
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days + 1
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], create_uri)
|
||||
txn_data.days = old_days
|
||||
voucher_data.days = old_days
|
||||
|
||||
# Success
|
||||
form = txn_data.new_form(self.csrf_token)
|
||||
form = voucher_data.new_form(self.csrf_token)
|
||||
response = self.client.post(store_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
txn_id: int = match_txn_detail(response.headers["Location"])
|
||||
voucher_id: int = match_voucher_detail(response.headers["Location"])
|
||||
with self.app.app_context():
|
||||
txn = db.session.get(Transaction, txn_id)
|
||||
for offset in txn.currencies[0].debit:
|
||||
voucher = db.session.get(Voucher, voucher_id)
|
||||
for offset in voucher.currencies[0].debit:
|
||||
self.assertIsNotNone(offset.original_entry_id)
|
||||
|
||||
def test_edit_payable_offset(self) -> None:
|
||||
@ -492,28 +498,28 @@ class OffsetTestCase(unittest.TestCase):
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
from accounting.models import Account, Transaction
|
||||
txn_data: TransactionData = self.data.t_p_of2
|
||||
edit_uri: str = f"{PREFIX}/{txn_data.id}/edit?next=%2F_next"
|
||||
update_uri: str = f"{PREFIX}/{txn_data.id}/update"
|
||||
from accounting.models import Account, Voucher
|
||||
voucher_data: VoucherData = self.data.v_p_of2
|
||||
edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
|
||||
update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
|
||||
form: dict[str, str]
|
||||
response: httpx.Response
|
||||
|
||||
txn_data.days = self.data.t_p_or2.days
|
||||
txn_data.currencies[0].debit[0].amount = Decimal("1100")
|
||||
txn_data.currencies[0].credit[0].amount = Decimal("1100")
|
||||
txn_data.currencies[0].debit[2].amount = Decimal("900")
|
||||
txn_data.currencies[0].credit[2].amount = Decimal("900")
|
||||
voucher_data.days = self.data.v_p_or2.days
|
||||
voucher_data.currencies[0].debit[0].amount = Decimal("1100")
|
||||
voucher_data.currencies[0].credit[0].amount = Decimal("1100")
|
||||
voucher_data.currencies[0].debit[2].amount = Decimal("900")
|
||||
voucher_data.currencies[0].credit[2].amount = Decimal("900")
|
||||
|
||||
# Non-existing original entry ID
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = "9999"
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# The same side
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = self.data.e_r_or1d.id
|
||||
form["currency-1-debit-1-account_code"] = self.data.e_r_or1d.account
|
||||
form["currency-1-debit-1-amount"] = "100"
|
||||
@ -527,8 +533,8 @@ class OffsetTestCase(unittest.TestCase):
|
||||
account = Account.find_by_code(Accounts.PAYABLE)
|
||||
account.is_need_offset = False
|
||||
db.session.commit()
|
||||
response = self.client.post(update_uri,
|
||||
data=txn_data.update_form(self.csrf_token))
|
||||
response = self.client.post(
|
||||
update_uri, data=voucher_data.update_form(self.csrf_token))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
with self.app.app_context():
|
||||
@ -537,7 +543,7 @@ class OffsetTestCase(unittest.TestCase):
|
||||
db.session.commit()
|
||||
|
||||
# The original entry is also an offset
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-original_entry_id"] = self.data.e_r_of1c.id
|
||||
form["currency-1-debit-1-account_code"] = self.data.e_r_of1c.account
|
||||
response = self.client.post(update_uri, data=form)
|
||||
@ -545,56 +551,58 @@ class OffsetTestCase(unittest.TestCase):
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not the same currency
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-code"] = "EUR"
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not the same account
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not exceeding net balance - partially offset
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-amount"] \
|
||||
= str(txn_data.currencies[0].debit[0].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[0].amount + Decimal("0.01"))
|
||||
form["currency-1-credit-1-amount"] \
|
||||
= str(txn_data.currencies[0].credit[0].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[0].amount
|
||||
+ Decimal("0.01"))
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not exceeding net balance - unmatched
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-3-amount"] \
|
||||
= str(txn_data.currencies[0].debit[2].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[2].amount + Decimal("0.01"))
|
||||
form["currency-1-credit-3-amount"] \
|
||||
= str(txn_data.currencies[0].credit[2].amount + Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[2].amount
|
||||
+ Decimal("0.01"))
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not before the original entries
|
||||
old_days: int = txn_data.days
|
||||
txn_data.days = old_days + 1
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days + 1
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
txn_data.days = old_days
|
||||
voucher_data.days = old_days
|
||||
|
||||
# Success
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
txn_id: int = match_txn_detail(response.headers["Location"])
|
||||
voucher_id: int = match_voucher_detail(response.headers["Location"])
|
||||
with self.app.app_context():
|
||||
txn = db.session.get(Transaction, txn_id)
|
||||
for offset in txn.currencies[0].debit:
|
||||
voucher = db.session.get(Voucher, voucher_id)
|
||||
for offset in voucher.currencies[0].debit:
|
||||
self.assertIsNotNone(offset.original_entry_id)
|
||||
|
||||
def test_edit_payable_original_entry(self) -> None:
|
||||
@ -602,84 +610,86 @@ class OffsetTestCase(unittest.TestCase):
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
from accounting.models import Transaction
|
||||
txn_data: TransactionData = self.data.t_p_or1
|
||||
edit_uri: str = f"{PREFIX}/{txn_data.id}/edit?next=%2F_next"
|
||||
update_uri: str = f"{PREFIX}/{txn_data.id}/update"
|
||||
from accounting.models import Voucher
|
||||
voucher_data: VoucherData = self.data.v_p_or1
|
||||
edit_uri: str = f"{PREFIX}/{voucher_data.id}/edit?next=%2F_next"
|
||||
update_uri: str = f"{PREFIX}/{voucher_data.id}/update"
|
||||
form: dict[str, str]
|
||||
response: httpx.Response
|
||||
|
||||
txn_data.days = self.data.t_p_of1.days
|
||||
txn_data.currencies[0].debit[0].amount = Decimal("1200")
|
||||
txn_data.currencies[0].credit[0].amount = Decimal("1200")
|
||||
txn_data.currencies[0].debit[1].amount = Decimal("0.9")
|
||||
txn_data.currencies[0].credit[1].amount = Decimal("0.9")
|
||||
voucher_data.days = self.data.v_p_of1.days
|
||||
voucher_data.currencies[0].debit[0].amount = Decimal("1200")
|
||||
voucher_data.currencies[0].credit[0].amount = Decimal("1200")
|
||||
voucher_data.currencies[0].debit[1].amount = Decimal("0.9")
|
||||
voucher_data.currencies[0].credit[1].amount = Decimal("0.9")
|
||||
|
||||
# Not the same currency
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-code"] = "EUR"
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not the same account
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not less than offset total - partially offset
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-1-amount"] \
|
||||
= str(txn_data.currencies[0].debit[0].amount - Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[0].amount - Decimal("0.01"))
|
||||
form["currency-1-credit-1-amount"] \
|
||||
= str(txn_data.currencies[0].credit[0].amount - Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[0].amount
|
||||
- Decimal("0.01"))
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not less than offset total - fully offset
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
form["currency-1-debit-2-amount"] \
|
||||
= str(txn_data.currencies[0].debit[1].amount - Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].debit[1].amount - Decimal("0.01"))
|
||||
form["currency-1-credit-2-amount"] \
|
||||
= str(txn_data.currencies[0].credit[1].amount - Decimal("0.01"))
|
||||
= str(voucher_data.currencies[0].credit[1].amount
|
||||
- Decimal("0.01"))
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Not after the offset entries
|
||||
old_days: int = txn_data.days
|
||||
txn_data.days = old_days - 1
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
old_days: int = voucher_data.days
|
||||
voucher_data.days = old_days - 1
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
txn_data.days = old_days
|
||||
voucher_data.days = old_days
|
||||
|
||||
# Not deleting matched original entries
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
del form["currency-1-credit-1-eid"]
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"], edit_uri)
|
||||
|
||||
# Success
|
||||
form = txn_data.update_form(self.csrf_token)
|
||||
form = voucher_data.update_form(self.csrf_token)
|
||||
response = self.client.post(update_uri, data=form)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["Location"],
|
||||
f"{PREFIX}/{txn_data.id}?next=%2F_next")
|
||||
f"{PREFIX}/{voucher_data.id}?next=%2F_next")
|
||||
|
||||
# The original entry is always before the offset entry, even when they
|
||||
# happen in the same day
|
||||
with self.app.app_context():
|
||||
txn_or: Transaction | None = db.session.get(
|
||||
Transaction, txn_data.id)
|
||||
self.assertIsNotNone(txn_or)
|
||||
txn_of: Transaction | None = db.session.get(
|
||||
Transaction, self.data.t_p_of1.id)
|
||||
self.assertIsNotNone(txn_of)
|
||||
self.assertEqual(txn_or.date, txn_of.date)
|
||||
self.assertLess(txn_or.no, txn_of.no)
|
||||
voucher_or: Voucher | None = db.session.get(
|
||||
Voucher, voucher_data.id)
|
||||
self.assertIsNotNone(voucher_or)
|
||||
voucher_of: Voucher | None = db.session.get(
|
||||
Voucher, self.data.v_p_of1.id)
|
||||
self.assertIsNotNone(voucher_of)
|
||||
self.assertEqual(voucher_or.date, voucher_of.date)
|
||||
self.assertLess(voucher_or.no, voucher_of.no)
|
||||
|
@ -25,7 +25,7 @@ from flask import Flask
|
||||
from flask.testing import FlaskCliRunner
|
||||
|
||||
from testlib import create_test_app, get_client
|
||||
from testlib_txn import Accounts, NEXT_URI, add_txn
|
||||
from testlib_voucher import Accounts, NEXT_URI, add_voucher
|
||||
|
||||
|
||||
class SummeryEditorTestCase(unittest.TestCase):
|
||||
@ -41,7 +41,7 @@ class SummeryEditorTestCase(unittest.TestCase):
|
||||
|
||||
runner: FlaskCliRunner = self.app.test_cli_runner()
|
||||
with self.app.app_context():
|
||||
from accounting.models import BaseAccount, Transaction, \
|
||||
from accounting.models import BaseAccount, Voucher, \
|
||||
JournalEntry
|
||||
result: Result
|
||||
result = runner.invoke(args="init-db")
|
||||
@ -55,7 +55,7 @@ class SummeryEditorTestCase(unittest.TestCase):
|
||||
result = runner.invoke(args=["accounting-init-accounts",
|
||||
"-u", "editor"])
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
Transaction.query.delete()
|
||||
Voucher.query.delete()
|
||||
JournalEntry.query.delete()
|
||||
|
||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
||||
@ -65,9 +65,9 @@ class SummeryEditorTestCase(unittest.TestCase):
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
from accounting.transaction.utils.summary_editor import SummaryEditor
|
||||
from accounting.voucher.utils.summary_editor import SummaryEditor
|
||||
for form in get_form_data(self.csrf_token):
|
||||
add_txn(self.client, form)
|
||||
add_voucher(self.client, form)
|
||||
with self.app.app_context():
|
||||
editor: SummaryEditor = SummaryEditor()
|
||||
|
||||
@ -159,22 +159,22 @@ class SummeryEditorTestCase(unittest.TestCase):
|
||||
|
||||
|
||||
def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
||||
"""Returns the form data for multiple transaction forms.
|
||||
"""Returns the form data for multiple voucher forms.
|
||||
|
||||
:param csrf_token: The CSRF token.
|
||||
:return: A list of the form data.
|
||||
"""
|
||||
txn_date: str = date.today().isoformat()
|
||||
voucher_date: str = date.today().isoformat()
|
||||
return [{"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn_date,
|
||||
"date": voucher_date,
|
||||
"currency-0-code": "USD",
|
||||
"currency-0-credit-0-account_code": Accounts.SERVICE,
|
||||
"currency-0-credit-0-summary": " Salary ",
|
||||
"currency-0-credit-0-amount": "2500"},
|
||||
{"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn_date,
|
||||
"date": voucher_date,
|
||||
"currency-0-code": "USD",
|
||||
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||
"currency-0-debit-0-summary": " Lunch—Fish ",
|
||||
@ -196,7 +196,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
||||
"currency-0-credit-2-amount": "4.25"},
|
||||
{"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn_date,
|
||||
"date": voucher_date,
|
||||
"currency-0-code": "USD",
|
||||
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||
"currency-0-debit-0-summary": " Lunch—Salad ",
|
||||
@ -212,7 +212,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
||||
"currency-0-credit-1-amount": "8.28"},
|
||||
{"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn_date,
|
||||
"date": voucher_date,
|
||||
"currency-0-code": "USD",
|
||||
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||
"currency-0-debit-0-summary": " Lunch—Pizza ",
|
||||
@ -228,14 +228,14 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
||||
"currency-0-credit-1-amount": "7.47"},
|
||||
{"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn_date,
|
||||
"date": voucher_date,
|
||||
"currency-0-code": "USD",
|
||||
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
||||
"currency-0-debit-0-summary": " Airplane—Lake City↔Hill Town ",
|
||||
"currency-0-debit-0-amount": "800"},
|
||||
{"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn_date,
|
||||
"date": voucher_date,
|
||||
"currency-0-code": "USD",
|
||||
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
||||
"currency-0-debit-0-summary": " Bus—323—Downtown→Museum ",
|
||||
@ -263,7 +263,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
||||
"currency-0-credit-3-amount": "4.4"},
|
||||
{"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn_date,
|
||||
"date": voucher_date,
|
||||
"currency-0-code": "USD",
|
||||
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
||||
"currency-0-debit-0-summary": " Taxi—Museum→Office ",
|
||||
@ -309,7 +309,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
||||
"currency-0-credit-6-amount": "5.5"},
|
||||
{"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn_date,
|
||||
"date": voucher_date,
|
||||
"currency-0-code": "USD",
|
||||
"currency-0-debit-0-account_code": Accounts.PETTY_CASH,
|
||||
"currency-0-debit-0-summary": " Dinner—Steak ",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -26,7 +26,7 @@ import httpx
|
||||
from flask import Flask
|
||||
|
||||
from test_site import db
|
||||
from testlib_txn import Accounts, match_txn_detail, NEXT_URI
|
||||
from testlib_voucher import Accounts, match_voucher_detail, NEXT_URI
|
||||
|
||||
|
||||
class JournalEntryData:
|
||||
@ -41,7 +41,7 @@ class JournalEntryData:
|
||||
:param amount: The amount.
|
||||
:param original_entry: The original entry.
|
||||
"""
|
||||
self.txn: TransactionData | None = None
|
||||
self.voucher: VoucherData | None = None
|
||||
self.id: int = -1
|
||||
self.no: int = -1
|
||||
self.original_entry: JournalEntryData | None = original_entry
|
||||
@ -73,11 +73,11 @@ class JournalEntryData:
|
||||
|
||||
|
||||
class CurrencyData:
|
||||
"""The transaction currency data."""
|
||||
"""The voucher currency data."""
|
||||
|
||||
def __init__(self, currency: str, debit: list[JournalEntryData],
|
||||
credit: list[JournalEntryData]):
|
||||
"""Constructs the transaction currency data.
|
||||
"""Constructs the voucher currency data.
|
||||
|
||||
:param currency: The currency code.
|
||||
:param debit: The debit journal entries.
|
||||
@ -104,14 +104,14 @@ class CurrencyData:
|
||||
return form
|
||||
|
||||
|
||||
class TransactionData:
|
||||
"""The transaction data."""
|
||||
class VoucherData:
|
||||
"""The voucher data."""
|
||||
|
||||
def __init__(self, days: int, currencies: list[CurrencyData]):
|
||||
"""Constructs a transaction.
|
||||
"""Constructs a voucher.
|
||||
|
||||
:param days: The number of days before today.
|
||||
:param currencies: The transaction currency data.
|
||||
:param currencies: The voucher currency data.
|
||||
"""
|
||||
self.id: int = -1
|
||||
self.days: int = days
|
||||
@ -119,38 +119,38 @@ class TransactionData:
|
||||
self.note: str | None = None
|
||||
for currency in self.currencies:
|
||||
for entry in currency.debit:
|
||||
entry.txn = self
|
||||
entry.voucher = self
|
||||
for entry in currency.credit:
|
||||
entry.txn = self
|
||||
entry.voucher = self
|
||||
|
||||
def new_form(self, csrf_token: str) -> dict[str, str]:
|
||||
"""Returns the transaction as a form.
|
||||
"""Returns the voucher as a creation form.
|
||||
|
||||
:param csrf_token: The CSRF token.
|
||||
:return: The transaction as a form.
|
||||
:return: The voucher as a creation form.
|
||||
"""
|
||||
return self.__form(csrf_token, is_update=False)
|
||||
|
||||
def update_form(self, csrf_token: str) -> dict[str, str]:
|
||||
"""Returns the transaction as a form.
|
||||
"""Returns the voucher as a update form.
|
||||
|
||||
:param csrf_token: The CSRF token.
|
||||
:return: The transaction as a form.
|
||||
:return: The voucher as a update form.
|
||||
"""
|
||||
return self.__form(csrf_token, is_update=True)
|
||||
|
||||
def __form(self, csrf_token: str, is_update: bool = False) \
|
||||
-> dict[str, str]:
|
||||
"""Returns the transaction as a form.
|
||||
"""Returns the voucher as a form.
|
||||
|
||||
:param csrf_token: The CSRF token.
|
||||
:param is_update: True for an update operation, or False otherwise
|
||||
:return: The transaction as a form.
|
||||
:return: The voucher as a form.
|
||||
"""
|
||||
txn_date: date = date.today() - timedelta(days=self.days)
|
||||
voucher_date: date = date.today() - timedelta(days=self.days)
|
||||
form: dict[str, str] = {"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn_date.isoformat()}
|
||||
"date": voucher_date.isoformat()}
|
||||
for i in range(len(self.currencies)):
|
||||
form.update(self.currencies[i].form(i + 1, is_update))
|
||||
if self.note is not None:
|
||||
@ -205,24 +205,24 @@ class TestData:
|
||||
self.e_p_or4d, self.e_p_or4c = couple(
|
||||
"Envelop", "0.9", Accounts.OFFICE, Accounts.PAYABLE)
|
||||
|
||||
# Original transactions
|
||||
self.t_r_or1: TransactionData = TransactionData(
|
||||
# Original vouchers
|
||||
self.v_r_or1: VoucherData = VoucherData(
|
||||
50, [CurrencyData("USD", [self.e_r_or1d, self.e_r_or4d],
|
||||
[self.e_r_or1c, self.e_r_or4c])])
|
||||
self.t_r_or2: TransactionData = TransactionData(
|
||||
self.v_r_or2: VoucherData = VoucherData(
|
||||
30, [CurrencyData("USD", [self.e_r_or2d, self.e_r_or3d],
|
||||
[self.e_r_or2c, self.e_r_or3c])])
|
||||
self.t_p_or1: TransactionData = TransactionData(
|
||||
self.v_p_or1: VoucherData = VoucherData(
|
||||
40, [CurrencyData("USD", [self.e_p_or1d, self.e_p_or4d],
|
||||
[self.e_p_or1c, self.e_p_or4c])])
|
||||
self.t_p_or2: TransactionData = TransactionData(
|
||||
self.v_p_or2: VoucherData = VoucherData(
|
||||
20, [CurrencyData("USD", [self.e_p_or2d, self.e_p_or3d],
|
||||
[self.e_p_or2c, self.e_p_or3c])])
|
||||
|
||||
self.__add_txn(self.t_r_or1)
|
||||
self.__add_txn(self.t_r_or2)
|
||||
self.__add_txn(self.t_p_or1)
|
||||
self.__add_txn(self.t_p_or2)
|
||||
self.__add_voucher(self.v_r_or1)
|
||||
self.__add_voucher(self.v_r_or2)
|
||||
self.__add_voucher(self.v_p_or1)
|
||||
self.__add_voucher(self.v_p_or2)
|
||||
|
||||
# Receivable offset entries
|
||||
self.e_r_of1d, self.e_r_of1c = couple(
|
||||
@ -258,52 +258,52 @@ class TestData:
|
||||
"Envelop", "0.9", Accounts.PAYABLE, Accounts.CASH)
|
||||
self.e_p_of5d.original_entry = self.e_p_or4c
|
||||
|
||||
# Offset transactions
|
||||
self.t_r_of1: TransactionData = TransactionData(
|
||||
# Offset vouchers
|
||||
self.v_r_of1: VoucherData = VoucherData(
|
||||
25, [CurrencyData("USD", [self.e_r_of1d], [self.e_r_of1c])])
|
||||
self.t_r_of2: TransactionData = TransactionData(
|
||||
self.v_r_of2: VoucherData = VoucherData(
|
||||
20, [CurrencyData("USD",
|
||||
[self.e_r_of2d, self.e_r_of3d, self.e_r_of4d],
|
||||
[self.e_r_of2c, self.e_r_of3c, self.e_r_of4c])])
|
||||
self.t_r_of3: TransactionData = TransactionData(
|
||||
self.v_r_of3: VoucherData = VoucherData(
|
||||
15, [CurrencyData("USD", [self.e_r_of5d], [self.e_r_of5c])])
|
||||
self.t_p_of1: TransactionData = TransactionData(
|
||||
self.v_p_of1: VoucherData = VoucherData(
|
||||
15, [CurrencyData("USD", [self.e_p_of1d], [self.e_p_of1c])])
|
||||
self.t_p_of2: TransactionData = TransactionData(
|
||||
self.v_p_of2: VoucherData = VoucherData(
|
||||
10, [CurrencyData("USD",
|
||||
[self.e_p_of2d, self.e_p_of3d, self.e_p_of4d],
|
||||
[self.e_p_of2c, self.e_p_of3c, self.e_p_of4c])])
|
||||
self.t_p_of3: TransactionData = TransactionData(
|
||||
self.v_p_of3: VoucherData = VoucherData(
|
||||
5, [CurrencyData("USD", [self.e_p_of5d], [self.e_p_of5c])])
|
||||
|
||||
self.__add_txn(self.t_r_of1)
|
||||
self.__add_txn(self.t_r_of2)
|
||||
self.__add_txn(self.t_r_of3)
|
||||
self.__add_txn(self.t_p_of1)
|
||||
self.__add_txn(self.t_p_of2)
|
||||
self.__add_txn(self.t_p_of3)
|
||||
self.__add_voucher(self.v_r_of1)
|
||||
self.__add_voucher(self.v_r_of2)
|
||||
self.__add_voucher(self.v_r_of3)
|
||||
self.__add_voucher(self.v_p_of1)
|
||||
self.__add_voucher(self.v_p_of2)
|
||||
self.__add_voucher(self.v_p_of3)
|
||||
|
||||
def __add_txn(self, txn_data: TransactionData) -> None:
|
||||
"""Adds a transaction.
|
||||
def __add_voucher(self, voucher_data: VoucherData) -> None:
|
||||
"""Adds a voucher.
|
||||
|
||||
:param txn_data: The transaction data.
|
||||
:param voucher_data: The voucher data.
|
||||
:return: None.
|
||||
"""
|
||||
from accounting.models import Transaction
|
||||
store_uri: str = "/accounting/transactions/store/transfer"
|
||||
from accounting.models import Voucher
|
||||
store_uri: str = "/accounting/vouchers/store/transfer"
|
||||
|
||||
response: httpx.Response = self.client.post(
|
||||
store_uri, data=txn_data.new_form(self.csrf_token))
|
||||
store_uri, data=voucher_data.new_form(self.csrf_token))
|
||||
assert response.status_code == 302
|
||||
txn_id: int = match_txn_detail(response.headers["Location"])
|
||||
txn_data.id = txn_id
|
||||
voucher_id: int = match_voucher_detail(response.headers["Location"])
|
||||
voucher_data.id = voucher_id
|
||||
with self.app.app_context():
|
||||
txn: Transaction | None = db.session.get(Transaction, txn_id)
|
||||
assert txn is not None
|
||||
for i in range(len(txn.currencies)):
|
||||
for j in range(len(txn.currencies[i].debit)):
|
||||
txn_data.currencies[i].debit[j].id \
|
||||
= txn.currencies[i].debit[j].id
|
||||
for j in range(len(txn.currencies[i].credit)):
|
||||
txn_data.currencies[i].credit[j].id \
|
||||
= txn.currencies[i].credit[j].id
|
||||
voucher: Voucher | None = db.session.get(Voucher, voucher_id)
|
||||
assert voucher is not None
|
||||
for i in range(len(voucher.currencies)):
|
||||
for j in range(len(voucher.currencies[i].debit)):
|
||||
voucher_data.currencies[i].debit[j].id \
|
||||
= voucher.currencies[i].debit[j].id
|
||||
for j in range(len(voucher.currencies[i].credit)):
|
||||
voucher_data.currencies[i].credit[j].id \
|
||||
= voucher.currencies[i].credit[j].id
|
||||
|
@ -14,7 +14,7 @@
|
||||
# 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 common test libraries for the transaction test cases.
|
||||
"""The common test libraries for the voucher test cases.
|
||||
|
||||
"""
|
||||
import re
|
||||
@ -57,10 +57,10 @@ class Accounts:
|
||||
|
||||
|
||||
def get_add_form(csrf_token: str) -> dict[str, str]:
|
||||
"""Returns the form data to add a new transaction.
|
||||
"""Returns the form data to add a new voucher.
|
||||
|
||||
:param csrf_token: The CSRF token.
|
||||
:return: The form data to add a new transaction.
|
||||
:return: The form data to add a new voucher.
|
||||
"""
|
||||
return {"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
@ -124,28 +124,28 @@ def get_add_form(csrf_token: str) -> dict[str, str]:
|
||||
"note": f"\n \n\n \n{NON_EMPTY_NOTE} \n \n\n "}
|
||||
|
||||
|
||||
def get_unchanged_update_form(txn_id: int, app: Flask, csrf_token: str) \
|
||||
def get_unchanged_update_form(voucher_id: int, app: Flask, csrf_token: str) \
|
||||
-> dict[str, str]:
|
||||
"""Returns the form data to update a transaction, where the data are not
|
||||
"""Returns the form data to update a voucher, where the data are not
|
||||
changed.
|
||||
|
||||
:param txn_id: The transaction ID.
|
||||
:param voucher_id: The voucher ID.
|
||||
:param app: The Flask application.
|
||||
:param csrf_token: The CSRF token.
|
||||
:return: The form data to update the transaction, where the data are not
|
||||
:return: The form data to update the voucher, where the data are not
|
||||
changed.
|
||||
"""
|
||||
from accounting.models import Transaction, TransactionCurrency
|
||||
from accounting.models import Voucher, VoucherCurrency
|
||||
with app.app_context():
|
||||
txn: Transaction | None = db.session.get(Transaction, txn_id)
|
||||
assert txn is not None
|
||||
currencies: list[TransactionCurrency] = txn.currencies
|
||||
voucher: Voucher | None = db.session.get(Voucher, voucher_id)
|
||||
assert voucher is not None
|
||||
currencies: list[VoucherCurrency] = voucher.currencies
|
||||
|
||||
form: dict[str, str] = {"csrf_token": csrf_token,
|
||||
"next": NEXT_URI,
|
||||
"date": txn.date,
|
||||
"note": " \n \n\n " if txn.note is None
|
||||
else f"\n \n\n \n \n{txn.note} \n\n "}
|
||||
"date": voucher.date,
|
||||
"note": " \n \n\n " if voucher.note is None
|
||||
else f"\n \n\n \n \n{voucher.note} \n\n "}
|
||||
currency_indices_used: set[int] = set()
|
||||
currency_no: int = 0
|
||||
for currency in currencies:
|
||||
@ -200,21 +200,21 @@ def __get_new_index(indices_used: set[int]) -> int:
|
||||
return index
|
||||
|
||||
|
||||
def get_update_form(txn_id: int, app: Flask,
|
||||
def get_update_form(voucher_id: int, app: Flask,
|
||||
csrf_token: str, is_debit: bool | None) -> dict[str, str]:
|
||||
"""Returns the form data to update a transaction, where the data are
|
||||
"""Returns the form data to update a voucher, where the data are
|
||||
changed.
|
||||
|
||||
:param txn_id: The transaction ID.
|
||||
:param voucher_id: The voucher ID.
|
||||
:param app: The Flask application.
|
||||
:param csrf_token: The CSRF token.
|
||||
:param is_debit: True for a cash expense transaction, False for a cash
|
||||
income transaction, or None for a transfer transaction
|
||||
:return: The form data to update the transaction, where the data are
|
||||
:param is_debit: True for a cash disbursement voucher, False for a cash
|
||||
receipt voucher, or None for a transfer voucher
|
||||
:return: The form data to update the voucher, where the data are
|
||||
changed.
|
||||
"""
|
||||
form: dict[str, str] = get_unchanged_update_form(
|
||||
txn_id, app, csrf_token)
|
||||
voucher_id, app, csrf_token)
|
||||
|
||||
# Mess up the entries in a currency
|
||||
currency_prefix: str = __get_currency_prefix(form, "USD")
|
||||
@ -240,7 +240,7 @@ def __mess_up_debit(form: dict[str, str], currency_prefix: str) \
|
||||
key: str
|
||||
m: re.Match
|
||||
|
||||
# Remove the office expense
|
||||
# Remove the office disbursement
|
||||
key = [x for x in form
|
||||
if x.startswith(currency_prefix)
|
||||
and form[x] == Accounts.OFFICE][0]
|
||||
@ -249,7 +249,7 @@ def __mess_up_debit(form: dict[str, str], currency_prefix: str) \
|
||||
entry_prefix: str = m.group(1)
|
||||
amount: Decimal = Decimal(form[f"{entry_prefix}amount"])
|
||||
form = {x: form[x] for x in form if not x.startswith(entry_prefix)}
|
||||
# Add a new travel expense
|
||||
# Add a new travel disbursement
|
||||
indices: set[int] = set()
|
||||
for key in form:
|
||||
m = re.match(r"^.+-(\d+)-amount$", key)
|
||||
@ -279,7 +279,7 @@ def __mess_up_credit(form: dict[str, str], currency_prefix: str) \
|
||||
key: str
|
||||
m: re.Match
|
||||
|
||||
# Remove the sales income
|
||||
# Remove the sales receipt
|
||||
key = [x for x in form
|
||||
if x.startswith(currency_prefix)
|
||||
and form[x] == Accounts.SALES][0]
|
||||
@ -288,7 +288,7 @@ def __mess_up_credit(form: dict[str, str], currency_prefix: str) \
|
||||
entry_prefix: str = m.group(1)
|
||||
amount: Decimal = Decimal(form[f"{entry_prefix}amount"])
|
||||
form = {x: form[x] for x in form if not x.startswith(entry_prefix)}
|
||||
# Add a new agency income
|
||||
# Add a new agency receipt
|
||||
indices: set[int] = set()
|
||||
for key in form:
|
||||
m = re.match(r"^.+-(\d+)-amount$", key)
|
||||
@ -389,35 +389,34 @@ def __get_currency_prefix(form: dict[str, str], code: str) -> str:
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def add_txn(client: httpx.Client, form: dict[str, str]) -> int:
|
||||
"""Adds a transfer transaction.
|
||||
def add_voucher(client: httpx.Client, form: dict[str, str]) -> int:
|
||||
"""Adds a transfer voucher.
|
||||
|
||||
:param client: The client.
|
||||
:param form: The form data.
|
||||
:return: The newly-added transaction ID.
|
||||
:return: The newly-added voucher ID.
|
||||
"""
|
||||
prefix: str = "/accounting/transactions"
|
||||
txn_type: str = "transfer"
|
||||
prefix: str = "/accounting/vouchers"
|
||||
voucher_type: str = "transfer"
|
||||
if len({x for x in form if "-debit-" in x}) == 0:
|
||||
txn_type = "income"
|
||||
voucher_type = "receipt"
|
||||
elif len({x for x in form if "-credit-" in x}) == 0:
|
||||
txn_type = "expense"
|
||||
store_uri = f"{prefix}/store/{txn_type}"
|
||||
voucher_type = "disbursement"
|
||||
store_uri = f"{prefix}/store/{voucher_type}"
|
||||
response: httpx.Response = client.post(store_uri, data=form)
|
||||
assert response.status_code == 302
|
||||
return match_txn_detail(response.headers["Location"])
|
||||
return match_voucher_detail(response.headers["Location"])
|
||||
|
||||
|
||||
def match_txn_detail(location: str) -> int:
|
||||
"""Validates if the redirect location is the transaction detail, and
|
||||
returns the transaction ID on success.
|
||||
def match_voucher_detail(location: str) -> int:
|
||||
"""Validates if the redirect location is the voucher detail, and
|
||||
returns the voucher ID on success.
|
||||
|
||||
:param location: The redirect location.
|
||||
:return: The transaction ID.
|
||||
:raise AssertionError: When the location is not the transaction detail.
|
||||
:return: The voucher ID.
|
||||
:raise AssertionError: When the location is not the voucher detail.
|
||||
"""
|
||||
m: re.Match = re.match(
|
||||
r"^/accounting/transactions/(\d+)\?next=%2F_next",
|
||||
m: re.Match = re.match(r"^/accounting/vouchers/(\d+)\?next=%2F_next",
|
||||
location)
|
||||
assert m is not None
|
||||
return int(m.group(1))
|
Loading…
Reference in New Issue
Block a user