Ejemplo n.º 1
0
def fill_residuals(entries: List[Directive],
                   options: Dict[str, Any]) -> List[Directive]:
    ret = []
    for entry in entries:
        if not isinstance(entry, Transaction):
            ret.append(entry)
            continue
        # Create residual postings for each party and add up all postings into real accounts
        # If the original transaction was not balanced we book residuals to error account
        # so we don't accidentially make it looks balanced for a subaccount in some cases.
        original_residual = interpolate.compute_residual(entry.postings)
        tolerances = interpolate.infer_tolerances(entry.postings, options)
        if original_residual.is_small(tolerances):
            subaccount_tmpl = '[Residuals]:[{}]'
        else:
            subaccount_tmpl = '[Error]:[{}]'
        postings = entry.postings[:]
        postings_by_party = defaultdict(list)
        for posting in entry.postings:
            party = posting.account.rsplit(':', 1)[1].strip('[]')
            postings_by_party[party].append(posting)
        for party, party_postings in postings_by_party.items():
            residual = interpolate.compute_residual(party_postings)
            # Use square brackets in account name to avoid collision with actual accounts
            subaccount = subaccount_tmpl.format(party)
            postings += utils.get_residual_postings(residual, subaccount)
        ret.append(entry._replace(postings=postings))
    return ret
Ejemplo n.º 2
0
    def test_fill_residual_posting(self, entries, _, __):
        """
        2001-01-01 open Assets:Account1
        2001-01-01 open Assets:Other

        2014-01-01 *
          Assets:Account1      100.00 USD
          Assets:Other        -100.00 USD

        2014-01-02 *
          Assets:Account1      100.00 USD
          Assets:Other        -100.00 USD

        2014-01-03 *
          Assets:Account1      100.00 USD
          Assets:Other        -100.0000001 USD

        2014-01-04 *
          Assets:Account1      100.00 USD
          Assets:Other        -112.69 CAD @ 0.8875 USD
        """
        account = 'Equity:Rounding'
        entries = [entry for entry in entries if isinstance(entry, data.Transaction)]
        for index in 0, 1:
            entry = interpolate.fill_residual_posting(entries[index], account)
            self.assertEqualEntries([entries[index]], [entry])
            residual = interpolate.compute_residual(entry.postings)
            self.assertTrue(residual.is_empty())

        entry = interpolate.fill_residual_posting(entries[2], account)
        self.assertEqualEntries("""

        2014-01-03 *
          Assets:Account1      100.00 USD
          Assets:Other        -100.0000001 USD
          Equity:Rounding        0.0000001 USD

        """, [entry])
        residual = interpolate.compute_residual(entry.postings)
        # Note: The residual calcualtion ignores postings inserted by the
        # rounding account.
        self.assertFalse(residual.is_empty())
        self.assertEqual(inventory.from_string('-0.0000001 USD'), residual)

        entry = interpolate.fill_residual_posting(entries[3], account)
        self.assertEqualEntries("""

        2014-01-04 *
          Assets:Account1     100.00 USD
          Assets:Other       -112.69 CAD @ 0.8875 USD
          Equity:Rounding   0.012375 USD

        """, [entry])
        residual = interpolate.compute_residual(entry.postings)
        # Same as above.
        self.assertFalse(residual.is_empty())
        self.assertEqual(inventory.from_string('-0.012375 USD'), residual)
Ejemplo n.º 3
0
    def apply_txn(self, txn: Transaction) -> typing.List[Member]:
        bc_txn = txn.beancount_txn

        # Ensure that the transaction balances
        residual = bcinterp.compute_residual(bc_txn.postings)
        tolerances = bcinterp.infer_tolerances(bc_txn.postings, self.bc_options_map)
        assert residual.is_small(tolerances), "Imbalanced transaction generated"

        # add the transaction to the ledger
        while True:
            try:
                with self.git_transaction():
                    beancount.parser.printer.print_entry(bc_txn, file=self.instance_ledger)
                    self.instance_ledger.flush()
                    self.add_file(self.instance_ledger_name)
            except subprocess.SubprocessError:
                self.pull_changes()
            else:
                break

        changed_members = {}
        # Once it's durable, apply it to the live state
        for posting in bc_txn.postings:
            if posting.account in self.accounts_raw:
                member = self.accounts_raw[posting.account]
                member.balance.add_amount(posting.units)
                changed_members[member.internal_name] = member
        return list(changed_members.values())
Ejemplo n.º 4
0
def validate_check_transaction_balances(entries, options_map):
    """Check again that all transaction postings balance, as users may have
    transformed transactions.

    Args:
      entries: A list of directives.
      unused_options_map: An options map.
    Returns:
      A list of new errors, if any were found.
    """
    # Note: this is a bit slow; we could limit our checks to the original
    # transactions by using the hash function in the loader.
    errors = []
    for entry in entries:
        if isinstance(entry, Transaction):
            # IMPORTANT: This validation is _crucial_ and cannot be skipped.
            # This is where we actually detect and warn on unbalancing
            # transactions. This _must_ come after the user routines, because
            # unbalancing input is legal, as those types of transactions may be
            # "fixed up" by a user-plugin. In other words, we want to allow
            # users to input unbalancing transactions as long as the final
            # transactions objects that appear on the stream (after processing
            # the plugins) are balanced. See {9e6c14b51a59}.
            #
            # Detect complete sets of postings that have residual balance;
            residual = interpolate.compute_residual(entry.postings)
            tolerances = interpolate.infer_tolerances(entry.postings, options_map)
            if not residual.is_small(tolerances):
                errors.append(
                    ValidationError(entry.meta,
                                    "Transaction does not balance: {}".format(residual),
                                    entry))

    return errors
Ejemplo n.º 5
0
def simple_interpolation(entries, options_map):
    """Run a local interpolation on a list of incomplete entries from the parser.

    Note: this does not take previous positions into account.

    !WARNING!!! This destructively modifies some of the Transaction entries directly.

    Args:
      incomplete_entries: A list of directives, with some postings possibly left
        with incomplete amounts as produced by the parser.
      options_map: An options dict as produced by the parser.
    Returns:
      A pair of
        entries: A list of interpolated entries with all their postings completed.
        errors: New errors produced during interpolation.
    """
    entries_with_lots, errors = convert_lot_specs_to_lots(entries, options_map)

    for entry in entries_with_lots:
        if not isinstance(entry, Transaction):
            continue
        # Balance incomplete auto-postings and set the parent link to this
        # entry as well.
        balance_errors = interpolate.balance_incomplete_postings(entry, options_map)
        if balance_errors:
            errors.extend(balance_errors)

        # Check that the balance actually is empty.
        if __sanity_checks__:
            residual = interpolate.compute_residual(entry.postings)
            tolerances = interpolate.infer_tolerances(entry.postings, options_map)
            assert residual.is_small(tolerances, options_map['default_tolerance']), (
                "Invalid residual {}".format(residual))

    return entries_with_lots, errors
Ejemplo n.º 6
0
    def test_compute_residual(self):

        # Try with two accounts.
        residual = interpolate.compute_residual([
            P(None, "Assets:Bank:Checking", "105.50", "USD"),
            P(None, "Assets:Bank:Checking", "-194.50", "USD"),
            ])
        self.assertEqual(inventory.from_string("-89 USD"), residual.units())

        # Try with more accounts.
        residual = interpolate.compute_residual([
            P(None, "Assets:Bank:Checking", "105.50", "USD"),
            P(None, "Assets:Bank:Checking", "-194.50", "USD"),
            P(None, "Assets:Bank:Investing", "5", "AAPL"),
            P(None, "Assets:Bank:Savings", "89.00", "USD"),
            ])
        self.assertEqual(inventory.from_string("5 AAPL"), residual.units())
Ejemplo n.º 7
0
def interpolate_group(postings, balances, currency, tolerances):
    """Interpolate missing numbers in the set of postings.

    Args:
      postings: A list of Posting instances.
      balances: A dict of account to its ante-inventory.
      currency: The weight currency of this group, used for reporting errors.
      tolerances: A dict of currency to tolerance values.
    Returns:
      A tuple of
        postings: A list of new posting instances.
        errors: A list of errors generated during interpolation.
        interpolated: A boolean, true if we did have to interpolate.

      In the case of an error, this returns the original list of postings, which
      is still incomplete. If an error is returned, you should probably skip the
      transaction altogether, or just not include the postings in it. (An
      alternative behaviour would be to return only the list of valid postings,
      but that would likely result in an unbalanced transaction. We do it this
      way by choice.)
    """
    errors = []

    # Figure out which type of amount is missing, by creating a list of
    # incomplete postings and which type of units is missing.
    incomplete = []
    for index, posting in enumerate(postings):
        units = posting.units
        cost = posting.cost
        price = posting.price

        # Identify incomplete parts of the Posting components.
        if units.number is MISSING:
            incomplete.append((MissingType.UNITS, index))

        if isinstance(cost, CostSpec):
            if cost and cost.number_per is MISSING:
                incomplete.append((MissingType.COST_PER, index))
            if cost and cost.number_total is MISSING:
                incomplete.append((MissingType.COST_TOTAL, index))
        else:
            # Check that a resolved instance of Cost never needs interpolation.
            #
            # Note that in theory we could support the interpolation of regular
            # per-unit costs in these if we wanted to; but because they're all
            # reducing postings that have been booked earlier, those never need
            # to be interpolated.
            if cost is not None:
                assert isinstance(cost.number, Decimal), (
                    "Internal error: cost has no number: {}; on postings: {}".
                    format(cost, postings))

        if price and price.number is MISSING:
            incomplete.append((MissingType.PRICE, index))

    # The replacement posting for the incomplete posting of this group.
    new_posting = None

    if len(incomplete) == 0:
        # If there are no missing numbers, just convert the CostSpec to Cost and
        # return that.
        out_postings = [
            convert_costspec_to_cost(posting) for posting in postings
        ]

    elif len(incomplete) > 1:
        # If there is more than a single value to be interpolated, generate an
        # error and return no postings.
        _, posting_index = incomplete[0]
        errors.append(
            InterpolationError(
                postings[posting_index].meta,
                "Too many missing numbers for currency group '{}'".format(
                    currency), None))
        out_postings = []

    else:
        # If there is a single missing number, calculate it and fill it in here.
        missing, index = incomplete[0]
        incomplete_posting = postings[index]

        # Convert augmenting postings' costs from CostSpec to corresponding Cost
        # instances, except for the incomplete posting.
        new_postings = [(posting if posting is incomplete_posting else
                         convert_costspec_to_cost(posting))
                        for posting in postings]

        # Compute the balance of the other postings.
        residual = interpolate.compute_residual(
            posting for posting in new_postings
            if posting is not incomplete_posting)
        assert len(
            residual) < 2, "Internal error in grouping postings by currencies."
        if not residual.is_empty():
            respos = next(iter(residual))
            assert respos.cost is None, (
                "Internal error; cost appears in weight calculation.")
            assert respos.units.currency == currency, (
                "Internal error; residual different than currency group.")
            weight = -respos.units.number
            weight_currency = respos.units.currency
        else:
            weight = ZERO
            weight_currency = currency

        if missing == MissingType.UNITS:
            units = incomplete_posting.units
            cost = incomplete_posting.cost
            if cost:
                # Handle the special case where we only have total cost.
                if cost.number_per == ZERO:
                    errors.append(
                        InterpolationError(
                            incomplete_posting.meta,
                            "Cannot infer per-unit cost only from total",
                            None))
                    return postings, errors, True

                assert cost.currency == weight_currency, (
                    "Internal error; residual currency different than missing currency."
                )
                cost_total = cost.number_total or ZERO
                units_number = (weight - cost_total) / cost.number_per

            elif incomplete_posting.price:
                assert incomplete_posting.price.currency == weight_currency, (
                    "Internal error; residual currency different than missing currency."
                )
                units_number = weight / incomplete_posting.price.number

            else:
                assert units.currency == weight_currency, (
                    "Internal error; residual currency different than missing currency."
                )
                units_number = weight

            # Quantize the interpolated units if necessary.
            units_number = interpolate.quantize_with_tolerance(
                tolerances, units.currency, units_number)

            if weight != ZERO:
                new_pos = Position(Amount(units_number, units.currency), cost)
                new_posting = incomplete_posting._replace(units=new_pos.units,
                                                          cost=new_pos.cost)
            else:
                new_posting = None

        elif missing == MissingType.COST_PER:
            units = incomplete_posting.units
            cost = incomplete_posting.cost
            assert cost.currency == weight_currency, (
                "Internal error; residual currency different than missing currency."
            )
            if units.number != ZERO:
                number_per = (weight -
                              (cost.number_total or ZERO)) / units.number
                new_cost = cost._replace(number_per=number_per)
                new_pos = Position(units, new_cost)
                new_posting = incomplete_posting._replace(units=new_pos.units,
                                                          cost=new_pos.cost)
            else:
                new_posting = None

        elif missing == MissingType.COST_TOTAL:
            units = incomplete_posting.units
            cost = incomplete_posting.cost
            assert cost.currency == weight_currency, (
                "Internal error; residual currency different than missing currency."
            )
            number_total = (weight - cost.number_per * units.number)
            new_cost = cost._replace(number_total=number_total)
            new_pos = Position(units, new_cost)
            new_posting = incomplete_posting._replace(units=new_pos.units,
                                                      cost=new_pos.cost)

        elif missing == MissingType.PRICE:
            units = incomplete_posting.units
            cost = incomplete_posting.cost
            if cost is not None:
                errors.append(
                    InterpolationError(
                        incomplete_posting.meta,
                        "Cannot infer price for postings with units held at cost",
                        None))
                return postings, errors, True
            else:
                price = incomplete_posting.price
                assert price.currency == weight_currency, (
                    "Internal error; residual currency different than missing currency."
                )
                new_price_number = abs(weight / units.number)
                new_posting = incomplete_posting._replace(
                    price=Amount(new_price_number, price.currency))

        else:
            assert False, "Internal error; Invalid missing type."

        # Replace the number in the posting.
        if new_posting is not None:
            # Set meta-data on the new posting to indicate it was interpolated.
            if new_posting.meta is None:
                new_posting = new_posting._replace(meta={})
            new_posting.meta[interpolate.AUTOMATIC_META] = True

            # Convert augmenting posting costs from CostSpec to a corresponding
            # Cost instance.
            new_postings[index] = convert_costspec_to_cost(new_posting)
        else:
            del new_postings[index]
        out_postings = new_postings

    assert all(not isinstance(posting.cost, CostSpec)
               for posting in out_postings)

    # Check that units are non-zero and that no cost remains negative; issue an
    # error if this is the case.
    for posting in out_postings:
        if posting.cost is None:
            continue
        # If there is a cost, we don't allow either a cost value of zero,
        # nor a zero number of units. Note that we allow a price of zero as
        # the only special case allowed (for conversion entries), but never
        # for costs.
        if posting.units.number == ZERO:
            errors.append(
                InterpolationError(
                    posting.meta, 'Amount is zero: "{}"'.format(posting.units),
                    None))
        if posting.cost.number is not None and posting.cost.number < ZERO:
            errors.append(
                InterpolationError(
                    posting.meta,
                    'Cost is negative: "{}"'.format(posting.cost), None))

    return out_postings, errors, (new_posting is not None)
def render_entry_context(entries, options_map, entry):
    """Render the context before and after a particular transaction is applied.

    Args:
      entries: A list of directives.
      options_map: A dict of options, as produced by the parser.
      entry: The entry instance which should be rendered. (Note that this object is
        expected to be in the set of entries, not just structurally equal.)
    Returns:
      A multiline string of text, which consists of the context before the
      transaction is applied, the transaction itself, and the context after it
      is applied. You can just print that, it is in form that is intended to be
      consumed by the user.
    """
    oss = io.StringIO()

    meta = entry.meta
    print("Hash:{}".format(compare.hash_entry(entry)), file=oss)
    print("Location: {}:{}".format(meta["filename"], meta["lineno"]), file=oss)

    # Get the list of accounts sorted by the order in which they appear in the
    # closest entry.
    order = {}
    if isinstance(entry, data.Transaction):
        order = {
            posting.account: index
            for index, posting in enumerate(entry.postings)
        }
    accounts = sorted(getters.get_entry_accounts(entry),
                      key=lambda account: order.get(account, 10000))

    # Accumulate the balances of these accounts up to the entry.
    balance_before, balance_after = interpolate.compute_entry_context(
        entries, entry)

    # Create a format line for printing the contents of account balances.
    max_account_width = max(map(len, accounts)) if accounts else 1
    position_line = '{{:1}} {{:{width}}}  {{:>49}}'.format(
        width=max_account_width)

    # Print the context before.
    print(file=oss)
    print("------------ Balances before transaction", file=oss)
    print(file=oss)
    before_hashes = set()
    for account in accounts:
        positions = balance_before[account].get_positions()
        for position in positions:
            before_hashes.add((account, hash(position)))
            print(position_line.format('', account, str(position)), file=oss)
        if not positions:
            print(position_line.format('', account, ''), file=oss)
        print(file=oss)

    # Print the entry itself.
    print(file=oss)
    print("------------ Transaction", file=oss)
    print(file=oss)
    dcontext = options_map['dcontext']
    printer.print_entry(entry, dcontext, render_weights=True, file=oss)

    if isinstance(entry, data.Transaction):
        print(file=oss)

        # Print residuals.
        residual = interpolate.compute_residual(entry.postings)
        if not residual.is_empty():
            # Note: We render the residual at maximum precision, for debugging.
            print('Residual: {}'.format(residual), file=oss)

        # Dump the tolerances used.
        tolerances = interpolate.infer_tolerances(entry.postings, options_map)
        if tolerances:
            print('Tolerances: {}'.format(', '.join(
                '{}={}'.format(key, value)
                for key, value in sorted(tolerances.items()))),
                  file=oss)

        # Compute the total cost basis.
        cost_basis = inventory.Inventory(pos for pos in entry.postings
                                         if pos.cost is not None).reduce(
                                             convert.get_cost)
        if not cost_basis.is_empty():
            print('Basis: {}'.format(cost_basis), file=oss)

    # Print the context after.
    print(file=oss)
    print("------------ Balances after transaction", file=oss)
    print(file=oss)
    for account in accounts:
        positions = balance_after[account].get_positions()
        for position in positions:
            changed = (account, hash(position)) not in before_hashes
            print(position_line.format('*' if changed else '', account,
                                       str(position)),
                  file=oss)
        if not positions:
            print(position_line.format('', account, ''), file=oss)
        print(file=oss)

    return oss.getvalue()
Ejemplo n.º 9
0
def render_entry_context(entries, options_map, entry, parsed_entry=None):
    """Render the context before and after a particular transaction is applied.

    Args:
      entries: A list of directives.
      options_map: A dict of options, as produced by the parser.
      entry: The entry instance which should be rendered. (Note that this object is
        expected to be in the set of entries, not just structurally equal.)
      parsed_entry: An optional incomplete, parsed but not booked nor interpolated
        entry. If this is provided, this is used for inspecting the list of prior
        accounts and it is also rendered.
    Returns:
      A multiline string of text, which consists of the context before the
      transaction is applied, the transaction itself, and the context after it
      is applied. You can just print that, it is in form that is intended to be
      consumed by the user.
    """
    oss = io.StringIO()
    pr = functools.partial(print, file=oss)
    header = "** {} --------------------------------"

    meta = entry.meta
    pr(header.format("Transaction Id"))
    pr()
    pr("Hash:{}".format(compare.hash_entry(entry)))
    pr("Location: {}:{}".format(meta["filename"], meta["lineno"]))
    pr()
    pr()

    # Get the list of accounts sorted by the order in which they appear in the
    # closest entry.
    order = {}
    if parsed_entry is None:
        parsed_entry = entry
    if isinstance(parsed_entry, data.Transaction):
        order = {
            posting.account: index
            for index, posting in enumerate(parsed_entry.postings)
        }
    accounts = sorted(getters.get_entry_accounts(parsed_entry),
                      key=lambda account: order.get(account, 10000))

    # Accumulate the balances of these accounts up to the entry.
    balance_before, balance_after = interpolate.compute_entry_context(
        entries, entry, additional_accounts=accounts)

    # Create a format line for printing the contents of account balances.
    max_account_width = max(map(len, accounts)) if accounts else 1
    position_line = '{{:1}} {{:{width}}}  {{:>49}}'.format(
        width=max_account_width)

    # Print the context before.
    pr(header.format("Balances before transaction"))
    pr()
    before_hashes = set()
    average_costs = {}
    for account in accounts:
        balance = balance_before[account]

        pc_balances = balance.split()
        for currency, pc_balance in pc_balances.items():
            if len(pc_balance) > 1:
                average_costs[account] = pc_balance.average()

        positions = balance.get_positions()
        for position in positions:
            before_hashes.add((account, hash(position)))
            pr(position_line.format('', account, str(position)))
        if not positions:
            pr(position_line.format('', account, ''))
        pr()
    pr()

    # Print average cost per account, if relevant.
    if average_costs:
        pr(header.format("Average Costs"))
        pr()
        for account, average_cost in sorted(average_costs.items()):
            for position in average_cost:
                pr(position_line.format('', account, str(position)))
        pr()
        pr()

    # Print the entry itself.
    dcontext = options_map['dcontext']
    pr(header.format("Unbooked Transaction"))
    pr()
    if parsed_entry:
        printer.print_entry(parsed_entry,
                            dcontext,
                            render_weights=True,
                            file=oss)
    pr()

    pr(header.format("Transaction"))
    pr()
    printer.print_entry(entry, dcontext, render_weights=True, file=oss)
    pr()

    if isinstance(entry, data.Transaction):
        pr(header.format("Residual and Tolerances"))
        pr()

        # Print residuals.
        residual = interpolate.compute_residual(entry.postings)
        if not residual.is_empty():
            # Note: We render the residual at maximum precision, for debugging.
            pr('Residual: {}'.format(residual))

        # Dump the tolerances used.
        tolerances = interpolate.infer_tolerances(entry.postings, options_map)
        if tolerances:
            pr('Tolerances: {}'.format(', '.join(
                '{}={}'.format(key, value)
                for key, value in sorted(tolerances.items()))))

        # Compute the total cost basis.
        cost_basis = inventory.Inventory(pos for pos in entry.postings
                                         if pos.cost is not None).reduce(
                                             convert.get_cost)
        if not cost_basis.is_empty():
            pr('Basis: {}'.format(cost_basis))
        pr()
        pr()

    # Print the context after.
    pr(header.format("Balances after transaction"))
    pr()
    for account in accounts:
        positions = balance_after[account].get_positions()
        for position in positions:
            changed = (account, hash(position)) not in before_hashes
            print(position_line.format('*' if changed else '', account,
                                       str(position)),
                  file=oss)
        if not positions:
            pr(position_line.format('', account, ''))
        pr()

    return oss.getvalue()
Ejemplo n.º 10
0
def book(entries, options_map, unused_booking_methods):
    """Run a local interpolation on a list of incomplete entries from the parser.

    Note: this does not take previous positions into account.

    !WARNING!!! This destructively modifies some of the Transaction entries directly.

    Args:
      incomplete_entries: A list of directives, with some postings possibly left
        with incomplete amounts as produced by the parser.
      options_map: An options dict as produced by the parser.
      unused_booking_methods: (Unused.)
    Returns:
      A pair of
        entries: A list of interpolated entries with all their postings completed.
        errors: New errors produced during interpolation.
    """
    # Perform simple booking, that is convert the CostSpec instances to Cost,
    # not even looking at inventory contents.
    entries_with_lots, errors = convert_lot_specs_to_lots(entries)

    new_entries = []
    for entry in entries_with_lots:
        if isinstance(entry, Transaction):

            # Check that incompleteness may only occur at the posting level; reject
            # otherwise. These cases are handled by the FULL booking algorithm but
            # not so well by the SIMPLE algorithm. The new parsing is more liberal
            # than this old code can handle, so explicitly reject cases where it
            # would fail.
            skip = False
            for posting in entry.postings:
                units = posting.units
                if units is not MISSING:
                    if units.number is MISSING or units.currency is MISSING:
                        errors.append(
                            SimpleBookingError(
                                entry.meta,
                                "Missing number or currency on units not handled",
                                None))
                        skip = True
                        break

                price = posting.price
                if price is not None:
                    if price.number is MISSING or price.currency is MISSING:
                        errors.append(
                            SimpleBookingError(
                                entry.meta,
                                "Missing number or currency on price not handled",
                                None))
                        skip = True
                        break
            if skip:
                continue

            # Balance incomplete auto-postings and set the parent link to this
            # entry as well.
            balance_errors = balance_incomplete_postings(entry, options_map)
            if balance_errors:
                errors.extend(balance_errors)

            # Check that the balance actually is empty.
            if __sanity_checks__:
                residual = interpolate.compute_residual(entry.postings)
                tolerances = interpolate.infer_tolerances(
                    entry.postings, options_map)
                assert residual.is_small(
                    tolerances), "Invalid residual {}".format(residual)

        new_entries.append(entry)

    return new_entries, errors