Rename "currency" to "commodity"

Apologies to hypothetical API users
This commit is contained in:
RunasSudo 2020-04-04 04:15:32 +11:00
parent 5080aa7ba7
commit b200501e37
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
17 changed files with 171 additions and 171 deletions

View File

@ -1,7 +1,7 @@
# Set up how we will call Ledger
ledger_file: /path/to/ledger.journal
ledger_args: ['--pedantic', '--recursive-aliases']
report_currency: ['$', True] # True if prefix, False if suffix
report_commodity: ['$', True] # True if prefix, False if suffix
# Tell ledger-pyreport about the top-level account categories
assets_account: Assets

View File

@ -1,7 +1,7 @@
# Set up how we will call Ledger
ledger_file: demo/ledger.journal
ledger_args: []
report_currency: ['$', True] # True if prefix, False if suffix
report_commodity: ['$', True] # True if prefix, False if suffix
# Tell ledger-pyreport about the top-level account categories
assets_account: Assets

View File

@ -40,27 +40,27 @@ def trial():
compare = int(flask.request.args['compare'])
cash = flask.request.args.get('cash', False)
report_currency = Currency(*config['report_currency'])
report_commodity = Commodity(*config['report_commodity'])
if compare == 0:
# Get trial balance
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
trial_balance = accounting.trial_balance(l, date, pstart, report_currency)
l = accounting.ledger_to_cash(l, report_commodity)
trial_balance = accounting.trial_balance(l, date, pstart, report_commodity)
total_dr = Amount(0, report_currency)
total_cr = Amount(0, report_currency)
total_dr = Amount(0, report_commodity)
total_cr = Amount(0, report_commodity)
for account in l.accounts.values():
# Display in "cost basis" as we have already accounted for unrealised gains
balance = trial_balance.get_balance(account).exchange(report_currency, True)
balance = trial_balance.get_balance(account).exchange(report_commodity, True)
if balance > 0:
total_dr += balance
else:
total_cr -= balance
return flask.render_template('trial.html', date=date, pstart=pstart, trial_balance=trial_balance, accounts=sorted(l.accounts.values(), key=lambda a: a.name), total_dr=total_dr, total_cr=total_cr, report_currency=report_currency)
return flask.render_template('trial.html', date=date, pstart=pstart, trial_balance=trial_balance, accounts=sorted(l.accounts.values(), key=lambda a: a.name), total_dr=total_dr, total_cr=total_cr, report_commodity=report_commodity)
else:
# Get multiple trial balances for comparison
dates = [date.replace(year=date.year - i) for i in range(0, compare + 1)]
@ -68,16 +68,16 @@ def trial():
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
trial_balances = [accounting.trial_balance(l.clone(), d, p, report_currency) for d, p in zip(dates, pstarts)]
l = accounting.ledger_to_cash(l, report_commodity)
trial_balances = [accounting.trial_balance(l.clone(), d, p, report_commodity) for d, p in zip(dates, pstarts)]
# Delete accounts with always zero balances
accounts = sorted(l.accounts.values(), key=lambda a: a.name)
for account in accounts[:]:
if all(t.get_balance(account).exchange(report_currency, True).near_zero for t in trial_balances):
if all(t.get_balance(account).exchange(report_commodity, True).near_zero for t in trial_balances):
accounts.remove(account)
return flask.render_template('trial_multiple.html', trial_balances=trial_balances, accounts=accounts, report_currency=report_currency, cash=cash)
return flask.render_template('trial_multiple.html', trial_balances=trial_balances, accounts=accounts, report_commodity=report_commodity, cash=cash)
@app.route('/balance')
def balance():
@ -89,19 +89,19 @@ def balance():
dates = [date.replace(year=date.year - i) for i in range(0, compare + 1)]
pstarts = [pstart.replace(year=pstart.year - i) for i in range(0, compare + 1)]
report_currency = Currency(*config['report_currency'])
report_commodity = Commodity(*config['report_commodity'])
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
balance_sheets = [accounting.balance_sheet(accounting.trial_balance(l.clone(), d, p, report_currency)) for d, p in zip(dates, pstarts)]
l = accounting.ledger_to_cash(l, report_commodity)
balance_sheets = [accounting.balance_sheet(accounting.trial_balance(l.clone(), d, p, report_commodity)) for d, p in zip(dates, pstarts)]
# Delete accounts with always zero balances
accounts = list(l.accounts.values())
for account in accounts[:]:
if all(b.get_balance(account).exchange(report_currency, True).near_zero and b.get_total(account).exchange(report_currency, True).near_zero for b in balance_sheets):
if all(b.get_balance(account).exchange(report_commodity, True).near_zero and b.get_total(account).exchange(report_commodity, True).near_zero for b in balance_sheets):
accounts.remove(account)
return flask.render_template('balance.html', ledger=l, balance_sheets=balance_sheets, accounts=accounts, config=config, report_currency=report_currency, cash=cash)
return flask.render_template('balance.html', ledger=l, balance_sheets=balance_sheets, accounts=accounts, config=config, report_commodity=report_commodity, cash=cash)
def describe_period(date_end, date_beg):
if date_end == (date_beg.replace(year=date_beg.year + 1) - timedelta(days=1)):
@ -122,11 +122,11 @@ def pandl():
dates_beg = [date_beg.replace(year=date_beg.year - i) for i in range(0, compare + 1)]
dates_end = [date_end.replace(year=date_end.year - i) for i in range(0, compare + 1)]
report_currency = Currency(*config['report_currency'])
report_commodity = Commodity(*config['report_commodity'])
l = ledger.raw_transactions_at_date(date_end)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
pandls = [accounting.trial_balance(l.clone(), de, db, report_currency) for de, db in zip(dates_end, dates_beg)]
l = accounting.ledger_to_cash(l, report_commodity)
pandls = [accounting.trial_balance(l.clone(), de, db, report_commodity) for de, db in zip(dates_end, dates_beg)]
# Delete accounts with always zero balances
accounts = list(l.accounts.values())
@ -134,7 +134,7 @@ def pandl():
if all(p.get_balance(account) == 0 and p.get_total(account) == 0 for p in pandls):
accounts.remove(account)
return flask.render_template('pandl.html', period=describe_period(date_end, date_beg), ledger=l, pandls=pandls, accounts=accounts, config=config, report_currency=report_currency, cash=cash, scope=scope)
return flask.render_template('pandl.html', period=describe_period(date_end, date_beg), ledger=l, pandls=pandls, accounts=accounts, config=config, report_commodity=report_commodity, cash=cash, scope=scope)
@app.route('/cashflow')
def cashflow():
@ -146,7 +146,7 @@ def cashflow():
dates_beg = [date_beg.replace(year=date_beg.year - i) for i in range(0, compare + 1)]
dates_end = [date_end.replace(year=date_end.year - i) for i in range(0, compare + 1)]
report_currency = Currency(*config['report_currency'])
report_commodity = Commodity(*config['report_commodity'])
l = ledger.raw_transactions_at_date(date_end)
cash_accounts = [a for a in l.accounts.values() if a.is_cash]
@ -157,18 +157,18 @@ def cashflow():
cashflows = []
profits = []
for de, db in zip(dates_end, dates_beg):
tb = accounting.trial_balance(l.clone(), db - timedelta(days=1), db, report_currency)
opening_balances.append(sum((tb.get_balance(a) for a in cash_accounts), Balance()).exchange(report_currency, True))
tb = accounting.trial_balance(l.clone(), db - timedelta(days=1), db, report_commodity)
opening_balances.append(sum((tb.get_balance(a) for a in cash_accounts), Balance()).exchange(report_commodity, True))
tb = accounting.trial_balance(l.clone(), de, db, report_currency)
closing_balances.append(sum((tb.get_balance(a) for a in cash_accounts), Balance()).exchange(report_currency, True))
tb = accounting.trial_balance(l.clone(), de, db, report_commodity)
closing_balances.append(sum((tb.get_balance(a) for a in cash_accounts), Balance()).exchange(report_commodity, True))
if method == 'direct':
# Determine transactions affecting cash assets
cashflows.append(accounting.account_flows(tb.ledger, de, db, cash_accounts, True))
else:
# Determine net profit (loss)
profits.append(-(tb.get_total(tb.ledger.get_account(config['income_account'])) + tb.get_total(tb.ledger.get_account(config['expenses_account'])) + tb.get_total(tb.ledger.get_account(config['oci_account']))).exchange(report_currency, True))
profits.append(-(tb.get_total(tb.ledger.get_account(config['income_account'])) + tb.get_total(tb.ledger.get_account(config['expenses_account'])) + tb.get_total(tb.ledger.get_account(config['oci_account']))).exchange(report_commodity, True))
# Determine transactions affecting equity, liabilities and non-cash assets
noncash_accounts = [a for a in l.accounts.values() if a.is_equity or a.is_liability or (a.is_asset and not a.is_cash)]
@ -181,9 +181,9 @@ def cashflow():
accounts.remove(account)
if method == 'direct':
return flask.render_template('cashflow_direct.html', period=describe_period(date_end, date_beg), ledger=l, cashflows=cashflows, opening_balances=opening_balances, closing_balances=closing_balances, accounts=accounts, config=config, report_currency=report_currency)
return flask.render_template('cashflow_direct.html', period=describe_period(date_end, date_beg), ledger=l, cashflows=cashflows, opening_balances=opening_balances, closing_balances=closing_balances, accounts=accounts, config=config, report_commodity=report_commodity)
else:
return flask.render_template('cashflow_indirect.html', period=describe_period(date_end, date_beg), ledger=l, cashflows=cashflows, profits=profits, opening_balances=opening_balances, closing_balances=closing_balances, accounts=accounts, config=config, report_currency=report_currency)
return flask.render_template('cashflow_indirect.html', period=describe_period(date_end, date_beg), ledger=l, cashflows=cashflows, profits=profits, opening_balances=opening_balances, closing_balances=closing_balances, accounts=accounts, config=config, report_commodity=report_commodity)
@app.route('/transactions')
def transactions():
@ -193,24 +193,24 @@ def transactions():
cash = flask.request.args.get('cash', False)
commodity = flask.request.args.get('commodity', False)
report_currency = Currency(*config['report_currency'])
report_commodity = Commodity(*config['report_commodity'])
# General ledger
l = ledger.raw_transactions_at_date(date_end)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
l = accounting.ledger_to_cash(l, report_commodity)
# Unrealized gains
l = accounting.trial_balance(l, date_end, date_beg, report_currency).ledger
l = accounting.trial_balance(l, date_end, date_beg, report_commodity).ledger
if not account:
# General Ledger
transactions = [t for t in l.transactions if t.date <= date_end and t.date >= date_beg]
total_dr = sum((p.amount for t in transactions for p in t.postings if p.amount > 0), Balance()).exchange(report_currency, True)
total_cr = sum((p.amount for t in transactions for p in t.postings if p.amount < 0), Balance()).exchange(report_currency, True)
total_dr = sum((p.amount for t in transactions for p in t.postings if p.amount > 0), Balance()).exchange(report_commodity, True)
total_cr = sum((p.amount for t in transactions for p in t.postings if p.amount < 0), Balance()).exchange(report_commodity, True)
return flask.render_template('transactions.html', date_beg=date_beg, date_end=date_end, period=describe_period(date_end, date_beg), account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_currency=report_currency, cash=cash)
return flask.render_template('transactions.html', date_beg=date_beg, date_end=date_end, period=describe_period(date_end, date_beg), account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_commodity=report_commodity, cash=cash)
elif commodity:
# Account Transactions with commodity detail
account = l.get_account(account)
@ -221,18 +221,18 @@ def transactions():
closing_balance = accounting.trial_balance_raw(l, date_end, date_beg).get_balance(account).clean()
def matching_posting(transaction, amount):
return next((p for p in transaction.postings if p.account == account and p.amount.currency == amount.currency), None)
return next((p for p in transaction.postings if p.account == account and p.amount.commodity == amount.commodity), None)
return flask.render_template('transactions_commodity.html', date_beg=date_beg, date_end=date_end, period=describe_period(date_end, date_beg), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_currency=report_currency, cash=cash, timedelta=timedelta, matching_posting=matching_posting)
return flask.render_template('transactions_commodity.html', date_beg=date_beg, date_end=date_end, period=describe_period(date_end, date_beg), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_commodity=report_commodity, cash=cash, timedelta=timedelta, matching_posting=matching_posting)
else:
# Account Transactions
account = l.get_account(account)
transactions = [t for t in l.transactions if t.date <= date_end and t.date >= date_beg and any(p.account == account for p in t.postings)]
opening_balance = accounting.trial_balance_raw(l, date_beg - timedelta(days=1), date_beg).get_balance(account).exchange(report_currency, True)
closing_balance = accounting.trial_balance_raw(l, date_end, date_beg).get_balance(account).exchange(report_currency, True)
opening_balance = accounting.trial_balance_raw(l, date_beg - timedelta(days=1), date_beg).get_balance(account).exchange(report_commodity, True)
closing_balance = accounting.trial_balance_raw(l, date_end, date_beg).get_balance(account).exchange(report_commodity, True)
return flask.render_template('transactions.html', date_beg=date_beg, date_end=date_end, period=describe_period(date_end, date_beg), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_currency=report_currency, cash=cash, timedelta=timedelta)
return flask.render_template('transactions.html', date_beg=date_beg, date_end=date_end, period=describe_period(date_end, date_beg), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_commodity=report_commodity, cash=cash, timedelta=timedelta)
@app.route('/transaction')
def transaction():
@ -240,12 +240,12 @@ def transaction():
cash = flask.request.args.get('cash', False)
commodity = flask.request.args.get('commodity', False)
report_currency = Currency(*config['report_currency'])
report_commodity = Commodity(*config['report_commodity'])
# General ledger
l = ledger.raw_transactions_at_date(None)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
l = accounting.ledger_to_cash(l, report_commodity)
transaction = next((t for t in l.transactions if str(t.id) == tid))
@ -253,11 +253,11 @@ def transaction():
total_dr = sum((p.amount for p in transaction.postings if p.amount > 0), Balance()).clean()
total_cr = sum((p.amount for p in transaction.postings if p.amount < 0), Balance()).clean()
totals = itertools.zip_longest(total_dr.amounts, total_cr.amounts)
return flask.render_template('transaction_commodity.html', ledger=l, transaction=transaction, totals=totals, total_dr=total_dr.exchange(report_currency, True), total_cr=total_cr.exchange(report_currency, True), report_currency=report_currency, cash=cash)
return flask.render_template('transaction_commodity.html', ledger=l, transaction=transaction, totals=totals, total_dr=total_dr.exchange(report_commodity, True), total_cr=total_cr.exchange(report_commodity, True), report_commodity=report_commodity, cash=cash)
else:
total_dr = sum((p.amount for p in transaction.postings if p.amount > 0), Balance()).exchange(report_currency, True)
total_cr = sum((p.amount for p in transaction.postings if p.amount < 0), Balance()).exchange(report_currency, True)
return flask.render_template('transaction.html', ledger=l, transaction=transaction, total_dr=total_dr, total_cr=total_cr, report_currency=report_currency, cash=cash)
total_dr = sum((p.amount for p in transaction.postings if p.amount > 0), Balance()).exchange(report_commodity, True)
total_cr = sum((p.amount for p in transaction.postings if p.amount < 0), Balance()).exchange(report_commodity, True)
return flask.render_template('transaction.html', ledger=l, transaction=transaction, total_dr=total_dr, total_cr=total_cr, report_commodity=report_commodity, cash=cash)
# Template filters
@ -289,21 +289,21 @@ def filter_amount_positive(amt):
return flask.Markup('<span title="{}">{:,.2f}</span>'.format(amt.tostr(False), amt.amount).replace(',', '&#8239;'))
@app.template_filter('bc')
def filter_currency_positive(amt):
if amt.currency.is_prefix:
return flask.Markup('<span title="{}">{}{:,.2f}</span>'.format(amt.tostr(False), amt.currency.name, amt.amount).replace(',', '&#8239;'))
def filter_commodity_positive(amt):
if amt.commodity.is_prefix:
return flask.Markup('<span title="{}">{}{:,.2f}</span>'.format(amt.tostr(False), amt.commodity.name, amt.amount).replace(',', '&#8239;'))
else:
return flask.Markup('<span title="{}">{:,.2f} {}</span>'.format(amt.tostr(False), amt.amount, amt.currency.name).replace(',', '&#8239;'))
return flask.Markup('<span title="{}">{:,.2f} {}</span>'.format(amt.tostr(False), amt.amount, amt.commodity.name).replace(',', '&#8239;'))
@app.template_filter('bt')
def filter_currency_table_positive(amt, show_price, link=None):
def filter_commodity_table_positive(amt, show_price, link=None):
result = []
if amt.currency.is_prefix:
amt_str = filter_currency_positive(amt)
if amt.commodity.is_prefix:
amt_str = filter_commodity_positive(amt)
cur_str = ''
else:
amt_str = '{:,.2f}'.format(amt.amount).replace(',', '&#8239;')
cur_str = amt.currency.name
cur_str = amt.commodity.name
amt_full = amt.tostr(False)
@ -311,8 +311,8 @@ def filter_currency_table_positive(amt, show_price, link=None):
result.append('<td><span title="{}">{}</span></td>'.format(amt_full, cur_str))
if show_price:
if amt.currency.price:
result.append('<td><span title="{}">{{{}}}</span></td>'.format(amt_full, filter_currency_positive(amt.currency.price)))
if amt.commodity.price:
result.append('<td><span title="{}">{{{}}}</span></td>'.format(amt_full, filter_commodity_positive(amt.commodity.price)))
else:
result.append('<td></td>')
@ -326,16 +326,16 @@ def debug_noncash_transactions():
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
account = flask.request.args.get('account')
report_currency = Currency(*config['report_currency'])
report_commodity = Commodity(*config['report_commodity'])
l = ledger.raw_transactions_at_date(date)
account = l.get_account(account)
transactions = [t for t in l.transactions if any(p.account == account for p in t.postings)]
accounting.account_to_cash(account, report_currency)
accounting.account_to_cash(account, report_commodity)
return flask.render_template('debug_noncash_transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=account, ledger=l, transactions=transactions, report_currency=report_currency)
return flask.render_template('debug_noncash_transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=account, ledger=l, transactions=transactions, report_commodity=report_commodity)
@app.route('/debug/imbalances')
def debug_imbalances():
@ -343,15 +343,15 @@ def debug_imbalances():
pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d')
cash = flask.request.args.get('cash', False)
report_currency = Currency(*config['report_currency'])
report_commodity = Commodity(*config['report_commodity'])
l = ledger.raw_transactions_at_date(date)
if cash:
l = accounting.ledger_to_cash(l, report_currency)
l = accounting.ledger_to_cash(l, report_commodity)
transactions = [t for t in l.transactions if t.date <= date and t.date >= pstart and not sum((p.amount for p in t.postings), Balance()).exchange(report_currency, True).near_zero]
transactions = [t for t in l.transactions if t.date <= date and t.date >= pstart and not sum((p.amount for p in t.postings), Balance()).exchange(report_commodity, True).near_zero]
total_dr = sum((p.amount for t in transactions for p in t.postings if p.amount > 0), Balance()).exchange(report_currency, True)
total_cr = sum((p.amount for t in transactions for p in t.postings if p.amount < 0), Balance()).exchange(report_currency, True)
total_dr = sum((p.amount for t in transactions for p in t.postings if p.amount > 0), Balance()).exchange(report_commodity, True)
total_cr = sum((p.amount for t in transactions for p in t.postings if p.amount < 0), Balance()).exchange(report_commodity, True)
return flask.render_template('transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_currency=report_currency, cash=cash)
return flask.render_template('transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_commodity=report_commodity, cash=cash)

View File

@ -41,9 +41,9 @@ def trial_balance_raw(ledger, date, pstart):
return tb
# Trial balance with unrealized gains and OCI
def trial_balance(ledger, date, pstart, currency):
tb_date, r_date = _add_unrealized_gains(trial_balance_raw(ledger, date, pstart), currency)
tb_pstart, r_pstart = _add_unrealized_gains(trial_balance_raw(ledger, pstart - timedelta(days=1), pstart), currency)
def trial_balance(ledger, date, pstart, commodity):
tb_date, r_date = _add_unrealized_gains(trial_balance_raw(ledger, date, pstart), commodity)
tb_pstart, r_pstart = _add_unrealized_gains(trial_balance_raw(ledger, pstart - timedelta(days=1), pstart), commodity)
for account in set(list(r_date.keys()) + list(r_pstart.keys())):
if account in r_pstart:
@ -66,7 +66,7 @@ def trial_balance(ledger, date, pstart, currency):
return tb_date
# Adjust (in place) a trial balance for unrealized gains without accumulating OCI
def _add_unrealized_gains(tb, currency):
def _add_unrealized_gains(tb, commodity):
results = {}
unrealized_gain_account = tb.ledger.get_account(config['unrealized_gains'])
@ -74,8 +74,8 @@ def _add_unrealized_gains(tb, currency):
if not account.is_market:
continue
total_cost = tb.get_balance(account).exchange(currency, True)
total_market = tb.get_balance(account).exchange(currency, False, tb.date, tb.ledger)
total_cost = tb.get_balance(account).exchange(commodity, True)
total_market = tb.get_balance(account).exchange(commodity, False, tb.date, tb.ledger)
unrealized_gain = total_market - total_cost
if unrealized_gain != 0:
@ -105,7 +105,7 @@ def balance_sheet(tb):
return tb
def account_to_cash(account, currency):
def account_to_cash(account, commodity):
# Apply FIFO methodology to match postings
balance = [] # list of [posting, amount to balance, amount remaining, balancing list of [posting, amount balanced]]
@ -117,7 +117,7 @@ def account_to_cash(account, currency):
pass
else:
# Try to balance postings
amount_to_balance = posting.amount.exchange(currency, True).amount
amount_to_balance = posting.amount.exchange(commodity, True).amount
while amount_to_balance != 0:
balancing_posting = next((b for b in balance if b[2] != 0 and math.copysign(1, b[2]) != math.copysign(1, amount_to_balance)), None)
@ -142,11 +142,11 @@ def account_to_cash(account, currency):
# Finalise balanced postings
for orig_posting, amount_to_balance, amount_remaining, balancing_postings in balance:
posting = Posting(orig_posting.transaction, orig_posting.account, Amount(amount_to_balance, currency))
posting = Posting(orig_posting.transaction, orig_posting.account, Amount(amount_to_balance, commodity))
posting.transaction.postings.append(posting)
for balancing_posting, amount_balanced in balancing_postings:
posting.transaction.postings.append(Posting(posting.transaction, balancing_posting.account, Amount(amount_balanced, currency)))
posting.transaction.postings.append(Posting(posting.transaction, balancing_posting.account, Amount(amount_balanced, commodity)))
if balancing_posting in balancing_posting.transaction.postings:
balancing_posting.transaction.postings.remove(balancing_posting)
@ -154,16 +154,16 @@ def account_to_cash(account, currency):
if amount_remaining != 0:
if posting.account.is_asset:
# Cash - charge any unbalanced remainder to Other Income
posting.transaction.postings.append(Posting(posting.transaction, account.ledger.get_account(config['cash_other_income']), Amount(-amount_remaining, currency)))
posting.transaction.postings.append(Posting(posting.transaction, account.ledger.get_account(config['cash_other_income']), Amount(-amount_remaining, commodity)))
else:
# Liabilities, etc. - discard any unbalanced remainder
posting.amount.amount -= amount_remaining
# Adjust (in place) a ledger to convert accounting to a cash basis
def ledger_to_cash(ledger, currency):
def ledger_to_cash(ledger, commodity):
for account in list(ledger.accounts.values()):
if not (account.is_cash or account.is_income or account.is_expense or account.is_equity):
account_to_cash(account, currency)
account_to_cash(account, commodity)
return ledger

View File

@ -36,7 +36,7 @@
{% endif %}
</td>
{% for balance_sheet in balance_sheets %}
{% set amount = (-balance_sheet.get_balance(account) if invert else balance_sheet.get_balance(account)).exchange(report_currency, True) %}
{% set amount = (-balance_sheet.get_balance(account) if invert else balance_sheet.get_balance(account)).exchange(report_commodity, True) %}
<td>
{% if not amount.near_zero %}
{% if account.name == config['current_year_earnings'] %}
@ -74,14 +74,14 @@
<tr class="total">
<td>Total {{ account_class.bits[-1] }} {{ label }}</td>
{% for balance_sheet in balance_sheets %}<td>{{ (-balance_sheet.get_total(account_class) if invert else balance_sheet.get_total(account_class)).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for balance_sheet in balance_sheets %}<td>{{ (-balance_sheet.get_total(account_class) if invert else balance_sheet.get_total(account_class)).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
{% endfor %}
<tr class="total">
<td>Total {{ label }}</td>
{% for balance_sheet in balance_sheets %}<td>{{ (-balance_sheet.get_total(root) if invert else balance_sheet.get_total(root)).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for balance_sheet in balance_sheets %}<td>{{ (-balance_sheet.get_total(root) if invert else balance_sheet.get_total(root)).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
{% endmacro %}
@ -113,7 +113,7 @@
<tr class="total">
<td>Total Equity</td>
{% for balance_sheet in balance_sheets %}<td>{{ -balance_sheet.get_total(ledger.get_account(config['equity_account'])).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for balance_sheet in balance_sheets %}<td>{{ -balance_sheet.get_total(ledger.get_account(config['equity_account'])).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
</table>
{% endblock %}

View File

@ -28,7 +28,7 @@
{% endif %}
</td>
{% for cashflow in cashflows %}
{% set amount = (-cashflow.get_balance(account) if invert else cashflow.get_balance(account)).exchange(report_currency, True) %}
{% set amount = (-cashflow.get_balance(account) if invert else cashflow.get_balance(account)).exchange(report_commodity, True) %}
<td>{% if not amount.near_zero %}{{ amount|a('/transactions?' + {'date': cashflow.date.strftime('%Y-%m-%d'), 'pstart': cashflow.pstart.strftime('%Y-%m-%d'), 'account': account.name, 'cash': 'on' if cash else ''}|urlencode) }}{% endif %}</td>
{% endfor %}
</tr>
@ -55,7 +55,7 @@
{% endfor %}
<tr class="total">
<td>Net Cash Inflow (Outflow)</td>
{% for cashflow in cashflows %}<td>{{ cashflow.get_total(ledger.root_account).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for cashflow in cashflows %}<td>{{ cashflow.get_total(ledger.root_account).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
<tr><td colspan="{{ cashflows|length + 1 }}">&nbsp;</td></tr>
@ -66,7 +66,7 @@
</tr>
<tr>
<td>Net Cash Inflow (Outflow)</td>
{% for cashflow in cashflows %}<td>{{ cashflow.get_total(ledger.root_account).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for cashflow in cashflows %}<td>{{ cashflow.get_total(ledger.root_account).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
<tr class="total">
<td>Closing Cash</td>

View File

@ -28,7 +28,7 @@
{% endif %}
</td>
{% for cashflow in cashflows %}
{% set amount = (-cashflow.get_balance(account) if invert else cashflow.get_balance(account)).exchange(report_currency, True) %}
{% set amount = (-cashflow.get_balance(account) if invert else cashflow.get_balance(account)).exchange(report_commodity, True) %}
<td>{% if not amount.near_zero %}{{ amount|a('/transactions?' + {'date': cashflow.date.strftime('%Y-%m-%d'), 'pstart': cashflow.pstart.strftime('%Y-%m-%d'), 'account': account.name, 'cash': 'on' if cash else ''}|urlencode) }}{% endif %}</td>
{% endfor %}
</tr>
@ -65,11 +65,11 @@
{% endfor %}
<tr class="total">
<td>Total Adjustments</td>
{% for cashflow in cashflows %}<td>{{ cashflow.get_total(ledger.root_account).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for cashflow in cashflows %}<td>{{ cashflow.get_total(ledger.root_account).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
<tr class="total">
<td>Net Cash Inflow (Outflow)</td>
{% for cashflow in cashflows %}<td>{{ (profits[loop.index0] + cashflow.get_total(ledger.root_account).exchange(report_currency, True))|a }}</td>{% endfor %}
{% for cashflow in cashflows %}<td>{{ (profits[loop.index0] + cashflow.get_total(ledger.root_account).exchange(report_commodity, True))|a }}</td>{% endfor %}
</tr>
<tr><td colspan="{{ cashflows|length + 1 }}">&nbsp;</td></tr>
@ -80,7 +80,7 @@
</tr>
<tr>
<td>Net Cash Inflow (Outflow)</td>
{% for cashflow in cashflows %}<td>{{ (profits[loop.index0] + cashflow.get_total(ledger.root_account).exchange(report_currency, True))|a }}</td>{% endfor %}
{% for cashflow in cashflows %}<td>{{ (profits[loop.index0] + cashflow.get_total(ledger.root_account).exchange(report_commodity, True))|a }}</td>{% endfor %}
</tr>
<tr class="total">
<td>Closing Cash</td>

View File

@ -36,7 +36,7 @@
{% for transaction in transactions %}
{% for posting in transaction.postings %}
{% set amount = posting.exchange(report_currency, transaction.date) %}
{% set amount = posting.exchange(report_commodity, transaction.date) %}
<tr>
<td>{% if loop.first %}{{ transaction.date.strftime('%Y-%m-%d') }}{% endif %}</td>
<td>{% if loop.first %}{{ transaction.description }}{% endif %}</td>

View File

@ -28,7 +28,7 @@
{% endif %}
</td>
{% for pandl in pandls %}
{% set amount = (-pandl.get_balance(account) if invert else pandl.get_balance(account)).exchange(report_currency, True) %}
{% set amount = (-pandl.get_balance(account) if invert else pandl.get_balance(account)).exchange(report_commodity, True) %}
<td>{% if not amount.near_zero %}{{ amount|a('/transactions?' + {'date': pandl.date.strftime('%Y-%m-%d'), 'pstart': pandl.pstart.strftime('%Y-%m-%d'), 'account': account.name, 'cash': 'on' if cash else ''}|urlencode) }}{% endif %}</td>
{% endfor %}
</tr>
@ -55,7 +55,7 @@
<tr class="total">
<td>Total {{ label }}</td>
{% for pandl in pandls %}<td>{{ (-pandl.get_total(root) if invert else pandl.get_total(root)).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for pandl in pandls %}<td>{{ (-pandl.get_total(root) if invert else pandl.get_total(root)).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
{% endmacro %}
@ -76,7 +76,7 @@
<tr class="total">
<td>Net Profit (Loss)</td>
{% for pandl in pandls %}<td>{{ -(pandl.get_total(ledger.get_account(config['income_account'])) + pandl.get_total(ledger.get_account(config['expenses_account']))).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for pandl in pandls %}<td>{{ -(pandl.get_total(ledger.get_account(config['income_account'])) + pandl.get_total(ledger.get_account(config['expenses_account']))).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
{% else %}
<tr>
@ -85,7 +85,7 @@
</tr>
<tr>
<td>Net Profit (Loss)</td>
{% for pandl in pandls %}<td>{{ -(pandl.get_total(ledger.get_account(config['income_account'])) + pandl.get_total(ledger.get_account(config['expenses_account']))).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for pandl in pandls %}<td>{{ -(pandl.get_total(ledger.get_account(config['income_account'])) + pandl.get_total(ledger.get_account(config['expenses_account']))).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
{% endif %}
@ -99,13 +99,13 @@
<tr class="total">
<td>Total Other Comprehensive Income</td>
{% for pandl in pandls %}<td>{{ -pandl.get_total(ledger.get_account(config['oci_account'])).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for pandl in pandls %}<td>{{ -pandl.get_total(ledger.get_account(config['oci_account'])).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
<tr><td colspan="{{ pandls|length + 1}}">&nbsp;</td></tr>
<tr class="total">
<td>Total Comprehensive Income</td>
{% for pandl in pandls %}<td>{{ -(pandl.get_total(ledger.get_account(config['income_account'])) + pandl.get_total(ledger.get_account(config['expenses_account'])) + pandl.get_total(ledger.get_account(config['oci_account']))).exchange(report_currency, True)|a }}</td>{% endfor %}
{% for pandl in pandls %}<td>{{ -(pandl.get_total(ledger.get_account(config['income_account'])) + pandl.get_total(ledger.get_account(config['expenses_account'])) + pandl.get_total(ledger.get_account(config['oci_account']))).exchange(report_commodity, True)|a }}</td>{% endfor %}
</tr>
{% endif %}
</table>

View File

@ -51,7 +51,7 @@
</tr>
{% for posting in transaction.postings %}
{% set amount = posting.exchange(report_currency, transaction.date) %}
{% set amount = posting.exchange(report_commodity, transaction.date) %}
{% set trn_url = '/transaction?' + {'tid': transaction.id, 'cash': 'on' if cash else ''}|urlencode %}
<tr>
<td>{{ posting.comment }}</td>

View File

@ -42,7 +42,7 @@
<th style="width: 50%;">Description</th>
<th style="width: 50%;">Account</th>
<th style="text-align: right; min-width: 4em;">Dr</th> {# Amount #}
<th style="min-width: 2em;"></th> {# Currency #}
<th style="min-width: 2em;"></th> {# Commodity #}
<th style="min-width: 2em;"></th> {# Price #}
<th style="text-align: right; min-width: 4em;">Cr</th>
<th style="min-width: 2em;"></th>

View File

@ -68,7 +68,7 @@
{% for transaction in transactions %}
{% for posting in transaction.postings if posting.account != account %}
{% set amount = posting.exchange(report_currency, transaction.date) %}
{% set amount = posting.exchange(report_commodity, transaction.date) %}
{% set trn_url = '/transaction?' + {'tid': transaction.id, 'cash': 'on' if cash else ''}|urlencode %}
<tr>
<td>{% if loop.first %}{% if transaction.id %}<a href="{{ trn_url }}">{% endif %}{{ transaction.date.strftime('%Y-%m-%d') }}{% if transaction.id %}</a>{% endif %}{% endif %}</td>

View File

@ -31,9 +31,9 @@
<th style="width: 100%;">Description</th>
<th style="min-width: 1em;"></th> {# Dr/Cr #}
<th style="text-align: right; min-width: 4em;">Amount</th>
<th style="min-width: 2em;"></th> {# Currency #}
<th style="min-width: 2em;"></th> {# Commodity #}
<th style="text-align: right; min-width: 4em;">Balance</th>
<th style="min-width: 2em;"></th> {# Currency #}
<th style="min-width: 2em;"></th> {# Commodity #}
<th style="min-width: 2em;"></th> {# Price #}
<th style="min-width: 1em;"></th>
</tr>
@ -106,7 +106,7 @@
<td></td>
<td></td>
<td></td>
{% set closing_balance = closing_balance.exchange(report_currency, True) %}
{% set closing_balance = closing_balance.exchange(report_commodity, True) %}
{{ closing_balance|abs|bt(True) }}
<td>{% if closing_balance >= 0 %}Dr{% else %}Cr{% endif %}</td>
</tr>

View File

@ -32,7 +32,7 @@
</tr>
{% for account in accounts %}
{# Display in "cost basis" as we have already accounted for unrealised gains #}
{% set balance = trial_balance.get_balance(account).exchange(report_currency, True) %}
{% set balance = trial_balance.get_balance(account).exchange(report_commodity, True) %}
{% set trn_url = "/transactions?" + {'date_end': trial_balance.date.strftime('%Y-%m-%d'), 'date_beg': trial_balance.pstart.strftime('%Y-%m-%d'), 'account': account.name, 'cash': 'on' if cash else ''}|urlencode %}
{% if not balance.near_zero %}
<tr>

View File

@ -33,7 +33,7 @@
<tr>
<td>{{ account.name }}</td>
{% for trial_balance in trial_balances %}
{% set balance = trial_balance.get_balance(account).exchange(report_currency, True) %}
{% set balance = trial_balance.get_balance(account).exchange(report_commodity, True) %}
<td>{% if not balance.near_zero %}<a href="/transactions?{{ {'date_end': trial_balance.date.strftime('%Y-%m-%d'), 'date_beg': trial_balance.pstart.strftime('%Y-%m-%d'), 'account': account.name, 'cash': 'on' if cash else ''}|urlencode }}">{{ balance|abs|b }} {% if balance >= 0 %}Dr{% else %}Cr{% endif %}</a>{% endif %}</td>
{% endfor %}
</tr>

View File

@ -61,19 +61,19 @@ def parse_amount(amount):
price_str = None
if amount_str[0] in list('0123456789-'):
# Currency follows number
# Commodity follows number
bits = amount_str.split()
amount_num = Decimal(bits[0])
currency = Currency(bits[1].strip('"'), False)
commodity = Commodity(bits[1].strip('"'), False)
else:
# Currency precedes number
currency = Currency(amount_str[0], True)
# Commodity precedes number
commodity = Commodity(amount_str[0], True)
amount_num = Decimal(amount_str[1:])
if price_str:
currency.price = parse_amount(price_str)
commodity.price = parse_amount(price_str)
return Amount(amount_num, currency)
return Amount(amount_num, commodity)
def get_pricedb():
output = run_ledger('prices', '--prices-format', '%(quoted(format_date(date))),%(quoted(display_account)),%(quoted(display_amount))\n')
@ -81,8 +81,8 @@ def get_pricedb():
prices = []
reader = csv.reader(output.splitlines(), dialect='ledger')
for date_str, currency, price_str in reader:
prices.append((datetime.strptime(date_str, '%Y-%m-%d'), currency.strip('"'), parse_amount(price_str)))
for date_str, commodity, price_str in reader:
prices.append((datetime.strptime(date_str, '%Y-%m-%d'), commodity.strip('"'), parse_amount(price_str)))
return prices

View File

@ -51,11 +51,11 @@ class Ledger:
return account
def get_price(self, currency_from, currency_to, date):
prices = [p for p in self.prices if p[1] == currency_from.name and p[2].currency == currency_to and p[0].date() <= date.date()]
def get_price(self, commodity_from, commodity_to, date):
prices = [p for p in self.prices if p[1] == commodity_from.name and p[2].commodity == commodity_to and p[0].date() <= date.date()]
if not prices:
raise Exception('No price information for {} to {} at {:%Y-%m-%d}'.format(currency_from, currency_to, date))
raise Exception('No price information for {} to {} at {:%Y-%m-%d}'.format(commodity_from, commodity_to, date))
return max(prices, key=lambda p: p[0])[2]
@ -88,11 +88,11 @@ class Posting:
def __repr__(self):
return '<Posting "{}" {}>'.format(self.account.name, self.amount.tostr(False))
def exchange(self, currency, date):
if self.amount.currency.name == currency.name and self.amount.currency.is_prefix == currency.is_prefix:
def exchange(self, commodity, date):
if self.amount.commodity.name == commodity.name and self.amount.commodity.is_prefix == commodity.is_prefix:
return Amount(self.amount)
return self.amount.exchange(currency, True) # Cost basis
return self.amount.exchange(commodity, True) # Cost basis
class Account:
def __init__(self, ledger, name):
@ -150,24 +150,24 @@ class Account:
return self.is_asset or self.is_liability
class Amount:
def __init__(self, amount, currency=None):
def __init__(self, amount, commodity=None):
if isinstance(amount, Amount):
self.amount = amount.amount
self.currency = amount.currency
elif currency is None:
raise TypeError('currency is required')
self.commodity = amount.commodity
elif commodity is None:
raise TypeError('commodity is required')
else:
self.amount = Decimal(amount)
self.currency = currency
self.commodity = commodity
def tostr(self, round=True):
if self.currency.is_prefix:
amount_str = ('{}{:.2f}' if round else '{}{}').format(self.currency.name, self.amount)
if self.commodity.is_prefix:
amount_str = ('{}{:.2f}' if round else '{}{}').format(self.commodity.name, self.amount)
else:
amount_str = ('{:.2f} {}' if round else '{} {}').format(self.amount, self.currency.name)
amount_str = ('{:.2f} {}' if round else '{} {}').format(self.amount, self.commodity.name)
if self.currency.price:
return '{} {{{}}}'.format(amount_str, self.currency.price.tostr(round))
if self.commodity.price:
return '{} {{{}}}'.format(amount_str, self.commodity.price.tostr(round))
return amount_str
def __repr__(self):
@ -176,12 +176,12 @@ class Amount:
def __str__(self):
return self.tostr()
def compatible_currency(func):
def compatible_commodity(func):
@functools.wraps(func)
def wrapped(self, other):
if isinstance(other, Amount):
if other.currency != self.currency:
raise TypeError('Cannot combine Amounts of currency {} and {}'.format(self.currency.name, other.currency.name))
if other.commodity != self.commodity:
raise TypeError('Cannot combine Amounts of commodity {} and {}'.format(self.commodity.name, other.commodity.name))
other = other.amount
elif other != 0:
raise TypeError('Cannot combine Amount with non-zero number')
@ -189,15 +189,15 @@ class Amount:
return wrapped
def __neg__(self):
return Amount(-self.amount, self.currency)
return Amount(-self.amount, self.commodity)
def __abs__(self):
return Amount(abs(self.amount), self.currency)
return Amount(abs(self.amount), self.commodity)
def __eq__(self, other):
if isinstance(other, Amount):
if self.amount == 0 and other.amount == 0:
return True
if other.currency != self.currency:
if other.commodity != self.commodity:
return False
return self.amount == other.amount
@ -205,46 +205,46 @@ class Amount:
return self.amount == 0
raise TypeError('Cannot compare Amount with non-zero number')
@compatible_currency
@compatible_commodity
def __ne__(self, other):
return self.amount != other
@compatible_currency
@compatible_commodity
def __gt__(self, other):
return self.amount > other
@compatible_currency
@compatible_commodity
def __ge__(self, other):
return self.amount >= other
@compatible_currency
@compatible_commodity
def __lt__(self, other):
return self.amount < other
@compatible_currency
@compatible_commodity
def __le__(self, other):
return self.amount <= other
@compatible_currency
@compatible_commodity
def __add__(self, other):
return Amount(self.amount + other, self.currency)
@compatible_currency
return Amount(self.amount + other, self.commodity)
@compatible_commodity
def __radd__(self, other):
return Amount(other + self.amount, self.currency)
@compatible_currency
return Amount(other + self.amount, self.commodity)
@compatible_commodity
def __sub__(self, other):
return Amount(self.amount - other, self.currency)
@compatible_currency
return Amount(self.amount - other, self.commodity)
@compatible_commodity
def __rsub__(self, other):
return Amount(other - self.amount, self.currency)
return Amount(other - self.amount, self.commodity)
def exchange(self, currency, is_cost, price=None):
if self.currency.name == currency.name and self.currency.is_prefix == currency.is_prefix:
def exchange(self, commodity, is_cost, price=None):
if self.commodity.name == commodity.name and self.commodity.is_prefix == commodity.is_prefix:
return Amount(self)
if is_cost and self.currency.price and self.currency.price.currency.name == currency.name and self.currency.price.currency.is_prefix == currency.is_prefix:
return Amount(self.amount * self.currency.price.amount, currency)
if is_cost and self.commodity.price and self.commodity.price.commodity.name == commodity.name and self.commodity.price.commodity.is_prefix == commodity.is_prefix:
return Amount(self.amount * self.commodity.price.amount, commodity)
if price:
return Amount(self.amount * price.amount, currency)
return Amount(self.amount * price.amount, commodity)
raise TypeError('Cannot exchange {} to {}'.format(self.currency, currency))
raise TypeError('Cannot exchange {} to {}'.format(self.commodity, commodity))
@property
def near_zero(self):
@ -259,26 +259,26 @@ class Balance:
def tidy(self):
new_amounts = []
for amount in self.amounts:
new_amount = next((a for a in new_amounts if a.currency == amount.currency), None)
new_amount = next((a for a in new_amounts if a.commodity == amount.commodity), None)
return Balance(new_amounts)
def clean(self):
return Balance([a for a in self.amounts if a != 0])
def exchange(self, currency, is_cost, date=None, ledger=None):
result = Amount(0, currency)
def exchange(self, commodity, is_cost, date=None, ledger=None):
result = Amount(0, commodity)
for amount in self.amounts:
if is_cost or (amount.currency.name == currency.name and amount.currency.is_prefix == amount.currency.is_prefix):
result += amount.exchange(currency, is_cost)
if is_cost or (amount.commodity.name == commodity.name and amount.commodity.is_prefix == amount.commodity.is_prefix):
result += amount.exchange(commodity, is_cost)
else:
if any(p[1] == amount.currency.name for p in ledger.prices):
# This currency has price information
if any(p[1] == amount.commodity.name for p in ledger.prices):
# This commodity has price information
# Measured at fair value
result += amount.exchange(currency, is_cost, ledger.get_price(amount.currency, currency, date))
result += amount.exchange(commodity, is_cost, ledger.get_price(amount.commodity, commodity, date))
else:
# This currency has no price information
# This commodity has no price information
# Measured at historical cost
result += amount.exchange(currency, True)
result += amount.exchange(commodity, True)
return result
def __neg__(self):
@ -299,18 +299,18 @@ class Balance:
if isinstance(other, Balance):
for amount in other.amounts:
new_amount = next((a for a in new_amounts if a.currency == amount.currency), None)
new_amount = next((a for a in new_amounts if a.commodity == amount.commodity), None)
if new_amount is None:
new_amount = Amount(0, amount.currency)
new_amount = Amount(0, amount.commodity)
new_amounts.append(new_amount)
new_amount.amount += amount.amount
#if new_amount == 0:
# new_amounts.remove(new_amount)
elif isinstance(other, Amount):
new_amount = next((a for a in new_amounts if a.currency == other.currency), None)
new_amount = next((a for a in new_amounts if a.commodity == other.commodity), None)
if new_amount is None:
new_amount = Amount(0, other.currency)
new_amount = Amount(0, other.commodity)
new_amounts.append(new_amount)
new_amount.amount += other.amount
@ -326,17 +326,17 @@ class Balance:
def __sub__(self, other):
return self + (-other)
class Currency:
class Commodity:
def __init__(self, name, is_prefix, price=None):
self.name = name
self.is_prefix = is_prefix
self.price = price
def __repr__(self):
return '<Currency {} ({})>'.format(self.name, 'prefix' if self.is_prefix else 'suffix')
return '<Commodity {} ({})>'.format(self.name, 'prefix' if self.is_prefix else 'suffix')
def __eq__(self, other):
if not isinstance(other, Currency):
if not isinstance(other, Commodity):
return False
return self.name == other.name and self.is_prefix == other.is_prefix and self.price == other.price