Source code for Acquire.Accounting._ledger
import uuid as _uuid
from copy import copy as _copy
__all__ = ["Ledger"]
[docs]class Ledger:
"""This is a static class which manages the global ledger for the
entire accounting service
"""
[docs] @staticmethod
def get_key(uid):
"""Return the object store key for the transactionrecord with
UID=uid
"""
return "accounting/transactions/%s" % (str(uid))
[docs] @staticmethod
def load_transaction(uid, bucket=None):
"""Load the transactionrecord with UID=uid from the ledger"""
if bucket is None:
from Acquire.Service import get_service_account_bucket \
as _get_service_account_bucket
bucket = _get_service_account_bucket()
from Acquire.Accounting import TransactionRecord as _TransactionRecord
from Acquire.ObjectStore import ObjectStore as _ObjectStore
data = _ObjectStore.get_object_from_json(bucket, Ledger.get_key(uid))
if data is None:
from Acquire.Accounting import LedgerError
raise LedgerError("There is no transaction recorded in the "
"ledger with UID=%s (at key %s)" %
(uid, Ledger.get_key(uid)))
return _TransactionRecord.from_data(data)
[docs] @staticmethod
def save_transaction(record, bucket=None):
"""Save the passed transactionrecord to the object store"""
from Acquire.Accounting import TransactionRecord as _TransactionRecord
if not isinstance(record, _TransactionRecord):
raise TypeError("You can only write TransactionRecord objects "
"to the ledger!")
if not record.is_null():
if bucket is None:
from Acquire.Service import get_service_account_bucket \
as _get_service_account_bucket
bucket = _get_service_account_bucket()
from Acquire.ObjectStore import ObjectStore as _ObjectStore
_ObjectStore.set_object_from_json(bucket,
Ledger.get_key(record.uid()),
record.to_data())
[docs] @staticmethod
def refund(refund, bucket=None):
"""Create and record a new transaction from the passed refund. This
applies the refund, thereby transferring value from the credit
account to the debit account of the corresponding transaction.
Note that you can only refund a transaction once!
This returns the (already recorded) TransactionRecord for the
refund
"""
from Acquire.Accounting import Refund as _Refund
from Acquire.Accounting import Account as _Account
from Acquire.Accounting import DebitNote as _DebitNote
from Acquire.Accounting import CreditNote as _CreditNote
from Acquire.Accounting import PairedNote as _PairedNote
from Acquire.Accounting import TransactionRecord as _TransactionRecord
if not isinstance(refund, _Refund):
raise TypeError("The Refund must be of type Refund")
if refund.is_null():
return _TransactionRecord()
if bucket is None:
from Acquire.Service import get_service_account_bucket \
as _get_service_account_bucket
bucket = _get_service_account_bucket()
# return value from the credit to debit accounts
debit_account = _Account(uid=refund.debit_account_uid(),
bucket=bucket)
credit_account = _Account(uid=refund.credit_account_uid(),
bucket=bucket)
# remember that a refund debits from the original credit account...
# (and can only refund completed (DIRECT) transactions)
debit_note = _DebitNote(refund=refund, account=credit_account,
bucket=bucket)
# now create the credit note to return the value into the debit account
try:
credit_note = _CreditNote(debit_note=debit_note,
refund=refund,
account=debit_account,
bucket=bucket)
except Exception as e:
# delete the debit note
try:
debit_account._delete_note(debit_note, bucket=bucket)
except:
pass
# reset the transaction to its original state
try:
_TransactionRecord.load_test_and_set(
refund.transaction_uid(),
_TransactionState.REFUNDING,
_TransactionState.DIRECT,
bucket=bucket)
except:
pass
raise e
try:
paired_notes = _PairedNote.create(debit_note, credit_note)
except Exception as e:
# delete all records...!
try:
debit_account._delete_note(debit_note, bucket=bucket)
except:
pass
try:
credit_account._delete_note(credit_note, bucket=bucket)
except:
pass
# reset the transaction to the pending state
try:
_TransactionRecord.load_test_and_set(
refund.transaction_uid(),
_TransactionState.REFUNDING,
_TransactionState.DIRECT,
bucket=bucket)
except:
pass
raise e
# now record the two entries to the ledger. The below function
# is guaranteed not to raise an exception
return Ledger._record_to_ledger(paired_notes, refund=refund,
bucket=bucket)
[docs] @staticmethod
def receipt(receipt, bucket=None):
"""Create and record a new transaction from the passed receipt. This
applies the receipt, thereby actually transferring value from the
debit account to the credit account of the corresponding
transaction. Note that you can only receipt a transaction once!
This returns the (already recorded) TransactionRecord for the
receipt
"""
from Acquire.Accounting import Receipt as _Receipt
from Acquire.Accounting import Account as _Account
from Acquire.Accounting import DebitNote as _DebitNote
from Acquire.Accounting import CreditNote as _CreditNote
from Acquire.Accounting import TransactionRecord as _TransactionRecord
from Acquire.Accounting import PairedNote as _PairedNote
if not isinstance(receipt, _Receipt):
raise TypeError("The Receipt must be of type Receipt")
if receipt.is_null():
return _TransactionRecord()
if bucket is None:
from Acquire.Service import get_service_account_bucket \
as _get_service_account_bucket
bucket = _get_service_account_bucket()
# extract value into the debit note
debit_account = _Account(uid=receipt.debit_account_uid(),
bucket=bucket)
credit_account = _Account(uid=receipt.credit_account_uid(),
bucket=bucket)
debit_note = _DebitNote(receipt=receipt, account=debit_account,
bucket=bucket)
# now create the credit note to put the value into the credit account
try:
credit_note = _CreditNote(debit_note=debit_note,
receipt=receipt,
account=credit_account,
bucket=bucket)
except Exception as e:
# delete the debit note
try:
debit_account._delete_note(debit_note, bucket=bucket)
except:
pass
# reset the transaction to the pending state
try:
_TransactionRecord.load_test_and_set(
receipt.transaction_uid(),
_TransactionState.RECEIPTING,
_TransactionState.PENDING,
bucket=bucket)
except:
pass
raise e
try:
paired_notes = _PairedNote.create(debit_note, credit_note)
except Exception as e:
# delete all records...!
try:
debit_account._delete_note(debit_note, bucket=bucket)
except:
pass
try:
credit_account._delete_note(credit_note, bucket=bucket)
except:
pass
# reset the transaction to the pending state
try:
_TransactionRecord.load_test_and_set(
receipt.transaction_uid(),
_TransactionState.RECEIPTING,
_TransactionState.PENDING,
bucket=bucket)
except:
pass
raise e
# now record the two entries to the ledger. The below function
# is guaranteed not to raise an exception
return Ledger._record_to_ledger(paired_notes, receipt=receipt,
bucket=bucket)
[docs] @staticmethod
def perform(transactions, debit_account, credit_account, authorisation,
is_provisional=False, receipt_by=None, bucket=None):
"""Perform the passed transaction(s) between 'debit_account' and
'credit_account', recording the 'authorisation' for this
transaction. If 'is_provisional' then record this as a provisional
transaction (liability for the debit_account, future unspendable
income for the 'credit_account'). Payment won't actually be taken
until the transaction is 'receipted' (which may be for less than
(but not more than) then provisional value, and which must take
place before 'receipt_by' (which will default to one week in
the future if not supplied - the actual time is encoded
in the returned TransactionRecords). Returns the (already
recorded) TransactionRecord.
Note that if several transactions are passed, then they must all
succeed. If one of them fails then they are immediately refunded.
"""
from Acquire.Accounting import Account as _Account
from Acquire.Identity import Authorisation as _Authorisation
from Acquire.Accounting import DebitNote as _DebitNote
from Acquire.Accounting import CreditNote as _CreditNote
from Acquire.Accounting import Transaction as _Transaction
from Acquire.Accounting import PairedNote as _PairedNote
if not isinstance(debit_account, _Account):
raise TypeError("The Debit Account must be of type Account")
if not isinstance(credit_account, _Account):
raise TypeError("The Credit Account must be of type Account")
if not isinstance(authorisation, _Authorisation):
raise TypeError("The Authorisation must be of type Authorisation")
if is_provisional:
is_provisional = True
else:
is_provisional = False
try:
transactions[0]
except:
transactions = [transactions]
# remove any zero transactions, as they are not worth recording
t = []
for transaction in transactions:
if not isinstance(transaction, _Transaction):
raise TypeError("The Transaction must be of type Transaction")
if transaction.value() >= 0:
t.append(transaction)
transactions = t
if bucket is None:
from Acquire.Service import get_service_account_bucket \
as _get_service_account_bucket
bucket = _get_service_account_bucket()
# first, try to debit all of the transactions. If any fail (e.g.
# because there is insufficient balance) then they are all
# immediately refunded
debit_notes = []
try:
for transaction in transactions:
debit_notes.append(_DebitNote(transaction, debit_account,
authorisation, is_provisional,
receipt_by, bucket=bucket))
# ensure the receipt_by date for all notes is the same
if is_provisional and (receipt_by is None):
receipt_by = debit_notes[0].receipt_by()
except Exception as e:
# refund all of the completed debits
credit_notes = []
debit_error = str(e)
try:
for debit_note in debit_notes:
debit_account._delete_note(debit_note, bucket=bucket)
except Exception as e:
from Acquire.Accounting import UnbalancedLedgerError
raise UnbalancedLedgerError(
"We have an unbalanced ledger as it was not "
"possible to refund a multi-part refused credit (%s): "
"Credit refusal error = %s. Refund error = %s" %
(str(debit_note), str(debit_error), str(e)))
# raise the original error to show that, e.g. there was
# insufficient balance
raise e
# now create the credit note(s) for this transaction. This will credit
# the account, thereby transferring value from the debit_note(s) to
# that account. If this fails then the debit_note(s) needs to
# be refunded
credit_notes = {}
has_error = False
credit_error = Exception()
for debit_note in debit_notes:
try:
credit_note = _CreditNote(debit_note, credit_account,
bucket=bucket)
credit_notes[debit_note.uid()] = credit_note
except Exception as e:
has_error = True
credit_error = e
break
if has_error:
# something went wrong crediting the account... We need to refund
# the transaction - first retract the credit notes...
try:
for credit_note in credit_notes.values():
credit_account._delete_note(credit_note, bucket=bucket)
except Exception as e:
from Acquire.Accounting import UnbalancedLedgerError
raise UnbalancedLedgerError(
"We have an unbalanced ledger as it was not "
"possible to credit a multi-part debit (%s): Credit "
"refusal error = %s. Refund error = %s" %
(debit_notes, str(credit_error), str(e)))
# now refund all of the debit notes
try:
for debit_note in debit_notes:
debit_account._delete_note(debit_note, bucket=bucket)
except Exception as e:
from Acquire.Accounting import UnbalancedLedgerError
raise UnbalancedLedgerError(
"We have an unbalanced ledger as it was not "
"possible to credit a multi-part debit (%s): Credit "
"refusal error = %s. Refund error = %s" %
(debit_notes, str(credit_error), str(e)))
raise credit_error
try:
paired_notes = _PairedNote.create(debit_notes, credit_notes)
except Exception as e:
# delete all of the notes...
for debit_note in debit_notes:
try:
debit_account._delete_note(debit_note, bucket=bucket)
except:
pass
for credit_note in credit_notes:
try:
credit_account._delete_note(credit_note, bucket=bucket)
except:
pass
raise e
# now write the paired entries to the ledger. The below function
# is guaranteed not to raise an exception
return Ledger._record_to_ledger(paired_notes, is_provisional,
bucket=bucket)
@staticmethod
def _record_to_ledger(paired_notes, is_provisional=False,
receipt=None, refund=None, bucket=None):
"""Internal function used to generate and record transaction records
from the passed paired debit- and credit-note(s). This will write
the transaction record(s) to the object store, and will also return
the record(s).
"""
from Acquire.Accounting import Receipt as _Receipt
from Acquire.Accounting import Refund as _Refund
from Acquire.Accounting import TransactionRecord as _TransactionRecord
from Acquire.Accounting import TransactionState as _TransactionState
if receipt is not None:
if not isinstance(receipt, _Receipt):
raise TypeError("Receipts must be of type 'Receipt'")
if refund is not None:
if not isinstance(refund, _Refund):
raise TypeError("Refunds must be of type 'Refund'")
try:
records = []
if bucket is None:
from Acquire.Service import get_service_account_bucket \
as _get_service_account_bucket
bucket = _get_service_account_bucket()
for paired_note in paired_notes:
record = _TransactionRecord()
record._debit_note = paired_note.debit_note()
record._credit_note = paired_note.credit_note()
if is_provisional:
record._transaction_state = _TransactionState.PROVISIONAL
else:
record._transaction_state = _TransactionState.DIRECT
if receipt is not None:
record._receipt = receipt
if refund is not None:
record._refund = refund
Ledger.save_transaction(record, bucket)
records.append(record)
return records
except:
# an error occuring here will break the system, which will
# require manual cleaning. Mark this as broken!
try:
Ledger._set_truly_broken(paired_notes, bucket)
except:
pass
raise SystemError("The ledger is in a very broken state!")
@staticmethod
def _set_truly_broken(paired_notes, bucket):
"""Internal function called when an irrecoverable error state
is detected. This records the notes that caused the error and
places the affected accounts into an error state
"""
raise NotImplementedError("_set_truly_broken needs to be implemented!")