-
Notifications
You must be signed in to change notification settings - Fork 74
/
ledger.py
205 lines (170 loc) · 6.5 KB
/
ledger.py
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
from __future__ import unicode_literals
import json
import re
from functools import singledispatch
from locale import getdefaultlocale
from .core import Account, Book, Commodity, Price, Transaction
"""original script from https://github.com/MatzeB/pygnucash/blob/master/gnucash2ledger.py by Matthias Braun matze@braunis.de
adapted for:
- python 3 support
- new string formatting
"""
try:
import babel
import babel.numbers
BABEL_AVAILABLE = True
except ImportError:
BABEL_AVAILABLE = False
@singledispatch
def ledger(obj, **kwargs):
raise NotImplemented
CURRENCY_RE = re.compile("^[A-Z]{3}$")
NUMBER_RE = re.compile(r"(^|\s+)[-+]?([0-9]*(\.|,)[0-9]+|[0-9]+)($|\s+)") # regexp to identify a float with blank spaces
NUMERIC_SPACE = re.compile(r"[0-9\s]")
CHARS_ONLY = re.compile(r"[A-Za-z]+")
def quote_commodity(mnemonic):
if not CHARS_ONLY.fullmatch(mnemonic):
return json.dumps(mnemonic)
else:
return mnemonic
def format_commodity(mnemonic, locale):
if CURRENCY_RE.match(mnemonic) or True:
# format the currency via BABEL if available and then remove the number/amount
s = format_currency(0, 0, mnemonic, locale)
return NUMBER_RE.sub("", s)
else:
# just quote the commodity
return quote_commodity(mnemonic)
def format_currency(amount, decimals, currency, locale=False, decimal_quantization=True):
currency = quote_commodity(currency)
if locale is True:
locale = getdefaultlocale()[0]
if BABEL_AVAILABLE is False:
raise ValueError(f"You must install babel ('pip install babel') to export to ledger in your locale '{locale}'")
else:
return babel.numbers.format_currency(
amount,
currency,
format=None,
locale=locale,
currency_digits=True,
format_type="standard",
decimal_quantization=decimal_quantization,
)
else:
# local hand made version
if decimal_quantization:
digits = decimals
else:
digits = max(decimals, len((str(amount) + ".").split(".")[1].rstrip("0")))
return "{} {:,.{}f}".format(currency, amount, digits)
@ledger.register(Transaction)
def _(tr, locale=False, **kwargs):
"""Return a ledger-cli alike representation of the transaction"""
s = [
"{:%Y-%m-%d} {}{}\n".format(
tr.post_date,
"({}) ".format(tr.num.replace(")", "")) if tr.num else "",
tr.description,
)
]
if tr.notes:
s.append("\t;{}\n".format(tr.notes))
for split in sorted(
tr.splits,
key=lambda split: (split.value, split.transaction_guid, split.account_guid),
):
if split.account.commodity.mnemonic == "template":
return ""
if split.reconcile_state in ["c", "y"]:
s.append("\t* {:38} ".format(split.account.fullname))
else:
s.append("\t{:40} ".format(split.account.fullname))
if split.account.commodity != tr.currency:
s.append(
"{quantity} @@ {amount}".format(
quantity=format_currency(
split.quantity,
split.account.commodity.precision,
split.account.commodity.mnemonic,
locale,
decimal_quantization=False,
),
amount=format_currency(
abs(split.value),
tr.currency.precision,
tr.currency.mnemonic,
locale,
),
)
)
else:
s.append(format_currency(split.value, tr.currency.precision, tr.currency.mnemonic, locale))
if split.memo:
s.append(" ; {:20}".format(split.memo))
s.append("\n")
return "".join(s)
@ledger.register(Commodity)
def _(cdty, locale=False, commodity_notes=False, **kwargs):
"""Return a ledger-cli alike representation of the commodity"""
if cdty.mnemonic in ["", "template"]:
return ""
res = "commodity {}\n".format(format_commodity(cdty.mnemonic, locale))
if cdty.fullname != "" and commodity_notes:
res += "\tnote {}\n".format(cdty.fullname, locale)
res += "\n"
return res
@ledger.register(Account)
def _(acc, short_account_names=False, locale=False, **kwargs):
"""Return a ledger-cli alike representation of the account"""
# ignore "dummy" accounts
if acc.type is None or acc.parent is None:
return ""
if acc.commodity.mnemonic == "template":
return ""
if short_account_names:
res = "account {}\n".format(acc.name)
else:
res = "account {}\n".format(acc.fullname)
if acc.description != "":
res += "\tnote {}\n".format(acc.description)
if acc.commodity.is_currency():
res += '\tcheck commodity == "{}"\n'.format(format_commodity(acc.commodity.mnemonic,locale)) # .replace('"', '\\"'))
return res
@ledger.register(Price)
def _(price, locale=False, **kwargs):
"""Return a ledger-cli alike representation of the price"""
return "P {:%Y-%m-%d %H:%M:%S} {} {}\n".format(
price.date,
format_commodity(price.commodity.mnemonic, locale),
format_currency(
price.value,
price.currency.precision,
price.currency.mnemonic,
locale,
decimal_quantization=False,
),
)
@ledger.register(Book)
def _(book, **kwargs):
"""Return a ledger-cli alike representation of the book"""
res = []
# Commodities
for commodity in sorted(book.commodities, key=lambda cdty: cdty.mnemonic):
res.append(ledger(commodity, **kwargs))
# Accounts
if kwargs.get("short_account_names"): # check that no ambiguity in account names
accounts = [acc.name for acc in book.accounts]
if len(accounts) != len(set(accounts)):
raise ValueError("You have duplicate short names in your book. " "You cannot use the 'short_account_names' option.")
for acc in book.accounts:
res.append(ledger(acc, **kwargs))
res.append("\n")
# Prices
for price in sorted(book.prices, key=lambda x: (x.commodity_guid, x.currency_guid, x.date)):
res.append(ledger(price, **kwargs))
res.append("\n")
for trans in sorted(book.transactions, key=lambda x: x.post_date):
res.append(ledger(trans, **kwargs))
res.append("\n")
return "".join(res)