Пример #1
0
    def test_pad_check_balances(self, entries, errors, __):
        """
          2013-05-01 open Assets:Checking
          2013-05-01 open Assets:Cash
          2013-05-01 open Equity:Opening-Balances

          2013-05-01 pad  Assets:Checking   Equity:Opening-Balances

          2013-05-03 txn "Add 20$"
            Assets:Checking                        10 USD
            Assets:Cash

          2013-05-10 balance Assets:Checking      105 USD

          2013-05-15 txn "Add 20$"
            Assets:Checking                        20 USD
            Assets:Cash

          2013-05-16 txn "Add 20$"
            Assets:Checking                        20 USD
            Assets:Cash

          2013-06-01 balance Assets:Checking      145 USD

        """
        post_map = realization.postings_by_account(entries)
        txn_postings = post_map['Assets:Checking']

        balances = []
        pad_balance = inventory.Inventory()
        for txn_posting in txn_postings:
            if isinstance(txn_posting, data.TxnPosting):
                position_, _ = pad_balance.add_position(
                    txn_posting.posting.position)
                self.assertFalse(position_.is_negative_at_cost())
            balances.append((type(txn_posting), pad_balance.get_units('USD')))

        self.assertEqual(balances, [(data.Open, A('0.00 USD')),
                                    (data.Pad, A('0.00 USD')),
                                    (data.TxnPosting, A('95.00 USD')),
                                    (data.TxnPosting, A('105.00 USD')),
                                    (data.Balance, A('105.00 USD')),
                                    (data.TxnPosting, A('125.00 USD')),
                                    (data.TxnPosting, A('145.00 USD')),
                                    (data.Balance, A('145.00 USD'))])
Пример #2
0
    def test_postings_by_account(self, entries, errors, _):
        """
        option "plugin_processing_mode" "raw"

        2012-01-01 open Expenses:Restaurant
        2012-01-01 open Expenses:Movie
        2012-01-01 open Assets:Cash
        2012-01-01 open Liabilities:CreditCard
        2012-01-01 open Equity:Opening-Balances

        2012-01-15 pad Liabilities:CreditCard Equity:Opening-Balances

        2012-03-01 * "Food"
          Expenses:Restaurant     100 CAD
          Assets:Cash            -100 CAD

        2012-03-10 * "Food again"
          Expenses:Restaurant      80 CAD
          Liabilities:CreditCard  -80 CAD

        ;; Two postings on the same account.
        2012-03-15 * "Two Movies"
          Expenses:Movie           10 CAD
          Expenses:Movie           10 CAD
          Liabilities:CreditCard  -20 CAD

        2012-03-20 note Liabilities:CreditCard "Called Amex, asked about 100 CAD dinner"

        2012-03-28 document Liabilities:CreditCard "march-statement.pdf"

        2013-04-01 balance Liabilities:CreditCard   204 CAD

        2014-01-01 close Liabilities:CreditCard
        """
        self.assertEqual(0, len(errors))

        txn_postings_map = realization.postings_by_account(entries)
        self.assertTrue(isinstance(txn_postings_map, dict))

        self.assertEqual([data.Open, data.TxnPosting],
                         list(map(type, txn_postings_map['Assets:Cash'])))

        self.assertEqual([data.Open, data.TxnPosting, data.TxnPosting],
                         list(
                             map(type,
                                 txn_postings_map['Expenses:Restaurant'])))

        self.assertEqual([data.Open, data.TxnPosting, data.TxnPosting],
                         list(map(type, txn_postings_map['Expenses:Movie'])))

        self.assertEqual(
            [
                data.Open,
                data.Pad,
                data.TxnPosting,
                data.TxnPosting,  # data.TxnPosting,
                data.Note,
                data.Document,
                data.Balance,
                data.Close
            ],
            list(map(type, txn_postings_map['Liabilities:CreditCard'])))

        self.assertEqual([data.Open, data.Pad],
                         list(
                             map(type,
                                 txn_postings_map['Equity:Opening-Balances'])))
Пример #3
0
def pad(entries, options_map):
    """Insert transaction entries for to fulfill a subsequent balance check.

    Synthesize and insert Transaction entries right after Pad entries in order
    to fulfill checks in the padded accounts. Returns a new list of entries.
    Note that this doesn't pad across parent-child relationships, it is a very
    simple kind of pad. (I have found this to be sufficient in practice, and
    simpler to implement and understand.)

    Furthermore, this pads for a single currency only, that is, balance checks
    are specified only for one currency at a time, and pads will only be
    inserted for those currencies.

    Args:
      entries: A list of directives.
      options_map: A parser options dict.
    Returns:
      A new list of directives, with Pad entries inserte, and a list of new
      errors produced.
    """
    pad_errors = []

    # Find all the pad entries and group them by account.
    pads = list(misc_utils.filter_type(entries, data.Pad))
    pad_dict = misc_utils.groupby(lambda x: x.account, pads)

    # Partially realize the postings, so we can iterate them by account.
    by_account = realization.postings_by_account(entries)

    # A dict of pad -> list of entries to be inserted.
    new_entries = {id(pad): [] for pad in pads}

    # Process each account that has a padding group.
    for account_, pad_list in sorted(pad_dict.items()):

        # Last encountered / currency active pad entry.
        active_pad = None

        # Gather all the postings for the account and its children.
        postings = []
        is_child = account.parent_matcher(account_)
        for item_account, item_postings in by_account.items():
            if is_child(item_account):
                postings.extend(item_postings)
        postings.sort(key=data.posting_sortkey)

        # A set of currencies already padded so far in this account.
        padded_lots = set()

        pad_balance = inventory.Inventory()
        for entry in postings:

            assert not isinstance(entry, data.Posting)
            if isinstance(entry, data.TxnPosting):
                # This is a transaction; update the running balance for this
                # account.
                pad_balance.add_position(entry.posting.position)

            elif isinstance(entry, data.Pad):
                if entry.account == account_:
                    # Mark this newly encountered pad as active and allow all lots
                    # to be padded heretofore.
                    active_pad = entry
                    padded_lots = set()

            elif isinstance(entry, data.Balance):
                check_amount = entry.amount

                # Compare the current balance amount to the expected one from
                # the check entry. IMPORTANT: You need to understand that this
                # does not check a single position, but rather checks that the
                # total amount for a particular currency (which itself is
                # distinct from the cost).
                balance_amount = pad_balance.get_units(check_amount.currency)
                diff_amount = amount.amount_sub(balance_amount, check_amount)

                # Use the specified tolerance or automatically infer it.
                tolerance = balance.get_tolerance(entry, options_map)

                if abs(diff_amount.number) > tolerance:
                    # The check fails; we need to pad.

                    # Pad only if pad entry is active and we haven't already
                    # padded that lot since it was last encountered.
                    if active_pad and (check_amount.currency
                                       not in padded_lots):

                        # Note: we decide that it's an error to try to pad
                        # positions at cost; we check here that all the existing
                        # positions with that currency have no cost.
                        positions = [
                            pos for pos in pad_balance.get_positions()
                            if pos.lot.currency == check_amount.currency
                        ]
                        for position_ in positions:
                            if position_.lot.cost is not None:
                                pad_errors.append(
                                    PadError(entry.meta, (
                                        "Attempt to pad an entry with cost for "
                                        "balance: {}".format(pad_balance)),
                                             active_pad))

                        # Thus our padding lot is without cost by default.
                        lot = position.Lot(check_amount.currency, None, None)
                        diff_position = position.Position(
                            lot, check_amount.number - balance_amount.number)

                        # Synthesize a new transaction entry for the difference.
                        narration = ('(Padding inserted for Balance of {} for '
                                     'difference {})').format(
                                         check_amount, diff_position)
                        new_entry = data.Transaction(active_pad.meta.copy(),
                                                     active_pad.date,
                                                     flags.FLAG_PADDING, None,
                                                     narration, None, None, [])

                        new_entry.postings.append(
                            data.Posting(active_pad.account, diff_position,
                                         None, None, None))
                        new_entry.postings.append(
                            data.Posting(active_pad.source_account,
                                         -diff_position, None, None, None))

                        # Save it for later insertion after the active pad.
                        new_entries[id(active_pad)].append(new_entry)

                        # Fixup the running balance.
                        position_, _ = pad_balance.add_position(diff_position)
                        if position_.is_negative_at_cost():
                            raise ValueError(
                                "Position held at cost goes negative: {}".
                                format(position_))

                # Mark this lot as padded. Further checks should not pad this lot.
                padded_lots.add(check_amount.currency)

    # Insert the newly created entries right after the pad entries that created them.
    padded_entries = []
    for entry in entries:
        padded_entries.append(entry)
        if isinstance(entry, data.Pad):
            entry_list = new_entries[id(entry)]
            if entry_list:
                padded_entries.extend(entry_list)
            else:
                # Generate errors on unused pad entries.
                pad_errors.append(
                    PadError(entry.meta, "Unused Pad entry", entry))

    return padded_entries, pad_errors