def test_constructors(self):
     Position(Lot('USD', None, None), D('123.45'))
     Position(Lot('USD', A('74.00 CAD'), None), D('123.45'))
     Position(Lot('USD', A('74.00 CAD'), date(2013, 2, 3)), D('123.45'))
     with self.assertRaises(Exception):
         Position(None, D('123.45'))
     Position(Lot('USD', None, None), None)
    def test_from_amounts(self):
        pos = from_amounts(A('10.00 USD'))
        self.assertEqual(Position(Lot("USD", None, None), D('10')), pos)

        pos = from_amounts(A('10 HOOL'), A('510.00 USD'))
        self.assertEqual(Position(Lot("HOOL", A('510 USD'), None), D('10')),
                         pos)
Exemple #3
0
    def test_from_amounts(self):
        pos = from_amounts(A('10.00 USD'))
        self.assertEqual(Position(A("10 USD")), pos)

        pos = from_amounts(A('10 HOOL'), A('510.00 USD'))
        self.assertEqual(
            Position(A("10 HOOL"), Cost(D('510'), 'USD', None, None)), pos)
Exemple #4
0
 def test_currency_pair(self):
     self.assertEqual(("USD", None),
                      Position(A('100 USD'), None).currency_pair())
     self.assertEqual(("AAPL", "USD"),
                      Position(A('100 AAPL'),
                               Cost('43.23', 'USD', None,
                                    None)).currency_pair())
    def test_compare_zero_to_none(self):
        pos1 = Position(Lot("CAD", None, None), ZERO)
        pos_none = None
        self.assertEqual(pos1, pos_none)
        self.assertEqual(pos_none, pos1)

        pos2 = Position(Lot("USD", None, None), ZERO)
        self.assertNotEqual(pos1, pos2)
Exemple #6
0
    def test_quantities(self):
        pos = Position(A("10 USD"), None)
        self.assertEqual(A('10 USD'), pos.units)
        self.assertEqual(A('10 USD'), convert.get_cost(pos))

        pos = Position(A("10 USD"), Cost(D('1.5'), 'AUD', None, None))
        self.assertEqual(A('10 USD'), pos.units)
        self.assertEqual(A('15 AUD'), convert.get_cost(pos))
Exemple #7
0
    def test_compare_zero_to_none(self):
        pos1 = Position(Amount(ZERO, "CAD"), None)
        pos_none = None
        self.assertEqual(pos1, pos_none)
        self.assertEqual(pos_none, pos1)

        pos2 = Position(Amount(ZERO, "USD"), None)
        self.assertNotEqual(pos1, pos2)
Exemple #8
0
    def add_amount(self, units, cost=None):
        """Add to this inventory using amount and cost. This adds with strict lot
        matching, that is, no partial matches are done on the arguments to the
        keys of the inventory.

        Args:
          units: An Amount instance to add.
          cost: An instance of Cost or None, as a key to the inventory.
        Returns:
          A pair of (position, booking) where 'position' is the position that
          that was modified BEFORE it was modified, and where 'booking' is a
          Booking enum that hints at how the lot was booked to this inventory.
          Position may be None if there is no corresponding Position object,
          e.g. the position was deleted.
        """
        if ASSERTS_TYPES:
            assert isinstance(
                units, Amount), ("Internal error: {!r} (type: {})".format(
                    units,
                    type(units).__name__))
            assert cost is None or isinstance(
                cost, Cost), ("Internal error: {!r} (type: {})".format(
                    cost,
                    type(cost).__name__))

        # Find the position.
        key = (units.currency, cost)
        pos = self.get(key, None)

        if pos is not None:
            # Note: In order to augment or reduce, all the fields have to match.

            # Check if reducing.
            booking = (Booking.REDUCED
                       if not same_sign(pos.units.number, units.number) else
                       Booking.AUGMENTED)

            # Compute the new number of units.
            number = pos.units.number + units.number
            if number == ZERO:
                # If empty, delete the position.
                del self[key]
            else:
                # Otherwise update it.
                self[key] = Position(Amount(number, units.currency), cost)
        else:
            # If not found, create a new one.
            if units.number == ZERO:
                booking = Booking.IGNORED
            else:
                self[key] = Position(units, cost)
                booking = Booking.CREATED

        return pos, booking
Exemple #9
0
    def test_eq_and_sortkey__bycost(self):
        pos1 = Position(A("1 USD"), None)
        pos2 = Position(A("1 USD"), Cost(D('10'), 'USD', None, None))
        pos3 = Position(A("1 USD"), Cost(D('11'), 'USD', None, None))
        pos4 = Position(A("1 USD"), Cost(D('12'), 'USD', None, None))

        positions = [pos3, pos2, pos1, pos4]
        self.assertEqual([pos1, pos2, pos3, pos4], sorted(positions))

        for _ in range(64):
            random.shuffle(positions)
            self.assertEqual([pos1, pos2, pos3, pos4], sorted(positions))
Exemple #10
0
    def test_eq_and_sortkey__bycost(self):
        pos1 = Position(Lot("USD", None, None), D('1'))
        pos2 = Position(Lot("USD", A('10 USD'), None), D('1'))
        pos3 = Position(Lot("USD", A('11 USD'), None), D('1'))
        pos4 = Position(Lot("USD", A('12 USD'), None), D('1'))

        positions = [pos3, pos2, pos1, pos4]
        self.assertEqual([pos1, pos2, pos3, pos4], sorted(positions))

        for _ in range(64):
            random.shuffle(positions)
            self.assertEqual([pos1, pos2, pos3, pos4], sorted(positions))
Exemple #11
0
def create_simple_posting_with_cost(entry, account,
                                    number, currency,
                                    cost_number, cost_currency):
    """Create a simple posting on the entry, with just a number and currency (no cost).

    Args:
      entry: The entry instance to add the posting to.
      account: A string, the account to use on the posting.
      number: A Decimal number or string to use in the posting's Amount.
      currency: A string, the currency for the Amount.
      cost_number: A Decimal number or string to use for the posting's cost Amount.
      cost_currency: a string, the currency for the cost Amount.
    Returns:
      An instance of Posting, and as a side-effect the entry has had its list of
      postings modified with the new Posting instance.
    """
    if isinstance(account, str):
        pass
    if not isinstance(number, Decimal):
        number = D(number)
    if cost_number and not isinstance(cost_number, Decimal):
        cost_number = D(cost_number)
    cost = Amount(cost_number, cost_currency)
    position = Position(Lot(currency, cost, None), number)
    posting = Posting(account, position, None, None, None)
    if entry is not None:
        entry.postings.append(posting)
    return posting
Exemple #12
0
    def __neg__(self):
        """Return an inventory with the negative of values of this one.

        Returns:
          An instance of Inventory.
        """
        return Inventory(
            [Position(position.lot, -(position.number)) for position in self])
Exemple #13
0
    def test_eq_and_sortkey(self):
        pos1 = Position(Lot("USD", None, None), D('200'))
        pos2 = Position(Lot("USD", None, None), D('201'))
        pos3 = Position(Lot("CAD", None, None), D('100'))
        pos4 = Position(Lot("CAD", None, None), D('101'))
        pos5 = Position(Lot("ZZZ", None, None), D('50'))
        positions = [pos5, pos4, pos3, pos2, pos1]
        positions.sort()

        self.assertTrue(pos1 < pos2)
        self.assertTrue(pos2 < pos3)
        self.assertTrue(pos3 < pos4)
        self.assertTrue(pos4 < pos5)

        self.assertTrue(positions[0] is pos1)
        self.assertTrue(positions[1] is pos2)
        self.assertTrue(positions[2] is pos3)
        self.assertTrue(positions[3] is pos4)
        self.assertTrue(positions[4] is pos5)
Exemple #14
0
    def test_eq_and_sortkey(self):
        pos1 = Position(A("200 USD"), None)
        pos2 = Position(A("201 USD"), None)
        pos3 = Position(A("100 CAD"), None)
        pos4 = Position(A("101 CAD"), None)
        pos5 = Position(A("50 ZZZ"), None)
        positions = [pos5, pos4, pos3, pos2, pos1]
        positions.sort()

        self.assertTrue(pos1 < pos2)
        self.assertTrue(pos2 < pos3)
        self.assertTrue(pos3 < pos4)
        self.assertTrue(pos4 < pos5)

        self.assertTrue(positions[0] is pos1)
        self.assertTrue(positions[1] is pos2)
        self.assertTrue(positions[2] is pos3)
        self.assertTrue(positions[3] is pos4)
        self.assertTrue(positions[4] is pos5)
Exemple #15
0
    def reduce(self, reducer, *args):
        """Reduce inventory.

        Note that this returns a simple :class:`CounterInventory` with just
        currencies as keys.
        """
        counter = CounterInventory()
        for (currency, cost), number in self.items():
            pos = Position(Amount(number, currency), cost)
            amount = reducer(pos, *args)
            counter.add(amount.currency, amount.number)
        return counter
Exemple #16
0
    def __mul__(self, scalar):
        """Scale/multiply the contents of the inventory.

        Args:
          scalar: A Decimal.
        Returns:
          An instance of Inventory.
        """
        return Inventory([
            Position(position.lot, position.number * scalar)
            for position in self
        ])
Exemple #17
0
    def reduce(self, reducer: Callable[..., Amount],
               *args: Any) -> SimpleCounterInventory:
        """Reduce inventory.

        Note that this returns a simple :class:`CounterInventory` with just
        currencies as keys.
        """
        counter = SimpleCounterInventory()
        for (currency, cost), number in self.items():
            pos = Position(Amount(number, currency), cost)
            amount = reducer(pos, *args)
            assert amount.number is not None
            counter.add(amount.currency, amount.number)
        return counter
Exemple #18
0
    def position(self, filename, lineno, amount, lot_spec):
        """Process a position grammar rule.

        Args:
          filename: The current filename.
          lineno: The current line number.
          amount: An instance of Amount for the position.
          lot_spec: An instance of LotSpec.
        Returns:
          A new instance of Position.
        """
        if lot_spec is None:
            lot_spec = LotSpec(None, None, None, None, None)
        # FIXME: Remove this assert for performance reasons.
        assert isinstance(
            lot_spec,
            LotSpec), ("Invalid type for Position.lot: %s (%s)".format(
                type(lot_spec), lot_spec))
        return Position(lot_spec._replace(currency=amount.currency),
                        amount.number)
Exemple #19
0
    def _get_create_position(self, lot):
        """Find or create a position associated with the given lot.

        Args:
          lot: An instance of Lot to key by.
        Returns:
          An pair of
            found: An instance of Position, either the position that was found, or a new
              Position instance that was created for this lot.
            created: A boolean, true if the position had to be created.
        """
        for position in self:
            if position.lot == lot:
                found = position
                created = False
                break
        else:
            found = Position(lot, ZERO)
            self.append(found)
            created = True
        return found, created
def depreciate(entries, options_map, config):
    """Add depreciation entries for fixed assets.  See module docstring for more
    details and example"""

    config_obj = eval(config, {}, {})
    if not isinstance(config_obj, dict):
        raise RuntimeError("Invalid plugin configuration: should be a single dict.")

    depr_method = config_obj.pop('method', 'WDV')
    year_closing_month = config_obj.pop('year_closing_month', 12)
    half_depr = config_obj.pop('half_depr', True)
    depr_account = config_obj.pop('account', "Expenses:Depreciation")
    expense_subaccount = config_obj.pop('expense_subaccount', False)
    asset_subaccount = config_obj.pop('asset_subaccount', False)

    if depr_method not in ['WDV','CRA']:
        raise RuntimeError("Specified depreciation method in plugin not implemented")

    if not 0 < year_closing_month <= 12:
        raise RuntimeError("Invalid year-closing-month specified")

    errors = []
    depr_candidates = []
    for entry in entries:
        date = entry.date
        try:
            for p in entry.postings:
                if 'depreciation' in p.meta:
                    depr_candidates.append((date, p, entry))
        except (AttributeError):
            pass
    for date, posting, entry in depr_candidates:
        narration, rate = posting.meta['depreciation'].split('@')
        narration = narration.strip()
        rate = Decimal(rate)

        orig_val = posting.position.get_units()
        current_val = orig_val
        new_dates = get_closing_dates(date, year_closing_month)

        for d in new_dates:
            if depr_method == 'WDR':
                if half_depr and d - date < datetime.timedelta(180):
                    # Asset used for less than 180 days, use half the rate allowed.
                    rate_used = rate/2
                    narration_suffix = " - Half Depreciation (<180days)"
                else:
                    rate_used = rate
                    narration_suffix = ""

            elif depr_method == 'CRA':
                if half_depr and d < datetime.date(date.year+1, date.month, date.day):
                   # Asset purchased this year, use half of rate allowed
                    rate_used = rate/2
                    narration_suffix = " - Half Depreciation (Same year)"
                else:
                    rate_used = rate
                    narration_suffix = ""

            multiplier = Decimal(config_obj.get(str(d.year),1))
            rate_used = rate_used*multiplier
            current_depr = amount_mult(current_val, rate_used)

            account = posting.account
            if asset_subaccount:
                account += ":Depreciation"

            depr_account_used = depr_account
            if expense_subaccount:
                depr_account_used = depr_account + ":" + narration.split(" ")[0]

            p1 = data.Posting(account=account,
                              price=None,
                              meta=None,
                              flag=None,
                              position=Position.from_amounts(amount_mult(current_depr, Decimal(-1))))
            p2 = data.Posting(account=depr_account_used,
                              price=None,
                              meta=None,
                              flag=None,
                              position=Position.from_amounts(current_depr))

            e = entry._replace(narration=narration + narration_suffix,
                               date=d,
                               flag='*',
                               payee=None,
                               tags={'AUTO-DEPRECIATION'},
                               postings=[p1, p2])
            entries.append(e)

            current_val = amount_sub(current_val, current_depr)

    return entries, errors
Exemple #21
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)
Exemple #22
0
 def test_from_string__with_everything(self):
     pos = from_string(
         '20 HOOL {*, 532.43 # 20.00 USD, "e4dc1a361022", 2014-06-15}')
     cost = A('533.43 USD')
     lot_date = datetime.date(2014, 6, 15)
     self.assertEqual(Position(Lot("HOOL", cost, lot_date), D('20')), pos)
Exemple #23
0
 def test_from_string__with_merge_cost_spec(self):
     pos = from_string('1.1 HOOL {*}')
     self.assertEqual(Position(Lot("HOOL", None, None), D('1.1')), pos)
Exemple #24
0
 def test_from_string__with_compound_cost(self):
     pos = from_string('1.1 HOOL {500.00 # 11.00 USD}')
     self.assertEqual(
         Position(Lot("HOOL", A('510.00 USD'), None), D('1.1')), pos)
Exemple #25
0
 def test_from_string__with_label(self):
     pos = from_string('2.2 HOOL {"78c3f7f1315b"}')
     self.assertEqual(Position(Lot("HOOL", None, None), D('2.2')), pos)
Exemple #26
0
def get_incomplete_postings(entry, options_map):
    """Balance an entry with auto-postings and return an updated list of completed postings.

    Returns a new list of balanced postings, with the incomplete postings
    replaced with completed ones. This is probably the only place where there
    is a bit of non-trivial logic in this entire project (and the rewrite was
    to make sure it was *that* simple.)

    Note that inferred postings are tagged via metatada with an '__automatic__'
    field added to them with a true boolean value.

    Note: The 'postings' parameter may be modified or destroyed for performance
    reasons; don't reuse it.

    Args:
      entry: An instance of a valid directive.
      options_map: A dict of options, as produced by the parser.
    Returns:
      A tuple of:
        postings: a list of new postings to replace the entry's unbalanced
          postings.
        inserted: A boolean set to true if we've inserted new postings.
        errors: A list of balance errors generated during the balancing process.
        residual: A Inventory instance, the residual amounts after balancing
          the postings.
        tolerances: The tolerances inferred in the process, using the postings
          provided.
    """
    # Make a copy of the original list of postings.
    postings = list(entry.postings)

    # Errors during balancing.
    balance_errors = []

    # The list of postings without and with an explicit position.
    auto_postings_indices = []

    # Currencies seen in complete postings.
    currencies = set()

    # An inventory to accumulate the residual balance.
    residual = Inventory()

    # A dict of values for default tolerances.
    if options_map['use_legacy_fixed_tolerances']:
        # This is supported only to support an easy transition for users.
        # Users should be able to revert to this easily.
        tolerances = {}
        default_tolerances = LEGACY_DEFAULT_TOLERANCES
    else:
        tolerances = infer_tolerances(postings, options_map)
        default_tolerances = options_map['default_tolerance']

    # Process all the postings.
    has_nonzero_amount = False
    has_regular_postings = False
    for i, posting in enumerate(postings):
        position = posting.position

        if position is None:
            # This posting will have to get auto-completed.
            auto_postings_indices.append(i)
        else:
            currencies.add(position.lot.currency)

            # Compute the amount to balance and update the inventory.
            weight = get_posting_weight(posting)
            residual.add_amount(weight)

            has_regular_postings = True
            if weight:
                has_nonzero_amount = True

    # If there are auto-postings, fill them in.
    has_inserted = False
    if auto_postings_indices:

        # If there are too many such postings, we can't do anything, barf.
        if len(auto_postings_indices) > 1:
            balance_errors.append(
                BalanceError(entry.meta,
                             "Too many auto-postings; cannot fill in", entry))
            # Delete the redundant auto-postings.
            for index in sorted(auto_postings_indices[1:], reverse=1):
                del postings[index]

        index = auto_postings_indices[0]
        old_posting = postings[index]
        assert old_posting.price is None

        residual_positions = residual.get_positions()

        # If there are no residual positions, we want to still insert a posting
        # but with a zero position for each currency, so that the posting shows
        # up anyhow. We insert one such posting for each currency seen in the
        # complete postings. Note: if all the non-auto postings are zero, we
        # want to avoid sending a warning; the input text clearly implies the
        # author knows this would be useless.
        new_postings = []
        if not residual_positions and (
            (has_regular_postings and has_nonzero_amount)
                or not has_regular_postings):
            balance_errors.append(
                BalanceError(entry.meta,
                             "Useless auto-posting: {}".format(residual),
                             entry))
            for currency in currencies:
                position = Position(Lot(currency, None, None), ZERO)
                meta = copy.copy(old_posting.meta) if old_posting.meta else {}
                meta[AUTOMATIC_META] = True
                new_postings.append(
                    Posting(old_posting.account, position, None,
                            old_posting.flag, old_posting.meta))
                has_inserted = True
        else:
            # Convert all the residual positions in inventory into a posting for
            # each position.
            for position in residual_positions:
                position = -position

                # Applying rounding to the default tolerance, if there is one.
                tolerance = inventory.get_tolerance(tolerances,
                                                    default_tolerances,
                                                    position.lot.currency)
                if tolerance:
                    quantum = (tolerance * 2).normalize()

                    # If the tolerance is a neat number provided by the user,
                    # quantize the inferred numbers. See doc on quantize():
                    #
                    # Unlike other operations, if the length of the coefficient
                    # after the quantize operation would be greater than
                    # precision, then an InvalidOperation is signaled. This
                    # guarantees that, unless there is an error condition, the
                    # quantized exponent is always equal to that of the
                    # right-hand operand.
                    if len(quantum.as_tuple().digits) < MAX_TOLERANCE_DIGITS:
                        position.number = position.number.quantize(quantum)

                meta = copy.copy(old_posting.meta) if old_posting.meta else {}
                meta[AUTOMATIC_META] = True
                new_postings.append(
                    Posting(old_posting.account, position, None,
                            old_posting.flag, meta))
                has_inserted = True

                # Update the residuals inventory.
                weight = position.get_weight(None)
                residual.add_amount(weight)

        postings[index:index + 1] = new_postings

    else:
        # Checking for unbalancing transactions has been moved to the validation
        # stage, so although we already have the input transaction's residuals
        # conveniently precomputed here, we are postponing the check to allow
        # plugins to "fixup" unbalancing transactions. We want to allow users to
        # be able to input unbalancing transactions as long as the final
        # transactions objects that appear on the stream (after processing the
        # plugins) are balanced. See {9e6c14b51a59}.
        pass

    return (postings, has_inserted, balance_errors, residual, tolerances)
Exemple #27
0
 def test_from_string__with_spaces(self):
     pos = from_string(' - 111.2934  CAD ')
     self.assertEqual(Position(Lot("CAD", None, None), D('-111.2934')), pos)
    def test_from_string(self):
        inv = inventory.from_string('')
        self.assertEqual(Inventory(), inv)

        inv = inventory.from_string('10 USD')
        self.assertEqual(
            Inventory([Position(Lot("USD", None, None), D('10'))]),
            inv)

        inv = inventory.from_string(' 10.00  USD ')
        self.assertEqual(
            Inventory([Position(Lot("USD", None, None), D('10'))]),
            inv)

        inv = inventory.from_string('1 USD, 2 CAD')
        self.assertEqual(
            Inventory([Position(Lot("USD", None, None), D('1')),
                       Position(Lot("CAD", None, None), D('2'))]),
            inv)

        inv = inventory.from_string('2.2 HOOL {532.43 USD}, 3.413 EUR')
        self.assertEqual(
            Inventory([Position(Lot("HOOL", A('532.43 USD'), None),
                                D('2.2')),
                       Position(Lot("EUR", None, None), D('3.413'))]),
            inv)

        inv = inventory.from_string(
            '2.2 HOOL {532.43 USD}, 2.3 HOOL {564.00 USD, 2015-07-14}, 3.413 EUR')
        self.assertEqual(
            Inventory([Position(Lot("HOOL", A('532.43 USD'), None),
                                D('2.2')),
                       Position(Lot("HOOL", A('564.00 USD'), datetime.date(2015, 7, 14)),
                                D('2.3')),
                       Position(Lot("EUR", None, None),
                                D('3.413'))]),
            inv)

        inv = inventory.from_string(
            '1.1 HOOL {500.00 # 11.00 USD}, 100 CAD')
        self.assertEqual(
            Inventory([Position(Lot("HOOL", A('510.00 USD'), None),
                                D('1.1')),
                       Position(Lot("CAD", None, None),
                                D('100'))]),
            inv)
Exemple #29
0
 def test_from_string__with_cost(self):
     pos = from_string('2.2 HOOL {532.43 USD}')
     cost = A('532.43 USD')
     self.assertEqual(Position(Lot("HOOL", cost, None), D('2.2')), pos)
Exemple #30
0
    def test_from_string(self):
        inv = inventory.from_string('')
        self.assertEqual(Inventory(), inv)

        inv = inventory.from_string('10 USD')
        self.assertEqual(
            Inventory([Position(A("10 USD"))]),
            inv)

        inv = inventory.from_string(' 10.00  USD ')
        self.assertEqual(
            Inventory([Position(A("10 USD"))]),
            inv)

        inv = inventory.from_string('1 USD, 2 CAD')
        self.assertEqual(
            Inventory([Position(A("1 USD")),
                       Position(A("2 CAD"))]),
            inv)

        inv = inventory.from_string('2.2 HOOL {532.43 USD}, 3.413 EUR')
        self.assertEqual(
            Inventory([Position(A("2.2 HOOL"), Cost(D('532.43'), 'USD', None, None)),
                       Position(A("3.413 EUR"))]),
            inv)

        inv = inventory.from_string(
            '2.2 HOOL {532.43 USD}, 2.3 HOOL {564.00 USD, 2015-07-14}, 3.413 EUR')
        self.assertEqual(
            Inventory([Position(A("2.2 HOOL"),
                                Cost(D('532.43'), 'USD', None, None)),
                       Position(A("2.3 HOOL"),
                                Cost(D('564.00'), 'USD', datetime.date(2015, 7, 14), None)),
                       Position(A("3.413 EUR"))]),
            inv)

        inv = inventory.from_string(
            '1.1 HOOL {500.00 # 11.00 USD}, 100 CAD')
        self.assertEqual(
            Inventory([Position(A("1.1 HOOL"),
                                Cost(D('510.00'), 'USD', None, None)),
                       Position(A("100 CAD"))]),
            inv)
Exemple #31
0
 def test_from_string__with_cost_and_date(self):
     pos = from_string('2.2 HOOL {532.43 USD, 2014-06-15}')
     cost = A('532.43 USD')
     lot_date = datetime.date(2014, 6, 15)
     self.assertEqual(Position(Lot("HOOL", cost, lot_date), D('2.2')), pos)