diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 8c0a1de3..06d631dd 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -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 }} diff --git a/gnucash_books/complex_sample.gnucash b/gnucash_books/complex_sample.gnucash index 202a8033..f5fd79fd 100644 Binary files a/gnucash_books/complex_sample.gnucash and b/gnucash_books/complex_sample.gnucash differ diff --git a/piecash/core/commodity.py b/piecash/core/commodity.py index 4af2d703..96a4100a 100644 --- a/piecash/core/commodity.py +++ b/piecash/core/commodity.py @@ -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 @@ -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": @@ -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 @@ -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 @@ -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 @@ -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() @@ -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: @@ -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)) diff --git a/piecash/core/factories.py b/piecash/core/factories.py index fa60b38e..97847577 100644 --- a/piecash/core/factories.py +++ b/piecash/core/factories.py @@ -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 diff --git a/piecash/core/transaction.py b/piecash/core/transaction.py index dfe5218a..f21488a6 100644 --- a/piecash/core/transaction.py +++ b/piecash/core/transaction.py @@ -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" ) diff --git a/piecash/ledger.py b/piecash/ledger.py index d4685603..65ac17d6 100644 --- a/piecash/ledger.py +++ b/piecash/ledger.py @@ -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 matze@braunis.de adapted for: @@ -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, @@ -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)) @@ -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 @@ -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") diff --git a/piecash/scripts/export.py b/piecash/scripts/export.py index 4438b868..be6990ef 100644 --- a/piecash/scripts/export.py +++ b/piecash/scripts/export.py @@ -66,3 +66,6 @@ def export(book, entities, output, inactive): ) output.write(res) + +if __name__ == '__main__': + cli() \ No newline at end of file diff --git a/piecash/scripts/qif_export.py b/piecash/scripts/qif_export.py index e754dd12..4ef8e64b 100644 --- a/piecash/scripts/qif_export.py +++ b/piecash/scripts/qif_export.py @@ -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(