def DoRemovalOfOption(txn, balances): entry = CreateTransaction(txn, allow_fees=True) item = txn['transactionItem'] inst = item['instrument'] assert inst['assetType'] == 'OPTION' symbol = GetOptionName(inst, entry.date.year) account = config['asset_position'].format(symbol='Options') # Note: The contract size isn't present. If we find varying contract # size we could consider calling the API again to find out what it is. amt = DF(item['amount'], QO) amt *= CSIZE # Find the current amount in the given account to figure out the side of the # position and the appropriate side to remove. {492fa5292636} balance = balances[account] try: pos = balance[(symbol, None)] sign = -1 if pos.units.number > 0 else 1 except KeyError: # Could not find the position. Go short. sign = -1 cost = CostSpec(None, None, None, None, None, False) entry.postings.extend([ Posting(account, Amount(sign * amt, symbol), cost, Amount(ZERO, USD)), Posting(config['pnl']), ]) return entry
def get_market_value(pos, price_map, date=None): """Get the market value of a Position. This differs from the convert.get_value function in Beancount by returning the cost value if no price can be found. Args: pos: A Position. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. Returns: An Amount, with value converted or if the conversion failed just the cost value (or the units if the position has no cost). """ units_ = pos.units cost_ = pos.cost value_currency = cost_.currency if cost_ else None if value_currency: base_quote = (units_.currency, value_currency) _, price_number = get_price(price_map, base_quote, date) if price_number is not None: return Amount(units_.number * price_number, value_currency) return Amount(units_.number * cost_.number, value_currency) return units_
def test_comparisons(self): amount1 = Amount(D('100'), 'USD') amount2 = Amount(D('100'), 'USD') self.assertEqual(amount1, amount2) amount3 = Amount(D('101'), 'USD') self.assertNotEqual(amount1, amount3)
def load_fund_transactions(filename: str, account: str) -> List[FundTransaction]: expected_field_names = [ 'Date', 'Fund', 'Category', 'Description', 'Price', 'Amount', 'Shares', 'Total Shares', 'Total Value' ] transactions = [] # type: List[FundTransaction] filename = os.path.abspath(filename) with open(filename, 'r', encoding='utf-8', newline='') as csvfile: reader = csv.DictReader(csvfile) if reader.fieldnames != expected_field_names: raise RuntimeError( 'Actual field names %r != expected field names %r' % (reader.fieldnames, expected_field_names)) for line_i, row in enumerate(reader): transactions.append( FundTransaction( account=account, date=datetime.datetime.strptime(row['Date'], date_format).date(), description=row['Category'], memo=row['Description'], price=parse_amount(row['Price']), amount=parse_amount(row['Amount']), units=Amount(parse_number(row['Shares']), row['Fund']), balance=Amount(parse_number(row['Total Shares']), row['Fund']), filename=filename, line=line_i + 1, )) return transactions
def test_add(self): self.assertEqual(Amount(D('117.02'), 'CAD'), amount.add(Amount(D('100'), 'CAD'), Amount(D('17.02'), 'CAD'))) with self.assertRaises(ValueError): amount.add(Amount(D('100'), 'USD'), Amount(D('17.02'), 'CAD'))
def extract(self, file, existing_entries=None): # Open the CSV file and create directives. entries = [] index = 0 with open(file.name, 'rb') as f: eml = parser.BytesParser().parse(fp=f) b = base64.b64decode(eml.get_payload()[0].get_payload()) d = BeautifulSoup(b, "lxml") date_range = d.findAll(text=re.compile( '\d{4}\/\d{1,2}\/\d{1,2}-\d{4}\/\d{1,2}\/\d{1,2}'))[0] transaction_date = dateparse( date_range.split('-')[1].split('(')[0]).date() balance = '-' + d.find(src="https://pbdw.ebank.cmbchina.com/" "cbmresource/22/dyzd/jpkdyzd/xbgbdt/bqhkzz.jpg")\ .parent.parent.find_next_sibling( 'td').select('font')[0].text.replace('¥', '').replace(',', '').strip() txn_balance = data.Balance(account=self.account_name, amount=Amount(D(balance), 'CNY'), meta=data.new_metadata(".", 1000), tolerance=None, diff_amount=None, date=transaction_date) entries.append(txn_balance) bands = d.select('#fixBand29 #loopBand2>table>tr') for band in bands: tds = band.select('td #fixBand15 table table td') if len(tds) == 0: continue trade_date = tds[1].text.strip() if trade_date == '': trade_date = tds[2].text.strip() # date = datetime.strptime(trade_date,'%m%d').replace(year=transaction_date.year).date() date = datetime.strptime(trade_date, '%m%d') if date.month == 12 and transaction_date.month == 1: date = date.replace(year=transaction_date.year - 1).date() else: date = date.replace(year=transaction_date.year).date() full_descriptions = tds[3].text.strip().split('-') payee = full_descriptions[0] narration = '-'.join(full_descriptions[1:]) real_currency = 'CNY' real_price = tds[4].text.replace('¥', '').replace('\xa0', '').strip() # print("Importing {} at {}".format(narration, date)) flag = "*" amount = -Amount(D(real_price), real_currency) meta = data.new_metadata(file.name, index) txn = data.Transaction( meta, date, self.FLAG, payee, narration, data.EMPTY_SET, data.EMPTY_SET, [ data.Posting(self.account_name, amount, None, None, None, None), ]) entries.append(txn) # Insert a final balance check. return entries
def load_balances(filename: str, date: datetime.date, account: str) -> List[ImportedBalance]: expected_field_names = [ 'Fund', 'Name', 'Shares (#)', 'Closing Price', 'Closing Value' ] balances = [] # type: List[ImportedBalance] filename = os.path.abspath(filename) with open(filename, 'r', encoding='utf-8', newline='') as csvfile: reader = csv.DictReader(csvfile) if reader.fieldnames != expected_field_names: raise RuntimeError( 'Actual field names %r != expected field names %r' % (reader.fieldnames, expected_field_names)) for line_i, row in enumerate(reader): balances.append( ImportedBalance( account=account, date=date, units=Amount(parse_number(row['Shares (#)']), row['Fund']), price=Amount(parse_number(row['Closing Price']), 'USD'), market_value=Amount(parse_number(row['Closing Value']), 'USD'), filename=filename, line=line_i + 1, )) return balances
def average(self): """Average all lots of the same currency together.. Returns: An instance of Inventory. """ groups = collections.defaultdict(list) for position in self: lot = position.lot key = (lot.currency, lot.cost.currency if lot.cost else None) groups[key].append(position) average_inventory = Inventory() for (currency, cost_currency), positions in groups.items(): total_units = sum(position.number for position in positions) units_amount = Amount(total_units, currency) if cost_currency: total_cost = sum(position.get_cost().number for position in positions) cost_amount = Amount(total_cost / total_units, cost_currency) else: cost_amount = None average_inventory.add_amount(units_amount, cost_amount) return average_inventory
def test_sub(self): self.assertEqual(Amount(D('82.98'), 'CAD'), amount.sub(Amount(D('100'), 'CAD'), Amount(D('17.02'), 'CAD'))) with self.assertRaises(ValueError): amount.sub(Amount(D('100'), 'USD'), Amount(D('17.02'), 'CAD'))
def test_hash(self): amount = Amount(D('100,034.027456'), 'USD') self.assertTrue({amount: True}) self.assertTrue({amount}) amount2 = Amount(D('100,034.027456'), 'CAD') self.assertEqual(2, len({amount: True, amount2: False}))
def get_miles_expirations(accapi, options): """Show expiry of airline miles, rewards points""" exclude = '' exclude_option = options.get('exclude_currencies', '') if exclude_option: exclude = "AND not currency ~ '{currencies}'".format( currencies=exclude_option) sql = """ SELECT account, sum(number) AS Balance, currency as Points, LAST(date) AS Latest_Transaction WHERE not currency ~ '{currencies}' AND account ~ '{accounts_pattern}' {exclude} GROUP BY account,Points ORDER BY LAST(date) """.format( currencies=accapi.get_operating_currencies_regex(), accounts_pattern=options.get('accounts_pattern', 'Assets'), exclude=exclude, ) rtypes, rrows = accapi.query_func(sql) if not rtypes: return [], {}, [[]] # our output table is slightly different from our query table: retrow_types = rtypes[:-1] + [('value', int), ('expiry', datetime.date)] RetRow = collections.namedtuple('RetRow', [i[0] for i in retrow_types]) commodities = accapi.get_commodity_directives() def get_miles_metadata(miles): try: return commodities[miles].meta except: return {} ret_rows = [] for row in rrows: meta = get_miles_metadata(row.points) value = meta.get('points-value', Amount(Decimal(0), 'NONE')) converted_value = Amount(value.number * row.balance, value.currency) expiry_months = meta.get('expiry-months', 0) if expiry_months >= 0: expiry = row.latest_transaction + datetime.timedelta( int(expiry_months) * 365 / 12) else: expiry = datetime.date.max ret_rows.append(RetRow(*row[:-1], converted_value, expiry)) ret_rows.sort(key=lambda r: r[-1]) return retrow_types, ret_rows
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)
def test_training_examples(tmpdir): check_source( tmpdir, source_spec={ 'module': 'beancount_import.source.mint', 'filename': mint_filename, }, journal_contents=r""" plugin "beancount.plugins.auto_accounts" 1900-01-01 open Liabilities:Credit-Card USD mint_id: "My Credit Card" 1900-01-01 open Assets:Checking USD mint_id: "My Checking" 2013-11-27 * "CR CARD PAYMENT ALEXANDRIA VA" Liabilities:Credit-Card 66.88 USD date: 2013-11-27 source_desc: "CR CARD PAYMENT ALEXANDRIA VA" cleared: TRUE Assets:Checking -66.88 USD date: 2013-12-02 source_desc: "NATIONAL FEDERAL DES:TRNSFR" cleared: TRUE 2016-08-10 * "STARBUCKS STORE 12345" Liabilities:Credit-Card -2.45 USD date: 2016-08-10 source_desc: "STARBUCKS STORE 12345" cleared: TRUE Expenses:Coffee 2.45 USD """, accounts=frozenset([ 'Assets:Checking', 'Liabilities:Credit-Card', ]), pending=[], training_examples=[ (PredictionInput(source_account='Liabilities:Credit-Card', amount=Amount(D('66.88'), 'USD'), date=datetime.date(2013, 11, 27), key_value_pairs={ 'desc': 'CR CARD PAYMENT ALEXANDRIA VA' }), 'Assets:Checking'), (PredictionInput(source_account='Assets:Checking', amount=Amount(D('-66.88'), 'USD'), date=datetime.date(2013, 12, 2), key_value_pairs={ 'desc': 'NATIONAL FEDERAL DES:TRNSFR' }), 'Liabilities:Credit-Card'), (PredictionInput(source_account='Liabilities:Credit-Card', amount=Amount(D('-2.45'), 'USD'), date=datetime.date(2016, 8, 10), key_value_pairs={'desc': 'STARBUCKS STORE 12345' }), 'Expenses:Coffee'), ], )
def test_tostring(self): amount1 = Amount(D('100034.023'), 'USD') self.assertEqual('100034.023 USD', str(amount1)) amount2 = Amount(D('0.00000001'), 'BTC') self.assertEqual('0.00000001 BTC', str(amount2)) dcontext = display_context.DisplayContext() dformat = dcontext.build(commas=True) self.assertEqual('100,034.023 USD', amount1.to_string(dformat))
def test_constructor(self): amount = Amount(D('100,034.02'), 'USD') self.assertEqual(amount.number, D('100034.02')) # Ensure that it is possible to initialize the number to any object. # This is used when creating incomplete objects. class Dummy: pass amount = Amount(Dummy, Dummy) self.assertIs(amount.number, Dummy) self.assertIs(amount.currency, Dummy)
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 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 posting(self, filename, lineno, account, units, cost, price, istotal, flag): """Process a posting grammar rule. Args: filename: the current filename. lineno: the current line number. account: A string, the account of the posting. position: An instance of Position from the grammar rule. price: Either None, or an instance of Amount that is the cost of the position. istotal: A bool, True if the price is for the total amount being parsed, or False if the price is for each lot of the position. flag: A string, one-character, the flag associated with this posting. Returns: A new Posting object, with no parent entry. """ # Prices may not be negative. if not __allow_negative_prices__: if price and isinstance(price.number, Decimal) and price.number < ZERO: meta = new_metadata(filename, lineno) self.errors.append( ParserError(meta, ( "Negative prices are not allowed: {} " "(see http://furius.ca/beancount/doc/bug-negative-prices " "for workaround)").format(price), None)) # Fix it and continue. price = Amount(abs(price.number), price.currency) # If the price is specified for the entire amount, compute the effective # price here and forget about that detail of the input syntax. if istotal: if units.number == ZERO: number = ZERO else: if __allow_negative_prices__: number = price.number / units.number else: number = price.number / abs(units.number) price = Amount(number, price.currency) # Note: Allow zero prices because we need them for round-trips for # conversion entries. # # if price is not None and price.number == ZERO: # self.errors.append( # ParserError(meta, "Price is zero: {}".format(price), None)) meta = new_metadata(filename, lineno) return Posting(account, units, cost, price, chr(flag) if flag else None, meta)
def get_key_from_posting(entry: Transaction, posting: Posting, source_postings: List[Posting], source_desc: str, posting_date: datetime.date) -> MatchKey: del entry del source_postings transaction_type = posting.meta and posting.meta.get(TRANSACTION_TYPE_KEY) if isinstance(posting.cost, CostSpec): cost = Amount(posting.cost.number_per, posting.cost.currency) elif isinstance(posting.cost, Cost): cost = Amount(posting.cost.number, posting.cost.currency) else: cost = None return MatchKey(posting.account, posting_date, source_desc, transaction_type, cost, posting.units)
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 CreateFeesPostings(txn) -> List[data.Posting]: """Get postings for fees.""" postings = [] fees = txn['fees'] commission = fees.pop('commission') if commission: postings.append( Posting(config['commission'], Amount(DF(commission), USD))) fees.pop('optRegFee', None) fees.pop('secFee', None) for unused_name, number in sorted(fees.items()): if not number: continue postings.append(Posting(config['fees'], Amount(DF(number), USD))) return postings
def test_fromstring(self): amount1 = Amount(D('100'), 'USD') amount2 = Amount.from_string('100 USD') self.assertEqual(amount1, amount2) Amount.from_string(' 100.00 USD ') with self.assertRaises(ValueError): Amount.from_string('100') with self.assertRaises(ValueError): Amount.from_string('USD') with self.assertRaises(ValueError): Amount.from_string('100.00 U')
def test_mutation(self): amount1 = Amount(D('100'), 'USD') # Test how changing existing attributes should fail. with self.assertRaises(AttributeError) as ctx: amount1.currency = 'CAD' self.assertRegex("can't set attribute", str(ctx.exception)) with self.assertRaises(AttributeError) as ctx: amount1.number = D('200') self.assertRegex("can't set attribute", str(ctx.exception)) # Try setting a new attribute. with self.assertRaises(AttributeError): amount1.something = 42
def create_simple_posting_with_cost_or_price(entry, account, number, currency, price_number=None, price_currency=None, cost_number=None, cost_currency=None, costspec=None): """Create a simple posting on the entry, with a cost (for purchases) or price (for sell transactions). 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. price_number: A Decimal number or string to use for the posting's price Amount. price_currency: a string, the currency for the price 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) units = Amount(number, currency) if not (price_number or cost_number): print("Either price ({}) or cost ({}) must be specified ({})".format( price_number, cost_number, entry)) import pdb pdb.set_trace() raise Exception( "Either price ({}) or cost ({}) must be specified".format( price_number, cost_number)) price = Amount(price_number, price_currency) if price_number else None cost = Cost(cost_number, cost_currency, None, None) if cost_number else None cost = costspec if costspec else cost posting = data.Posting(account, units, cost, price, None, None) if entry is not None: entry.postings.append(posting) return posting
def load_balances(filename: str) -> List[RawBalance]: expected_field_names = [ 'Name', 'Currency', 'Balance', 'Last Updated', 'State', 'Last Transaction' ] balances = [] filename = os.path.abspath(filename) with open(filename, 'r', encoding='utf-8', newline='') as csvfile: reader = csv.DictReader(csvfile) if reader.fieldnames != expected_field_names: raise RuntimeError( 'Actual field names %r != expected field names %r' % (reader.fieldnames, expected_field_names)) for line_i, row in enumerate(reader): date_str = row['Last Transaction'].strip() if not date_str: continue date = datetime.datetime.strptime(date_str, mint_date_format).date() balances.append( RawBalance( account=row['Name'], date=date, amount=Amount(D(row['Balance']), row['Currency']), filename=filename, line=line_i + 1)) return balances
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 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) units = Amount(number, currency) cost = Cost(cost_number, cost_currency, None, None) posting = Posting(account, units, cost, None, None, None) if entry is not None: entry.postings.append(posting) return posting
def _booking_method_xifo(entry, posting, matches, reverse_order): """FIFO and LIFO booking method implementations.""" booked_reductions = [] booked_matches = [] errors = [] insufficient = False # Each up the positions. sign = -1 if posting.units.number < ZERO else 1 remaining = abs(posting.units.number) for match in sorted(matches, key=lambda p: p.cost and p.cost.date, reverse=reverse_order): if remaining <= ZERO: break # If the inventory somehow ended up with mixed lots, skip this one. if match.units.number * sign > ZERO: continue # Compute the amount of units we can reduce from this leg. size = min(abs(match.units.number), remaining) booked_reductions.append( posting._replace(units=Amount(size * sign, match.units.currency), cost=match.cost)) booked_matches.append(match) remaining -= size # If we couldn't eat up all the requested reduction, return an error. insufficient = (remaining > ZERO) return booked_reductions, booked_matches, errors, insufficient
def deserialise(json_entry): """Parse JSON to a Beancount entry. Args: json_entry: The entry. Raises: KeyError: if one of the required entry fields is missing. FavaAPIException: if the type of the given entry is not supported. """ if json_entry['type'] == 'Transaction': date = util.date.parse_date(json_entry['date'])[0] narration, tags, links = extract_tags_links(json_entry['narration']) postings = [deserialise_posting(pos) for pos in json_entry['postings']] return data.Transaction(json_entry['meta'], date, json_entry['flag'], json_entry['payee'], narration, tags, links, postings) if json_entry['type'] == 'Balance': date = util.date.parse_date(json_entry['date'])[0] number = parse_number(json_entry['number']) amount = Amount(number, json_entry['currency']) return data.Balance(json_entry['meta'], date, json_entry['account'], amount, None, None) if json_entry['type'] == 'Note': date = util.date.parse_date(json_entry['date'])[0] comment = json_entry['comment'].replace('"', '') return data.Note(json_entry['meta'], date, json_entry['account'], comment) raise FavaAPIException('Unsupported entry type.')
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 oneliner(entries, options_map, config): """Parse note oneliners into valid transactions. For example, 1999-12-31 note Assets:Cash "Income:Test -16.18 EUR * Description goes here *" """ errors = [] new_entries = [] for entry in entries: if(isinstance(entry, data.Note) and entry.comment[-1:] == "*"): comment = entry.comment try: k = None maybe_cost = RE_COST.findall(comment) if len(maybe_cost) > 0: amount = maybe_cost[0].split()[0] currency = maybe_cost[0].split()[1] cost = Cost(D(amount), currency, None, None) k = mul(cost, D(-1)) comment = RE_COST.sub('', comment) else: cost = None maybe_price = RE_PRICE.findall(comment) if len(maybe_price) > 0: price = Amount.from_string(maybe_price[0]) k = k or mul(price, D(-1)) comment = RE_PRICE.sub('', comment) else: price = None comment_tuple = comment.split() other_account = comment_tuple[0] units = Amount.from_string(' '.join(comment_tuple[1:3])) flag = comment_tuple[3] narration_tmp = ' '.join(comment_tuple[4:-1]) tags = {'NoteToTx'} for tag in RE_TAG.findall(narration_tmp): tags.add( tag[1] ) narration = RE_TAG.sub('', narration_tmp).rstrip() k = k or Amount(D(-1), units.currency) # print(type(cost), cost, type(price), price, type(units), units, k, comment) p1 = data.Posting(account=other_account, units=units, cost=cost, price=price, flag=None, meta={'filename': entry.meta['filename'], 'lineno': entry.meta['lineno']}) p2 = data.Posting(account=entry.account, units=mul(k, units.number), cost=cost, price=None, flag=None, meta={'filename': entry.meta['filename'], 'lineno': entry.meta['lineno']}) e = data.Transaction(date=entry.date, flag=flag, payee=None, # TODO narration=narration, tags=tags, # TODO links=EMPTY_SET, # TODO postings=[p1, p2], meta=entry.meta) new_entries.append(e) # print(e) except: print('beancount-oneliner error:', entry, sys.exc_info()) else: new_entries.append(entry) return new_entries, errors