def _wct(self, account_name): if not account_name: account_name = '^(Assets:|Liabilities:)' _, wrow = query.run_query( self.ledger.entries, self.ledger.options, ''' select number(only("USD", sum(position))) where account = "{}" and (meta('_cleared') = True or number < 0) ''', account_name) if not wrow: wrow = [[Decimal()]] _, crow = query.run_query( self.ledger.entries, self.ledger.options, ''' select number(only("USD", sum(position))) where account = "{}" and (meta('_cleared') = True) ''', account_name) if not crow: crow = [[Decimal()]] _, trow = query.run_query( self.ledger.entries, self.ledger.options, ''' select number(only("USD", sum(position))) where account = "{}" ''', account_name) if not trow: trow = [[Decimal()]] return wrow[0][0], crow[0][0], trow[0][0]
def rolling_average(series, num_periods=None): """Compute the rolling average of a series of (date, number) points. Args: series: A list of (date, number) pairs, where 'date' is a datetime.date instance, and 'number' is a Decimal object. num_periods: An integer, the number of points to average over. If None, average over the entire series. Returns: A new series of (date, average) points. """ if num_periods is None: num_periods = len(series) + 1 average = [] for index, (date, _) in enumerate(series): start_date, start_number = series[max(0, index - num_periods)] end_date, end_number = series[index] days = (end_date - start_date).days if days == 0: continue number = end_number - start_number scaling = Decimal(365 / days) point = (date, (number * scaling).quantize(Decimal('0.01'))) average.append(point) return average
def decimalize(number_list): decimalized_list = [] for element in number_list: if isinstance(element, str): decimalized_list.append(Decimal(element)) else: decimalized_list.append((Decimal(element[0]), ) + element[1:]) return decimalized_list
def extract(self, file_): entries = [] if not self.identify(file_): return [] with open(file_.name, encoding=self.file_encoding) as fd: reader = csv.DictReader(fd, delimiter=',', quoting=csv.QUOTE_MINIMAL, quotechar='"') for index, line in enumerate(reader): meta = data.new_metadata(file_.name, index) s_amount_eur = self._translate('amount_eur') s_amount_foreign_currency = self._translate( 'amount_foreign_currency') s_date = self._translate('date') s_payee = self._translate('payee') s_payment_reference = self._translate('payment_reference') s_type_foreign_currency = self._translate( 'type_foreign_currency') if line[s_amount_eur]: amount = Decimal(line[s_amount_eur]) currency = 'EUR' else: amount = Decimal(line[s_amount_foreign_currency]) currency = line[s_type_foreign_currency] postings = [ data.Posting( self.account, Amount(amount, currency), None, None, None, None, ) ] entries.append( data.Transaction( meta, datetime.strptime(line[s_date], '%Y-%m-%d').date(), self.FLAG, line[s_payee], line[s_payment_reference], data.EMPTY_SET, data.EMPTY_SET, postings, )) return entries
def test_extract_multiple_transactions(importer, filename): with open(filename, 'wb') as fd: fd.write( _format( ''' {header} "2019-12-28","MAX MUSTERMANN","{iban_number}","Income","Muster GmbH","Income","-56.78","","","" "2020-01-05","Muster SARL","{iban_number}","Outgoing Transfer","Muster Fr payment","Income","-42.24","","","" "2020-01-03","Muster GmbH","{iban_number}","Outgoing Transfer","Muster De payment","Income","-12.34","","","" ''', # NOQA language=importer.language, )) with open(filename) as fd: transactions = importer.extract(fd) date = importer.file_date(fd) assert date == datetime.date(2020, 1, 5) assert len(transactions) == 3 # first assert transactions[0].date == datetime.date(2019, 12, 28) assert transactions[0].payee == 'MAX MUSTERMANN' assert transactions[0].narration == 'Muster GmbH' assert len(transactions[0].postings) == 1 assert transactions[0].postings[0].account == 'Assets:N26' assert transactions[0].postings[0].units.currency == 'EUR' assert transactions[0].postings[0].units.number == Decimal('-56.78') # second assert transactions[1].date == datetime.date(2020, 1, 5) assert transactions[1].payee == 'Muster SARL' assert transactions[1].narration == 'Muster Fr payment' assert len(transactions[1].postings) == 1 assert transactions[1].postings[0].account == 'Assets:N26' assert transactions[1].postings[0].units.currency == 'EUR' assert transactions[1].postings[0].units.number == Decimal('-42.24') # third assert transactions[2].date == datetime.date(2020, 1, 3) assert transactions[2].payee == 'Muster GmbH' assert transactions[2].narration == 'Muster De payment' assert len(transactions[2].postings) == 1 assert transactions[2].postings[0].account == 'Assets:N26' assert transactions[2].postings[0].units.currency == 'EUR' assert transactions[2].postings[0].units.number == Decimal('-12.34')
def bucketize(vbalance, accapi): price_map = accapi.build_price_map() commodity_map = accapi.get_commodity_map() base_currency = accapi.get_operating_currencies()[0] meta_prefix = 'asset_allocation_' meta_prefix_len = len(meta_prefix) # Main part: put each commodity's value into asset buckets asset_buckets = collections.defaultdict(int) for pos in vbalance.get_positions(): amount = convert.convert_position(pos, base_currency, price_map) if amount.number < 0: # print("Warning: skipping negative balance:", pos) #TODO continue if amount.currency == pos.units.currency and amount.currency != base_currency: sys.stderr.write( "Error: unable to convert {} to base currency {} (Missing price directive?)\n" .format(pos, base_currency)) sys.exit(1) commodity = pos.units.currency metas = commodity_map[commodity].meta unallocated = Decimal('100') for meta in metas: if meta.startswith(meta_prefix): bucket = meta[meta_prefix_len:] asset_buckets[bucket] += amount.number * (metas[meta] / 100) unallocated -= metas[meta] if unallocated: print( "Warning: {} asset_allocation_* metadata does not add up to 100%. Padding with 'unknown'." .format(commodity)) asset_buckets['unknown'] += amount.number * (unallocated / 100) return asset_buckets
def test_budgets_dateline_monthly(): BUDGET = Decimal(100) budgets = get_budgets('2014-05-01 custom "budget" Expenses:Books "monthly" {} EUR'.format(BUDGET)) # noqa assert budgets.budget('Expenses:Books', date(2016, 1, 1), date(2016, 1, 2))['EUR'] == BUDGET / number_of_days_in_period('monthly', date(2016, 1, 1)) # noqa assert budgets.budget('Expenses:Books', date(2016, 2, 1), date(2016, 2, 2))['EUR'] == BUDGET / number_of_days_in_period('monthly', date(2016, 2, 1)) # noqa assert budgets.budget('Expenses:Books', date(2018, 3, 31), date(2018, 4, 1))['EUR'] == BUDGET / number_of_days_in_period('monthly', date(2016, 3, 31)) # noqa
def test_budgets_dateline_weekly(): BUDGET = Decimal(21) budgets = get_budgets('2016-05-01 custom "budget" Expenses:Books "weekly" {} EUR'.format(BUDGET)) # noqa assert budgets.budget('Expenses:Books', date(2016, 5, 1), date(2016, 5, 2))['EUR'] == BUDGET / number_of_days_in_period('weekly', date(2016, 5, 1)) # noqa assert budgets.budget('Expenses:Books', date(2016, 9, 1), date(2016, 9, 2))['EUR'] == BUDGET / number_of_days_in_period('weekly', date(2016, 9, 1)) # noqa assert budgets.budget('Expenses:Books', date(2018, 12, 31), date(2019, 1, 1))['EUR'] == BUDGET / number_of_days_in_period('weekly', date(2018, 12, 31)) # noqa
def test_extract_single_transaction(importer, filename): with open(filename, 'wb') as fd: fd.write( _format( ''' {header} "2019-10-10","Muster GmbH","{iban_number}","Outgoing Transfer","Muster payment","Miscellaneous","-12.34","","","" ''', # NOQA language=importer.language, )) with open(filename) as fd: transactions = importer.extract(fd) date = importer.file_date(fd) assert date == datetime.date(2019, 10, 10) assert len(transactions) == 1 assert transactions[0].date == datetime.date(2019, 10, 10) assert transactions[0].payee == 'Muster GmbH' assert transactions[0].narration == 'Muster payment' assert len(transactions[0].postings) == 1 assert transactions[0].postings[0].account == 'Assets:N26' assert transactions[0].postings[0].units.currency == 'EUR' assert transactions[0].postings[0].units.number == Decimal('-12.34')
def format_currency(value, currency=None, show_if_zero=False): """Format a value using the derived precision for a specified currency.""" if not value and not show_if_zero: return '' if value == 0.0: return g.ledger.quantize(Decimal(0.0), currency) return g.ledger.quantize(value, currency)
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_budgets_dateline_yearly(): BUDGET = Decimal(99999.87) budgets = get_budgets('2010-01-01 custom "budget" Expenses:Books "yearly" {} EUR'.format(BUDGET)) # noqa assert budgets.budget('Expenses:Books', date(2011, 2, 1), date(2011, 2, 2))['EUR'] == BUDGET / number_of_days_in_period('yearly', date(2011, 2, 1)) # noqa assert budgets.budget('Expenses:Books', date(2015, 5, 30), date(2015, 5, 31))['EUR'] == BUDGET / number_of_days_in_period('yearly', date(2015, 5, 30)) # noqa assert budgets.budget('Expenses:Books', date(2016, 8, 15), date(2016, 8, 16))['EUR'] == BUDGET / number_of_days_in_period('yearly', date(2016, 8, 15)) # noqa
def main(): parser = argparse.ArgumentParser(description=__doc__.strip()) parser.add_argument('filename', help='Beancount filename') parser.add_argument('account', help='Root account to consider') parser.add_argument('-c', '--currency', action='store', default='USD', help="The currency to pull out of the inventory") args = parser.parse_args() # Load the Beancount input file. entries, _, options_map = loader.load_file(args.filename) # Compute monthly time intervals. start_date = datetime.date(2013, 1, 28) dateiter = iter( rrule.rrule(rrule.MONTHLY, dtstart=datetime.datetime(2013, 1, 1), until=datetime.datetime.now())) # Compute cumulative totals accumulated at those dates. curve = [(datetime.date(2013, 1, 28), Decimal())] date = next(dateiter).date() balance = inventory.Inventory() is_account = account.parent_matcher(args.account) for entry in entries: if entry.date >= date: # At the boundary, save the date and total number. try: total = -balance.get_units(args.currency).number curve.append((date, total)) date = next(dateiter).date() except StopIteration: break # Sum up the amounts from those accounts. if isinstance(entry, data.Transaction): for posting in entry.postings: if is_account(posting.account): balance.add_position(posting.position) # Compute multiple averages over fixed windows of a number of months and # plot them. months = [None, 12, 6, 3] for num in months: series = rolling_average(curve, num) pyplot.plot([date for date, total in series], [total for date, total in series], label=str(num)) print('{:10}: {:10,.2f}'.format(num if num is not None else 0, series[-1][1])) # Show that joint plot. pyplot.legend() pyplot.tight_layout() pyplot.show()
def __init__(self, entries, options_map, currency): self.entries = entries self.options_map = options_map self.currency = currency if self.currency: self.etype = "envelope" + self.currency else: self.etype = "envelope" self.start_date, self.budget_accounts, self.mappings, self.income_accounts = self._find_envelop_settings( ) if not self.currency: self.currency = self._find_currency(options_map) decimal_precison = '0.00' self.Q = Decimal(decimal_precison) # Compute start of period # TODO get start date from journal today = datetime.date.today() self.date_start = datetime.datetime.strptime(self.start_date, '%Y-%m').date() # TODO should be able to assert errors # Compute end of period self.date_end = datetime.date(today.year, today.month, today.day) self.price_map = prices.build_price_map(entries) self.acctypes = options.get_account_types(options_map)
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_budgets_dateline_daily(): BUDGET = Decimal(2.5) budgets = get_budgets('2016-05-01 custom "budget" Expenses:Books "daily" {} EUR'.format(BUDGET)) # noqa assert budgets.budget('Expenses:Books', date(2016, 5, 1), date(2016, 5, 2))['EUR'] == BUDGET # noqa assert budgets.budget('Expenses:Books', date(2016, 5, 1), date(2016, 5, 3))['EUR'] == BUDGET * 2 # noqa assert budgets.budget('Expenses:Books', date(2016, 9, 2), date(2016, 9, 3))['EUR'] == BUDGET # noqa assert budgets.budget('Expenses:Books', date(2018, 12, 31), date(2019, 1, 1))['EUR'] == BUDGET # noqa
def get_negative(self): """Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inserse of this Position. """ # Note: We use Decimal() for efficiency. return Position(self.lot, Decimal(-self.number))
def test_budgets_dateline_quarterly(): BUDGET = Decimal(123456.7) budgets = get_budgets('2014-05-01 custom "budget" Expenses:Books "quarterly" {} EUR'.format(BUDGET)) # noqa assert budgets.budget('Expenses:Books', date(2016, 2, 1), date(2016, 2, 2))['EUR'] == BUDGET / number_of_days_in_period('quarterly', date(2016, 2, 1)) # noqa assert budgets.budget('Expenses:Books', date(2016, 5, 30), date(2016, 5, 31))['EUR'] == BUDGET / number_of_days_in_period('quarterly', date(2016, 5, 30)) # noqa assert budgets.budget('Expenses:Books', date(2016, 8, 15), date(2016, 8, 16))['EUR'] == BUDGET / number_of_days_in_period('quarterly', date(2016, 8, 15)) # noqa assert budgets.budget('Expenses:Books', date(2016, 11, 15), date(2016, 11, 16))['EUR'] == BUDGET / number_of_days_in_period('quarterly', date(2016, 11, 15)) # noqa
def scale_inventory(balance, tax_adj): '''Scale inventory by tax adjustment''' scaled_balance = inventory.Inventory() for pos in balance.get_positions(): scaled_pos = amount.Amount(pos.units.number * (Decimal(tax_adj / 100)), pos.units.currency) scaled_balance.add_amount(scaled_pos) return scaled_balance
def _get_posting(account: str, units: str, currency: str, meta: Dict[str, Any]): return Posting(account=Account(account), units=Amount(number=Decimal(units), currency=Currency(currency)), cost=None, price=None, flag=None, meta=meta)
def test_from_dict(self): self.assertEqual( self.post_amount, Posting('testy', Amount(Decimal('3.14'), 'PIE'), cost=None, price=None, flag=None, meta=None))
def test_from_name(self): self.assertEqual( self.post_name, Posting('test', Amount(Decimal('0.00'), 'USD'), cost=None, price=None, flag=None, meta=None))
def _calc_budget_budgeted(self): rows = {} for e in self.entries: if isinstance(e, Custom) and e.type == "envelope": if e.values[0].value == "allocate": month = f"{e.date.year}-{e.date.month:02}" self.envelope_df.loc[e.values[1].value, (month, 'budgeted')] = Decimal( e.values[2].value)
def __copy__(self): """Shallow copy, except for the lot, which can be shared. This is important for performance reasons; a lot of time is spent here during balancing. Returns: A shallow copy of this position. """ # Note: We use Decimal() for efficiency. return Position(self.lot, Decimal(self.number))
def test_extract(importer, filename): with open(filename) as fd: operations = importer.extract(fd) operations_test = [ { 'date': datetime.date(2020, 4, 17), 'amount': Decimal('-14.90'), 'payee': '* OP DEBIT BANQUE', }, { 'date': datetime.date(2020, 4, 17), 'amount': Decimal('4.40'), 'payee': '* OP CREDIT BANQUE', }, { 'date': datetime.date(2020, 4, 20), 'amount': Decimal('24.00'), 'payee': 'VIR SEPA ENTRANT', }, { 'date': datetime.date(2020, 4, 21), 'amount': Decimal('-63.43'), 'payee': 'CB ACHAT 1', }, { 'date': datetime.date(2020, 4, 22), 'amount': Decimal('-63.11'), 'payee': 'CB ACHAT 2', }, { 'date': datetime.date(2020, 4, 27), 'amount': Decimal('-20.00'), 'payee': 'PRLV Prlvt 1', }, { 'date': datetime.date(2020, 5, 15), 'amount': Decimal('-7.32'), 'payee': 'CB ACHAT 3', }, ] op_name_test = [op_test['payee'] for op_test in operations_test] assert len(operations) == len(operations_test) for op in operations: assert op.payee in op_name_test, 'Missing operation' op_test = operations_test[op_name_test.index(op.payee)] assert op.payee == op_test['payee'], 'Wrong payee name' assert op.date == op_test['date'], 'Wrong date' assert len(op.postings) == 1 assert op.postings[0].account == 'Assets:CE', 'Wrong account name' assert op.postings[0].units.currency == 'EUR', 'Wrong currency' assert op.postings[0].units.number == op_test['amount'], 'Wrong amount'
def test_extract(importer, filename): with open(filename) as fd: operations = importer.extract(fd) operations_test = [ { 'date': datetime.date(2020, 12, 1), 'amount': Decimal('-59.17'), 'payee': 'Desc Debit 2 11/30', }, { 'date': datetime.date(2020, 10, 13), 'amount': Decimal('100.00'), 'payee': 'Desc Credit 2', }, { 'date': datetime.date(2020, 10, 5), 'amount': Decimal('-11.78'), 'payee': 'Desc Debit 1 10/04', }, { 'date': datetime.date(2020, 10, 5), 'amount': Decimal('465.53'), 'payee': 'Desc Credit 1 1465436878 WEB ID: 453233521', }, ] op_name_test = [op_test['payee'] for op_test in operations_test] assert len(operations) == len(operations_test) for op in operations: assert op.payee in op_name_test, 'Missing operation' op_test = operations_test[op_name_test.index(op.payee)] assert op.payee == op_test['payee'], 'Wrong payee name' assert op.date == op_test['date'], 'Wrong date' assert len(op.postings) == 1 assert op.postings[0].account == 'Assets:CB', 'Wrong account name' assert op.postings[0].units.currency == 'USD', 'Wrong currency' assert op.postings[0].units.number == op_test['amount'], 'Wrong amount'
def extract_data(self): raw_data = [] lineno = 0 for line in self._contents: lineno += 1 # Split the line into values = [ x.lstrip() for x in list(filter(None, re.split(r" ", line))) ] if values and len(values) > 0: column = None field_name = None gather_data = False for i, token in enumerate(values): # Convert negative numbers with trailing minus sign if token.endswith("-"): token = "-" + token.rstrip("-") # Look for 'data' found_data = re.search(self.data_regex, token) if found_data is not None: if not gather_data and field_name is not None: gather_data = True column = TxtData(field_name.getvalue(), [], self.filename, lineno) field_name = None if column is not None: column.data.append(Decimal(token)) # Stop gathering data, # - add column to list, # - start gathering field_name string elif gather_data: assert column raw_data.append(column) gather_data = False column = None field_name = StringIO() field_name.write(token) # Start gathering field_name string elif field_name is None: field_name = StringIO() field_name.write(token) # Continue gathering field_name string else: field_name.write(f" {token}") if gather_data: raw_data.append(column) for txt_data in raw_data: if len(txt_data.data) >= 2: self.data.append(txt_data) self.log_status(f"------[ {self.filename} ]------") [self.log_status(pformat(x, indent=2)) for x in self.data] self.log_status("########################")
def test_quantize_basic(self): dcontext = display_context.DisplayContext() dcontext.update(Decimal('1.23'), 'USD') self.assertEqual(Decimal('3.23'), dcontext.quantize(Decimal('3.23253343'), 'USD')) dcontext.update(Decimal('1.2301'), 'USD') dcontext.update(Decimal('1.2302'), 'USD') self.assertEqual(Decimal('3.2325'), dcontext.quantize(Decimal('3.23253343'), 'USD'))
def balayageJSONtable(self, jsondata, afficherCost: bool = False): """Une procédure qui balaye toutes les lignes du JSON""" self.postings = [] self.total = 0 for ligne in jsondata["table"]: # Si debogage, affichage de l'extraction if self.debug: print(ligne) print(parse_datetime(ligne["date"]).date) if ligne["valeurpart"] == "": ligne["valeurpart"] = "1.00" ligne["nbpart"] = ligne["montant"] if afficherCost and re.match("-", ligne["nbpart"]) is None: cost = position.Cost( Decimal( float(ligne["montant"].replace(",", ".").replace( " ", "").replace("\xa0", "").replace(r"\u00a", "")) / float(ligne["nbpart"].replace(",", ".").replace( " ", "").replace("\xa0", "").replace( r"\u00a", ""))).quantize(Decimal(".0001")), "EUR", None, None, ) else: cost = None self.postings.append( data.Posting( account=self.accountList[jsondata["compte"]] + ":" + ligne["isin"].replace(" ", "").upper(), units=amount.Amount( Decimal(ligne["nbpart"].replace(",", ".").replace( " ", "").replace("\xa0", "").replace(r"\u00a", "")), ligne["isin"].replace(" ", "").upper(), ), cost=cost, flag=None, meta=None, price=amount.Amount( Decimal( abs( float(ligne["montant"].replace( ",", ".").replace(" ", "").replace( "\xa0", "").replace(r"\u00a", "")) / float( ligne["nbpart"].replace(",", ".").replace( " ", "").replace("\xa0", "").replace( r"\u00a", "")))).quantize( Decimal(".0001")), "EUR", ), )) self.total = self.total + Decimal(ligne["montant"].replace( ",", ".").replace(" ", "").replace("\xa0", "").replace( r"\u00a", ""))
def NUMBER(self, number): """Process a NUMBER token. Convert into Decimal. Args: number: a str, the number to be converted. Returns: A Decimal instance built of the number string. """ # Note: We don't use D() for efficiency here. # The lexer will only yield valid number strings. if ',' in number: number = number.replace(',', '') return Decimal(number)