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)
def Transaction(self, entry, oss): strings = [] # Insert a posting to absorb the residual if necessary. This is # sometimes needed because Ledger bases its balancing precision on the # *last* number of digits used on that currency. This is believed to be # a bug, so instead, we simply insert a rounding account to absorb the # residual and precisely balance the transaction. entry = interpolate.fill_residual_posting(entry, ROUNDING_ACCOUNT) if entry.tags: for tag in sorted(entry.tags): strings.append(';; Tag: #{}'.format(tag)) if entry.links: for link in sorted(entry.links): strings.append(';; Link: ^{}'.format(link)) # Compute the string for the payee and narration line. if entry.payee: strings.append('{} |'.format(entry.payee)) if entry.narration: strings.append(entry.narration) oss.write('{e.date:%Y/%m/%d} {flag} {}\n'.format(' '.join(strings), flag=entry.flag or '', e=entry)) for posting in entry.postings: self.Posting(posting, entry, oss)
def Transaction(self, entry): # Insert a posting to absorb the residual if necessary. This is # sometimes needed because Ledger bases its balancing precision on the # *last* number of digits used on that currency. This is believed to be # a bug, so instead, we simply insert a rounding account to absorb the # residual and precisely balance the transaction. entry = interpolate.fill_residual_posting(entry, ROUNDING_ACCOUNT) # Remove postings which wouldn't be displayed (due to precision # rounding amounts to 0.00) entry = filter_rounding_postings(entry, self.dformat) # Compute the string for the payee and narration line. strings = [] if entry.payee: strings.append(f"{ledger_str(entry.payee)} |") if entry.narration: strings.append(ledger_str(entry.narration)) meta = user_meta(entry.meta or {}) self.io.write(f"{entry.date:%Y-%m-%d}") auxdate_key = self.config.get("auxdate") if auxdate_key and isinstance(meta.get(auxdate_key), datetime.date): self.io.write(f"={meta[auxdate_key]:%Y-%m-%d}") del meta[auxdate_key] flag = ledger_flag(entry.flag) if flag: self.io.write(" " + flag) code_key = self.config.get("code") if code_key and not meta.get(code_key) is None: code = meta[code_key] self.io.write(" (" + str(code) + ")") del meta[code_key] payee = " ".join(strings) if payee: self.io.write(" " + payee) self.io.write("\n") indent = " " * self.config["indent"] if entry.tags: self.io.write(indent + "; {}:\n".format(":, ".join(sorted(entry.tags)))) if entry.links: self.io.write(indent + "; Link: {}\n".format(" ".join(sorted(entry.links)))) for key, val in meta.items(): meta = self.format_meta(key, val) if meta: self.io.write(indent + f"; {meta}\n") # If a posting without an amount is given and several amounts would # be added when balancing, beancount will create several postings. # But we ignore the amount on those postings (since they were added # by beancount and not the user), which means we may end up with # two or more postings with no amount, which is not valid. # Therefore, only take *one* posting by looking at the line number. seen = set() for posting in sorted(entry.postings, key=lambda p: get_lineno(p)): lineno = get_lineno(posting) if lineno is not None: if lineno in seen: continue seen.add(lineno) self.Posting(posting, entry)