Exemple #1
0
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
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
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)
Exemple #7
0
    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
Exemple #8
0
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)
Exemple #9
0
 def test_mult(self):
     amount_ = Amount(D('100'), 'CAD')
     self.assertEqual(Amount(D('102.1'), 'CAD'),
                      amount.mul(amount_, D('1.021')))
Exemple #10
0
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
Exemple #11
0
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
Exemple #13
0
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