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 process_transaction(entry: Transaction, viewpoint: str) -> Optional[Transaction]: if viewpoint == 'everyone': return entry suffix = f':[{viewpoint}]' postings = [] relevant = False for posting in entry.postings: if posting.account.startswith('[Residuals]:'): if not posting.account.endswith(suffix): postings.append( posting._replace(units=amount.mul(posting.units, Decimal(-1)), )) else: if posting.account.endswith(suffix): postings.append( posting._replace(account=posting.account.rsplit(':', 1)[0], )) if posting.account.endswith(suffix): relevant = True if postings: return entry._replace( flag=entry.flag if relevant else 'T', postings=postings, )
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=None)) # 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=None)) 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 push_amount_into_stack(stack, amount): if not stack: stack.append(amount) elif stack[-1] == '*': stack[-2] = mul(stack[-2], amount.number) stack.pop() elif stack[-1] == '/': stack[-2] = div(stack[-2], amount.number) stack.pop() else: stack.append(amount)
def _transaction_feature( entry: Transaction, account: str, negated: bool) -> Tuple[Optional[str], Counter]: link_key = entry.meta.get('share_link_key', None) posting_features = [] for posting in entry.postings: if utils.main_account(posting.account) != account: continue if negated: units = amount.mul(posting.units, Decimal(-1)) else: units = posting.units posting_features.append(units) return link_key, Counter(posting_features)
def _posting_to_sell(pos): """Return a posting to sell fixed assets. Parameters ---------- pos A instance of posting. """ units = convert.get_units(pos) new_units = amount.mul(units, Decimal(-1)) new_meta = pos.meta.copy() try: del new_meta['useful_life'] del new_meta['residual_value'] except KeyError: pass return pos._replace(units=new_units, meta=new_meta)
def transaction_from(self, amount_, date, purpose): second_leg_account = self.default_adjacent_account # account second_leg_flag = "!" # Begin transaction customization area if "Some defining text" in purpose: second_leg_flag = None purpose = "Overwrite purpose" second_leg_account = "Defining:Account" # End transaction customization area postings = [] first_leg = data.Posting( self.account, # account amount_, # amount None, # units None, # price None, # flag None, # meta ) postings.append(first_leg) second_leg = data.Posting( second_leg_account, # account amount.mul(amount_, D(-1)), # amount None, # units None, # price second_leg_flag, # flag None, # meta ) postings.append(second_leg) transaction = data.Transaction( data.new_metadata(self.file.name, 1), # meta date, # date self.flag, # flag " ", # payee purpose, # narration data.EMPTY_SET, # tags data.EMPTY_SET, # links postings, # postings ) return transaction
def distribute_commission_on_metadata(commission, postings): """Distributed the commission to the postings which have a cost basis. This function returns nothing; it inserts metadata on the postings. Args: commission: An Amount instance, the total amount of commission for this trade. postings: A list of postings. """ trade_postings = [] for posting in postings: if posting.cost is not None: cost = (posting.price.number if posting.price else posting.cost.number) trade_postings.append((posting, posting.units.number * cost)) total = sum(cost for _, cost in trade_postings) for posting, cost in trade_postings: if 'commission' not in posting.meta: posting.meta['commission'] = inventory.Inventory() posting_commission = amount.mul(commission, (cost / total)) posting.meta['commission'].add_amount(posting_commission)
def test_mult(self): amount_ = Amount(D('100'), 'CAD') self.assertEqual(Amount(D('102.1'), 'CAD'), amount.mul(amount_, D('1.021')))
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
def validate_sell_gains(entries, options_map): """Check the sum of asset account totals for lots sold with a price on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. """ errors = [] acc_types = options.get_account_types(options_map) proceed_types = set([ acc_types.assets, acc_types.liabilities, acc_types.equity, acc_types.expenses ]) for entry in entries: if not isinstance(entry, data.Transaction): continue # Find transactions whose lots at cost all have a price. postings_at_cost = [ posting for posting in entry.postings if posting.cost is not None ] if not postings_at_cost or not all(posting.price is not None for posting in postings_at_cost): continue # Accumulate the total expected proceeds and the sum of the asset and # expenses legs. total_price = inventory.Inventory() total_proceeds = inventory.Inventory() for posting in entry.postings: # If the posting is held at cost, add the priced value to the balance. if posting.cost is not None: assert posting.price is not None price = posting.price total_price.add_amount(amount.mul(price, -posting.units.number)) else: # Otherwise, use the weight and ignore postings to Income accounts. atype = account_types.get_account_type(posting.account) if atype in proceed_types: total_proceeds.add_amount(convert.get_weight(posting)) # Compare inventories, currency by currency. dict_price = { pos.units.currency: pos.units.number for pos in total_price } dict_proceeds = { pos.units.currency: pos.units.number for pos in total_proceeds } tolerances = interpolate.infer_tolerances(entry.postings, options_map) invalid = False for currency, price_number in dict_price.items(): # Accept a looser than usual tolerance because rounding occurs # differently. Also, it would be difficult for the user to satisfy # two sets of constraints manually. tolerance = tolerances.get(currency) * EXTRA_TOLERANCE_MULTIPLIER proceeds_number = dict_proceeds.pop(currency, ZERO) diff = abs(price_number - proceeds_number) if diff > tolerance: invalid = True break if invalid or dict_proceeds: errors.append( SellGainsError( entry.meta, "Invalid price vs. proceeds/gains: {} vs. {}".format( total_price, total_proceeds), entry)) return entries, errors
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
def depreciate(entries, options_map, config): """Add depreciation entries for fixed assets. See module docstring for more details and example""" config_obj = eval(config, {}, {}) if not isinstance(config_obj, dict): raise RuntimeError("Invalid plugin configuration: should be a single dict.") depr_method = config_obj.pop('method', 'WDV') year_closing_month = config_obj.pop('year_closing_month', 12) half_depr = config_obj.pop('half_depr', True) depr_account = config_obj.pop('account', "Expenses:Depreciation") expense_subaccount = config_obj.pop('expense_subaccount', False) asset_subaccount = config_obj.pop('asset_subaccount', False) if depr_method not in ['WDV','CRA']: raise RuntimeError("Specified depreciation method in plugin not implemented") if not 0 < year_closing_month <= 12: raise RuntimeError("Invalid year-closing-month specified") errors = [] depr_candidates = [] for entry in entries: date = entry.date try: for p in entry.postings: if 'depreciation' in p.meta: depr_candidates.append((date, p, entry)) except (AttributeError, TypeError): pass for date, posting, entry in depr_candidates: narration, rate = posting.meta['depreciation'].split('@') narration = narration.strip() rate = Decimal(rate) rate_used = rate orig_val = posting.units current_val = orig_val new_dates = get_closing_dates(date, year_closing_month) for d in new_dates: if depr_method == 'WDV': if half_depr and d - date < datetime.timedelta(180): # Asset used for less than 180 days, use half the rate allowed. rate_used = rate/2 narration_suffix = " - Half Depreciation (<180days)" else: rate_used = rate narration_suffix = "" elif depr_method == 'CRA': if half_depr and d < datetime.date(date.year+1, date.month, date.day): # Asset purchased this year, use half of rate allowed rate_used = rate/2 narration_suffix = " - Half Depreciation (Same year)" else: rate_used = rate narration_suffix = "" multiplier = Decimal(config_obj.get(str(d.year),1)) rate_used = rate_used*multiplier current_depr = mul(current_val, rate_used) account = posting.account if asset_subaccount: account += ":Depreciation" depr_account_used = depr_account if expense_subaccount: depr_account_used = depr_account + ":" + narration.split(" ")[0] p1 = data.Posting(account=account, price=None, cost=None, meta=None, flag=None, units=mul(current_depr, Decimal(-1))) p2 = data.Posting(account=depr_account_used, price=None, cost=None, meta=None, flag=None, units=current_depr) e = entry._replace(narration=narration + narration_suffix, date=d, flag='*', payee=None, tags={'AUTO-DEPRECIATION'}, postings=[p1, p2]) entries.append(e) current_val = sub(current_val, current_depr) return entries, errors
def depreciate(entries, options_map, config): """Add depreciation entries for fixed assets. See module docstring for more details and example""" config_obj = eval(config, {}, {}) if not isinstance(config_obj, dict): raise RuntimeError("Invalid plugin configuration: should be a single dict.") depr_method = config_obj.pop("method", "WDV") year_closing_month = config_obj.pop("year_closing_month", 12) half_depr = config_obj.pop("half_depr", True) depr_account = config_obj.pop("account", "Expenses:Depreciation") expense_subaccount = config_obj.pop("expense_subaccount", False) asset_subaccount = config_obj.pop("asset_subaccount", False) if depr_method not in ["WDV", "CRA"]: raise RuntimeError("Specified depreciation method in plugin not implemented") if not 0 < year_closing_month <= 12: raise RuntimeError("Invalid year-closing-month specified") errors = [] depr_candidates = [] for entry in entries: date = entry.date try: for p in entry.postings: if "depreciation" in p.meta: depr_candidates.append((date, p, entry)) except (AttributeError, TypeError): pass for date, posting, entry in depr_candidates: narration, rate = posting.meta["depreciation"].split("@") narration = narration.strip() rate = Decimal(rate) rate_used = rate orig_val = posting.units current_val = orig_val new_dates = get_closing_dates(date, year_closing_month) for d in new_dates: if depr_method == "WDV": if half_depr and d - date < datetime.timedelta(180): # Asset used for less than 180 days, use half the rate allowed. rate_used = rate / 2 narration_suffix = " - Half Depreciation (<180days)" else: rate_used = rate narration_suffix = "" elif depr_method == "CRA": if half_depr and d < datetime.date(date.year + 1, date.month, date.day): # Asset purchased this year, use half of rate allowed rate_used = rate / 2 narration_suffix = " - Half Depreciation (Same year)" else: rate_used = rate narration_suffix = "" multiplier = Decimal(config_obj.get(str(d.year), 1)) rate_used = rate_used * multiplier current_depr = mul(current_val, rate_used) account = posting.account if asset_subaccount: account += ":Depreciation" depr_account_used = depr_account if expense_subaccount: depr_account_used = depr_account + ":" + narration.split(" ")[0] p1 = data.Posting( account=account, price=None, cost=None, meta=None, flag=None, units=mul(current_depr, Decimal(-1)) ) p2 = data.Posting( account=depr_account_used, price=None, cost=None, meta=None, flag=None, units=current_depr ) e = entry._replace( narration=narration + narration_suffix, date=d, flag="*", payee=None, tags={"AUTO-DEPRECIATION"}, postings=[p1, p2], ) entries.append(e) current_val = sub(current_val, current_depr) return entries, errors