1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
|
#!/usr/bin/env python
import sys
from datetime import datetime
# The following literate program will demonstrate, by example, how to use the
# Ledger Python module to access your data and build custom reports using the
# magic of Python.
import ledger
print "Welcome to the Ledger.Python demo!"
# Some quick helper functions to help us assert various types of truth
# throughout the script.
def assertEqual(pat, candidate):
if pat != candidate:
raise Exception("FAILED: %s != %s" % (pat, candidate))
sys.exit(1)
###############################################################################
#
# COMMODITIES
#
# Every amount in Ledger has a commodity, even if it is the "null commodity".
# What's special about commodities are not just their symbol, but how they
# alter the way amounts are displayed.
#
# For example, internally Ledger uses infinite precision rational numbers,
# which have no decimal point. So how does it know that $1.00 / $0.75 should
# be displayed as $1.33, and not with an infinitely repeating decimal? It
# does it by consulting the commodity.
#
# Whenever an amount is encountered in your data file, Ledger observes how you
# specified it:
# - How many digits of precision did you use?
# - Was the commodity name before or after the amount?
# - Was the commodity separated from the amount by a space?
# - Did you use thousands markers (1,000)?
# - Did you use European-style numbers (1.000,00)?
#
# By tracking this information for each commodity, Ledger knows how you want
# to see the amount in your reports. This way, dollars can be output as
# $123.56, while stock options could be output as 10.113 AAPL.
#
# Your program can access the known set of commodities using the global
# `ledger.commodities'. This object behaves like a dict, and support all of
# the non-modifying dict protocol methods. If you wish to create a new
# commodity without parsing an amount, you can use the method
# `find_or_create':
comms = ledger.commodities
usd = comms.find_or_create('$')
eur = comms.find_or_create('EUR')
xcd = comms.find_or_create('XCD')
assert not comms.find('CAD')
assert not comms.has_key('CAD')
assert not 'CAD' in comms
# The above mentioned commodity display attributes can be set using commodity
# display flags. This is not something you will usually be doing, however, as
# these flags can be inferred correctly from a large enough set of sample
# amounts, such as those found in your data file. If you live in Europe and
# want all amounts to default to the European-style, set the static variable
# `european_by_default'.
eur.add_flags(ledger.COMMODITY_STYLE_DECIMAL_COMMA)
assert eur.has_flags(ledger.COMMODITY_STYLE_DECIMAL_COMMA)
assert not eur.has_flags(ledger.COMMODITY_STYLE_THOUSANDS)
comms.european_by_default = True
# There are a few built-in commodities: null, %, h, m and s. Normally you
# don't need to worry about them, but they'll show up if you examine all the
# keys in the commodities dict.
assertEqual([u'', u'$', u'%', u'EUR', u'XCD', u'h', u'm', u's'],
sorted(comms.keys()))
# All the styles of dict iteration are supported:
for symbol in comms.iterkeys():
pass
for commodity in comms.itervalues():
pass
#for symbol, commodity in comms.iteritems():
# pass
#for symbol, commodity in comms:
# pass
# Another important thing about commodities is that they remember if they've
# been exchanged for another commodity, and what the conversion rate was on
# that date. You can record specific conversion rates for any date using the
# `exchange' method.
comms.exchange(eur, ledger.Amount('$0.77')) # Trade 1 EUR for $0.77
comms.exchange(eur, ledger.Amount('$0.66'), datetime.now())
# For the most part, however, you won't be interacting with commodities
# directly, except maybe to look at their `symbol'.
assertEqual('$', usd.symbol)
assertEqual('$', comms['$'].symbol)
###############################################################################
#
# AMOUNTS & BALANCES
#
# Ledger deals with two basic numerical values: Amount and Balance objects.
# An Amount is an infinite-precision rational with an associated commodity
# (even if it is the null commodity, which is called an "uncommoditized
# amount"). A Balance is a collection of Amounts of differing commodities.
#
# Amounts support all the math operations you might expect of an integer,
# except it carries a commodity. Let's take dollars for example:
zero = ledger.Amount("$0")
one = ledger.Amount("$1")
oneb = ledger.Amount("$1")
two = ledger.Amount("$2")
three = ledger.Amount("3") # uncommoditized
assert one == oneb # numeric equality, not identity
assert one != two
assert not zero # tests if it would *display* as a zero
assert one < two
assert one > zero
# For addition and subtraction, only amounts of the same commodity may be
# used, unless one of the amounts has no commodity at all -- in which case the
# result uses the commodity of the other value. Adding $10 to 10 EUR, for
# example, causes an ArithmeticError exception, but adding 10 to $10 gives
# $20.
four = ledger.Amount(two) # make a copy
four += two
assertEqual(four, two + two)
assertEqual(zero, one - one)
try:
two += ledger.Amount("20 EUR")
assert False
except ArithmeticError:
pass
# Use `number' to get the uncommoditized version of an Amount
assertEqual(three, (two + one).number())
# Multiplication and division does supports Amounts of different commodities,
# however:
# - If either amount is uncommoditized, the result carries the commodity of
# the other amount.
# - Otherwise, the result always carries the commodity of the first amount.
five = ledger.Amount("5 CAD")
assertEqual(one, two / two)
assertEqual(five, (five * ledger.Amount("$2")) - ledger.Amount("5"))
# An amount's commodity determines the decimal precision it's displayed with.
# However, this "precision" is a notional thing only. You can tell an amount
# to ignore its display precision by setting `keep_precision' to True.
# (Uncommoditized amounts ignore the value of `keep_precision', and assume it
# is always True). In this case, Ledger does its best to maintain maximal
# precision by watching how the Amount is used. That is, 1.01 * 1.01 yields a
# precision of 4. This tracking is just a best estimate, however, since
# internally Ledger never uses floating-point values.
amt = ledger.Amount('$100.12')
mini = ledger.Amount('0.00045')
assert not amt.keep_precision
assertEqual(5, mini.precision)
assertEqual(5, mini.display_precision) # display_precision == precision
assertEqual(2, amt.precision)
assertEqual(2, amt.display_precision)
mini *= mini
amt *= amt
assertEqual(10, mini.precision)
assertEqual(10, mini.display_precision)
assertEqual(4, amt.precision)
assertEqual(2, amt.display_precision)
# There are several other supported math operations:
amt = ledger.Amount('$100.12')
market = ((ledger.Amount('1 EUR') / ledger.Amount('$0.77')) * amt)
assertEqual(market, amt.value(eur)) # find present market value
assertEqual('$-100.12', str(amt.negated())) # negate the amount
assertEqual('$-100.12', str(- amt)) # negate it more simply
assertEqual('$0.01', str(amt.inverted())) # reverse NUM/DEM
assertEqual('$100.12', str(amt.rounded())) # round it to display precision
assertEqual('$100.12', str(amt.truncated())) # truncate to display precision
assertEqual('$100.00', str(amt.floored())) # floor it to nearest integral
assertEqual('$100.12', str(abs(amt))) # absolute value
assertEqual('$100.12', str(amt)) # render to a string
assertEqual('100.12', amt.quantity_string()) # render quantity to a string
assertEqual('100.12', str(amt.number())) # strip away commodity
assertEqual(1, amt.sign()) # -1, 0 or 1
assert amt.is_nonzero() # True if display amount nonzero
assert not amt.is_zero() # True if display amount is zero
assert not amt.is_realzero() # True only if value is 0/0
assert not amt.is_null() # True if uninitialized
# Amounts can also be converted the standard floats and integers, although
# this is not recommend since it can lose precision.
assertEqual(100.12, amt.to_double())
assert amt.fits_in_long() # there is no `fits_in_double'
assertEqual(100, amt.to_long())
# Finally, amounts can be annotated to provide additional information about
# "lots" of a given commodity. This example shows $100.12 that was purchased
# on 2009/10/01 for 140 EUR. Lot information can be accessed through via the
# Amount's `annotation' property. You can also strip away lot details to get
# the underlying amount. If you want the total price of any Amount, by
# multiplying by its per-unit lot price, call the `Amount.price' method
# instead of the `Annotation.price' property.
amt2 = ledger.Amount('$100.12 {140 EUR} [2009/10/01]')
assert amt2.has_annotation()
assertEqual(amt, amt2.strip_annotations())
assertEqual(ledger.Amount('140 EUR'), amt2.annotation.price)
assertEqual(ledger.Amount('14016,8 EUR'), amt2.price()) # european amount!
###############################################################################
#
# VALUES
#
# As common as Amounts and Balances are, there is a more prevalent numeric
# type you will encounter when generating reports: Value objects. A Value is
# a variadic type that can be any of the following types:
# - Amount
# - Balance
# - boolean
# - integer
# - datetime
# - date
# - string
# - regex
# - sequence
#
# The reason for the variadic type is that it supports dynamic self-promotion.
# For example, it is illegal to add two Amounts of different commodities, but
# it is not illegal to add two Value amounts of different commodities. In the
# former case an exception in raised, but in the latter the Value simply
# promotes itself to a Balance object to make the addition valid.
#
# Values are not used by any of Ledger's data objects (Journal, Transaction,
# Posting or Account), but they are used extensively by value expressions.
val = ledger.Value('$100.00')
assert val.is_amount()
assertEqual('$', val.to_amount().commodity.symbol)
# JOURNALS
#journal.find_account('')
#journal.find_or_create_account('')
# ACCOUNTS
#account.name
#account.fullname()
#account.amount
#account.total
# TRANSACTIONS
#txn.payee
# POSTINGS
#post.account
# REPORTING
#journal.collect('-M food')
#journal.collect_accounts('^assets ^liab ^equity')
print 'Demo completed successfully.'
|