sdementen / piecash

Pythonic interface to GnuCash SQL documents
Other
290 stars 73 forks source link

Is this project dead? #215

Closed andrew521 closed 5 months ago

andrew521 commented 1 year ago

Nothing new for more than 2 years. Is the project still alive? Any new release to be expected?

sdementen commented 1 year ago

I do not maintain this project anymore. I leave it as it is as it may still prove useful for some users and I would happily transfer the ownership to someone else .

On Sat, 4 Nov 2023, 15:44 andrew521, @.***> wrote:

Nothing new for more than 2 years. Is the project still alive? Any new release to be expected?

— Reply to this email directly, view it on GitHub https://github.com/sdementen/piecash/issues/215, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAJ6S5WU5LBJI5SGQ4WNEZDYCZIELAVCNFSM6AAAAAA65T7PM2VHI2DSMVQWIX3LMV43ASLTON2WKOZRHE3TOMZXGU4DINY . You are receiving this because you are subscribed to this thread.Message ID: @.***>

andrew521 commented 10 months ago

any chance for a patch to quote commodity names? or at least some guidance to fix the issue?

https://github.com/sdementen/piecash/issues/210

Commodity names can have any character, including white-space. However, if you include white-space or numeric characters, the commodity name must be enclosed in double quotes ‘"

sdementen commented 10 months ago

would you have a sample gnucash book to test a patch ? or the kind of commodity mnemonic/fullname and what you would like to see as ledger output ?

the code to format the commodity is here https://github.com/sdementen/piecash/blob/master/piecash/ledger.py#L131

sdementen commented 10 months ago

could you replace the ledger.py by the following and tell me if it fixes the issue ?

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]")

def quote_commodity(mnemonic):
    if NUMERIC_SPACE.search(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, **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)

    res += '\tcheck commodity == "{}"\n'.format(acc.commodity.mnemonic)  # .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)
andrew521 commented 10 months ago

Here are some commodities. Thanks.

"BRK.B" - stock "AAPL 250117C00125000" - option "CAUX4 P0770" - futures option "T 2 06/30/24" - treasury notes

andrew521 commented 10 months ago

I did some testing and it is almost good. Still not working for these commodities:

andrew521 commented 10 months ago

RCI-B.TO is still not quoted and for the other 2 getting hundreds of warnings like this: Warning: "/mnt/e/Data/Invest/data//ldg-t14/t14.j", line 27591: Transaction check failed: (commodity == "TDB164") this is line 27591:

2008-11-12 TD Canadian Money Market
    Assets:RRSP:SDRRSP - TD:TD Canadian Money Market  "TDB164" -9,541.11800 @@ CAD 95,411.18
    Assets:RRSP:RRSP-GIC               CAD 95,411.18

edit: more testing:. on account definitions the new code ads 'check commodity == "TDB...' which doesn't work for non-currency accounts.

account Assets::RRSP:TD Group RRSP:TD Balanced Income
        check commodity == "TDB160"
sdementen commented 9 months ago

you can remplace the quote_commodity function by:

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

this will quote all commities that are not just ASCII alphabetical characters

sdementen commented 9 months ago

RCI-B.TO is still not quoted and for the other 2 getting hundreds of warnings like this: Warning: "/mnt/e/Data/Invest/data//ldg-t14/t14.j", line 27591: Transaction check failed: (commodity == "TDB164") this is line 27591:

2008-11-12 TD Canadian Money Market
  Assets:RRSP:SDRRSP - TD:TD Canadian Money Market  "TDB164" -9,541.11800 @@ CAD 95,411.18
  Assets:RRSP:RRSP-GIC               CAD 95,411.18

edit: more testing:. on account definitions the new code ads 'check commodity == "TDB...' which doesn't work for non-currency accounts.

account Assets::RRSP:TD Group RRSP:TD Balanced Income
        check commodity == "TDB160"

so we should just not generate the line "check commodity" for non currency accounts ?

andrew521 commented 9 months ago

probably not generating "check commodity" for non currency would be the best approach.

sdementen commented 5 months ago

I have merged in master the suggested changes