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!")