Skip to content

Commit

Permalink
Merge pull request #220 from sdementen/export_ledger_commodities
Browse files Browse the repository at this point in the history
Export ledger commodities
  • Loading branch information
sdementen authored Jun 17, 2024
2 parents de64b99 + 93a546d commit 70f9f29
Show file tree
Hide file tree
Showing 8 changed files with 42 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
coverage run --source=piecash setup.py test
- name: Upload coverage data to coveralls.io
run: |
python -m pip install coveralls==2.2
python -m pip install coveralls==3.3.1
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Binary file modified gnucash_books/complex_sample.gnucash
Binary file not shown.
33 changes: 11 additions & 22 deletions piecash/core/commodity.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@ class Price(DeclarativeBaseGuid):
foreign_keys=[currency_guid],
)

def __init__(
self, commodity, currency, date, value, type="unknown", source="user:price"
):
def __init__(self, commodity, currency, date, value, type="unknown", source="user:price"):
self.commodity = commodity
self.currency = currency
assert _type(date) is datetime.date
Expand All @@ -89,9 +87,7 @@ def __init__(
self.source = source

def __str__(self):
return "Price<{:%Y-%m-%d} : {} {}/{}>".format(
self.date, self.value, self.currency.mnemonic, self.commodity.mnemonic
)
return "Price<{:%Y-%m-%d} : {} {}/{}>".format(self.date, self.value, self.currency.mnemonic, self.commodity.mnemonic)

def object_to_validate(self, change):
if change[-1] != "deleted":
Expand Down Expand Up @@ -159,9 +155,7 @@ class Commodity(DeclarativeBaseGuid):
def base_currency(self):
b = self.book
if b is None:
raise GnucashException(
"The commodity should be linked to a session to have a 'base_currency'"
)
raise GnucashException("The commodity should be linked to a session to have a 'base_currency'")

if self.namespace == "CURRENCY":
# get the base currency as first commodity in DB
Expand Down Expand Up @@ -238,6 +232,9 @@ def __str__(self):
def precision(self):
return len(str(self.fraction)) - 1

def is_currency(self):
return self.namespace == "CURRENCY"

def currency_conversion(self, currency):
"""
Return the latest conversion factor to convert self to currency
Expand All @@ -253,16 +250,12 @@ def currency_conversion(self, currency):
"""
# conversion is done from self.commodity to commodity (if possible)
sc2c = (
self.prices.filter_by(currency=currency).order_by(Price.date.desc()).first()
)
sc2c = self.prices.filter_by(currency=currency).order_by(Price.date.desc()).first()
if sc2c:
return sc2c.value

# conversion is done directly from commodity to self.commodity (if possible)
c2sc = (
currency.prices.filter_by(currency=self).order_by(Price.date.desc()).first()
)
c2sc = currency.prices.filter_by(currency=self).order_by(Price.date.desc()).first()
if c2sc:
return Decimal(1) / c2sc.value

Expand All @@ -285,9 +278,7 @@ def update_prices(self, start_date=None):
.. todo:: add some frequency to retrieve prices only every X (week, month, ...)
"""
if self.book is None:
raise GncPriceError(
"Cannot update price for a commodity not attached to a book"
)
raise GncPriceError("Cannot update price for a commodity not attached to a book")

# get last_price updated
last_price = self.prices.order_by(Price.date.desc()).limit(1).first()
Expand All @@ -298,7 +289,7 @@ def update_prices(self, start_date=None):
if last_price:
start_date = max(last_price.date + datetime.timedelta(days=1), start_date)

if self.namespace == "CURRENCY":
if self.is_currency():
# get reference currency (from book.root_account)
default_currency = self.base_currency
if default_currency == self:
Expand Down Expand Up @@ -337,8 +328,6 @@ def object_to_validate(self, change):
def validate(self):
# check uniqueness of namespace/mnemonic
try:
self.book.query(Commodity).filter_by(
namespace=self.namespace, mnemonic=self.mnemonic
).one()
self.book.query(Commodity).filter_by(namespace=self.namespace, mnemonic=self.mnemonic).one()
except MultipleResultsFound:
raise ValueError("{} already exists in this book".format(self))
2 changes: 1 addition & 1 deletion piecash/core/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def create_stock_accounts(
:class:`piecash.core.account.Account`: a tuple with the account under the broker_account where the stock is held
and the list of income accounts.
"""
if cdty.namespace == "CURRENCY":
if cdty.is_currency():
raise GnucashException(
"{} is a currency ! You can't create stock_accounts for currencies".format(
cdty
Expand Down
2 changes: 1 addition & 1 deletion piecash/core/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ def validate(self):
if old["STATE_CHANGES"][-1] == "deleted":
return

if self.currency.namespace != "CURRENCY":
if not self.currency.is_currency():
raise GncValidationError(
"You are assigning a non currency commodity to a transaction"
)
Expand Down
57 changes: 24 additions & 33 deletions piecash/ledger.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import unicode_literals

import json
import re
from functools import singledispatch
from locale import getdefaultlocale

from .core import Transaction, Account, Commodity, Price, Book
from .core import Account, Book, Commodity, Price, Transaction

"""original script from https://github.com/MatzeB/pygnucash/blob/master/gnucash2ledger.py by Matthias Braun [email protected]
adapted for:
Expand All @@ -27,34 +28,34 @@ def ledger(obj, **kwargs):


CURRENCY_RE = re.compile("^[A-Z]{3}$")
NUMBER_RE = re.compile("[0-9., ]")
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):
# format the commodity
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)

# remove the non currency part and real white spaces
return NUMBER_RE.sub("", s)
else:
if NUMBER_RE.search(mnemonic):
return '"{}"'.format(mnemonic)
else:
return mnemonic

return NUMBER_RE.sub("", s)
# just quote the commodity
return quote_commodity(mnemonic)


def format_currency(
amount, decimals, currency, locale=False, decimal_quantization=True
):
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}'"
)
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,
Expand Down Expand Up @@ -115,11 +116,7 @@ def _(tr, locale=False, **kwargs):
)
)
else:
s.append(
format_currency(
split.value, tr.currency.precision, tr.currency.mnemonic, locale
)
)
s.append(format_currency(split.value, tr.currency.precision, tr.currency.mnemonic, locale))

if split.memo:
s.append(" ; {:20}".format(split.memo))
Expand Down Expand Up @@ -157,9 +154,8 @@ def _(acc, short_account_names=False, **kwargs):
if acc.description != "":
res += "\tnote {}\n".format(acc.description)

res += '\tcheck commodity == "{}"\n'.format(
acc.commodity.mnemonic
) # .replace('"', '\\"'))
if acc.commodity.is_currency():
res += '\tcheck commodity == "{}"\n'.format(acc.commodity.mnemonic) # .replace('"', '\\"'))
return res


Expand Down Expand Up @@ -192,18 +188,13 @@ def _(book, **kwargs):
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."
)
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)
):
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")

Expand Down
3 changes: 3 additions & 0 deletions piecash/scripts/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ def export(book, entities, output, inactive):
)

output.write(res)

if __name__ == '__main__':
cli()
2 changes: 1 addition & 1 deletion piecash/scripts/qif_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def sort_split(sp):
continue # skip template transactions

splits = sorted(tr.splits, key=sort_split)
if all(sp.account.commodity.namespace == "CURRENCY" for sp in splits):
if all(sp.account.commodity.is_currency() for sp in splits):

sp1, sp2 = splits[:2]
item = _qif.Transaction(
Expand Down

0 comments on commit 70f9f29

Please sign in to comment.