Esempio n. 1
0
    def _get_current_balance(self, bucket=None):
        """Get the balance of the account now (the current balance). This
           returns a tuple of
           (balance, liability, receivable, spent_today).

           where 'balance' is the current real balance of the account,
           neglecting any outstanding liabilities or accounts receivable,
           where 'liability' is the current total liabilities,
           where 'receivable' is the current total accounts receivable, and
           where 'spent_today' is how much has been spent today (from midnight
           until now)
        """
        if bucket is None:
            bucket = _login_to_service_account()

        now = _datetime.datetime.now()
        now_ordinal = now.toordinal()

        try:
            last_update_ordinal = self._last_update_ordinal
        except:
            last_update_ordinal = None

        if last_update_ordinal != now_ordinal:
            # we are on a new day since the last update, so recalculate
            # the balance from scratch
            return self._recalculate_current_balance(bucket, now)
        else:
            # we have calculated the total before today. Get the transactions
            # since the last update and use these to update the daily spend
            # etc.
            return self._update_current_balance(bucket, now)
Esempio n. 2
0
    def _delete_note(self, note, bucket=None):
        """Internal function called to delete the passed note from the
           record. This is unsafe and should only be called from
           DebitNote.return_value or CreditNote.return_value (which
           themselves are only called from Ledger)
        """
        if note is None:
            return

        if isinstance(note, _DebitNote) or isinstance(note, _CreditNote):
            item_key = "%s/%s" % (self._key(), note.uid())

            if bucket is None:
                bucket = _login_to_service_account()

            # remove the note
            try:
                _ObjectStore.delete_object(bucket, item_key)
            except:
                pass

            # now remove all day-balances from the day before this note
            # to today. Hopefully this will prevent any ledger errors...
            day0 = _datetime.datetime.fromtimestamp(
                note.timestamp()).toordinal() - 1
            day1 = _datetime.datetime.now().toordinal()

            for day in range(day0, day1 + 1):
                balance_key = self._get_balance_key(
                    _datetime.datetime.fromordinal(day))

                try:
                    _ObjectStore.delete_object(bucket, balance_key)
                except:
                    pass
Esempio n. 3
0
    def __init__(self, key=None, timeout=10, lease_time=10, bucket=None):
        """Create the mutex. The immediately tries to lock the mutex
           for key 'key' and will block until a lock is successfully
           obtained (or until 'timeout' seconds has been reached, and an
           exception is then thrown). If the key is provided, then
           this is the (single) global mutex. Note that this is really
           a lease, as the mutex will only be held for a maximum of
           'lease_time' seconds. After this time the mutex will be
           automatically unlocked and made available to lock by
           others. You can renew the lease by re-locking the mutex.
        """
        if key is None:
            key = "mutexes/none"
        else:
            key = "mutexes/%s" % str(key).replace(" ", "_")

        if bucket is None:
            from Acquire.Service import login_to_service_account as \
                                       _login_to_service_account

            bucket = _login_to_service_account()

        self._bucket = bucket
        self._key = key
        self._secret = str(uuid.uuid4())
        self._is_locked = 0
        self.lock(timeout, lease_time)
Esempio n. 4
0
    def _record_daily_balance(self,
                              balance,
                              liability,
                              receivable,
                              datetime=None,
                              bucket=None):
        """Record the starting balance for the day containing 'datetime'
           as 'balance' with the starting outstanding liabilities at
           'liability' and starting outstanding accounts receivable at
           'receivable' If 'datetime' is none, then the balance
           for today is set.
        """
        if self.is_null():
            return

        if datetime is None:
            datetime = _datetime.datetime.now()

        balance = _create_decimal(balance)
        liability = _create_decimal(liability)
        receivable = _create_decimal(receivable)

        balance_key = self._get_balance_key(datetime)

        if bucket is None:
            bucket = _login_to_service_account()

        data = {
            "balance": str(balance),
            "liability": str(liability),
            "receivable": str(receivable)
        }

        _ObjectStore.set_object_from_json(bucket, balance_key, data)
Esempio n. 5
0
    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).
        """
        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:
                bucket = _login_to_service_account()

            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)

            if len(records) == 1:
                return records[0]
            else:
                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!")
Esempio n. 6
0
    def _load_account(self, bucket=None):
        """Load the current state of the account from the object store"""
        if self.is_null():
            return

        if bucket is None:
            bucket = _login_to_service_account()

        data = _ObjectStore.get_object_from_json(bucket, self._key())
        self.__dict__ = _copy(Account.from_data(data).__dict__)
Esempio n. 7
0
    def load_test_and_set(uid, expected_state, new_state,
                          bucket=None):
        """Static method to load up the Transaction record associated with
           the passed UID, check that the transaction state matches
           'expected_state', and if it does, to update the transaction
           state to 'new_state'. This returns the loaded (and updated)
           transaction
        """
        if bucket is None:
            bucket = _login_to_service_account()

        from ._ledger import Ledger as _Ledger

        try:
            mutex = _Mutex(uid, timeout=600, lease_time=600)
        except Exception as e:
            raise LedgerError("Cannot secure a Ledger mutex for transaction "
                              "'%s'. Error = %s" % (uid, str(e)))

        try:
            transaction = _Ledger.load_transaction(uid, bucket)

            if transaction.transaction_state() != expected_state:
                raise TransactionError(
                    "Cannot update the state of the transaction %s from "
                    "%s to %s as it is not in the expected state" %
                    (str(transaction), expected_state.value, new_state.value))

            transaction._transaction_state = new_state
        except:
            mutex.unlock()
            raise

        # now need to write anything back if the state isn't changed
        if expected_state == new_state:
            return transaction

        # make sure we have enough time remaining on the lease to be
        # able to write this result back to the object store...
        if mutex.seconds_remaining_on_lease() < 100:
            try:
                mutex.fully_unlock()
            except:
                pass

            return TransactionRecord.load_test_and_set(uid, expected_state,
                                                       new_state, bucket)

        try:
            _Ledger.save_transaction(transaction, bucket)
        except:
            mutex.unlock()
            raise

        return transaction
Esempio n. 8
0
    def save_transaction(record, bucket=None):
        """Save the passed transactionrecord to the object store"""
        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:
                bucket = _login_to_service_account()

            _ObjectStore.set_object_from_json(bucket,
                                              Ledger.get_key(record.uid()),
                                              record.to_data())
Esempio n. 9
0
    def list_accounts(self, bucket=None):
        """Return the names of all of the accounts in this group"""
        if bucket is None:
            bucket = _login_to_service_account()

        keys = _ObjectStore.get_all_object_names(bucket, self._root())

        accounts = []

        for key in keys:
            accounts.append(_encoded_to_string(key))

        return accounts
Esempio n. 10
0
    def load_transaction(uid, bucket=None):
        """Load the transactionrecord with UID=uid from the ledger"""
        if bucket is None:
            bucket = _login_to_service_account()

        data = _ObjectStore.get_object_from_json(bucket, Ledger.get_key(uid))

        if data is None:
            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)
Esempio n. 11
0
    def _credit(self, debit_note, bucket=None):
        """Credit the value in 'debit_note' to this account. If the debit_note
           shows that the payment is provisional then this will be recorded
           as accounts receivable. This will record the credit with the
           same UID as the debit identified in the debit_note, so that
           we can reconcile all credits against matching debits.
        """
        if not isinstance(debit_note, _DebitNote):
            raise TypeError("The passed debit note must be a DebitNote")

        if debit_note.value() <= 0:
            return

        if bucket is None:
            bucket = _login_to_service_account()

        if debit_note.is_provisional():
            encoded_value = _TransactionInfo.encode(
                _TransactionCode.ACCOUNT_RECEIVABLE, debit_note.value())
        else:
            encoded_value = _TransactionInfo.encode(_TransactionCode.CREDIT,
                                                    debit_note.value())

        # create a UID and timestamp for this credit and record
        # it in the account
        now = self._get_safe_now()

        # we need to record the exact timestamp of this credit...
        timestamp = now.timestamp()

        # and to create a key to find this credit later. The key is made
        # up from the date and timestamp of the credit and a random string
        day_key = "%4d-%02d-%02d/%s" % (now.year, now.month, now.day,
                                        timestamp)
        uid = "%s/%s" % (day_key, str(_uuid.uuid4())[0:8])

        item_key = "%s/%s/%s" % (self._key(), uid, encoded_value)

        # the line item records the UID of the debit note, so we can
        # find this debit note in the system and, from this, get the
        # original transaction in the transaction record
        l = _LineItem(debit_note.uid(), debit_note.authorisation())

        _ObjectStore.set_object_from_json(bucket, item_key, l.to_data())

        return (uid, timestamp)
Esempio n. 12
0
    def contains(self, account, bucket=None):
        """Return whether or not this group contains the passed account"""
        if not isinstance(account, _Account):
            raise TypeError("The passed account must be of type Account")

        if bucket is None:
            bucket = _login_to_service_account()

        # read the UID of the account in this group that matches the
        # passed account's name
        try:
            account_uid = _ObjectStore.get_string_object(
                bucket, self._account_key(account.name()))
        except:
            account_uid = None

        return account.uid() == account_uid
Esempio n. 13
0
    def _credit_receipt(self, debit_note, receipt, bucket=None):
        """Credit the value of the passed 'receipt' to this account. The
           receipt must be for a previous provisional credit, hence the
           money is awaiting transfer from accounts receivable.
        """
        if not isinstance(receipt, _Receipt):
            raise TypeError("The passed receipt must be a Receipt")

        if not isinstance(debit_note, _DebitNote):
            raise TypeError("The passed debit note must be a DebitNote")

        if receipt.is_null():
            return

        if bucket is None:
            bucket = _login_to_service_account()

        if receipt.receipted_value() != debit_note.value():
            raise ValueError("The receipted value does not match the value "
                             "of the debit note: %s versus %s" %
                             (receipt.receipted_value(), debit_note.value()))

        encoded_value = _TransactionInfo.encode(_TransactionCode.SENT_RECEIPT,
                                                receipt.value(),
                                                receipt.receipted_value())

        # create a UID and timestamp for this credit and record
        # it in the account
        now = self._get_safe_now()

        # we need to record the exact timestamp of this credit...
        timestamp = now.timestamp()

        # and to create a key to find this credit later. The key is made
        # up from the date and timestamp of the credit and a random string
        day_key = "%4d-%02d-%02d/%s" % (now.year, now.month, now.day,
                                        timestamp)
        uid = "%s/%s" % (day_key, str(_uuid.uuid4())[0:8])

        item_key = "%s/%s/%s" % (self._key(), uid, encoded_value)
        l = _LineItem(debit_note.uid(), receipt.authorisation())

        _ObjectStore.set_object_from_json(bucket, item_key, l.to_data())

        return (uid, timestamp)
Esempio n. 14
0
    def _get_daily_balance(self, bucket=None, datetime=None):
        """Get the daily starting balance for the passed datetime. This
           returns a tuple of
           (balance, liability, receivable).

           where 'balance' is the current real balance of the account,
           neglecting any outstanding liabilities or accounts receivable,
           where 'liability' is the current total liabilities,
           where 'receivable' is the current total accounts receivable, and

           If datetime is None then todays daily balance is returned. The
           daily balance is the balance at the start of the day. The
           actual balance at a particular time will be this starting
           balance plus/minus all of the transactions between the start
           of that day and the specified datetime
        """
        if self.is_null():
            return

        if bucket is None:
            bucket = _login_to_service_account()

        if datetime is None:
            datetime = _datetime.datetime.now()

        balance_key = self._get_balance_key(datetime)

        data = _ObjectStore.get_object_from_json(bucket, balance_key)

        if data is None:
            # there is no balance for this day. This means that we haven'y
            # yet calculated that day's balance. Do the accounting necessary
            # to construct that day's starting balance
            self._reconcile_daily_accounts(bucket)

            data = _ObjectStore.get_object_from_json(bucket, balance_key)

            if data is None:
                raise AccountError("The daily balance for account at date %s "
                                   "is not available" % str(datetime))

        return (_create_decimal(data["balance"]),
                _create_decimal(data["liability"]),
                _create_decimal(data["receivable"]))
Esempio n. 15
0
    def _get_transaction_keys_between(self, start_time, end_time, bucket=None):
        """Return all of the object store keys for transactions in this
           account beteen 'start_time' and 'end_time' (inclusive, e.g.
           start_time <= transaction <= end_time). This will return an
           empty list if there were no transactions in this time
        """
        if bucket is None:
            bucket = _login_to_service_account()

        if not isinstance(start_time, _datetime.datetime):
            raise TypeError("The start time must be a datetime object, "
                            "not a %s" % start_time.__class__)

        if not isinstance(end_time, _datetime.datetime):
            raise TypeError("The end time must be a datetime object, "
                            "not a %s" % end_time.__class__)

        start_day = start_time.toordinal()
        end_day = end_time.toordinal()

        start_timestamp = start_time.timestamp()
        end_timestamp = end_time.timestamp()

        keys = []

        for day in range(start_day, end_day + 1):
            day_date = _datetime.datetime.fromordinal(day)

            prefix = "%s/%4d-%02d-%02d" % (self._key(), day_date.year,
                                           day_date.month, day_date.day)

            day_keys = _ObjectStore.get_all_object_names(bucket, prefix)

            for day_key in day_keys:
                try:
                    timestamp = float(day_key.split("/")[0])
                except:
                    timestamp = 0

                if timestamp >= start_timestamp and timestamp <= end_timestamp:
                    keys.append("%s/%s" % (prefix, day_key))

        return keys
Esempio n. 16
0
    def _create_account(self, name, description):
        """Create the account from scratch"""
        if name is None or description is None:
            raise AccountError(
                "You must pass both a name and description to create a new "
                "account")

        if self._uid is not None:
            raise AccountError("You cannot create an account twice!")

        self._uid = str(_uuid.uuid4())
        self._name = str(name)
        self._description = str(description)
        self._overdraft_limit = _create_decimal(0)
        self._maximum_daily_limit = 0
        self._last_update_ordinal = None

        # initialise the account with a balance of zero
        bucket = _login_to_service_account()
        self._record_daily_balance(0, 0, 0, bucket=bucket)
        # make sure that this is saved to the object store
        self._save_account(bucket)
Esempio n. 17
0
    def _debit_refund(self, refund, bucket=None):
        """Debit the value of the passed 'refund' from this account. The
           refund must be for a previous completed credit. There is a risk
           that this value has been spent, so this is one of the only
           functions that allows a balance to drop below an overdraft or
           other limit (as the refund should always succeed).
        """
        if not isinstance(refund, _Refund):
            raise TypeError("The passed refund must be a Refund")

        if refund.is_null():
            return

        if bucket is None:
            bucket = _login_to_service_account()

        encoded_value = _TransactionInfo.encode(_TransactionCode.SENT_REFUND,
                                                refund.value())

        # create a UID and timestamp for this debit and record
        # it in the account
        now = self._get_safe_now()

        # we need to record the exact timestamp of this credit...
        timestamp = now.timestamp()

        # and to create a key to find this debit later. The key is made
        # up from the date and timestamp of the debit and a random string
        day_key = "%4d-%02d-%02d/%s" % (now.year, now.month, now.day,
                                        timestamp)
        uid = "%s/%s" % (day_key, str(_uuid.uuid4())[0:8])

        item_key = "%s/%s/%s" % (self._key(), uid, encoded_value)
        l = _LineItem(uid, refund.authorisation())

        _ObjectStore.set_object_from_json(bucket, item_key, l.to_data())

        return (uid, timestamp)
Esempio n. 18
0
    def get_account(self, name, bucket=None):
        """Return the account called 'name' from this group"""
        if bucket is None:
            bucket = _login_to_service_account()

        try:
            account_uid = _ObjectStore.get_string_object(
                bucket, self._account_key(name))
        except:
            account_uid = None

        if account_uid is None:
            # ensure that the user always has a "main" account
            if name == "main":
                return self.create_account("main",
                                           "primary user account",
                                           overdraft_limit=0,
                                           bucket=bucket)

            raise AccountError("There is no account called '%s' in the "
                               "group '%s'" % (name, self.group()))

        return _Account(uid=account_uid, bucket=bucket)
Esempio n. 19
0
    def _debit_receipt(self, receipt, bucket=None):
        """Debit the value of the passed 'receipt' from this account. The
           receipt must be for a previous provisional debit, hence
           the money should be available.
        """
        if not isinstance(receipt, _Receipt):
            raise TypeError("The passed receipt must be a Receipt")

        if receipt.is_null():
            return

        if bucket is None:
            bucket = _login_to_service_account()

        encoded_value = _TransactionInfo.encode(
            _TransactionCode.RECEIVED_RECEIPT, receipt.value(),
            receipt.receipted_value())

        # create a UID and timestamp for this debit and record
        # it in the account
        now = self._get_safe_now()

        # we need to record the exact timestamp of this credit...
        timestamp = now.timestamp()

        # and to create a key to find this debit later. The key is made
        # up from the date and timestamp of the debit and a random string
        day_key = "%4d-%02d-%02d/%s" % (now.year, now.month, now.day,
                                        timestamp)
        uid = "%s/%s" % (day_key, str(_uuid.uuid4())[0:8])

        item_key = "%s/%s/%s" % (self._key(), uid, encoded_value)
        l = _LineItem(uid, receipt.authorisation())

        _ObjectStore.set_object_from_json(bucket, item_key, l.to_data())

        return (uid, timestamp)
Esempio n. 20
0
    def _create_from_receipt(self, receipt, account, bucket):
        """Function used to construct a debit note by extracting
           the value specified in the passed receipt from the specified
           account. This is authorised using the authorisation held in
           the receipt, based on the original authorisation given in the
           provisional transaction. Note that the receipt must match
           up with a prior existing provisional transaction, and this
           must not have already been receipted or refunded. This will
           actually take value out of the passed account, with that
           value residing in this debit note until it is credited to
           another account
        """
        from ._receipt import Receipt as _Receipt

        if not isinstance(receipt, _Receipt):
            raise TypeError("You can only create a DebitNote with a "
                            "Receipt")

        if receipt.is_null():
            return

        if bucket is None:
            bucket = _login_to_service_account()

        from ._transactionrecord import TransactionRecord as _TransactionRecord
        from ._transactionrecord import TransactionState as _TransactionState
        from ._account import Account as _Account

        # get the transaction behind this receipt and move it into
        # the "receipting" state
        transaction = _TransactionRecord.load_test_and_set(
            receipt.transaction_uid(),
            _TransactionState.PROVISIONAL,
            _TransactionState.RECEIPTING,
            bucket=bucket)

        try:
            # ensure that the receipt matches the transaction...
            transaction.assert_matching_receipt(receipt)

            if account is None:
                account = _Account(transaction.debit_account_uid(), bucket)
            elif account.uid() != receipt.debit_account_uid():
                raise ValueError("The accounts do not match when debiting "
                                 "the receipt: %s versus %s" %
                                 (account.uid(), receipt.debit_account_uid()))

            # now move value from liability to debit, and then into this
            # debit note
            (uid, timestamp) = account._debit_receipt(receipt, bucket)

            self._transaction = receipt.transaction()
            self._account_uid = receipt.debit_account_uid()
            self._authorisation = receipt.authorisation()
            self._is_provisional = False

            self._timestamp = float(timestamp)
            self._uid = str(uid)
        except:
            # move the transaction back to its original state...
            _TransactionRecord.load_test_and_set(receipt.transaction_uid(),
                                                 _TransactionState.RECEIPTING,
                                                 _TransactionState.PROVISIONAL)
            raise
Esempio n. 21
0
    def perform(transactions,
                debit_account,
                credit_account,
                authorisation,
                is_provisional=False,
                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. 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.
        """

        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:
            bucket = _login_to_service_account()

        # 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,
                               bucket=bucket))
        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:
                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:
                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:
                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)
Esempio n. 22
0
    def _reconcile_daily_accounts(self, bucket=None):
        """Internal function used to reconcile the daily accounts.
           This ensures that every line item transaction is summed up
           so that the starting balance for each day is recorded into
           the object store
        """
        if self.is_null():
            return

        if bucket is None:
            bucket = _login_to_service_account()

        # work back from today to the first day of the account to calculate
        # all of the daily balances... We need to record every day of the
        # account to support quick lookups
        today = _datetime.datetime.now().toordinal()
        day = today
        last_data = None
        num_missing_days = 0

        while last_data is None:
            daytime = _datetime.datetime.fromordinal(day)
            key = self._get_balance_key(daytime)
            last_data = _ObjectStore.get_object_from_json(bucket, key)

            if last_data is None:
                day -= 1
                num_missing_days += 1

                if num_missing_days > 100:
                    # we need another strategy to find the last balance
                    break

        if last_data is None:
            # find the latest day by reading the keys in the object
            # store directly
            root = "%s/balance/" % self._key()
            keys = _ObjectStore.get_all_object_names(bucket, root)

            if keys is None or len(keys) == 0:
                raise AccountError("There is no daily balance recorded for "
                                   "the account with UID %s" % self.uid())

            # the encoding of the keys is such that, when sorted, the
            # last key must be the latest balance
            keys.sort()

            last_data = _ObjectStore.get_object_from_json(
                bucket, "%s%s" % (root, keys[-1]))
            day = _get_day_from_key(keys[-1]).toordinal()

            if last_data is None:
                raise AccountError("How can there be no data for key %s?" %
                                   keys[-1])

        # what was the balance on the last day?
        result = (_create_decimal(last_data["balance"]),
                  _create_decimal(last_data["liability"]),
                  _create_decimal(last_data["receivable"]))

        # ok, now we go from the last day until today and sum up the
        # line items from each day to create the daily balances
        # (not including today, as we only want the balance at the beginning
        #  of today)
        for d in range(day + 1, today + 1):
            day_time = _datetime.datetime.fromordinal(d)
            transaction_keys = self._get_transaction_keys_between(
                _datetime.datetime.fromordinal(d - 1), day_time)

            total = _sum_transactions(transaction_keys)

            result = (result[0] + total[0], result[1] + total[1],
                      result[2] + total[2])

            balance_key = self._get_balance_key(day_time)

            data = {}
            data["balance"] = str(result[0])
            data["liability"] = str(result[1])
            data["receivable"] = str(result[2])

            _ObjectStore.set_object_from_json(bucket, balance_key, data)
Esempio n. 23
0
    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
        """
        if not isinstance(receipt, _Receipt):
            raise TypeError("The Receipt must be of type Receipt")

        if receipt.is_null():
            return _TransactionRecord()

        if bucket is None:
            bucket = _login_to_service_account()

        # 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)
Esempio n. 24
0
    def create_account(self,
                       name,
                       description=None,
                       overdraft_limit=None,
                       bucket=None):
        """Create a new account called 'name' in this group. This will
           return the existing account if it already exists
        """
        if name is None:
            raise ValueError("You must pass a name of the new account")

        account_key = self._account_key(name)

        if bucket is None:
            bucket = _login_to_service_account()

        try:
            account_uid = _ObjectStore.get_string_object(bucket, account_key)
        except:
            account_uid = None

        if account_uid is not None:
            # this account already exists - just return it
            account = _Account(uid=account_uid, bucket=bucket)

            if overdraft_limit is not None:
                account.set_overdraft_limit(overdraft_limit, bucket=bucket)

            return account

        # make sure that no-one has created this account before
        m = _Mutex(account_key, timeout=600, lease_time=600, bucket=bucket)

        try:
            account_uid = _ObjectStore.get_string_object(bucket, account_key)
        except:
            account_uid = None

        if account_uid is not None:
            m.unlock()
            # this account already exists - just return it
            account = _Account(uid=account_uid, bucket=bucket)

            if overdraft_limit is not None:
                account.set_overdraft_limit(overdraft_limit, bucket=bucket)

            return account

        # write a temporary UID to the object store so that we
        # can ensure we are the only function to create it
        try:
            _ObjectStore.set_string_object(bucket, account_key,
                                           "under_construction")
        except:
            m.unlock()
            raise

        m.unlock()

        # ok - we are the only function creating this account. Let's try
        # to create it properly
        try:
            account = _Account(name=name,
                               description=description,
                               bucket=bucket)
        except:
            try:
                _ObjectStore.delete_object(bucket, account_key)
            except:
                pass

            raise

        if overdraft_limit is not None:
            account.set_overdraft_limit(overdraft_limit, bucket=bucket)

        _ObjectStore.set_string_object(bucket, account_key, account.uid())

        return account
Esempio n. 25
0
    def _debit(self, transaction, authorisation, is_provisional, bucket=None):
        """Debit the value of the passed transaction from this account based
           on the authorisation contained
           in 'authorisation'. This will create a unique ID (UID) for
           this debit and will return this together with the timestamp of the
           debit. If this transaction 'is_provisional' then it will be
           recorded as a liability.

           The UID will encode both the date of the debit and provide a random
           ID that together can be used to identify the transaction associated
           with this debit in the future.

           This will raise an exception if the debit cannot be completed, e.g.
           if the authorisation is invalid, if the debit exceeds a limit or
           there are insufficient funds in the account

           Note that this function is private as it should only be called
           by the DebitNote class
        """
        if self.is_null() or transaction.value() <= 0:
            return None

        if not isinstance(transaction, _Transaction):
            raise TypeError("The passed transaction must be a Transaction!")

        self.assert_valid_authorisation(authorisation)

        if bucket is None:
            bucket = _login_to_service_account()

        if self.available_balance(bucket) < transaction.value():
            raise InsufficientFundsError(
                "You cannot debit '%s' from account %s as there "
                "are insufficient funds in this account." %
                (transaction, str(self)))

        # create a UID and timestamp for this debit and record
        # it in the account
        now = self._get_safe_now()

        # we need to record the exact timestamp of this debit...
        timestamp = now.timestamp()

        # and to create a key to find this debit later. The key is made
        # up from the date and timestamp of the debit and a random string
        day_key = "%4d-%02d-%02d/%s" % (now.year, now.month, now.day,
                                        timestamp)
        uid = "%s/%s" % (day_key, str(_uuid.uuid4())[0:8])

        # the key in the object store is a combination of the key for this
        # account plus the uid for the debit plus the actual debit value.
        # We record the debit value in the key so that we can accumulate
        # the balance from just the key names
        if is_provisional:
            encoded_value = _TransactionInfo.encode(
                _TransactionCode.CURRENT_LIABILITY, transaction.value())
        else:
            encoded_value = _TransactionInfo.encode(_TransactionCode.DEBIT,
                                                    transaction.value())

        item_key = "%s/%s/%s" % (self._key(), uid, encoded_value)

        # create a line_item for this debit and save it to the object store
        line_item = _LineItem(uid, authorisation)

        _ObjectStore.set_object_from_json(bucket, item_key,
                                          line_item.to_data())

        if self.is_beyond_overdraft_limit(bucket):
            # This transaction has helped push the account beyond the
            # overdraft limit. Delete the transaction and raise
            # an InsufficientFundsError

            _ObjectStore.delete_object(bucket, item_key)
            raise InsufficientFundsError(
                "You cannot debit '%s' from account %s as there "
                "are insufficient funds in this account." %
                (transaction, str(self)))

        return (uid, timestamp)
Esempio n. 26
0
 def _save_account(self, bucket=None):
     """Save this account back to the object store"""
     if bucket is None:
         bucket = _login_to_service_account()
     _ObjectStore.set_object_from_json(bucket, self._key(), self.to_data())
Esempio n. 27
0
    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
        """
        if not isinstance(refund, _Refund):
            raise TypeError("The Refund must be of type Refund")

        if refund.is_null():
            return _TransactionRecord()

        if bucket is None:
            bucket = _login_to_service_account()

        # 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)