def test_quantities(self): pos = Position(Lot("USD", None, None), D('10')) self.assertEqual(A('10 USD'), pos.get_units()) self.assertEqual(A('10 USD'), pos.get_cost()) self.assertEqual(A('10 USD'), pos.get_weight()) self.assertEqual(A('16 AUD'), pos.get_weight(A('1.6 AUD'))) pos = Position(Lot("USD", A('1.5 AUD'), None), D('10')) self.assertEqual(A('10 USD'), pos.get_units()) self.assertEqual(A('15 AUD'), pos.get_cost()) self.assertEqual(A('15 AUD'), pos.get_weight()) self.assertEqual(A('15 AUD'), pos.get_weight(A('1.6 AUD'))) cost_pos = pos.cost() self.assertEqual(A('15 AUD'), cost_pos.get_units()) self.assertEqual(A('15 AUD'), cost_pos.get_cost()) self.assertEqual(A('15 AUD'), cost_pos.get_weight()) with self.assertRaises(AssertionError): self.assertEqual(A('15 AUD'), cost_pos.get_weight(A('1.6 AUD')))
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)