def _refresh(self, force_update=False): """Refresh the current status of this account. This fetches the latest data, e.g. balance, limits etc. Note that this limits you to refreshing at most once every five seconds... Args: force_update (bool, default=False): Force the refresh Returns: None """ if self.is_null(): from Acquire.Accounting import create_decimal as _create_decimal from Acquire.Accounting import Balance as _Balance self._overdraft_limit = _create_decimal(0) self._balance = _Balance() return if force_update: should_refresh = True else: should_refresh = False if self._last_update is None: should_refresh = True else: should_refresh = (_datetime.datetime.now() - self._last_update).seconds > 5 if not should_refresh: return if not self.is_logged_in(): raise PermissionError( "You cannot get information about this account " "until after the owner has successfully authenticated.") from Acquire.Client import Authorisation as _Authorisation from Acquire.Accounting import create_decimal as _create_decimal service = self.accounting_service() auth = _Authorisation(resource="get_info %s" % self._account_uid, user=self._user) args = { "authorisation": auth.to_data(), "account_name": self.name(), "account_uid": self.uid() } result = service.call_function(function="get_info", args=args) from Acquire.Accounting import Balance as _Balance self._balance = _Balance.from_data(result["balance"]) self._overdraft_limit = _create_decimal(result["overdraft_limit"]) self._description = result["description"] self._last_update = _datetime.datetime.now()
def split(value, description): """Split the passed transaction described by 'description' with value 'value' into a list of transactions that fit the maximum transaction limit. Args: value (float): Value to split between transactions description (str): Description of transactions Returns: list: List of Transactions """ value = _create_decimal(value) if value < Transaction.maximum_transaction_value(): t = Transaction(value, description) return [t] else: orig_value = value values = [] while value > Transaction.maximum_transaction_value(): values.append(Transaction.maximum_transaction_value()) value -= Transaction.maximum_transaction_value() if value > 0: values.append(value) total = 0 for value in values: total += value if total != orig_value: values[-1] -= (total - orig_value) transactions = [] for i in range(0, len(values)): transactions.append( Transaction( values[i], "%s: %d of %d" % (description, i + 1, len(values)))) values = None total = 0 for transaction in transactions: total += transaction.value() # ensure that the total is also rounded to 6 dp total = _create_decimal(total) if total != orig_value: from Acquire.Accounting import TransactionError raise TransactionError( "Error as split sum (%s) is not equal to the original " "value (%s)" % (total, orig_value)) return transactions
def _refresh(self, force_update=False): """Refresh the current status of this account. This fetches the latest data, e.g. balance, limits etc. Note that this limits you to refreshing at most once every five seconds... """ if self.is_null(): self._overdraft_limit = _create_decimal(0) self._balance = _create_decimal(0) self._liability = _create_decimal(0) self._receivable = _create_decimal(0) self._spent_today = _create_decimal(0) return if force_update: should_refresh = True else: should_refresh = False if self._last_update is None: should_refresh = True else: should_refresh = (_datetime.datetime.now() - self._last_update).seconds > 5 if not should_refresh: return if not self.is_logged_in(): raise PermissionError( "You cannot get information about this account " "until after the owner has successfully authenticated.") auth = _Authorisation(resource=self._account_uid, user=self._user) args = {"authorisation": auth.to_data(), "account_name": self.name()} privkey = _PrivateKey() result = _call_function( self._accounting_service.service_url(), "get_info", args=args, args_key=self._accounting_service.public_key(), response_key=privkey, public_cert=self._accounting_service.public_certificate()) self._overdraft_limit = _create_decimal(result["overdraft_limit"]) self._balance = _create_decimal(result["balance"]) self._liability = _create_decimal(result["liability"]) self._receivable = _create_decimal(result["receivable"]) self._spent_today = _create_decimal(result["spent_today"]) self._description = result["description"] self._last_update = _datetime.datetime.now()
def __init__(self, key=None): """Construct, optionally from the passed key""" if key is not None: t = TransactionInfo.from_key(key) import copy as _copy self.__dict__ = _copy.copy(t.__dict__) else: from Acquire.Accounting import create_decimal as _create_decimal self._value = _create_decimal(0) self._receipted_value = _create_decimal(0) self._code = None self._datetime = None self._uid = None
def receipt(self, credit_note, receipted_value=None): """Receipt the passed credit note that contains a request to transfer value from another account to the passed account """ if not self.is_logged_in(): raise PermissionError("You cannot receipt a credit note as the " "user has not yet logged in!") if credit_note.account_uid() != self.uid(): raise ValueError( "You cannot receipt a transaction from a different " "account! %s versus %s" % (credit_note.account_uid(), self.uid())) auth = _Authorisation(resource=self._account_uid, user=self._user) args = {"credit_note": credit_note.to_data(), "authorisation": auth.to_data()} if receipted_value is not None: args["receipted_value"] = str(_create_decimal(receipted_value)) privkey = _PrivateKey() result = _call_function( self._accounting_service.service_url(), "receipt", args=args, args_key=self._accounting_service.public_key(), response_key=privkey, public_cert=self._accounting_service.public_certificate()) return result["transaction_record"]
def from_data(data): """Construct and return a new CreditNote from the passed json-decoded dictionary Args: data (dict): JSON serialised dictionary of object Returns: CreditNote: CreditNote created from JSON data """ note = CreditNote() if (data and len(data) > 0): from Acquire.ObjectStore import string_to_datetime \ as _string_to_datetime from Acquire.Accounting import create_decimal as _create_decimal note._account_uid = data["account_uid"] note._debit_account_uid = data["debit_account_uid"] note._uid = data["uid"] note._debit_note_uid = data["debit_note_uid"] note._datetime = _string_to_datetime(data["datetime"]) note._value = _create_decimal(data["value"]) note._is_provisional = data["is_provisional"] if note._is_provisional: note._receipt_by = _string_to_datetime(data["receipt_by"]) return note
def deposit(user, value, description=None, accounting_service=None, accounting_url=None): """Tell the system to allow the user to deposit 'value' from their (real) financial account to the system accounts """ authorisation = _Authorisation(user=user) if accounting_service is None: accounting_service = _get_accounting_service(accounting_url) else: if not accounting_service.is_accounting_service(): raise TypeError("You can only deposit funds using an " "accounting service!") args = {"authorisation": authorisation.to_data()} if description is None: args["value"] = str(_create_decimal(value)) else: args["transaction"] = _Transaction(value, description).to_data() privkey = _PrivateKey() result = _call_function( accounting_service.service_url(), "deposit", args=args, args_key=accounting_service.public_key(), response_key=privkey, public_cert=accounting_service.public_certificate()) return result
def __init__(self, credit_note=None, authorisation=None, receipted_value=None): """Create a receipt for the transaction that resulted in the passed credit note. Specify the authorisation of the receipt, and optionally specify the actual receipted value (which may be less than the value in the passed credit note) """ if credit_note is None: self._credit_note = None self._authorisation = None from Acquire.Accounting import create_decimal as _create_decimal self._receipted_value = _create_decimal(0) return from Acquire.Accounting import CreditNote as _CreditNote from Acquire.Identity import Authorisation as _Authorisation if not isinstance(credit_note, _CreditNote): raise TypeError("The credit note must be of type CreditNote") if not isinstance(authorisation, _Authorisation): raise TypeError("The authorisation must be of type Authorisation") if not credit_note.is_provisional(): raise ValueError("You cannot receipt a transaction that was " "not provisional! - %s" % str(credit_note)) if receipted_value is not None: from Acquire.Accounting import create_decimal as _create_decimal receipted_value = _create_decimal(receipted_value) if receipted_value < 0: raise ValueError("You cannot receipt a value that is less " "than zero! %s" % receipted_value) elif receipted_value > credit_note.value(): raise ValueError("You cannot receipt a value that is greater " "than the value of the original credit " "note - %s versus %s" % (receipted_value, credit_note.value())) else: receipted_value = credit_note.value() self._credit_note = credit_note self._authorisation = authorisation self._receipted_value = receipted_value
def maximum_transaction_value(): """Return the maximum value for a single transaction. Currently this is 999999.999999 so that a transaction fits into a f013.6 string Returns: Decimal: Maximum transaction value """ return _create_decimal(999999.999999)
def available(self, overdraft_limit=None): """Return the available balance (balance - liability)""" if overdraft_limit is None: return self.balance() - self.liability() else: from Acquire.Accounting import create_decimal as _create_decimal overdraft_limit = _create_decimal(overdraft_limit) return self.balance() - self.liability() + overdraft_limit
def total(balances): """Return the sum of the passed balances""" from Acquire.Accounting import create_decimal as _create_decimal balance = _create_decimal(0) liability = _create_decimal(0) receivable = _create_decimal(0) for b in balances: if type(b) is not Balance: raise TypeError("You can only sum Balance objects!") balance += b._balance liability += b._liability receivable += b._receivable return Balance(balance=balance, liability=liability, receivable=receivable, _is_safe=True)
def value(self): """Return the value of the refund Decimal: Value of this refund """ if self.is_null(): from Acquire.Accounting import create_decimal as _create_decimal return _create_decimal(0) else: return self._credit_note.value()
def round(value): """Round the passed floating point value to the precision level of the transaction (currently 6 decimal places) Args: value (float): Value to convert to Decimal Returns: Decimal: Rounded value """ return _create_decimal(value)
def string_to_decimal(s, default=0): """Return the decimal that had been encoded via 'decimal_to_string'. This string must have been created via 'decimal_to_string' Args: s (str): String to convert to Decimal Returns: Decimal: Decimal version of string """ from Acquire.Accounting import create_decimal as _create_decimal return _create_decimal(s, default=default)
def value(self): """Return the original (provisional) value of the transaction Returns: Decimal: Value of this receipt """ if self.is_null(): from Acquire.Accounting import create_decimal as _create_decimal return _create_decimal(0) else: return self._credit_note.value()
def receipted_value(self): """Return the receipted value. This is guaranteed to be less than or equal to the provisional value in the attached CreditNote Returns: Decimal: Receipted value """ if self.is_null(): from Acquire.Accounting import create_decimal as _create_decimal return _create_decimal(0) else: return self._receipted_value
def __add__(self, other): """Add balances together""" if type(other) is Balance: return Balance(balance=self._balance + other._balance, liability=self._liability + other._liability, receivable=self._receivable + other._receivable, _is_safe=True) from Acquire.Accounting import TransactionInfo as _TransactionInfo if type(other) is _TransactionInfo: balance = self._balance liability = self._liability receivable = self._receivable if other.is_credit(): balance += other.value() elif other.is_debit(): balance -= other.value() elif other.is_liability(): liability += other.value() elif other.is_accounts_receivable(): receivable += other.value() elif other.is_received_receipt(): balance -= other.receipted_value() liability -= other.original_value() elif other.is_sent_receipt(): balance += other.receipted_value() receivable -= other.original_value() elif other.is_received_refund(): balance += other.value() elif other.is_sent_refund(): balance -= other.value() return Balance(balance=balance, liability=liability, receivable=receivable, _is_safe=True) from Acquire.Accounting import Transaction as _Transaction if type(other) is _Transaction: return Balance(balance=self._balance + other.value(), liability=self._liability, receivable=self._receivable, _is_safe=True) from Acquire.Accounting import create_decimal as _create_decimal value = _create_decimal(other) return Balance(balance=self._balance + value, liability=self._liability, receivable=self._receivable, _is_safe=True)
def from_data(data): """Return a newly constructed transaction from the passed dictionary that has been decoded from JSON Args: data (dict): Dictionary from JSON Returns: Transaction: Object created from JSON """ transaction = Transaction() if (data and len(data) > 0): from Acquire.Accounting import create_decimal as _create_decimal transaction._value = _create_decimal(data["value"]) transaction._description = data["description"] return transaction
def __init__(self, value=0, description=None): """Create a transaction with the passed value and description. Values are positive values with a minimum resolution of 1e-6 (6 decimal places) and cannot exceed 999999.999999 (i.e. cannot be 1 million or greater). This is to ensure that a value will always fit into a f13.6 string (which is used for keys). If you need larger transactions, then use the static 'split' function to break a single too-large transaction into a list of smaller transactions """ value = _create_decimal(value) if value > Transaction.maximum_transaction_value(): from Acquire.Accounting import TransactionError raise TransactionError( "You cannot create a transaction (%s) with a " "value greater than %s. Please " "use 'split' to break this transaction (%s) into " "several separate transactions" % (description, Transaction.maximum_transaction_value(), value)) # ensure that the value is limited in resolution to 6 decimal places self._value = value if self._value < 0: from Acquire.Accounting import TransactionError raise TransactionError( "You cannot create a transaction (%s) with a " "negative value! %s" % (description, value)) if description is None: if self._value > 0: from Acquire.Accounting import TransactionError raise TransactionError( "You must give a description to all non-zero " "transactions! %s" % self.value()) else: self._description = None else: # ensure we are using utf-8 encoded strings self._description = str(description).encode("utf-8") \ .decode("utf-8")
def from_data(data): """Return a Receipt from the passed JSON-decoded dictionary Args: data (dict): JSON dictionary to create object Returns: Receipt: Receipt created from JSON """ r = Receipt() if (data and len(data) > 0): from Acquire.Accounting import CreditNote as _CreditNote from Acquire.Accounting import create_decimal as _create_decimal from Acquire.Identity import Authorisation as _Authorisation r._credit_note = _CreditNote.from_data(data["credit_note"]) r._authorisation = _Authorisation.from_data(data["authorisation"]) r._receipted_value = _create_decimal(data["receipted_value"]) return r
def __sub__(self, other): """Add balances together""" if type(other) is type(Balance): return Balance(balance=self._balance - other._balance, liability=self._liability - other._liability, receivable=self._receivable - other._receivable, _is_safe=True) from Acquire.Accounting import Transaction as _Transaction if type(other) is _Transaction: return Balance(balance=self._balance - other.value(), liability=self._liability, receivable=self._receivable, _is_safe=True) from Acquire.Accounting import create_decimal as _create_decimal value = _create_decimal(other) return Balance(balance=self._balance + value, liability=self._liability, receivable=self._receivable, _is_safe=True)
def receipt(self, credit_note, receipted_value=None): """Receipt the passed credit note that contains a request to transfer value from another account to the passed account Args: credit_note (CreditNote): CreditNote to use to create receipt receipted_value (Decimal, default=None): Receipted value Returns: TransactionRecord: Record of this transaction """ if not self.is_logged_in(): raise PermissionError("You cannot receipt a credit note as the " "user has not yet logged in!") if credit_note.account_uid() != self.uid(): raise ValueError( "You cannot receipt a transaction from a different " "account! %s versus %s" % (credit_note.account_uid(), self.uid())) from Acquire.Client import Authorisation as _Authorisation from Acquire.Accounting import create_decimal as _create_decimal service = self.accounting_service() auth = _Authorisation(resource=self._account_uid, user=self._user) args = { "credit_note": credit_note.to_data(), "authorisation": auth.to_data() } if receipted_value is not None: args["receipted_value"] = str(_create_decimal(receipted_value)) result = service.call_function(function="receipt", args=args) return result["transaction_record"]
def __init__(self, debit_note=None, account=None, receipt=None, refund=None, bucket=None): """Create the corresponding credit note for the passed debit_note. This will credit value from the note to the passed account. The credit will use the same UID as the debit, and the same datetime. This will then be paired with the debit note to form a TransactionRecord that can be written to the ledger """ self._account_uid = None nargs = (receipt is not None) + (refund is not None) if nargs > 1: raise ValueError("You can create a CreditNote with a receipt " "or a refund - not both!") if receipt is not None: self._create_from_receipt(debit_note, receipt, account, bucket) elif refund is not None: self._create_from_refund(debit_note, refund, account, bucket) elif (debit_note is not None) and (account is not None): self._create_from_debit_note(debit_note, account, bucket) else: self._debit_account_uid = None self._datetime = None self._uid = None self._debit_note_uid = None from Acquire.Accounting import create_decimal as _create_decimal self._value = _create_decimal(0)
def __eq__(self, other): if isinstance(other, Transaction): return self.value() == other.value() and \ self.description() == other.description() else: return self.value() == _create_decimal(other)
def __gt__(self, other): if isinstance(other, Transaction): return self.value() > other.value() else: return self.value() > _create_decimal(other)
def create(credit_notes, authorisation, receipted_value=None): """Construct a series of receipts from the passed credit notes, each of which is authorised using the passed authorisation. If 'receipted_value' is specified, then the sum total of receipts will equal 'receipted_value'. This cannot be greater than the sum of the passed credit notes. If it is less then the value, then the difference is subtracted from the first receipts returned Args: credit_notes (list): List of credit notes to receipt autorisation (Authorisation): Authorisation for credit notes receipted_value (Decimal, default=None): Total value to receipt Returns: Receipt or list[Receipt]: If <= 1 credit note Receipt, else list of Receipts """ try: credit_note = credit_notes[0] except: return Receipt(credit_notes, authorisation, receipted_value) if len(credit_notes) == 0: return Receipt() elif len(credit_notes) == 1: return Receipt(credit_notes[0], authorisation, receipted_value) from Acquire.Accounting import create_decimal as _create_decimal total_value = _create_decimal(0) from Acquire.Accounting import CreditNote as _CreditNote for credit_note in credit_notes: if not isinstance(credit_note, _CreditNote): raise TypeError("The credit note must be of type CreditNote") total_value += credit_note.value() if receipted_value is None: receipted_value = total_value else: receipted_value = _create_decimal(receipted_value) if receipted_value < 0: raise ValueError("You cannot receipt a value that is less " "than zero! %s" % receipted_value) elif receipted_value > total_value: raise ValueError("You cannot receipt a value that is greater " "than the value of the original credit " "notes - %s versus %s" % (receipted_value, total_value)) receipts = [] if receipted_value == total_value: for credit_note in credit_notes: receipts.append(Receipt(credit_note, authorisation)) else: diff = total_value - receipted_value for credit_note in credit_notes: d = min(diff, credit_note.value()) diff -= d r = credit_note.value() - d receipts.append(Receipt(credit_note, authorisation, r)) return receipts
def from_key(key): """Extract information from the passed object store key. This looks for a string that is; isoformat_datetime/UID/transactioncode where transactioncode is a string that matches '2 letters followed by a number' CL000100.005000 DR000004.234100 etc. For sent and received receipts there are two values; the receipted value and the original estimate. These have the standard format if the values are the same, e.g. RR000100.005000 however, they have original value T receipted value if they are different, e.g. RR000100.005000T000090.000000 Args: key: Object store key """ from Acquire.ObjectStore import string_to_datetime \ as _string_to_datetime from Acquire.Accounting import create_decimal as _create_decimal parts = key.split("/") # start at the end... nparts = len(parts) for i in range(0, nparts): j = nparts - i - 1 t = TransactionInfo() try: t._datetime = _string_to_datetime(parts[j - 2]) except: continue t._uid = parts[j - 1] part = parts[j] try: code = TransactionInfo._get_code(part[0:2]) if code == TransactionCode.SENT_RECEIPT or \ code == TransactionCode.RECEIVED_RECEIPT: values = part[2:].split("T") try: value = _create_decimal(values[0]) receipted_value = _create_decimal(values[1]) t._code = code t._value = value t._receipted_value = receipted_value return t except: pass value = _create_decimal(part[2:]) t._code = code t._value = value t._receipted_value = None return t except: pass raise ValueError("Cannot extract transaction info from '%s'" % (key))