def _make_transfer_trade_journal_entry(self, t: TradeConfirmation): return Transaction(meta=collections.OrderedDict(), date=t.settlement_date, flag='*', payee=self.payee, narration='Transfer due to stock sale', tags=EMPTY_SET, links=EMPTY_SET, postings=[ Posting( account=self.asset_cash_account, units=-t.net_amount, cost=None, meta=collections.OrderedDict([ (POSTING_DATE_KEY, t.settlement_date), (TRADE_REFERENCE_NUMBER_KEY, '>' + t.reference_number), ]), price=None, flag=None, ), Posting( account=FIXME_ACCOUNT, units=t.net_amount, cost=None, meta=None, price=None, flag=None, ), ])
def _make_transfer_journal_entry(self, r: Release): date = r.settlement_date or r.release_date return Transaction( meta=collections.OrderedDict(), date=date, flag='*', payee=self.payee, narration='Stock Vesting - %s' % r.transfer_description, tags=EMPTY_SET, links=EMPTY_SET, postings=[ Posting( account=self.asset_cash_account, units=-r.transfer_amount, cost=None, meta=collections.OrderedDict([ (POSTING_DATE_KEY, date), (AWARD_ID_KEY, '>' + r.award_id), (AWARD_NOTE_KEY, r.transfer_description), ]), price=None, flag=None, ), Posting( account=FIXME_ACCOUNT, units=r.transfer_amount, cost=None, meta=None, price=None, flag=None, ), ])
def test_add_ignored(tmpdir): journal_path = create_journal( tmpdir, """ 2015-01-01 * "Test transaction 1" Assets:Account-A 100 USD Assets:Account-B """) ignored_path = create_journal( tmpdir, """ 2015-03-01 * "Test transaction 2" Assets:Account-A 100 USD Assets:Account-B """, name='ignored.beancount') editor = journal_editor.JournalEditor(journal_path, ignored_path) stage = editor.stage_changes() new_transaction = Transaction( meta=None, date=datetime.date(2015, 4, 1), flag='*', payee=None, narration='New transaction', tags=EMPTY_SET, links=EMPTY_SET, postings=[ Posting( account='Assets:Account-A', units=Amount(Decimal(3), 'USD'), cost=None, price=None, flag=None, meta=None), Posting( account='Assets:Account-B', units=MISSING, cost=None, price=None, flag=None, meta=None), ], ) stage.add_entry(new_transaction, ignored_path) result = stage.apply() check_file_contents( journal_path, """ 2015-01-01 * "Test transaction 1" Assets:Account-A 100 USD Assets:Account-B """) check_file_contents( ignored_path, """ 2015-03-01 * "Test transaction 2" Assets:Account-A 100 USD Assets:Account-B 2015-04-01 * "New transaction" Assets:Account-A 3 USD Assets:Account-B """) check_journal_entries(editor)
def test_process_match(self): self.skipTest("wip") narration = "Amazon.com*MA4TS16T0" tx = Transaction(narration=narration, date=None, flag=None, payee=None, tags={}, links={}, postings=[ Posting(account="Liablities:Card", units=Amount(D(100), "USD"), cost=None, price=None, flag="*", meta={}), Posting(account="Expenses:FIXME", units=Amount(D(-100), "USD"), cost=None, price=None, flag="!", meta={}) ], meta={ 'file_name': '', 'lineno': 0 }) m = Matcher([AMAZON_RULE]) results = m.process([tx]) self.assertEqual(len(results), 1) result = results[0] print(yaml.dump(AMAZON_RULE))
def _make_import_result(mint_entry: MintEntry) -> ImportResult: transaction = Transaction( meta=None, date=mint_entry.date, flag=FLAG_OKAY, payee=None, narration=mint_entry.source_desc, tags=EMPTY_SET, links=EMPTY_SET, postings=[ Posting( account=mint_entry.account, units=mint_entry.amount, cost=None, price=None, flag=None, meta=collections.OrderedDict( source_desc=mint_entry.source_desc, date=mint_entry.date, )), Posting( account=FIXME_ACCOUNT, units=-mint_entry.amount, cost=None, price=None, flag=None, meta=None, ), ]) return ImportResult( date=mint_entry.date, info=get_info(mint_entry), entries=[transaction])
def _make_transaction(self, raw_txn: RawTransaction, link: bool, is_transfer: bool): amount = original_amount = amount_parsing.parse_amount( raw_txn[CSV_AMOUNT_TOTAL_KEY]) txn_type = raw_txn[CSV_TYPE_KEY] is_payment_txn = txn_type == 'Payment' or txn_type == 'Charge' if is_transfer and is_payment_txn: amount = -amount txn_time = parse_csv_date(raw_txn[CSV_DATETIME_KEY]) assets_posting = Posting( account=self.assets_account, units=amount, cost=None, price=None, flag=None, meta=collections.OrderedDict([ (VENMO_TRANSFER_KEY if is_transfer else VENMO_PAYMENT_KEY, raw_txn[CSV_ID_KEY]), ('date', txn_time.date()), (VENMO_TYPE_KEY, txn_type), ]), ) note = re.sub(r'\s+', ' ', raw_txn[CSV_NOTE_KEY]) payee = 'Venmo' if is_payment_txn: if original_amount.number > ZERO: payee = assets_posting.meta[VENMO_PAYER_KEY] = raw_txn[ CSV_FROM_KEY if txn_type == 'Payment' else CSV_TO_KEY] else: payee = assets_posting.meta[VENMO_PAYEE_KEY] = raw_txn[ CSV_TO_KEY if txn_type == 'Payment' else CSV_FROM_KEY] if note: assets_posting.meta[VENMO_DESCRIPTION_KEY] = note if is_transfer: assets_posting.meta[VENMO_ACCOUNT_DESCRIPTION_KEY] = raw_txn[ CSV_DESTINATION_KEY] or raw_txn[CSV_FUNDING_SOURCE_KEY] links = EMPTY_SET if link: links = frozenset(['venmo.%s' % raw_txn[CSV_ID_KEY]]) return Transaction( meta=None, date=txn_time.date(), flag=FLAG_OKAY, payee=payee, narration='Transfer' if is_transfer else note, tags=EMPTY_SET, links=links, postings=[ assets_posting, Posting( account=FIXME_ACCOUNT, units=-amount, cost=None, price=None, flag=None, meta=None, ), ], )
def execute(self, csv_line, transaction=None): return ( False, Transaction( meta=None, date=None, flag="*", payee=None, narration=None, tags=None, links=None, postings=[ Posting( account=None, units=None, cost=None, price=None, flag=None, meta=None, ), Posting( account=None, units=None, cost=None, price=None, flag=None, meta=None, ), ], ), )
def new_filtered_entries(tx, params, get_amounts, selected_postings, config): """ Beancount plugin: Dublicates all transaction's postings over time. Args: tx: A transaction instance. params: A parser options dict. get_amounts: A function, i.e. distribute_over_period. selected_postings: A list of postings. config: A configuration string in JSON format given in source file. Returns: An array of transaction entries. """ all_pairs = [] for _, new_account, params, posting in selected_postings: dates, amounts = get_amounts(params, tx.date, posting.units.number, config) all_pairs.append( (dates, amounts, posting, new_account) ) map_of_dates = {} for dates, amounts, posting, new_account in all_pairs: for i in range( min(len(dates), len(amounts)) ): if(not dates[i] in map_of_dates): map_of_dates[dates[i]] = [] amount = Amount(amounts[i], posting.units.currency) # Income/Expense to be spread map_of_dates[dates[i]].append(Posting(account=new_account, units=amount, cost=None, price=None, flag=posting.flag, meta=new_metadata(tx.meta['filename'], tx.meta['lineno']))) # Asset/Liability that buffers the difference map_of_dates[dates[i]].append(Posting(account=posting.account, units=mul(amount, D(-1)), cost=None, price=None, flag=posting.flag, meta=new_metadata(tx.meta['filename'], tx.meta['lineno']))) new_transactions = [] for i, (date, postings) in enumerate(sorted(map_of_dates.items())): if len(postings) > 0: e = Transaction( date=date, meta=tx.meta, flag=tx.flag, payee=tx.payee, narration=tx.narration + config['suffix']%(i+1, len(dates)), tags={config['tag']}, links=tx.links, postings=postings) new_transactions.append(e) return new_transactions
def make_import_result(receipt: Any, receipt_directory: str, link_prefix: str) -> ImportResult: receipt_id = str(receipt['id']) if receipt['date']: date = datetime.datetime.strptime(receipt['date'], date_format).date() else: date = dateutil.parser.parse(receipt['submitted_at']).date() merchant = receipt['merchant'] note = receipt['note'] if note: payee = merchant narration = note else: payee = None narration = merchant if receipt['total']: amount = Amount( number=D(receipt['total']), currency=receipt['currency_code']) else: amount = Amount( number=ZERO, currency=receipt['currency_code']) postings = [ Posting( account=FIXME_ACCOUNT, units=amount, cost=None, meta=None, price=None, flag=None, ), Posting( account=FIXME_ACCOUNT, units=-amount, cost=None, meta=None, price=None, flag=None, ) ] return ImportResult( date=date, entries=[ Transaction( meta=collections.OrderedDict(), date=date, flag='*', payee=payee, narration=narration, links=frozenset([link_prefix + receipt_id]), tags=frozenset(), postings=postings, ), ], info=dict( type='image/jpeg', filename=os.path.realpath( os.path.join(receipt_directory, receipt_id + '.jpg')), ), )
def make_import_result(purchase: Any, link_prefix: str, tz_info: Optional[datetime.tzinfo], html_path: str) -> ImportResult: purchase_id = str(purchase['id']) date = datetime.datetime.fromtimestamp(purchase['timestamp'] / 1000, tz_info).date() payment_processor = purchase['payment_processor'] merchant = purchase['merchant'] items = purchase['items'] payee = ' - '.join(x for x in [payment_processor, merchant] if x is not None) # type: Optional[str] narration = '; '.join(x for x in items) # type: Optional[str] if not narration: narration = payee payee = None if purchase['currency'] is None or purchase['units'] is None: pos_amount = neg_amount = Amount(D('0.00'), 'USD') else: pos_amount = Amount( round(D(purchase['units']), 2), purchase['currency']) neg_amount = -pos_amount postings = [ Posting( account=FIXME_ACCOUNT, units=pos_amount, cost=None, meta=None, price=None, flag=None, ), Posting( account=FIXME_ACCOUNT, units=neg_amount, cost=None, meta=None, price=None, flag=None, ) ] return ImportResult( date=date, entries=[ Transaction( meta=collections.OrderedDict(), date=date, flag='*', payee=payee, narration=narration, links=frozenset([link_prefix + purchase_id]), tags=frozenset(), postings=postings, ), ], info=dict( type='text/html', filename=os.path.realpath(html_path), ), )
def extract(self, file: _FileMemo, existing_entries=None) -> List[Transaction]: payslip = file.convert(pdftotext) income_tax = Posting(account='Expenses:Tax:Income', units=self._find_amount('PAYE Tax', payslip), cost=None, price=None, flag=None, meta=None) national_insurance = Posting(account='Expenses:Tax:NationalInsurance', units=self._find_amount( 'National Insurance', payslip), cost=None, price=None, flag=None, meta=None) student_loan = Posting(account='Liabilities:StudentFinance', units=self._find_amount('Student Loan', payslip), cost=None, price=None, flag=None, meta=None) if self.student_loan else None net_pay = Posting(account=f'Assets:{self.asset}', units=self._find_amount('Net Pay', payslip), cost=None, price=None, flag=None, meta=None) salary = Posting(account=f'Income:Salary:{self.employer}', units=None, cost=None, price=None, flag=None, meta=None) postings: List[Posting] = list( filter(None, [ income_tax, national_insurance, student_loan, net_pay, salary ])) txn = Transaction(meta=new_metadata(file.name, int(1)), date=find_date(payslip), flag=FLAG_OKAY, payee=self.employer, narration='Salary', tags=EMPTY_SET, links=EMPTY_SET, postings=postings) return [txn]
def _make_trade_journal_entry(self, t: TradeConfirmation): txn = Transaction(meta=collections.OrderedDict(), date=t.settlement_date, flag='*', payee=self.payee, narration='Sell', tags=EMPTY_SET, links=EMPTY_SET, postings=[]) txn.postings.append( Posting( account=self.get_stock_account(t), units=-t.quantity, cost=CostSpec(number_per=MISSING, number_total=None, currency=t.gross_amount.currency, date=None, label=None, merge=False), price=t.share_price, meta={ POSTING_DATE_KEY: t.trade_date, TRADE_REFERENCE_NUMBER_KEY: t.reference_number }, flag=None, )) txn.postings.append( Posting( account=self.capital_gains_account + ':' + t.symbol, units=MISSING, meta=None, cost=None, price=None, flag=None, )) txn.postings.append( Posting( account=self.fees_account, units=t.fees, cost=None, meta={ POSTING_DATE_KEY: t.trade_date, TRADE_REFERENCE_NUMBER_KEY: t.reference_number, }, price=None, flag=None, )) txn.postings.append( Posting( account=self.asset_cash_account, units=t.net_amount, cost=None, meta=None, price=None, flag=None, )) return txn
def test_merged_posting_is_cleared(self): postings = [ Posting(account="a", units=Amount(D(10), "USD"), cost=None, price=None, flag=None, meta=None), Posting(account="a", units=Amount(D(10), "USD"), cost=None, price=None, flag=None, meta={"merge": True}), ] source = DescriptionBasedSource(lambda s: None) self.assertEqual(source.is_posting_cleared(postings[0]), False) self.assertEqual(source.is_posting_cleared(postings[1]), True)
def test_aggregate_holdings_list_with_price(): holdings = [ Posting('Assets', A('10 HOOL'), None, None, None, None), Posting('Assets', A('14 TEST'), Cost(D('10'), 'HOOL', None, None), D('12'), None, None), ] assert aggregate_holdings_list(holdings) == \ Posting('Assets', Amount(D('24'), '*'), Cost(D('6.25'), 'HOOL', None, None), D('178') / 24, None, None)
def get_postings(self) -> List[Posting]: postings = [ Posting( account=self.get_primary_account(), units=-Amount(Decimal(str(self.quantity)), currency=self.symbol), # TODO handle cost basis by parsing cost-basis lots CSV, so we don't end # up getting beancount errors due to ambiguity cost=CostSpec( number_per=MISSING, number_total=None, currency=MISSING, date=None, label=None, merge=None, ), price=Amount(self.price, currency="USD"), flag=None, meta=self.get_meta(), ), Posting( account=self.get_other_account(), units=self.amount, cost=None, price=None, flag=None, meta=self.get_meta(), ), ] if self.action != SchwabAction.SELL_TO_OPEN: # too early to realize gains/losses when opening a short position postings.append( Posting( account=self.get_cap_gains_account(), units=MISSING, cost=None, price=None, flag=None, meta={}, )) fees = self.fees if fees is not None: postings.append( Posting( account=self.fees_account, units=Amount(self.fees, currency="USD"), cost=None, price=None, flag=None, meta={}, )) return postings
def get_postings(self) -> List[Posting]: postings = [ Posting( account=self.get_primary_account(), units=Amount(Decimal(str(self.quantity)), currency=self.symbol), cost=CostSpec( number_per=self.price, number_total=None, currency="USD", date=None, label=None, merge=None, ), price=None, flag=None, meta=self.get_meta(), ), Posting( account=self.get_other_account(), units=self.amount, cost=None, price=None, flag=None, meta=self.get_meta(), ), ] if self.action == SchwabAction.BUY_TO_CLOSE: # need to record gains when closing a short position postings.append( Posting( account=self.get_cap_gains_account(), units=MISSING, cost=None, price=None, flag=None, meta={}, )) fees = self.fees if fees is not None: postings.append( Posting( account=self.fees_account, units=Amount(self.fees, currency="USD"), cost=None, price=None, flag=None, meta={}, )) return postings
def test_insert_entry_align(tmp_path) -> None: file_content = dedent("""\ 2016-02-26 * "Uncle Boons" "Eating out alone" Liabilities:US:Chase:Slate -24.84 USD Expenses:Food:Restaurant 24.84 USD """) samplefile = tmp_path / "example.beancount" samplefile.write_text(file_content) postings = [ Posting( "Liabilities:US:Chase:Slate", A("-10.00 USD"), None, None, None, None, ), Posting( "Expenses:Food", A("10.00 USD"), None, None, None, None, ), ] transaction = Transaction( {}, date(2016, 1, 1), "*", "new payee", "narr", None, None, postings, ) insert_entry(transaction, str(samplefile), [], 50) assert samplefile.read_text("utf-8") == dedent("""\ 2016-02-26 * "Uncle Boons" "Eating out alone" Liabilities:US:Chase:Slate -24.84 USD Expenses:Food:Restaurant 24.84 USD 2016-01-01 * "new payee" "narr" Liabilities:US:Chase:Slate -10.00 USD Expenses:Food 10.00 USD """)
def run(self): try: # remove anything that was there previously self.destination.clear() for account, entries in self.source.items(): print_stderr(f"Processing {account}") new_entries = [] for entry in entries: if type(entry) in SUPPORTED_DIRECTIVES: categorised_account = self.attempt_categorise(entry) if categorised_account: posting = Posting(categorised_account, None, None, None, None, None) new_postings = entry.postings + [posting] else: new_postings = entry.postings new_entry = Transaction( entry.meta, entry.date, entry.flag, entry.payee, entry.narration, entry.tags, entry.links, new_postings, ) else: new_entry = entry new_entries.append(new_entry) # this assignment needs to happen just once self.destination[account] = new_entries finally: # close the database self.destination.close() print_stderr("Written to $destination")
def get_matchable_posting_for_merge( x: PendingEntry) -> matching.MatchablePosting: if x.posting.units.number < ZERO: if len(x.transaction.postings) == 2: for p in x.transaction.postings: if id(p) != id(x.posting): posting = p break else: posting = Posting( account=x.posting.account, units=-x.posting.units, cost=None, price=None, flag=None, meta=None) return matching.MatchablePosting( posting=posting, weight=matching.get_posting_weight(posting), source_postings=[ p for p in x.transaction.postings if id(p) != id(x.posting) ], ) return matching.MatchablePosting( posting=x.posting, weight=matching.get_posting_weight(x.posting), source_postings=[x.posting], )
def _transform_transaction(self, truelayer_txn, beancount_account: Text, is_pending: bool = False) -> Transaction: """Transforms TrueLayer Transaction to beancount Transaction.""" number = abs(currency_to_decimal(truelayer_txn['amount'])) if truelayer_txn['transaction_type'] == 'DEBIT': number = -number elif truelayer_txn['transaction_type'] == 'CREDIT': pass else: assert False posting = Posting( account=beancount_account, units=Amount(number, truelayer_txn['currency']), cost=None, price=None, flag=None, meta=None, ) payee = (truelayer_txn.get('merchant_name', None) or truelayer_txn['meta'].get('provider_merchant_name', None)) return Transaction( meta=new_metadata('', 0), date=dateutil.parser.parse( truelayer_txn['timestamp']).astimezone().date(), flag='!' if is_pending else '*', payee=payee, narration=truelayer_txn['description'], tags=set(), links=set(), postings=[posting], )
def test_render_entries(example_ledger: FavaLedger, snapshot) -> None: entry1 = _get_entry(example_ledger, "Uncle Boons", "2016-04-09") entry2 = _get_entry(example_ledger, "BANK FEES", "2016-05-04") postings = [ Posting("Expenses:Food", A("10.00 USD"), None, None, None, None), ] transaction = Transaction( {}, date(2016, 1, 1), "*", "new payee", "narr", None, None, postings, ) entries = example_ledger.file.render_entries([entry1, entry2, transaction]) snapshot("\n".join(entries)) file_content = dedent( """\ 2016-04-09 * "Uncle Boons" "" #trip-new-york-2016 Liabilities:US:Chase:Slate -52.22 USD Expenses:Food:Restaurant 52.22 USD 2016-05-04 * "BANK FEES" "Monthly bank fee" Assets:US:BofA:Checking -4.00 USD Expenses:Financial:Fees 4.00 USD """ ) assert file_content == "\n".join( example_ledger.file.render_entries([entry1, entry2]) )
def get_final_holdings(entries, included_account_types=None, price_map=None, date=None): """Get a list of holdings by account (as Postings).""" simple_entries = [ entry for entry in entries if (not isinstance(entry, Transaction) or entry.flag != flags.FLAG_UNREALIZED) ] root_account = realization.realize(simple_entries) holdings = [] for real_account in sorted(list(realization.iter_children(root_account)), key=lambda ra: ra.account): account_type = account_types.get_account_type(real_account.account) if (included_account_types and account_type not in included_account_types): continue for pos in real_account.balance: price = None if pos.cost and price_map: base_quote = (pos.units.currency, pos.cost.currency) _, price = prices.get_price(price_map, base_quote, date) holdings.append( Posting(real_account.account, pos.units, pos.cost, price, None, None)) return holdings
def test_posting_merge_first_posting_does_nothing(self): postings = [ Posting(account="a", units=Amount(D(10), "USD"), cost=None, price=None, flag=None, meta={"merge": True}), Posting(account="a", units=Amount(D(20), "USD"), cost=None, price=None, flag=None, meta=None), ] result = list(unbook.group_postings_by_meta(postings)) assert len(result) == 2
def test_aggregate_holdings_by(): assert aggregate_holdings_list([]) is None holdings = [ Posting('Assets', A('10 HOOL'), None, None, None, None), Posting('Assets:Test', A('14 HOL'), None, None, None, None), ] assert aggregate_holdings_by(holdings, 'account') == holdings assert aggregate_holdings_by(holdings, 'currency') == holdings assert aggregate_holdings_by(holdings, 'invalid') == holdings holdings = [ Posting('Assets', A('10 HOOL'), None, None, None, None), Posting('Assets', A('14 HOOL'), None, None, None, None), ] assert aggregate_holdings_by(holdings, 'account') == \ [aggregate_holdings_list(holdings)]
def test_serialise_posting(pos, amount): pos = Posting('Assets:ETrade:Cash', *pos) json = { 'account': 'Assets:ETrade:Cash', 'amount': amount, } assert loads(dumps(serialise(pos))) == json assert deserialise_posting(json) == pos
def test_deserialise_posting( amount_cost_price: Tuple[Amount, Optional[CostSpec], Amount], amount_string: str, ) -> None: """Roundtrip is not possible here due to total price or calculation.""" pos = Posting("Assets", *amount_cost_price, None, None) json = {"account": "Assets", "amount": amount_string} assert deserialise_posting(json) == pos
def test_serialise_posting( amount_cost_price: Tuple[Amount, Optional[CostSpec], Amount], amount_string: str, ) -> None: pos = Posting("Assets", *amount_cost_price, None, None) json = {"account": "Assets", "amount": amount_string} assert loads(dumps(serialise(pos))) == json assert deserialise_posting(json) == pos
def add_posting(self, user, item_id): assert(user in self.get_users()) account = "Assets:Receivable:{}".format(user) if self.transaction is None: self.transaction = Transaction( flag="*", payee="kitty", narration="cococount purchase", tags=None, postings=[Posting("Assets:Items", number.MISSING, None, None, None, {})], links=None, meta={}, date=datetime.date.today()) price = self.latest_price(item_id) self.transaction.postings.append( Posting(account, price.amount, None, None, None, {})) self.log.debug(format_entry(self.transaction))
def get_residual_postings(residual: Inventory, account_rounding: str): return [ Posting(account=account_rounding, units=-position.units, cost=position.cost, price=None, flag=None, meta={}) for position in residual ]
def posting_distrib(posting: Posting, weight: Decimal, total_weight: Decimal): units = amount_distrib(posting.units, weight, total_weight) cost = posting.cost if isinstance(cost, CostSpec): cost = costspec_distrib(cost, weight, total_weight) return posting._replace( units=units, cost=cost, )