├── .gitattributes ├── .github └── workflows │ └── github-actions.yml ├── .gitignore ├── .readthedocs.yml ├── CHANGELOG.rst ├── DEVELOPER.rst ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.rst ├── bumpr.rc ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── .gitkeep │ ├── android.rst │ ├── api │ ├── piecash._common.rst │ ├── piecash._declbase.rst │ ├── piecash.budget.rst │ ├── piecash.business.invoice.rst │ ├── piecash.business.person.rst │ ├── piecash.business.rst │ ├── piecash.business.tax.rst │ ├── piecash.core._commodity_helper.rst │ ├── piecash.core.account.rst │ ├── piecash.core.book.rst │ ├── piecash.core.commodity.rst │ ├── piecash.core.currency_ISO.rst │ ├── piecash.core.factories.rst │ ├── piecash.core.rst │ ├── piecash.core.session.rst │ ├── piecash.core.transaction.rst │ ├── piecash.kvp.rst │ ├── piecash.ledger.rst │ ├── piecash.metadata.rst │ ├── piecash.rst │ └── piecash.sa_extra.rst │ ├── conf.py │ ├── doc │ ├── comparison.rst │ ├── doc.rst │ ├── github_links.rst │ └── resources.rst │ ├── index.rst │ ├── news.rst │ ├── object_model.rst │ ├── schema.png │ └── tutorial │ ├── examples.rst │ ├── existing_objects │ ├── account.rst │ ├── commodity.rst │ ├── invoices.rst │ ├── open_book.rst │ ├── others.rst │ ├── slots.rst │ └── transaction.rst │ ├── index_existing.rst │ ├── index_new.rst │ └── new_objects │ ├── create_account.rst │ ├── create_book.rst │ ├── create_business.rst │ ├── create_commodity.rst │ └── create_transaction.rst ├── examples ├── export_norme_A47.py ├── filtered_transaction_report.py ├── ipython │ ├── piecash_dataframes.ipynb │ ├── piecash_session.ipynb │ └── test.gnucash ├── read_currencies.py ├── read_currencies_sa.py ├── sandbox.py ├── simple_book.py ├── simple_book_transaction_creation.py ├── simple_delete_account.py ├── simple_export_transaction_csv.py ├── simple_extract_splits_to_pandas.py ├── simple_move_split.py ├── simple_session.py ├── simple_sqlite_create.py ├── simple_test.py └── simple_transaction_split_change.py ├── github_gnucash_projects.py ├── gnucash_books ├── README.md ├── all_account_types.gnucash ├── book_prices.gnucash ├── book_schtx.gnucash ├── complex_sample.gnucash ├── default_book.gnucash ├── empty_book.gnucash ├── ghost_kvp_scheduled_transaction.gnucash ├── investment.gnucash ├── invoices.gnucash ├── reference │ ├── 2_6 │ │ ├── default_2_6_21_basic.gnucash │ │ ├── default_2_6_21_basic.gnucash.sql │ │ ├── default_2_6_21_full_options.gnucash │ │ ├── default_2_6_21_full_options.gnucash.sql │ │ ├── default_piecash_0_18_basic.gnucash │ │ └── default_piecash_0_18_basic.gnucash.sql │ └── 3_0 │ │ ├── default_3_0_0_basic.gnucash │ │ ├── default_3_0_0_basic.gnucash.sql │ │ ├── default_3_0_0_full_options.gnucash │ │ ├── default_3_0_0_full_options.gnucash.sql │ │ ├── default_piecash_1_0_basic.gnucash │ │ └── default_piecash_1_0_basic.gnucash.sql ├── simple_sample.272.gnucash ├── simple_sample.gnucash └── test_book.gnucash ├── pavement.py ├── piecash ├── __init__.py ├── _common.py ├── _declbase.py ├── budget.py ├── business │ ├── __init__.py │ ├── invoice.py │ ├── person.py │ └── tax.py ├── core │ ├── __init__.py │ ├── _commodity_helper.py │ ├── account.py │ ├── book.py │ ├── commodity.py │ ├── currency_ISO.py │ ├── factories.py │ ├── session.py │ └── transaction.py ├── kvp.py ├── ledger.py ├── metadata.py ├── sa_extra.py ├── scripts │ ├── __init__.py │ ├── cli.py │ ├── export.py │ ├── ledger.py │ ├── piecash_prices.py │ ├── qif_export.py │ └── sql_helper.py └── yahoo_client.py ├── piecash_interpreter ├── piecash_interpreter.py └── piecash_interpreter.spec ├── requirements-dev.txt ├── setup.py ├── tests ├── books │ ├── all-accounts.gnucash │ ├── complex_sample.gnucash │ ├── default_3_0_0_basic.gnucash │ ├── default_3_0_0_full_options.gnucash │ ├── example_file.gnucash │ ├── foo.gnucash │ ├── ghost_kvp_scheduled_transaction.gnucash │ ├── investment.gnucash │ ├── invoices.gnucash │ ├── simple_sample.272.gnucash │ └── simple_sample.gnucash ├── references │ ├── file_template_full.commodity_notes_True.ledger │ ├── file_template_full.ledger │ ├── file_template_full.locale_True.ledger │ └── file_template_full.short_account_names_True.ledger ├── test_account.py ├── test_balance.py ├── test_book.py ├── test_business_owner.py ├── test_business_person.py ├── test_commodity.py ├── test_factories.py ├── test_helper.py ├── test_integration.py ├── test_invoice.py ├── test_ledger.py ├── test_model_common.py ├── test_model_core.py ├── test_session.py └── test_transaction.py └── tox.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | examples/ipython/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/workflows/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | services: 11 | postgres: 12 | image: postgres 13 | env: 14 | POSTGRES_PASSWORD: postgres_password_CI 15 | ports: 16 | - 5432:5432 17 | options: >- 18 | --health-cmd pg_isready 19 | --health-interval 10s 20 | --health-timeout 5s 21 | --health-retries 5 22 | mysql: 23 | image: mysql:latest 24 | ports: 25 | - 3306:3306 26 | env: 27 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 28 | # MYSQL_USER: gha_user 29 | MYSQL_ROOT_PASSWORD: root 30 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 31 | 32 | strategy: 33 | matrix: 34 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 35 | 36 | steps: 37 | - uses: actions/checkout@v2 38 | - name: Set up Python ${{ matrix.python-version }} 39 | uses: actions/setup-python@v2 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | - name: Set fr locale 43 | run: | 44 | sudo locale-gen fr_FR.UTF-8 45 | sudo update-locale LANG=fr_FR.UTF-8 46 | - name: Install dependencies 47 | run: | 48 | python -m pip install --upgrade pip wheel 49 | pip install -e .[test] 50 | # - name: Lint with flake8 51 | # run: | 52 | # flake8 53 | - name: Test with pytest 54 | run: | 55 | python setup.py test 56 | - name: Coverage 57 | run: | 58 | coverage run --source=piecash setup.py test 59 | - name: Upload coverage data to coveralls.io 60 | run: | 61 | python -m pip install coveralls==3.3.1 62 | coveralls --service=github 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Emacs rope configuration 2 | .ropeproject 3 | .project 4 | .pydevproject 5 | .settings 6 | 7 | # pycharm 8 | .idea 9 | 10 | # pyenv version file 11 | .python-version 12 | 13 | # Python 14 | *.py[co] 15 | 16 | ## Packages 17 | *.egg 18 | *.egg-info 19 | dist 20 | build 21 | eggs 22 | parts 23 | bin 24 | var 25 | sdist 26 | deb_dist 27 | develop-eggs 28 | .installed.cfg 29 | 30 | ## Installer logs 31 | pip-log.txt 32 | 33 | ## Unit test / coverage reports 34 | .coverage 35 | .tox 36 | 37 | ## Translations 38 | *.mo 39 | 40 | ## paver generated files 41 | /paver-minilib.zip 42 | /.pytest_cache/ 43 | /.cache/ 44 | /data/ 45 | /pg_data/ 46 | /.env 47 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF and ePub 17 | formats: all 18 | 19 | # Optionally set the version of Python and requirements required to build your docs 20 | python: 21 | version: 3.6 22 | install: 23 | - requirements: requirements-dev.txt 24 | - method: setuptools 25 | path: . -------------------------------------------------------------------------------- /DEVELOPER.rst: -------------------------------------------------------------------------------- 1 | Some note for developers: 2 | ------------------------- 3 | 4 | - to prepare a virtualenv for dev purposes:: 5 | 6 | pipenv install -e .[test,doc] 7 | 8 | - to generate the sdist dist\piecash-XXX.tar.gz:: 9 | 10 | python setup.py sdist 11 | 12 | - to upload file on PyPI:: 13 | 14 | python setup.py sdist upload 15 | 16 | - to generate the modules `modules.rst` and `piecash.rst` in the docs\source\doc folder, go to the docs\source\doc folder and:: 17 | 18 | sphinx-apidoc -o . ../../piecash 19 | 20 | - to build the doc (do not forget to `pipenv install -e .[doc]` before):: 21 | 22 | cd docs 23 | make html 24 | 25 | The documentation will be available through docs/build/html/index.html. 26 | 27 | - to test via tox and conda, create first the different environment with the relevant versions of python:: 28 | 29 | conda create -n py35 python=3.5 virtualenv 30 | conda create -n py36 python=3.6 virtualenv 31 | ... 32 | 33 | adapt tox.ini to point to the proper conda envs and then run:: 34 | 35 | tox 36 | 37 | - to release a new version: 38 | 1. update metadata.py 39 | 2. update changelog 40 | 3. `tag MM.mm.pp` 41 | 4. twine upload dist/* --repository piecash 42 | 43 | - to release a new version with gitflow: 44 | 0. git flow release start 0.18.0 45 | 1. update metadata.py 46 | 2. update changelog 47 | 3. git flow release finish 48 | 4. checkout master branch in git 49 | 5. python setup.py sdist upload 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sébastien de Menten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | CTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Informational files 2 | include README.rst 3 | include LICENSE 4 | 5 | # Include docs and tests. It's unclear whether convention dictates 6 | # including built docs. However, Sphinx doesn't include built docs, so 7 | # we are following their lead. 8 | graft docs 9 | prune docs/build 10 | graft tests 11 | 12 | # Exclude any compile Python files (most likely grafted by tests/ directory). 13 | global-exclude *.pyc 14 | 15 | # Setup-related things 16 | include pavement.py 17 | include setup.py 18 | include tox.ini 19 | 20 | # include examples 21 | include gnucash_books/* 22 | include scripts/* 23 | include examples/* 24 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | sqlalchemy = "*" 8 | sqlalchemy-utils = "*" 9 | pytz = "*" 10 | tzlocal = "*" 11 | click = "*" 12 | 13 | 14 | [dev-packages] 15 | babel = "*" 16 | tox-pipenv = "*" 17 | psycopg2 = "*" 18 | pymysql = "*" 19 | pandas = "*" 20 | qifparse = "*" 21 | requests = "*" 22 | pytest = "*" 23 | pytest-cov = "*" 24 | tox = "*" 25 | sphinx = "*" 26 | sphinxcontrib-programoutput = "*" 27 | sphinx-rtd-theme = "*" 28 | ipython = "*" 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | piecash 2 | ======= 3 | 4 | .. image:: https://badges.gitter.im/sdementen/piecash.svg 5 | :alt: Join the chat at https://gitter.im/sdementen/piecash 6 | :target: https://gitter.im/sdementen/piecash?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 7 | 8 | .. image:: https://github.com/sdementen/piecash/workflows/CI/badge.svg 9 | :target: https://github.com/sdementen/piecash/actions 10 | 11 | .. image:: https://readthedocs.org/projects/piecash/badge/?version=master 12 | :target: http://piecash.readthedocs.org 13 | 14 | .. image:: https://img.shields.io/pypi/v/piecash.svg 15 | :target: https://pypi.python.org/pypi/piecash 16 | 17 | .. image:: https://img.shields.io/pypi/pyversions/piecash.svg 18 | :target: https://pypi.python.org/pypi/piecash/ 19 | 20 | .. image:: https://img.shields.io/pypi/dd/piecash.svg 21 | :target: https://pypi.python.org/pypi/piecash/ 22 | 23 | .. image:: https://coveralls.io/repos/sdementen/piecash/badge.svg?branch=master&service=github 24 | :target: https://coveralls.io/github/sdementen/piecash?branch=master 25 | 26 | 27 | Piecash provides a simple and pythonic interface to GnuCash files stored in SQL (sqlite3, Postgres and MySQL). 28 | 29 | :Documentation: http://piecash.readthedocs.org. 30 | :Gitter: https://gitter.im/sdementen/piecash 31 | :Github: https://github.com/sdementen/piecash 32 | :PyPI: https://pypi.python.org/pypi/piecash 33 | 34 | 35 | It is a pure python package, tested on python 3.6 to 3.9, that can be used as an alternative to: 36 | 37 | - the official python bindings (as long as no advanced book modifications and/or engine calculations are needed). 38 | This is specially useful on Windows where the official python bindings may be tricky to install or if you want to work with 39 | python 3. 40 | - XML parsing/reading of XML GnuCash files if you prefer python over XML/XLST manipulations. 41 | 42 | piecash test suite runs successfully on Windows and Linux on the three supported SQL backends (sqlite3, Postgres and MySQL). 43 | piecash has also been successfully run on Android (sqlite3 backend) thanks to Kivy buildozer and python-for-android. 44 | 45 | It allows you to: 46 | 47 | - open existing GnuCash documents and access all objects within 48 | - modify objects or add new objects (accounts, transactions, prices, ...) 49 | - create new GnuCash documents from scratch 50 | 51 | Scripts are also available to: 52 | 53 | - export to ledger-cli format (http://www.ledger-cli.org/) 54 | - export to QIF format 55 | - import/export prices (CSV format) 56 | 57 | A simple example of a piecash script: 58 | 59 | .. code-block:: python 60 | 61 | with open_book("example.gnucash") as book: 62 | # get default currency of book 63 | print( book.default_currency ) # ==> Commodity 64 | 65 | # iterating over all splits in all books and print the transaction description: 66 | for acc in book.accounts: 67 | for sp in acc.splits: 68 | print(sp.transaction.description) 69 | 70 | The project has reached beta stage. 71 | 72 | .. important:: 73 | 74 | If you want to work with python 2.7/3.4 and books from gnucash 2.6.x series, you can use piecash 0.19.0. 75 | Versions of piecash as of 1.0.0 supports python >=3.6 and books from gnucash 3.0.x series. 76 | 77 | .. warning:: 78 | 79 | 1) Always do a backup of your gnucash file/DB before using piecash. 80 | 2) Test first your script by opening your file in readonly mode (which is the default mode) 81 | 82 | 83 | -------------------------------------------------------------------------------- /bumpr.rc: -------------------------------------------------------------------------------- 1 | [bumpr] 2 | verbose = True 3 | file = piecash/metadata.py 4 | regex = version \= '(?P.*)' 5 | vcs = git 6 | tests = tox 7 | ;publish = python setup.py sdist register upload 8 | clean = 9 | python setup.py clean 10 | rm -rf *egg-info build dist 11 | files = README.rst 12 | 13 | [bump] 14 | unsuffix = True 15 | message = Bump version {version} 16 | 17 | [prepare] 18 | unsuffix = True 19 | message = Prepare version {version} for next development cycle 20 | 21 | [changelog] 22 | file = CHANGELOG.rst 23 | prepare = In development 24 | separator = ~ 25 | bump = Version {version} ({date:%Y-%m-%d}) 26 | empty = Empty 27 | 28 | [readthedoc] 29 | id = piecash 30 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -W 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonGnuCashSQLinterface.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonGnuCashSQLinterface.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $HOME/.local/share/devhelp/PythonGnuCashSQLinterface" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $HOME/.local/share/devhelp/PythonGnuCashSQLinterface" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set SPHINXOPTS= 10 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 11 | set I18NSPHINXOPTS=%SPHINXOPTS% source 12 | if NOT "%PAPER%" == "" ( 13 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 14 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 15 | ) 16 | 17 | if "%1" == "" goto help 18 | 19 | if "%1" == "help" ( 20 | :help 21 | echo.Please use `make ^` where ^ is one of 22 | echo. html to make standalone HTML files 23 | echo. dirhtml to make HTML files named index.html in directories 24 | echo. singlehtml to make a single large HTML file 25 | echo. pickle to make pickle files 26 | echo. json to make JSON files 27 | echo. htmlhelp to make HTML files and a HTML help project 28 | echo. qthelp to make HTML files and a qthelp project 29 | echo. devhelp to make HTML files and a Devhelp project 30 | echo. epub to make an epub 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. linkcheck to check all external links for integrity 38 | echo. doctest to run all doctests embedded in the documentation if enabled 39 | goto end 40 | ) 41 | 42 | if "%1" == "clean" ( 43 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 44 | del /q /s %BUILDDIR%\* 45 | goto end 46 | ) 47 | 48 | if "%1" == "html" ( 49 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 50 | if errorlevel 1 exit /b 1 51 | echo. 52 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 53 | goto end 54 | ) 55 | 56 | if "%1" == "dirhtml" ( 57 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 58 | if errorlevel 1 exit /b 1 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "singlehtml" ( 65 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 66 | if errorlevel 1 exit /b 1 67 | echo. 68 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 69 | goto end 70 | ) 71 | 72 | if "%1" == "pickle" ( 73 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 74 | if errorlevel 1 exit /b 1 75 | echo. 76 | echo.Build finished; now you can process the pickle files. 77 | goto end 78 | ) 79 | 80 | if "%1" == "json" ( 81 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 82 | if errorlevel 1 exit /b 1 83 | echo. 84 | echo.Build finished; now you can process the JSON files. 85 | goto end 86 | ) 87 | 88 | if "%1" == "htmlhelp" ( 89 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 90 | if errorlevel 1 exit /b 1 91 | echo. 92 | echo.Build finished; now you can run HTML Help Workshop with the ^ 93 | .hhp project file in %BUILDDIR%/htmlhelp. 94 | goto end 95 | ) 96 | 97 | if "%1" == "qthelp" ( 98 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 99 | if errorlevel 1 exit /b 1 100 | echo. 101 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 102 | .qhcp project file in %BUILDDIR%/qthelp, like this: 103 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PythonGnuCashSQLinterface.qhcp 104 | echo.To view the help file: 105 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PythonGnuCashSQLinterface.qhc 106 | goto end 107 | ) 108 | 109 | if "%1" == "devhelp" ( 110 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished. 114 | goto end 115 | ) 116 | 117 | if "%1" == "epub" ( 118 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 122 | goto end 123 | ) 124 | 125 | if "%1" == "latex" ( 126 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 127 | if errorlevel 1 exit /b 1 128 | echo. 129 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 130 | goto end 131 | ) 132 | 133 | if "%1" == "text" ( 134 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 135 | if errorlevel 1 exit /b 1 136 | echo. 137 | echo.Build finished. The text files are in %BUILDDIR%/text. 138 | goto end 139 | ) 140 | 141 | if "%1" == "man" ( 142 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 143 | if errorlevel 1 exit /b 1 144 | echo. 145 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 146 | goto end 147 | ) 148 | 149 | if "%1" == "texinfo" ( 150 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 151 | if errorlevel 1 exit /b 1 152 | echo. 153 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 154 | goto end 155 | ) 156 | 157 | if "%1" == "gettext" ( 158 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 159 | if errorlevel 1 exit /b 1 160 | echo. 161 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 162 | goto end 163 | ) 164 | 165 | if "%1" == "changes" ( 166 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 167 | if errorlevel 1 exit /b 1 168 | echo. 169 | echo.The overview file is in %BUILDDIR%/changes. 170 | goto end 171 | ) 172 | 173 | if "%1" == "linkcheck" ( 174 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 175 | if errorlevel 1 exit /b 1 176 | echo. 177 | echo.Link check complete; look for any errors in the above output ^ 178 | or in %BUILDDIR%/linkcheck/output.txt. 179 | goto end 180 | ) 181 | 182 | if "%1" == "doctest" ( 183 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 184 | if errorlevel 1 exit /b 1 185 | echo. 186 | echo.Testing of doctests in the sources finished, look at the ^ 187 | results in %BUILDDIR%/doctest/output.txt. 188 | goto end 189 | ) 190 | 191 | :end 192 | -------------------------------------------------------------------------------- /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/docs/source/_static/.gitkeep -------------------------------------------------------------------------------- /docs/source/android.rst: -------------------------------------------------------------------------------- 1 | piecash on android 2 | ================== 3 | 4 | piecash can successfully run on android which opens interesting opportunities! 5 | 6 | Installing termux 7 | ----------------- 8 | 9 | First, you have to install Termux from the Play Store. 10 | 11 | You start Termux and: 12 | 13 | 1. edit your .bash_profile with:: 14 | 15 | export TZ=$(getprop persist.sys.timezone) 16 | export SHELL=$(which bash) 17 | 18 | 2. add the folder ~/storage with access to your android folders (also accessible via USB sync):: 19 | 20 | termux-setup-storage 21 | 22 | 23 | Installing python and piecash 24 | ----------------------------- 25 | 26 | You start Termux on your android and then: 27 | 28 | 1. Install python and pipenv:: 29 | 30 | pkg install python 31 | pip install pipenv 32 | 33 | 34 | 2. Install piecash for your project:: 35 | 36 | mkdir my-project 37 | cd my-project 38 | pipenv install piecash 39 | 40 | 41 | 3. Test piecash:: 42 | 43 | pipenv shell 44 | python 45 | >>> import piecash 46 | 47 | 48 | Use SSH with your android 49 | ------------------------- 50 | 51 | You can ssh easily in your android thanks to Termux. 52 | For this, on Termux on your android: 53 | 54 | 1. install openssh:: 55 | 56 | pkg install openssh 57 | 58 | 2. add your public key (id_rsa.pub) in the file `.ssh/authorized_keys` on Termux 59 | 60 | 3. run the sshd server:: 61 | 62 | sshd 63 | 64 | On your machine (laptop, ...): 65 | 66 | 1. configure your machine to access your android device:: 67 | 68 | Host android 69 | HostName 192.168.1.4 # <== put the IP address of your android 70 | User termux 71 | Port 8022 72 | 73 | 2. log in your android from your machine:: 74 | 75 | ssh android 76 | 77 | 78 | Use the USB Debugging with your android 79 | --------------------------------------- 80 | 81 | To be investigated...:: 82 | 83 | # on laptop 84 | adb forward tcp:8022 tcp:8022 && ssh localhost -p 8022 85 | 86 | # on android 87 | # On Android 4.2 and higher, the Developer options screen is hidden by default. To make it visible, go to Settings > About phone and tap Build number seven times. Return to the previous screen to find Developer options at the bottom. 88 | change USB Configuration to "charge only" or "PTP" 89 | 90 | # downloading https://developer.android.com/studio/run/win-usb.html 91 | # Click here to download the Google USB Driver ZIP file (Z 92 | # install legacy hardware (in device manager) 93 | # choose the folder of the zip drive and choose ADB interface 94 | 95 | 96 | References 97 | ---------- 98 | 99 | - https://glow.li/technology/2015/11/06/run-an-ssh-server-on-your-android-with-termux/ 100 | - https://termux.com/storage.html 101 | - https://developer.android.com/studio/releases/platform-tools.html 102 | - https://glow.li/technology/2016/9/20/access-termux-via-usb/ 103 | - https://github.com/termux/termux-packages/issues/352 104 | 105 | -------------------------------------------------------------------------------- /docs/source/api/piecash._common.rst: -------------------------------------------------------------------------------- 1 | piecash._common module 2 | ====================== 3 | 4 | .. automodule:: piecash._common 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash._declbase.rst: -------------------------------------------------------------------------------- 1 | piecash._declbase module 2 | ======================== 3 | 4 | .. automodule:: piecash._declbase 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.budget.rst: -------------------------------------------------------------------------------- 1 | piecash.budget module 2 | ===================== 3 | 4 | .. automodule:: piecash.budget 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.business.invoice.rst: -------------------------------------------------------------------------------- 1 | piecash.business.invoice module 2 | =============================== 3 | 4 | .. automodule:: piecash.business.invoice 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.business.person.rst: -------------------------------------------------------------------------------- 1 | piecash.business.person module 2 | ============================== 3 | 4 | .. automodule:: piecash.business.person 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.business.rst: -------------------------------------------------------------------------------- 1 | piecash.business package 2 | ======================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | piecash.business.invoice 10 | piecash.business.person 11 | piecash.business.tax 12 | 13 | Module contents 14 | --------------- 15 | 16 | .. automodule:: piecash.business 17 | :members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/api/piecash.business.tax.rst: -------------------------------------------------------------------------------- 1 | piecash.business.tax module 2 | =========================== 3 | 4 | .. automodule:: piecash.business.tax 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.core._commodity_helper.rst: -------------------------------------------------------------------------------- 1 | piecash.core._commodity_helper module 2 | ===================================== 3 | 4 | .. automodule:: piecash.core._commodity_helper 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.core.account.rst: -------------------------------------------------------------------------------- 1 | piecash.core.account module 2 | =========================== 3 | 4 | .. automodule:: piecash.core.account 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.core.book.rst: -------------------------------------------------------------------------------- 1 | piecash.core.book module 2 | ======================== 3 | 4 | .. automodule:: piecash.core.book 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.core.commodity.rst: -------------------------------------------------------------------------------- 1 | piecash.core.commodity module 2 | ============================= 3 | 4 | .. automodule:: piecash.core.commodity 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.core.currency_ISO.rst: -------------------------------------------------------------------------------- 1 | piecash.core.currency_ISO module 2 | ================================ 3 | 4 | .. automodule:: piecash.core.currency_ISO 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.core.factories.rst: -------------------------------------------------------------------------------- 1 | piecash.core.factories module 2 | ============================= 3 | 4 | .. automodule:: piecash.core.factories 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.core.rst: -------------------------------------------------------------------------------- 1 | piecash.core package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | piecash.core._commodity_helper 10 | piecash.core.account 11 | piecash.core.book 12 | piecash.core.commodity 13 | piecash.core.currency_ISO 14 | piecash.core.factories 15 | piecash.core.session 16 | piecash.core.transaction 17 | 18 | Module contents 19 | --------------- 20 | 21 | .. automodule:: piecash.core 22 | :members: 23 | :show-inheritance: 24 | -------------------------------------------------------------------------------- /docs/source/api/piecash.core.session.rst: -------------------------------------------------------------------------------- 1 | piecash.core.session module 2 | =========================== 3 | 4 | .. automodule:: piecash.core.session 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.core.transaction.rst: -------------------------------------------------------------------------------- 1 | piecash.core.transaction module 2 | =============================== 3 | 4 | .. automodule:: piecash.core.transaction 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.kvp.rst: -------------------------------------------------------------------------------- 1 | piecash.kvp module 2 | ================== 3 | 4 | .. automodule:: piecash.kvp 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.ledger.rst: -------------------------------------------------------------------------------- 1 | piecash.ledger module 2 | ===================== 3 | 4 | .. automodule:: piecash.ledger 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.metadata.rst: -------------------------------------------------------------------------------- 1 | piecash.metadata module 2 | ======================= 3 | 4 | .. automodule:: piecash.metadata 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/api/piecash.rst: -------------------------------------------------------------------------------- 1 | piecash package 2 | =============== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | piecash.business 10 | piecash.core 11 | 12 | Submodules 13 | ---------- 14 | 15 | .. toctree:: 16 | 17 | piecash._common 18 | piecash._declbase 19 | piecash.budget 20 | piecash.kvp 21 | piecash.ledger 22 | piecash.metadata 23 | piecash.sa_extra 24 | 25 | Module contents 26 | --------------- 27 | 28 | .. automodule:: piecash 29 | :members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/api/piecash.sa_extra.rst: -------------------------------------------------------------------------------- 1 | piecash.sa_extra module 2 | ======================= 3 | 4 | .. automodule:: piecash.sa_extra 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/doc/comparison.rst: -------------------------------------------------------------------------------- 1 | piecash and the official python bindings 2 | ======================================== 3 | 4 | piecash is an alternative to the python bindings that may be bundled with gnucash 5 | (http://wiki.gnucash.org/wiki/Python_Bindings). 6 | 7 | This page aims to give some elements of comparison between both python interfaces to better understand their relevancy 8 | to your needs. 9 | Information on the official python bindings may be incomplete (information gathered from mailing lists and wiki). 10 | 11 | Gnucash 3.0.x series 12 | -------------------- 13 | 14 | +------------------+----------------------------------+------------------------------------------+ 15 | | | piecash (>=1.0.0) | official python bindings (gnucash 3.0.n) | 16 | +------------------+----------------------------------+------------------------------------------+ 17 | | book format | gnucash 3.0.n | gnucash 3.0.n | 18 | +------------------+----------------------------------+------------------------------------------+ 19 | | environment | Python 3.6/3.7/3.8/3.9 | Python 3 | 20 | +------------------+----------------------------------+------------------------------------------+ 21 | | installation | pure python package | compilation (difficult on windows) | 22 | | | 'pip install piecash' | binaries (available on Linux) | 23 | +------------------+----------------------------------+------------------------------------------+ 24 | | requires GnuCash | no | yes | 25 | +------------------+----------------------------------+------------------------------------------+ 26 | | runs on Android | yes | no | 27 | +------------------+----------------------------------+------------------------------------------+ 28 | | gnucash files | SQL backend only | SQL backend and XML | 29 | +------------------+----------------------------------+------------------------------------------+ 30 | | documentation | yes (read the docs) | partial | 31 | | | actively developed | | 32 | +------------------+----------------------------------+------------------------------------------+ 33 | | functionalities | creation of new books | all functionalities provided | 34 | | | read/browse objects | by the GnuCash C/C++ engine | 35 | | | create objects (basic) | | 36 | | | update online prices | | 37 | +------------------+----------------------------------+------------------------------------------+ 38 | 39 | Gnucash 2.6.x series 40 | -------------------- 41 | 42 | +------------------+----------------------------------+------------------------------------------+ 43 | | | piecash (<=0.18.0) | official python bindings (gnucash 2.6.n) | 44 | +------------------+----------------------------------+------------------------------------------+ 45 | | book format | gnucash 2.6.n | gnucash 2.6.n | 46 | +------------------+----------------------------------+------------------------------------------+ 47 | | environment | Python 2.7 & 3.3/3.4/3.5/3.6 | Python 2.7 | 48 | +------------------+----------------------------------+------------------------------------------+ 49 | | installation | pure python package | compilation (difficult on windows) | 50 | | | 'pip install piecash' | binaries (available on Linux) | 51 | +------------------+----------------------------------+------------------------------------------+ 52 | | requires GnuCash | no | yes | 53 | +------------------+----------------------------------+------------------------------------------+ 54 | | runs on Android | yes | no | 55 | +------------------+----------------------------------+------------------------------------------+ 56 | | gnucash files | SQL backend only | SQL backend and XML | 57 | +------------------+----------------------------------+------------------------------------------+ 58 | | documentation | yes (read the docs) | partial | 59 | | | actively developed | | 60 | +------------------+----------------------------------+------------------------------------------+ 61 | | functionalities | creation of new books | all functionalities provided | 62 | | | read/browse objects | by the GnuCash C/C++ engine | 63 | | | create objects (basic) | | 64 | | | update online prices | | 65 | +------------------+----------------------------------+------------------------------------------+ -------------------------------------------------------------------------------- /docs/source/doc/resources.rst: -------------------------------------------------------------------------------- 1 | Resources 2 | ========= 3 | 4 | This page lists resources related to GnuCash, and more specifically, to the use of Python for GnuCash. 5 | 6 | GnuCash links 7 | ------------- 8 | 9 | - The official GnuCash page : http://www.gnucash.org/ 10 | - The official python bindings : http://wiki.gnucash.org/wiki/Python_Bindings (wiki) 11 | and http://svn.gnucash.org/docs/head/python_bindings_page.html (svn) 12 | 13 | Web resources 14 | ------------- 15 | - List (XML) of currencies with their ISO code : http://www.currency-iso.org/dam/downloads/table_a1.xml 16 | - Quandl (for exchange rates) : http://www.quandl.com 17 | - Yahoo! query language : https://developer.yahoo.com/yql/console/ 18 | 19 | Blogs & discussions 20 | ------------------- 21 | 22 | - Gitter chat room for developers and users: https://gitter.im/sdementen/piecash 23 | - blog with GnuCash/python links (not 100% correct): http://wideopenstudy.blogspot.be/search/label/GnuCash 24 | - on timezone in GnuCash: http://do-the-right-things.blogspot.be/2013/11/caveats-in-using-gnucash-time-zone.html 25 | - Google search on python in user mailing list: `python site:http://lists.gnucash.org/pipermail/gnucash-user" python `__ 26 | - Google search on python in devel mailing list: `python site:http://lists.gnucash.org/pipermail/gnucash-devel" python `__ 27 | 28 | Github projects related to GnuCash 29 | ---------------------------------- 30 | 31 | .. toctree:: 32 | :maxdepth: 1 33 | 34 | ./github_links.rst 35 | 36 | Python links 37 | ------------ 38 | 39 | - cross compilation of python executable from Linux to Windows : 40 | http://milkator.wordpress.com/2014/07/19/windows-executable-from-python-developing-in-ubuntu/ 41 | - SQLAlchemy page: http://www.sqlalchemy.org/ 42 | 43 | Threads used during the course of development 44 | --------------------------------------------- 45 | 46 | - sphinx error message: 47 | http://stackoverflow.com/questions/15249340/warning-document-isnt-included-in-any-toctree-for-included-file 48 | 49 | Thanks 50 | ------ 51 | 52 | None of this could be possible without : 53 | 54 | - the GnuCash project, its core team of developers and its active community of users 55 | - python and its packages amongst which sqlalchemy 56 | - github, readthedocs and travis-ci for managing code, docs and testing 57 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 4 2 | 3 | Welcome to the piecash documentation! 4 | ===================================== 5 | 6 | :Release: |version| 7 | :Date: |today| 8 | :Authors: sdementen 9 | :Project page: https://github.com/sdementen/piecash 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | news.rst 15 | 16 | Contents: 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | doc/doc.rst 22 | tutorial/index_existing.rst 23 | tutorial/index_new.rst 24 | tutorial/examples.rst 25 | doc/comparison.rst 26 | android.rst 27 | 28 | For developers 29 | ============== 30 | 31 | The complete api documentation (apidoc) : 32 | 33 | .. toctree:: 34 | :maxdepth: 1 35 | 36 | api/piecash.rst 37 | 38 | An overall view on the core objects in GnuCash: 39 | 40 | .. toctree:: 41 | :maxdepth: 1 42 | 43 | object_model.rst 44 | 45 | A list of resources used for the project: 46 | 47 | .. toctree:: 48 | :maxdepth: 1 49 | 50 | doc/resources.rst 51 | 52 | The todo list: 53 | 54 | - write more tests 55 | - review non core objects (:py:mod:`~piecash.budget`, :py:mod:`~piecash.business`) 56 | - build a single exe to ease install on windows (following http://milkator.wordpress.com/2014/07/19/windows-executable-from-python-developing-in-ubuntu/) 57 | 58 | Indices and tables 59 | ================== 60 | 61 | * :ref:`genindex` 62 | * :ref:`modindex` 63 | * :ref:`search` 64 | -------------------------------------------------------------------------------- /docs/source/news.rst: -------------------------------------------------------------------------------- 1 | .. include:: 2 | ../../CHANGELOG.rst -------------------------------------------------------------------------------- /docs/source/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/docs/source/schema.png -------------------------------------------------------------------------------- /docs/source/tutorial/examples.rst: -------------------------------------------------------------------------------- 1 | Examples of programs written with piecash 2 | ========================================= 3 | 4 | You can find examples of programs/scripts (loosely based on the scripts for the 5 | official python bindings for gnucash or on 6 | questions posted on the mailing list) in the examples subfolder. 7 | 8 | Creating and opening gnucash files 9 | ---------------------------------- 10 | .. literalinclude:: ../../../examples/simple_session.py 11 | 12 | Creating an account 13 | ------------------- 14 | .. literalinclude:: ../../../examples/simple_sqlite_create.py 15 | 16 | Creating a transaction 17 | ---------------------- 18 | .. literalinclude:: ../../../examples/simple_test.py 19 | 20 | Modifying existing transactions/splits 21 | -------------------------------------- 22 | .. literalinclude:: ../../../examples/simple_transaction_split_change.py 23 | 24 | Delete an account in a book 25 | --------------------------- 26 | .. literalinclude:: ../../../examples/simple_delete_account.py 27 | 28 | Save/cancel changes in a book 29 | ----------------------------- 30 | .. literalinclude:: ../../../examples/simple_book.py 31 | 32 | Create a book with some accounts and add a transaction 33 | ------------------------------------------------------ 34 | .. literalinclude:: ../../../examples/simple_book_transaction_creation.py 35 | 36 | Export transactions to a CSV file 37 | --------------------------------- 38 | .. literalinclude:: ../../../examples/simple_export_transaction_csv.py 39 | 40 | Extract Split information as pandas DataFrame 41 | --------------------------------------------- 42 | .. literalinclude:: ../../../examples/simple_extract_splits_to_pandas.py 43 | 44 | Filtered transaction reports 45 | ---------------------------- 46 | .. literalinclude:: ../../../examples/filtered_transaction_report.py 47 | -------------------------------------------------------------------------------- /docs/source/tutorial/existing_objects/account.rst: -------------------------------------------------------------------------------- 1 | Accounts 2 | -------- 3 | 4 | Accessing the accounts (:class:`piecash.core.account.Account`): 5 | 6 | .. ipython:: python 7 | 8 | book = open_book(gnucash_books + "simple_sample.gnucash", open_if_lock=True) 9 | 10 | # accessing the root_account 11 | root = book.root_account 12 | print(root) 13 | 14 | # accessing the first children account of a book 15 | acc = root.children[0] 16 | print(acc) 17 | 18 | # accessing attributes of an account 19 | print(f"Account name={acc.name}\n" 20 | f" commodity={acc.commodity.namespace}/{acc.commodity.mnemonic}\n" 21 | f" fullname={acc.fullname}\n" 22 | f" type={acc.type}") 23 | 24 | # calculating the balance of the accounts: 25 | for acc in root.children: 26 | print(f"Account balance for {acc.name}: {acc.get_balance()} (without sign reversal: {acc.get_balance(natural_sign=False)}") 27 | 28 | # accessing all splits related to an account 29 | for sp in acc.splits: 30 | print(f"account <{acc.fullname}> is involved in transaction '{sp.transaction.description}'") 31 | 32 | -------------------------------------------------------------------------------- /docs/source/tutorial/existing_objects/commodity.rst: -------------------------------------------------------------------------------- 1 | Commodities and Prices 2 | ---------------------- 3 | 4 | The list of all commodities in the book can be retrieved via the ``commodities`` attribute: 5 | 6 | .. ipython:: python 7 | 8 | book = open_book(gnucash_books + "book_prices.gnucash", open_if_lock=True) 9 | 10 | # all commodities 11 | print(book.commodities) 12 | 13 | cdty = book.commodities[0] 14 | 15 | # accessing attributes of a commodity 16 | print("Commodity namespace={cdty.namespace}\n" 17 | " mnemonic={cdty.mnemonic}\n" 18 | " cusip={cdty.cusip}\n" 19 | " fraction={cdty.fraction}".format(cdty=cdty)) 20 | 21 | The prices (:class:`piecash.core.commodity.Price`) of a commodity can be iterated through the ``prices`` attribute: 22 | 23 | .. ipython:: python 24 | 25 | # loop on the prices 26 | for cdty in book.commodities: 27 | for pr in cdty.prices: 28 | print("Price date={pr.date}" 29 | " value={pr.value} {pr.currency.mnemonic}/{pr.commodity.mnemonic}".format(pr=pr)) 30 | -------------------------------------------------------------------------------- /docs/source/tutorial/existing_objects/invoices.rst: -------------------------------------------------------------------------------- 1 | Invoices 2 | -------- 3 | 4 | The list of all invoices in the book can be retrieved via the ``invoices`` attribute: 5 | 6 | .. ipython:: python 7 | 8 | book = open_book(gnucash_books + "invoices.gnucash", open_if_lock=True) 9 | 10 | # all invoices 11 | for invoice in book.invoices: 12 | print(invoice) 13 | -------------------------------------------------------------------------------- /docs/source/tutorial/existing_objects/open_book.rst: -------------------------------------------------------------------------------- 1 | Opening an existing Book 2 | ------------------------ 3 | 4 | .. py:currentmodule:: piecash.core.book 5 | 6 | To open an existing GnuCash document (and get the related :class:`Book`), use the :func:`open_book` function:: 7 | 8 | import piecash 9 | 10 | # for a sqlite3 document 11 | book = piecash.open_book("existing_file.gnucash") 12 | 13 | # or through an URI connection string for sqlite3 14 | book = piecash.open_book(uri_conn="sqlite:///existing_file.gnucash") 15 | # or for postgres 16 | book = piecash.open_book(uri_conn="postgres://user:passwd@localhost/existing_gnucash_db") 17 | 18 | The documents are open as readonly per default. To allow RW access, specify explicitly readonly=False as:: 19 | 20 | book = piecash.open_book("existing_file.gnucash", readonly=False) 21 | 22 | When opening in full access (readonly=False), piecash will automatically create a backup file named 23 | filename.piecash_YYYYMMDD_HHMMSS with the original file. To avoid creating the backup file, specificy do_backup=False as:: 24 | 25 | book = piecash.open_book("existing_file.gnucash", readonly=False, do_backup=False) 26 | 27 | To force opening the file even through there is a lock on it, use the open_if_lock=True argument:: 28 | 29 | book = piecash.open_book("existing_file.gnucash", open_if_lock=True) 30 | 31 | Access to objects 32 | ----------------- 33 | 34 | Once a GnuCash book is opened through a :class:`piecash.core.book.Book`, GnuCash objects can be accessed 35 | through two different patterns: 36 | 37 | The object model 38 | 39 | In this mode, we access elements through their natural relations, starting from the book and jumping 40 | from one object to the other: 41 | 42 | .. ipython:: 43 | 44 | In [1]: book = open_book(gnucash_books + "default_book.gnucash") 45 | 46 | In [1]: book.root_account # accessing the root_account 47 | 48 | In [1]: # looping through the children accounts of the root_account 49 | ...: for acc in book.root_account.children: 50 | ...: print(acc) 51 | 52 | # accessing children accounts 53 | In [1]: 54 | ...: root = book.root_account # select the root_account 55 | ...: assets = root.children(name="Assets") # select child account by name 56 | ...: cur_assets = assets.children[0] # select child account by index 57 | ...: cash = cur_assets.children(type="CASH") # select child account by type 58 | ...: print(cash) 59 | 60 | In [1]: # get the commodity of an account 61 | ...: commo = cash.commodity 62 | ...: print(commo) 63 | 64 | In [1]: # get first ten accounts linked to the commodity commo 65 | ...: for acc in commo.accounts[:10]: 66 | ...: print(acc) 67 | 68 | 69 | The "table" access 70 | 71 | In this mode, we access elements through collections directly accessible from the book: 72 | 73 | .. ipython:: python 74 | 75 | book = open_book(gnucash_books + "default_book.gnucash") 76 | 77 | # accessing all accounts 78 | book.accounts 79 | 80 | # accessing all commodities 81 | book.commodities 82 | 83 | # accessing all transactions 84 | book.transactions 85 | 86 | 87 | Each of these collections can be either iterated or accessed through some indexation or filter mechanism (return 88 | first element of collection satisfying some criteria(s)): 89 | 90 | .. ipython:: python 91 | 92 | # iteration 93 | for acc in book.accounts: 94 | if acc.type == "ASSET": print(acc) 95 | 96 | # indexation (not very meaningful) 97 | book.accounts[10] 98 | 99 | # filter by name 100 | book.accounts(name="Garbage collection") 101 | 102 | # filter by type 103 | book.accounts(type="EXPENSE") 104 | 105 | # filter by fullname 106 | book.accounts(fullname="Expenses:Taxes:Social Security") 107 | 108 | # filter by multiple criteria 109 | book.accounts(commodity=book.commodities[0], name="Gas") 110 | 111 | The "SQLAlchemy" access (advanced users) 112 | 113 | In this mode, we access elements through SQLAlchemy queries on the SQLAlchemy session: 114 | 115 | .. ipython:: python 116 | 117 | # retrieve underlying SQLAlchemy session object 118 | session = book.session 119 | 120 | # get all account with name >= "T" 121 | session.query(Account).filter(Account.name>="T").all() 122 | 123 | # display underlying query 124 | str(session.query(Account).filter(Account.name>="T")) 125 | -------------------------------------------------------------------------------- /docs/source/tutorial/existing_objects/others.rst: -------------------------------------------------------------------------------- 1 | Other objects 2 | ------------- 3 | 4 | In fact, any object can be retrieved from the session through a generic ``get(**kwargs)`` method: 5 | 6 | .. ipython:: python 7 | 8 | book = open_book(gnucash_books + "invoices.gnucash", open_if_lock=True) 9 | 10 | from piecash import Account, Commodity, Budget, Vendor 11 | 12 | # accessing specific objects through the get method 13 | book.get(Account, name="Assets", parent=book.root_account) 14 | book.get(Commodity, namespace="CURRENCY", mnemonic="EUR") 15 | book.get(Budget, name="my first budget") 16 | book.get(Vendor, name="Looney") 17 | 18 | If you know SQLAlchemy, you can get access to the underlying :class:`~sqlalchemy.orm.session.Session` as ``book.session`` and execute 19 | queries using the piecash classes: 20 | 21 | .. ipython:: python 22 | 23 | from piecash import Account, Commodity, Budget, Vendor 24 | 25 | # get the SQLAlchemy session 26 | session = book.session 27 | 28 | # loop through all invoices 29 | for invoice in session.query(Invoice).all(): 30 | print(invoice.notes) 31 | 32 | .. note:: 33 | 34 | Easy access to objects from :mod:`piecash.business` and :mod:`piecash.budget` could be given directly from the session 35 | in future versions if deemed useful. 36 | -------------------------------------------------------------------------------- /docs/source/tutorial/existing_objects/slots.rst: -------------------------------------------------------------------------------- 1 | Working with slots 2 | ------------------ 3 | 4 | With regard to slots, GnuCash objects and Frames behave as dictionaries and all values are automatically 5 | converted back and forth to python objects: 6 | 7 | .. ipython:: python 8 | 9 | import datetime, decimal 10 | 11 | book = create_book() 12 | 13 | # retrieve list of slots 14 | print(book.slots) 15 | 16 | # set slots 17 | book["myintkey"] = 3 18 | book["mystrkey"] = "hello" 19 | book["myboolkey"] = True 20 | book["mydatekey"] = datetime.datetime.today().date() 21 | book["mydatetimekey"] = datetime.datetime.today() 22 | book["mynumerickey"] = decimal.Decimal("12.34567") 23 | book["account"] = book.root_account 24 | 25 | # iterate over all slots 26 | for k, v in book.iteritems(): 27 | print("slot={v} has key={k} and value={v.value} of type {t}".format(k=k,v=v,t=type(v.value))) 28 | 29 | # delete a slot 30 | del book["myintkey"] 31 | # delete all slots 32 | del book[:] 33 | 34 | # create a key/value in a slot frames (and create them if they do not exist) 35 | book["options/Accounts/Use trading accounts"]="t" 36 | # access a slot in frame in whatever notations 37 | s1=book["options/Accounts/Use trading accounts"] 38 | s2=book["options"]["Accounts/Use trading accounts"] 39 | s3=book["options/Accounts"]["Use trading accounts"] 40 | s4=book["options"]["Accounts"]["Use trading accounts"] 41 | assert s1==s2==s3==s4 42 | 43 | Slots of type GUID use the name of the slot to do the conversion back and forth between an object and its guid. For 44 | these slots, there is an explicit mapping between slot names and object types. 45 | -------------------------------------------------------------------------------- /docs/source/tutorial/existing_objects/transaction.rst: -------------------------------------------------------------------------------- 1 | Transactions and Splits 2 | ----------------------- 3 | 4 | The list of all transactions in the book can be retrieved via the ``transactions`` attribute: 5 | 6 | .. ipython:: python 7 | 8 | book = open_book(gnucash_books + "book_schtx.gnucash", open_if_lock=True) 9 | 10 | # all transactions (including transactions part of a scheduled transaction description) 11 | for tr in book.transactions: 12 | print(tr) 13 | 14 | # selecting first transaction generated from a scheduled transaction 15 | tr = [ tr for tr in book.transactions if tr.scheduled_transaction ][0] 16 | 17 | 18 | For a given transaction, the following attributes are accessible: 19 | 20 | .. ipython:: python 21 | 22 | # accessing attributes of a transaction 23 | print("Transaction description='{tr.description}'\n" 24 | " currency={tr.currency}\n" 25 | " post_date={tr.post_date}\n" 26 | " enter_date={tr.enter_date}".format(tr=tr)) 27 | 28 | # accessing the splits of the transaction 29 | tr.splits 30 | 31 | # identifying which split is a credit or a debit 32 | for sp in tr.splits: 33 | split_type = "credit" if sp.is_credit else "debit" 34 | print(f"{sp} is a {split_type}") 35 | 36 | # accessing the scheduled transaction 37 | [ sp for sp in tr.scheduled_transaction.template_account.splits] 38 | 39 | # closing the book 40 | book.close() 41 | -------------------------------------------------------------------------------- /docs/source/tutorial/index_existing.rst: -------------------------------------------------------------------------------- 1 | Tutorial : using existing objects 2 | ================================= 3 | 4 | .. include:: existing_objects/open_book.rst 5 | 6 | .. include:: existing_objects/account.rst 7 | 8 | .. include:: existing_objects/commodity.rst 9 | 10 | .. include:: existing_objects/transaction.rst 11 | 12 | .. include:: existing_objects/invoices.rst 13 | 14 | .. include:: existing_objects/others.rst 15 | 16 | .. include:: existing_objects/slots.rst 17 | 18 | -------------------------------------------------------------------------------- /docs/source/tutorial/index_new.rst: -------------------------------------------------------------------------------- 1 | Tutorial : creating new objects 2 | =============================== 3 | 4 | .. include:: new_objects/create_book.rst 5 | 6 | .. include:: new_objects/create_account.rst 7 | 8 | .. include:: new_objects/create_commodity.rst 9 | 10 | .. include:: new_objects/create_transaction.rst 11 | 12 | .. include:: new_objects/create_business.rst -------------------------------------------------------------------------------- /docs/source/tutorial/new_objects/create_account.rst: -------------------------------------------------------------------------------- 1 | Creating a new Account 2 | ---------------------- 3 | 4 | piecash can create new accounts (a :class:`piecash.core.account.Account`): 5 | 6 | .. ipython:: python 7 | 8 | from piecash import create_book, Account 9 | 10 | book = create_book(currency="EUR") 11 | 12 | # retrieve the default currency 13 | EUR = book.commodities.get(mnemonic="EUR") 14 | 15 | # creating a placeholder account 16 | acc = Account(name="My account", 17 | type="ASSET", 18 | parent=book.root_account, 19 | commodity=EUR, 20 | placeholder=True,) 21 | 22 | # creating a detailed sub-account 23 | subacc = Account(name="My sub account", 24 | type="BANK", 25 | parent=acc, 26 | commodity=EUR, 27 | commodity_scu=1000, 28 | description="my bank account", 29 | code="FR013334...",) 30 | 31 | book.save() 32 | 33 | book.accounts 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/source/tutorial/new_objects/create_book.rst: -------------------------------------------------------------------------------- 1 | Creating a new Book 2 | ------------------- 3 | 4 | .. py:currentmodule:: piecash.core.book 5 | 6 | piecash can create a new GnuCash document (a :class:`Book`) from scratch through the :func:`create_book` function. 7 | 8 | To create a in-memory sqlite3 document (useful to test piecash for instance), a simple call is enough: 9 | 10 | .. ipython:: python 11 | 12 | import piecash 13 | 14 | book = piecash.create_book() 15 | 16 | To create a file-based sqlite3 document: 17 | 18 | .. ipython:: python 19 | 20 | book = piecash.create_book("example_file.gnucash") 21 | # or equivalently (adding the overwrite=True argument to overwrite the file if it already exists) 22 | book = piecash.create_book(sqlite_file="example_file.gnucash", overwrite=True) 23 | # or equivalently 24 | book = piecash.create_book(uri_conn="sqlite:///example_file.gnucash", overwrite=True) 25 | 26 | and for a postgres document (needs the psycopg2 package installable via "pip install psycopg2"):: 27 | 28 | book = piecash.create_book(uri_conn="postgres://user:passwd@localhost/example_gnucash_db") 29 | 30 | 31 | .. note:: 32 | 33 | Per default, the currency of the document is the euro (EUR) but you can specify any other ISO currency through 34 | its ISO symbol: 35 | 36 | .. ipython:: python 37 | 38 | book = piecash.create_book(sqlite_file="example_file.gnucash", 39 | currency="USD", 40 | overwrite=True) 41 | 42 | If the document already exists, piecash will raise an exception. You can force piecash to overwrite an existing file/database 43 | (i.e. delete it and then recreate it) by passing the overwrite=True argument: 44 | 45 | .. ipython:: python 46 | 47 | book = piecash.create_book(sqlite_file="example_file.gnucash", overwrite=True) 48 | -------------------------------------------------------------------------------- /docs/source/tutorial/new_objects/create_business.rst: -------------------------------------------------------------------------------- 1 | Creating new Business objects 2 | ----------------------------- 3 | 4 | piecash can create new 'business' objects (this is a work in progress). 5 | 6 | To create a new customer (a :class:`piecash.business.person.Customer`): 7 | 8 | .. ipython:: python 9 | 10 | from piecash import create_book, Customer, Address 11 | 12 | # create a book (in memory) 13 | b = create_book(currency="EUR") 14 | # get the currency 15 | eur = b.default_currency 16 | 17 | # create a customer 18 | c1 = Customer(name="Mickey", currency=eur, address=Address(addr1="Sesame street 1", email="mickey@example.com")) 19 | # the customer has not yet an ID 20 | c1 21 | 22 | # we add it to the book 23 | b.add(c1) 24 | 25 | # flush the book 26 | b.flush() 27 | 28 | # the customer gets its ID 29 | print(c1) 30 | 31 | # or create a customer directly in a book (by specifying the book argument) 32 | c2 = Customer(name="Mickey", currency=eur, address=Address(addr1="Sesame street 1", email="mickey@example.com"), 33 | book=b) 34 | 35 | # the customer gets immediately its ID 36 | c2 37 | 38 | # the counter of the ID is accessible as 39 | b.counter_customer 40 | 41 | b.save() 42 | 43 | Similar functions are available to create new vendors (:class:`piecash.business.person.Vendor`) or employees (:class:`piecash.business.person.Employee`). 44 | 45 | There is also the possibility to set taxtables for customers or vendors as: 46 | 47 | .. ipython:: python 48 | 49 | from piecash import Taxtable, TaxtableEntry 50 | from decimal import Decimal 51 | 52 | # let us first create an account to which link a tax table entry 53 | acc = Account(name="MyTaxAcc", parent=b.root_account, commodity=b.currencies(mnemonic="EUR"), type="ASSET") 54 | 55 | # then create a table with on entry (6.5% on previous account 56 | tt = Taxtable(name="local taxes", entries=[ 57 | TaxtableEntry(type="percentage", 58 | amount=Decimal("6.5"), 59 | account=acc), 60 | ]) 61 | 62 | # and finally attach it to a customer 63 | c2.taxtable = tt 64 | 65 | b.save() 66 | 67 | print(b.taxtables) 68 | -------------------------------------------------------------------------------- /docs/source/tutorial/new_objects/create_commodity.rst: -------------------------------------------------------------------------------- 1 | Creating a new Commodity 2 | ------------------------ 3 | 4 | piecash can create new commodities (a :class:`piecash.core.commodity.Commodity`): 5 | 6 | .. ipython:: python 7 | 8 | from piecash import create_book, Commodity, factories 9 | 10 | # create a book (in memory) with some currency 11 | book = create_book(currency="EUR") 12 | 13 | print(book.commodities) 14 | 15 | # creating a new ISO currency (if not already available in s.commodities) (warning, object should be manually added to session) 16 | USD = factories.create_currency_from_ISO("USD") 17 | book.add(USD) # add to session 18 | 19 | # create a commodity (lookup on yahoo! finance, need web access) 20 | # (warning, object should be manually added to session if book kwarg is not included in constructor) 21 | # DOES NOT WORK ANYMORE DUE TO CLOSING OF YAHOO!FINANCE 22 | # apple = factories.create_stock_from_symbol("AAPL", book) 23 | 24 | # creating commodities using the constructor 25 | # (warning, object should be manually added to session if book kwarg is not included in constructor) 26 | 27 | # create a special "reward miles" Commodity using the constructor without book kwarg 28 | miles = Commodity(namespace="LOYALTY", mnemonic="Miles", fullname="Reward miles", fraction=1000000) 29 | book.add(miles) # add to session 30 | 31 | # create a special "unicorn hugs" Commodity using the constructor with book kwarg 32 | unhugs = Commodity(namespace="KINDNESS", mnemonic="Unhugs", fullname="Unicorn hugs", fraction=1, book=book) 33 | 34 | USD, miles, unhugs 35 | 36 | .. warning:: 37 | 38 | The following (creation of non ISO currencies) is explicitly forbidden by the GnuCash application. 39 | 40 | .. ipython:: python 41 | 42 | # create a bitcoin currency (warning, max 6 digits after comma, current GnuCash limitation) 43 | XBT = Commodity(namespace="CURRENCY", mnemonic="XBT", fullname="Bitcoin", fraction=1000000) 44 | book.add(XBT) # add to session 45 | 46 | XBT 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/source/tutorial/new_objects/create_transaction.rst: -------------------------------------------------------------------------------- 1 | Creating a new Transaction 2 | -------------------------- 3 | 4 | piecash can create new transactions (a :class:`piecash.core.transaction.Transaction`): 5 | 6 | .. ipython:: python 7 | 8 | from piecash import create_book, Account, Transaction, Split, GncImbalanceError, factories, ledger 9 | 10 | # create a book (in memory) 11 | book = create_book(currency="EUR") 12 | # get the EUR and create the USD currencies 13 | c1 = book.default_currency 14 | c2 = factories.create_currency_from_ISO("USD") 15 | # create two accounts 16 | a1 = Account("Acc 1", "ASSET", c1, parent=book.root_account) 17 | a2 = Account("Acc 2", "ASSET", c2, parent=book.root_account) 18 | # create a transaction from a1 to a2 19 | tr = Transaction(currency=c1, 20 | description="transfer", 21 | splits=[ 22 | Split(account=a1, value=-100), 23 | Split(account=a2, value=100, quantity=30) 24 | ]) 25 | book.flush() 26 | 27 | # ledger() returns a representation of the transaction in the ledger-cli format 28 | print(ledger(tr)) 29 | 30 | # change the book to use the "trading accounts" options 31 | book.use_trading_accounts = True 32 | # add a new transaction identical to the previous 33 | tr2 = Transaction(currency=c1, 34 | description="transfer 2", 35 | splits=[ 36 | Split(account=a1, value=-100), 37 | Split(account=a2, value=100, quantity=30) 38 | ]) 39 | print(ledger(tr2)) 40 | # when flushing, the trading accounts are created 41 | book.flush() 42 | print(ledger(tr2)) 43 | 44 | # trying to create an unbalanced transaction trigger an exception 45 | # (there is not automatic creation of an imbalance split) 46 | tr3 = Transaction(currency=c1, 47 | description="transfer imb", 48 | splits=[ 49 | Split(account=a1, value=-100), 50 | Split(account=a2, value=90, quantity=30) 51 | ]) 52 | print(ledger(tr3)) 53 | try: 54 | # the imbalance exception is triggered at flush time 55 | book.flush() 56 | except GncImbalanceError: 57 | print("Indeed, there is an imbalance !") 58 | -------------------------------------------------------------------------------- /examples/export_norme_A47.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import print_function 3 | 4 | import io 5 | import os.path 6 | 7 | import jinja2 8 | 9 | from piecash import open_book 10 | 11 | if __name__ == "__main__": 12 | this_folder = os.path.dirname(os.path.realpath(__file__)) 13 | 14 | with open_book( 15 | os.path.join(this_folder, "..", "gnucash_books", "CGT2015.gnucash"), 16 | open_if_lock=True, 17 | ) as book: 18 | transactions = book.transactions 19 | 20 | env = jinja2.Environment(trim_blocks=True, lstrip_blocks=True) 21 | xml = env.from_string( 22 | u""" 23 | 24 | 25 | 26 | 2016-12-31T00:00:00 27 | 28 | Le code du Journal 29 | Le libellé du Journal 30 | {% for i, ecriture in enumerate(transactions) %} 31 | 32 | {{ i }} 33 | {{ ecriture.post_date.strftime("%Y-%m-%d") }} 34 | {{ ecriture.description }} 35 | {{ ecriture.num }} 36 | {{ ecriture.post_date.strftime("%Y-%m-%d") }} 37 | {{ ecriture.post_date.strftime("%Y-%m-%d") }} 38 | {% for sp in ecriture.splits %} 39 | 40 | {{ sp.account.code }} 41 | {{ sp.account.name }} 42 | Le numéro de compte auxiliaire (à blanc si non utilisé) 43 | Le libellé de compte auxiliaire (à blanc si non utilisé) 44 | 45 | {{ abs(sp.value) }} 46 | {% if sp.value >0 %}c{% else %}d{% endif %} 47 | 48 | {% endfor %} 49 | 50 | {% endfor %} 51 | 52 | 53 | 54 | """ 55 | ).render( 56 | transactions=transactions, 57 | enumerate=enumerate, 58 | abs=abs, 59 | ) 60 | 61 | with io.open("resultat.xml", "w", encoding="utf-8") as f: 62 | f.write(xml) 63 | 64 | # pour référence, fichier généré à partir du xsd 65 | """ 66 | 67 | 68 | 69 | 2007-10-26T08:36:28 70 | 71 | 72 | string 73 | string 74 | 75 | 76 | string 77 | 2014-06-09+02:00 78 | string 79 | string 80 | 2009-05-16T14:42:28 81 | 82 | string 83 | 84 | 2002-11-05T09:01:03+01:00 85 | 2016-01-01T20:07:42 86 | 87 | 88 | string 89 | string 90 | 91 | 92 | string 93 | 94 | string 95 | 96 | 97 | string 98 | 99 | string 100 | 101 | string 102 | 103 | string 104 | 105 | 1.5E2 106 | 1.5E2 107 | 1.5E2 108 | c 109 | 110 | 111 | 112 | 113 | string 114 | string 115 | 116 | 117 | string 118 | 119 | string 120 | 121 | 122 | string 123 | 124 | string 125 | 126 | string 127 | 128 | string 129 | 130 | 1.5E2 131 | 1.5E2 132 | 1.5E2 133 | c 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | """ 142 | -------------------------------------------------------------------------------- /examples/filtered_transaction_report.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import datetime 3 | import re 4 | import os.path 5 | 6 | from piecash import open_book 7 | 8 | 9 | if __name__ == "__main__": 10 | this_folder = os.path.dirname(os.path.realpath(__file__)) 11 | s = open_book( 12 | os.path.join(this_folder, "..", "gnucash_books", "simple_sample.gnucash"), 13 | open_if_lock=True, 14 | ) 15 | else: 16 | s = open_book( 17 | os.path.join("gnucash_books", "simple_sample.gnucash"), open_if_lock=True 18 | ) 19 | 20 | # get default currency 21 | print(s.default_currency) 22 | 23 | regex_filter = re.compile("^/Rental/") 24 | 25 | # retrieve relevant transactions 26 | transactions = [ 27 | tr 28 | for tr in s.transactions # query all transactions in the book/session and filter them on 29 | if ( 30 | regex_filter.search(tr.description) # description field matching regex 31 | or any(regex_filter.search(spl.memo) for spl in tr.splits) 32 | ) # or memo field of any split of transaction 33 | and tr.post_date.date() >= datetime.date(2014, 11, 1) 34 | ] # and with post_date no later than begin nov. 35 | 36 | 37 | # output report with simple 'print' 38 | print( 39 | "Here are the transactions for the search criteria '{}':".format( 40 | regex_filter.pattern 41 | ) 42 | ) 43 | for tr in transactions: 44 | print("- {:%Y/%m/%d} : {}".format(tr.post_date, tr.description)) 45 | for spl in tr.splits: 46 | print( 47 | "\t{amount} {direction} {account} : {memo}".format( 48 | amount=abs(spl.value), 49 | direction="-->" if spl.value > 0 else "<--", 50 | account=spl.account.fullname, 51 | memo=spl.memo, 52 | ) 53 | ) 54 | 55 | # same with jinja2 templates 56 | try: 57 | import jinja2 58 | except ImportError: 59 | print( 60 | "\n\t*** Install jinja2 ('pip install jinja2') to test the jinja2 template version ***\n" 61 | ) 62 | jinja2 = None 63 | 64 | if jinja2: 65 | env = jinja2.Environment(trim_blocks=True, lstrip_blocks=True) 66 | print( 67 | env.from_string( 68 | """ 69 | Here are the transactions for the search criteria '{{regex.pattern}}': 70 | {% for tr in transactions %} 71 | - {{ tr.post_date.strftime("%Y/%m/%d") }} : {{ tr.description }} 72 | {% for spl in tr.splits %} 73 | {{ spl.value.__abs__() }} {% if spl.value < 0 %} --> {% else %} <-- {% endif %} {{ spl.account.fullname }} : {{ spl.memo }} 74 | {% endfor %} 75 | {% endfor %} 76 | """ 77 | ).render(transactions=transactions, regex=regex_filter) 78 | ) 79 | -------------------------------------------------------------------------------- /examples/ipython/piecash_session.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "", 4 | "signature": "sha256:236a09aaeb0caa732aeb11d222b1273030cca05de4a78dff613d05f3473235c8" 5 | }, 6 | "nbformat": 3, 7 | "nbformat_minor": 0, 8 | "worksheets": [ 9 | { 10 | "cells": [ 11 | { 12 | "cell_type": "code", 13 | "collapsed": false, 14 | "input": [ 15 | "import piecash\n", 16 | "import datetime\n", 17 | "piecash.__version__" 18 | ], 19 | "language": "python", 20 | "metadata": {}, 21 | "outputs": [ 22 | { 23 | "metadata": {}, 24 | "output_type": "pyout", 25 | "prompt_number": 1, 26 | "text": [ 27 | "'0.2.0'" 28 | ] 29 | } 30 | ], 31 | "prompt_number": 1 32 | }, 33 | { 34 | "cell_type": "code", 35 | "collapsed": false, 36 | "input": [ 37 | "# open a SQLAlchemy session linked to the test.gnucash file (as sqlite3 saved Book)\n", 38 | "s = piecash.open_book(\"test.gnucash\", readonly=False)" 39 | ], 40 | "language": "python", 41 | "metadata": {}, 42 | "outputs": [], 43 | "prompt_number": 2 44 | }, 45 | { 46 | "cell_type": "code", 47 | "collapsed": false, 48 | "input": [ 49 | "# retrieve the single Book object from the session (this is a sqlalchemy standard call)\n", 50 | "book = s.book\n", 51 | "# retrieve the EUR currency\n", 52 | "EUR = s.commodities.get(mnemonic=\"EUR\")" 53 | ], 54 | "language": "python", 55 | "metadata": {}, 56 | "outputs": [], 57 | "prompt_number": 3 58 | }, 59 | { 60 | "cell_type": "code", 61 | "collapsed": false, 62 | "input": [ 63 | "# from the book, retrieve the root account and display its children accounts\n", 64 | "book.root_account.children" 65 | ], 66 | "language": "python", 67 | "metadata": {}, 68 | "outputs": [ 69 | { 70 | "metadata": {}, 71 | "output_type": "pyout", 72 | "prompt_number": 4, 73 | "text": [ 74 | "[Account,\n", 75 | " Account,\n", 76 | " Account,\n", 77 | " Account,\n", 78 | " Account]" 79 | ] 80 | } 81 | ], 82 | "prompt_number": 4 83 | }, 84 | { 85 | "cell_type": "code", 86 | "collapsed": false, 87 | "input": [ 88 | "# retrieve the standard 3 default assets accounts (checking account, saving account, cash in wallet)\n", 89 | "curacc, savacc, cash = book.root_account.children[0].children[0].children" 90 | ], 91 | "language": "python", 92 | "metadata": {}, 93 | "outputs": [], 94 | "prompt_number": 5 95 | }, 96 | { 97 | "cell_type": "code", 98 | "collapsed": false, 99 | "input": [ 100 | "# check splits (they should be empty if the GnuCash book was an empty Book)\n", 101 | "savacc.splits, curacc.splits" 102 | ], 103 | "language": "python", 104 | "metadata": {}, 105 | "outputs": [ 106 | { 107 | "metadata": {}, 108 | "output_type": "pyout", 109 | "prompt_number": 6, 110 | "text": [ 111 | "([], [])" 112 | ] 113 | } 114 | ], 115 | "prompt_number": 6 116 | }, 117 | { 118 | "cell_type": "code", 119 | "collapsed": false, 120 | "input": [ 121 | "# create a transaction of 45 \u20ac from the saving account to the checking account\n", 122 | "tr = piecash.Transaction.single_transaction(datetime.date.today(),datetime.date.today(), \"transfer of money\", (4500, 100), savacc, curacc)" 123 | ], 124 | "language": "python", 125 | "metadata": {}, 126 | "outputs": [], 127 | "prompt_number": 7 128 | }, 129 | { 130 | "cell_type": "code", 131 | "collapsed": false, 132 | "input": [ 133 | "# check some attributes of the transaction\n", 134 | "tr.description, tr.splits" 135 | ], 136 | "language": "python", 137 | "metadata": {}, 138 | "outputs": [ 139 | { 140 | "metadata": {}, 141 | "output_type": "pyout", 142 | "prompt_number": 8, 143 | "text": [ 144 | "('transfer of money',\n", 145 | " [ (-4500, 100)>,\n", 146 | " (4500, 100)>])" 147 | ] 148 | } 149 | ], 150 | "prompt_number": 8 151 | }, 152 | { 153 | "cell_type": "code", 154 | "collapsed": false, 155 | "input": [ 156 | "# check the splits from the accounts point of view\n", 157 | "savacc.splits, curacc.splits" 158 | ], 159 | "language": "python", 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "metadata": {}, 164 | "output_type": "pyout", 165 | "prompt_number": 9, 166 | "text": [ 167 | "([ (-4500, 100)>],\n", 168 | " [ (4500, 100)>])" 169 | ] 170 | } 171 | ], 172 | "prompt_number": 9 173 | }, 174 | { 175 | "cell_type": "code", 176 | "collapsed": false, 177 | "input": [ 178 | "# cancel all changes in the session (i.e. undo all changes)\n", 179 | "s.cancel()" 180 | ], 181 | "language": "python", 182 | "metadata": {}, 183 | "outputs": [], 184 | "prompt_number": 10 185 | }, 186 | { 187 | "cell_type": "code", 188 | "collapsed": false, 189 | "input": [ 190 | "# check splits after the rollback (they should be unchanged)\n", 191 | "savacc.splits, curacc.splits" 192 | ], 193 | "language": "python", 194 | "metadata": {}, 195 | "outputs": [ 196 | { 197 | "metadata": {}, 198 | "output_type": "pyout", 199 | "prompt_number": 11, 200 | "text": [ 201 | "([], [])" 202 | ] 203 | } 204 | ], 205 | "prompt_number": 11 206 | } 207 | ], 208 | "metadata": {} 209 | } 210 | ] 211 | } -------------------------------------------------------------------------------- /examples/ipython/test.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/examples/ipython/test.gnucash -------------------------------------------------------------------------------- /examples/read_currencies.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read currency exchange rates. 3 | This functionality could be used to display the exchange rate graph, for example. 4 | 5 | The first (and only) parameter is the name of the GnuCash file to use. If not set, 6 | 'test.gnucash' is used. 7 | """ 8 | # pylint: disable=invalid-name 9 | import sys 10 | import piecash 11 | from piecash import Commodity 12 | 13 | # Variables 14 | filename = sys.argv[1] 15 | if filename is None: 16 | print("You need to specify a valid .gnucash file to use.") 17 | filename = "test.gnucash" 18 | 19 | symbol = "AUD" 20 | #################################### 21 | 22 | with piecash.open_book(filename, open_if_lock=True) as book: 23 | # , readonly=False, 24 | 25 | # Get all commodities. 26 | # The commodities (including currencies) in the book are only those used in accounts. 27 | # commodities = book.commodities 28 | 29 | # Get all the currencies in the book (i.e. for update). 30 | print("All currencies used in the book:") 31 | currencies = book.currencies 32 | for c in currencies: 33 | print(c) 34 | 35 | # Accessing individual records. 36 | 37 | print("\nSelected single currency details (" + symbol + "):") 38 | cdty = book.get(Commodity, namespace="CURRENCY", mnemonic=symbol) 39 | 40 | # Accessing attributes of a commodity. 41 | print( 42 | "Commodity namespace={cdty.namespace}\n" 43 | " mnemonic={cdty.mnemonic}\n" 44 | " cusip={cdty.cusip}\n" 45 | " fraction={cdty.fraction}".format(cdty=cdty) 46 | ) 47 | 48 | # Loop through the existing commodity prices. 49 | # This can be used to fetch the points for a price graph. 50 | print("\nHistorical prices:") 51 | for pr in cdty.prices: 52 | print( 53 | "Price date={pr.date}" 54 | " value={pr.value} {pr.currency.mnemonic}/{pr.commodity.mnemonic}".format( 55 | pr=pr 56 | ) 57 | ) 58 | 59 | # List of accounts which use the commodity: 60 | # cdty.accounts 61 | -------------------------------------------------------------------------------- /examples/read_currencies_sa.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read currency exchange rates. 3 | This example is similar to read_currencies script but uses SQLAlchemy directly 4 | to achieve the same result. 5 | 6 | The goal is simply to illustrate what happens under the hood. Hopefully this 7 | shows how simple the piecash API is. 8 | """ 9 | # pylint: disable=invalid-name 10 | import sys 11 | import piecash 12 | from piecash import Commodity 13 | 14 | # Variables 15 | filename = sys.argv[1] 16 | if filename is None: 17 | print("You need to specify a valid .gnucash file to use.") 18 | filename = "test.gnucash" 19 | 20 | symbol = "AUD" 21 | #################################### 22 | 23 | with piecash.open_book(filename, open_if_lock=True) as book: 24 | # , readonly=False, 25 | 26 | # SQLAlchemy session. 27 | session = book.session 28 | 29 | # query example: 30 | # accountsFiltered = session.query(Account).filter(Account.name >= "T").all() 31 | # SQLAlchemy methods: count, first, all, one... 32 | 33 | # Get all the currencies in the book (i.e. for update). 34 | print("All currencies used in the book:") 35 | currencies = ( 36 | session.query(Commodity).filter(Commodity.namespace == "CURRENCY").all() 37 | ) 38 | for c in currencies: 39 | print(c) 40 | 41 | # Accessing individual records. 42 | 43 | print("\nSelected single currency details (" + symbol + "):") 44 | cdty = ( 45 | session.query(Commodity) 46 | .filter(Commodity.namespace == "CURRENCY", Commodity.mnemonic == symbol) 47 | .first() 48 | ) 49 | 50 | # Accessing attributes of a commodity. 51 | print( 52 | "Commodity namespace={cdty.namespace}\n" 53 | " mnemonic={cdty.mnemonic}\n" 54 | " cusip={cdty.cusip}\n" 55 | " fraction={cdty.fraction}".format(cdty=cdty) 56 | ) 57 | 58 | # Loop through the existing commodity prices. 59 | # This can be used to fetch the points for a price graph. 60 | print("\nHistorical prices:") 61 | for pr in cdty.prices: 62 | print( 63 | "Price date={pr.date}" 64 | " value={pr.value} {pr.currency.mnemonic}/{pr.commodity.mnemonic}".format( 65 | pr=pr 66 | ) 67 | ) 68 | -------------------------------------------------------------------------------- /examples/simple_book.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from piecash import create_book 3 | 4 | # create by default an in memory sqlite version 5 | with create_book(echo=False) as book: 6 | 7 | print("Book is saved:", book.is_saved, end=" ") 8 | print(" ==> book description:", book.root_account.description) 9 | 10 | print("changing description...") 11 | book.root_account.description = "hello, book" 12 | print("Book is saved:", book.is_saved, end=" ") 13 | print(" ==> book description:", book.root_account.description) 14 | 15 | print("saving...") 16 | book.save() 17 | 18 | print("Book is saved:", book.is_saved, end=" ") 19 | print(" ==> book description:", book.root_account.description) 20 | 21 | print("changing description...") 22 | book.root_account.description = "nevermind, book" 23 | print("Book is saved:", book.is_saved, end=" ") 24 | print(" ==> book description:", book.root_account.description) 25 | 26 | print("cancel...") 27 | book.cancel() 28 | 29 | print("Book is saved:", book.is_saved, end=" ") 30 | print(" ==> book description:", book.root_account.description) 31 | -------------------------------------------------------------------------------- /examples/simple_book_transaction_creation.py: -------------------------------------------------------------------------------- 1 | from piecash import create_book, Account 2 | 3 | # create a book with some account tree structure 4 | with create_book( 5 | "../gnucash_books/simple_book_transaction_creation.gnucash", overwrite=True 6 | ) as mybook: 7 | mybook.root_account.children = [ 8 | Account( 9 | name="Expenses", 10 | type="EXPENSE", 11 | commodity=mybook.currencies(mnemonic="USD"), 12 | placeholder=True, 13 | children=[ 14 | Account( 15 | name="Some Expense Account", 16 | type="EXPENSE", 17 | commodity=mybook.currencies(mnemonic="USD"), 18 | ), 19 | ], 20 | ), 21 | Account( 22 | name="Assets", 23 | type="ASSET", 24 | commodity=mybook.currencies(mnemonic="USD"), 25 | placeholder=True, 26 | children=[ 27 | Account( 28 | name="Current Assets", 29 | type="BANK", 30 | commodity=mybook.currencies(mnemonic="USD"), 31 | placeholder=True, 32 | children=[ 33 | Account( 34 | name="Checking", 35 | type="BANK", 36 | commodity=mybook.currencies(mnemonic="USD"), 37 | ) 38 | ], 39 | ), 40 | ], 41 | ), 42 | ] 43 | # save the book 44 | mybook.save() 45 | 46 | from piecash import open_book, Transaction, Split 47 | from datetime import datetime 48 | from decimal import Decimal 49 | 50 | # reopen the book and add a transaction 51 | with open_book( 52 | "../gnucash_books/simple_book_transaction_creation.gnucash", 53 | open_if_lock=True, 54 | readonly=False, 55 | ) as mybook: 56 | today = datetime.now() 57 | # retrieve the currency from the book 58 | USD = mybook.currencies(mnemonic="USD") 59 | # define the amount as Decimal 60 | amount = Decimal("25.35") 61 | # retrieve accounts 62 | to_account = mybook.accounts(fullname="Expenses:Some Expense Account") 63 | from_account = mybook.accounts(fullname="Assets:Current Assets:Checking") 64 | # create the transaction with its two splits 65 | Transaction( 66 | post_date=today.date(), 67 | enter_date=today, 68 | currency=USD, 69 | description="Transaction Description!", 70 | splits=[ 71 | Split(account=to_account, value=amount, memo="Split Memo!"), 72 | Split(account=from_account, value=-amount, memo="Other Split Memo!"), 73 | ], 74 | ) 75 | # save the book 76 | mybook.save() 77 | 78 | from piecash import ledger 79 | 80 | # check the book by exporting to ledger format 81 | with open_book( 82 | "../gnucash_books/simple_book_transaction_creation.gnucash", open_if_lock=True 83 | ) as mybook: 84 | print(ledger(mybook)) 85 | -------------------------------------------------------------------------------- /examples/simple_delete_account.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pathlib import Path 3 | 4 | from piecash import open_book, Account 5 | 6 | GNUCASH_BOOK = "../gnucash_books/simple_sample.gnucash" 7 | 8 | # open the book and the export file 9 | with open_book(GNUCASH_BOOK, readonly=True, open_if_lock=True) as book: 10 | # show accounts 11 | print(book.accounts) 12 | print("Number of splits in the book:", len(book.splits)) 13 | # select the 3rd account 14 | account = book.accounts[2] 15 | print(account, " has splits: ", account.splits) 16 | 17 | # delete the account from the book 18 | book.delete(account) 19 | # flush the change 20 | book.flush() 21 | # check the account has disappeared from the book and its related split too 22 | print(book.accounts) 23 | print("Number of splits in the book:", len(book.splits)) 24 | 25 | # even if the account object and its related object still exists 26 | print(account, " has splits: ", account.splits) 27 | 28 | # do not forget to save the book if you want 29 | # your changes to be saved in the database 30 | -------------------------------------------------------------------------------- /examples/simple_export_transaction_csv.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pathlib import Path 3 | 4 | from piecash import open_book 5 | 6 | fields = [ 7 | "DATE", 8 | "TRANSACTION VALUE", 9 | "DEBIT/CREDIT INDICATOR", 10 | "ACCOUNT", 11 | "ACCOUNT CODE", 12 | "CONTRA ACCOUNT", 13 | "CONTRA ACCOUNT CODE", 14 | "ENTRY TEXT", 15 | ] 16 | 17 | GNUCASH_BOOK = "../gnucash_books/simple_sample.gnucash" 18 | CSV_EXPORT = "export.csv" 19 | REPORTING_YEAR = 2019 20 | 21 | # open the book and the export file 22 | with open_book(GNUCASH_BOOK, readonly=True, open_if_lock=True) as mybook, Path( 23 | CSV_EXPORT 24 | ).open("w", newline="") as f: 25 | # initialise the CSV writer 26 | csv_writer = csv.DictWriter(f, fieldnames=fields) 27 | csv_writer.writeheader() 28 | 29 | # iterate on all the transactions in the book 30 | for transaction in mybook.transactions: 31 | # filter transactions not in REPORTING_YEAR 32 | if transaction.post_date.year != REPORTING_YEAR: 33 | continue 34 | 35 | # handle only transactions with 2 splits 36 | if len(transaction.splits) != 2: 37 | print( 38 | f"skipping transaction {transaction} as it has more" 39 | f" than 2 splits in the transaction, dunno what to export to CSV" 40 | ) 41 | continue 42 | 43 | # assign the two splits of the transaction 44 | split_one, split_two = transaction.splits 45 | # build the dictionary with the data of the transaction 46 | data = dict( 47 | zip( 48 | fields, 49 | [ 50 | transaction.post_date, 51 | split_one.value, 52 | split_one.is_debit, 53 | split_one.account.name, 54 | split_one.account.code, 55 | split_two.account.name, 56 | split_two.account.code, 57 | transaction.description, 58 | ], 59 | ) 60 | ) 61 | # write the transaction to the CSV 62 | csv_writer.writerow(data) 63 | -------------------------------------------------------------------------------- /examples/simple_extract_splits_to_pandas.py: -------------------------------------------------------------------------------- 1 | from piecash import open_book 2 | 3 | # open a book 4 | with open_book("../gnucash_books/simple_sample.gnucash", open_if_lock=True) as mybook: 5 | # print all splits in account "Asset" 6 | asset = mybook.accounts(fullname="Asset") 7 | for split in asset.splits: 8 | print(split) 9 | 10 | # extract all split information to a pandas DataFrame 11 | df = mybook.splits_df() 12 | 13 | # print for account "Asset" some information on the splits 14 | print(df.loc[df["account.fullname"] == "Asset", ["transaction.post_date", "value"]]) 15 | -------------------------------------------------------------------------------- /examples/simple_move_split.py: -------------------------------------------------------------------------------- 1 | import random 2 | import pytest 3 | 4 | from piecash import create_book, Account, Transaction, Split, GncValidationError 5 | 6 | # create new book 7 | with create_book() as book: 8 | ra = book.root_account 9 | eur = book.default_currency 10 | 11 | # number of accounts 12 | N = 5 13 | # number of transactions 14 | T = 100 15 | 16 | # create accounts 17 | accounts = [ 18 | Account("account {}".format(i), "ASSET", eur, parent=ra) for i in range(N) 19 | ] 20 | 21 | # create transactions 22 | for i, v in enumerate(random.randrange(10) for j in range(T)): 23 | tx = Transaction( 24 | eur, 25 | "transaction {}".format(i), 26 | ) 27 | Split(accounts[random.randrange(N)], value=v, transaction=tx) 28 | Split(accounts[random.randrange(N)], value=-v, transaction=tx) 29 | book.save() 30 | 31 | # select two accounts 32 | acc = accounts[0] 33 | tacc = accounts[1] 34 | # move all splits from account acc to account tacc 35 | for spl in list(acc.splits): 36 | spl.account = tacc 37 | book.save() 38 | 39 | # check no more splits in account acc 40 | assert len(acc.splits) == 0 41 | 42 | # try to change a split account to an account that is a placeholder 43 | acc.placeholder = 1 44 | with pytest.raises(GncValidationError): 45 | spl.account = acc 46 | book.save() 47 | book.cancel() 48 | 49 | # set an account to a placeholder 50 | tx = book.transactions[0] 51 | tx.splits[0].account.placeholder = 1 52 | book.save() 53 | tx.description = "foo" 54 | with pytest.raises(GncValidationError): 55 | book.save() 56 | -------------------------------------------------------------------------------- /examples/simple_session.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | import tempfile 4 | 5 | from piecash import open_book, create_book, GnucashException 6 | 7 | 8 | FILE_1 = os.path.join(tempfile.gettempdir(), "not_there.gnucash") 9 | FILE_2 = os.path.join(tempfile.gettempdir(), "example_file.gnucash") 10 | 11 | if os.path.exists(FILE_2): 12 | os.remove(FILE_2) 13 | 14 | # open a file that isn't there, detect the error 15 | try: 16 | book = open_book(FILE_1) 17 | except GnucashException as backend_exception: 18 | print("OK", backend_exception) 19 | 20 | # create a new file, this requires a file type specification 21 | with create_book(FILE_2) as book: 22 | pass 23 | 24 | # open the new file, try to open it a second time, detect the lock 25 | # using the session as context manager automatically release the lock and close the session 26 | with open_book(FILE_2) as book: 27 | try: 28 | with open_book(FILE_2) as book_2: 29 | pass 30 | except GnucashException as backend_exception: 31 | print("OK", backend_exception) 32 | 33 | os.remove(FILE_2) 34 | -------------------------------------------------------------------------------- /examples/simple_sqlite_create.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## @file 3 | # @brief Example Script simple sqlite create 4 | # @ingroup python_bindings_examples 5 | 6 | from __future__ import print_function 7 | import os 8 | 9 | from piecash import create_book, Account, Commodity, open_book 10 | from piecash.core.factories import create_currency_from_ISO 11 | 12 | filename = os.path.abspath("test.blob") 13 | if os.path.exists(filename): 14 | os.remove(filename) 15 | 16 | with create_book(filename) as book: 17 | a = Account( 18 | parent=book.root_account, 19 | name="wow", 20 | type="ASSET", 21 | commodity=create_currency_from_ISO("CAD"), 22 | ) 23 | 24 | book.save() 25 | 26 | with open_book(filename) as book: 27 | print(book.root_account.children) 28 | print(book.commodities.get(mnemonic="CAD")) 29 | 30 | os.remove(filename) 31 | -------------------------------------------------------------------------------- /examples/simple_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # # @file 3 | # @brief Creates a basic set of accounts and a couple of transactions 4 | # @ingroup python_bindings_examples 5 | from decimal import Decimal 6 | import os 7 | import tempfile 8 | 9 | from piecash import create_book, Account, Transaction, Split, Commodity 10 | from piecash.core.factories import create_currency_from_ISO 11 | 12 | FILE_1 = os.path.join(tempfile.gettempdir(), "example.gnucash") 13 | 14 | with create_book(FILE_1, overwrite=True) as book: 15 | root_acct = book.root_account 16 | cad = create_currency_from_ISO("CAD") 17 | expenses_acct = Account( 18 | parent=root_acct, name="Expenses", type="EXPENSE", commodity=cad 19 | ) 20 | savings_acct = Account(parent=root_acct, name="Savings", type="BANK", commodity=cad) 21 | opening_acct = Account( 22 | parent=root_acct, name="Opening Balance", type="EQUITY", commodity=cad 23 | ) 24 | num1 = Decimal("4") 25 | num2 = Decimal("100") 26 | num3 = Decimal("15") 27 | 28 | # create transaction with core objects in one step 29 | trans1 = Transaction( 30 | currency=cad, 31 | description="Groceries", 32 | splits=[ 33 | Split(value=num1, account=expenses_acct), 34 | Split(value=-num1, account=savings_acct), 35 | ], 36 | ) 37 | 38 | # create transaction with core object in multiple steps 39 | trans2 = Transaction(currency=cad, description="Opening Savings Balance") 40 | 41 | split3 = Split(value=num2, account=savings_acct, transaction=trans2) 42 | 43 | split4 = Split(value=-num2, account=opening_acct, transaction=trans2) 44 | 45 | # create transaction with factory function 46 | from piecash.core.factories import single_transaction 47 | 48 | trans3 = single_transaction( 49 | None, None, "Pharmacy", num3, savings_acct, expenses_acct 50 | ) 51 | 52 | book.save() 53 | -------------------------------------------------------------------------------- /examples/simple_transaction_split_change.py: -------------------------------------------------------------------------------- 1 | from piecash import open_book, ledger, Split 2 | 3 | # open a book 4 | with open_book( 5 | "../gnucash_books/simple_sample.gnucash", readonly=True, open_if_lock=True 6 | ) as mybook: 7 | # iterate on all the transactions in the book 8 | for transaction in mybook.transactions: 9 | # add some extra text to the transaction description 10 | transaction.description = ( 11 | transaction.description + " (some extra info added to the description)" 12 | ) 13 | # iterate over all the splits of the transaction 14 | # as we will modify the transaction splits in the loop, 15 | # we need to use list(...) to take a copy of the splits at the start of the loop 16 | for split in list(transaction.splits): 17 | # create the new split (here a copy of the each existing split 18 | # in the transaction with value/quantity divided by 10) 19 | new_split = Split( 20 | account=split.account, 21 | value=split.value / 10, 22 | quantity=split.quantity / 10, 23 | memo="my new split", 24 | transaction=transaction, # attach the split to the current transaction 25 | ) 26 | # register the changes (but not save) 27 | mybook.flush() 28 | 29 | # print the book in ledger format to view the changes 30 | print(ledger(mybook)) 31 | 32 | # save the book 33 | # this will raise an error as readonly=True (change to readonly=False to successfully save the book) 34 | mybook.save() 35 | -------------------------------------------------------------------------------- /github_gnucash_projects.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from github import Github 5 | 6 | if __name__ == "__main__": 7 | languages = {} 8 | 9 | try: 10 | GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] 11 | except KeyError: 12 | raise ValueError( 13 | "You should have a valid Github token in your GITHUB_TOKEN environment variable" 14 | ) 15 | 16 | g = Github(GITHUB_TOKEN) 17 | for project in g.search_repositories(query="gnucash", sort="stars", order="desc"): 18 | print(project) 19 | if (project.name.lower() == "gnucash") or ( 20 | "mirror" in (project.description or "").lower() 21 | ): 22 | continue 23 | languages.setdefault(project.language, []).append(project) 24 | 25 | with open("docs/source/doc/github_links.rst", "w", encoding="UTF-8") as fo: 26 | list_of_projects = sorted( 27 | [(k or "", v) for k, v in languages.items()], 28 | key=lambda v: ("AAA" if v[0] == "Python" else v[0] or "zzz"), 29 | ) 30 | 31 | width = 50 32 | sep_row = "+" + "-" * width 33 | head_row = "+" + "=" * width 34 | row = "|" + " " * width 35 | 36 | print("Projects per language", file=fo) 37 | print("=====================", file=fo) 38 | print(file=fo) 39 | print( 40 | "This page lists all projects found by searching 'gnucash' on github (generated on {}) " 41 | "excluding mirrors of the gnucash repository. Projects with a '\*' are projects " 42 | "that have not been updated since 12 months.".format( 43 | datetime.datetime.today().replace(microsecond=0) 44 | ), 45 | file=fo, 46 | ) 47 | print(file=fo) 48 | print(sep_row + sep_row + sep_row + "+", file=fo) 49 | print( 50 | "|{:^50}|{:^50}|{:^50}|".format( 51 | "Language", "# of projects", "# of projects updated in last 12 months" 52 | ), 53 | file=fo, 54 | ) 55 | print(head_row + head_row + head_row + "+", file=fo) 56 | 57 | last12month = datetime.datetime.today() - datetime.timedelta(days=365) 58 | list_of_projects = [ 59 | ( 60 | lang or "Unknown", 61 | projects, 62 | {pr.html_url for pr in projects if pr.pushed_at >= last12month}, 63 | ) 64 | for (lang, projects) in list_of_projects 65 | ] 66 | 67 | for lang, projects, recent_projects in sorted( 68 | list_of_projects, key=lambda k: -len(k[2]) 69 | ): 70 | print( 71 | "|{:^50}|{:^50}|{:^50}|".format( 72 | ":ref:`{}`".format(lang), len(projects), len(recent_projects) 73 | ), 74 | file=fo, 75 | ) 76 | print(sep_row + sep_row + sep_row + "+", file=fo) 77 | 78 | print(file=fo) 79 | 80 | for lang, projects, recent_projects in list_of_projects: 81 | print(lang) 82 | print(".. _{}:".format(lang), file=fo) 83 | print(file=fo) 84 | print(lang or "Unknown", file=fo) 85 | print("-" * len(lang or "Unknown"), file=fo) 86 | print(file=fo) 87 | for project in sorted(projects, key=lambda pr: pr.name.lower()): 88 | description = project.description or "(No description available)" 89 | print( 90 | "{}`{name} <{html_url}>`__ by {user} (last commit on {pushed_at:%Y-%m-%d})\n" 91 | "\t{description}".format( 92 | "" if project.html_url in recent_projects else "\* ", 93 | user=project.owner.login, 94 | pushed_at=project.pushed_at, 95 | description=description, 96 | name=project.name, 97 | html_url=project.html_url, 98 | ), 99 | file=fo, 100 | ) 101 | print(file=fo) 102 | -------------------------------------------------------------------------------- /gnucash_books/README.md: -------------------------------------------------------------------------------- 1 | # Sample gnucash books 2 | 3 | This folder holds a set of gnucash books used for testing, examples and debugging. 4 | 5 | 6 | 7 | ## Default empty books 8 | 9 | The following gnucash books can be used to introspect the SQL schema: 10 | - reference/default_2_6_21_basic.gnucash: empty gnucash books created with gnucash 2.6.21 and no specific options 11 | - reference/default_2_6_21_full_options.gnucash: empty gnucash books created with gnucash 2.6.21 with multiple options enabled (trading accounts, ...) 12 | - reference/default_piecash_0_18_basic.gnucash: empty gnucash books created by piecash 13 | -------------------------------------------------------------------------------- /gnucash_books/all_account_types.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/all_account_types.gnucash -------------------------------------------------------------------------------- /gnucash_books/book_prices.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/book_prices.gnucash -------------------------------------------------------------------------------- /gnucash_books/book_schtx.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/book_schtx.gnucash -------------------------------------------------------------------------------- /gnucash_books/complex_sample.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/complex_sample.gnucash -------------------------------------------------------------------------------- /gnucash_books/default_book.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/default_book.gnucash -------------------------------------------------------------------------------- /gnucash_books/empty_book.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/empty_book.gnucash -------------------------------------------------------------------------------- /gnucash_books/ghost_kvp_scheduled_transaction.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/ghost_kvp_scheduled_transaction.gnucash -------------------------------------------------------------------------------- /gnucash_books/investment.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/investment.gnucash -------------------------------------------------------------------------------- /gnucash_books/invoices.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/invoices.gnucash -------------------------------------------------------------------------------- /gnucash_books/reference/2_6/default_2_6_21_basic.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/reference/2_6/default_2_6_21_basic.gnucash -------------------------------------------------------------------------------- /gnucash_books/reference/2_6/default_2_6_21_full_options.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/reference/2_6/default_2_6_21_full_options.gnucash -------------------------------------------------------------------------------- /gnucash_books/reference/2_6/default_piecash_0_18_basic.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/reference/2_6/default_piecash_0_18_basic.gnucash -------------------------------------------------------------------------------- /gnucash_books/reference/3_0/default_3_0_0_basic.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/reference/3_0/default_3_0_0_basic.gnucash -------------------------------------------------------------------------------- /gnucash_books/reference/3_0/default_3_0_0_full_options.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/reference/3_0/default_3_0_0_full_options.gnucash -------------------------------------------------------------------------------- /gnucash_books/reference/3_0/default_piecash_1_0_basic.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/reference/3_0/default_piecash_1_0_basic.gnucash -------------------------------------------------------------------------------- /gnucash_books/simple_sample.272.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/simple_sample.272.gnucash -------------------------------------------------------------------------------- /gnucash_books/simple_sample.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/simple_sample.gnucash -------------------------------------------------------------------------------- /gnucash_books/test_book.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/gnucash_books/test_book.gnucash -------------------------------------------------------------------------------- /piecash/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Python interface to GnuCash documents""" 3 | from . import metadata 4 | 5 | __version__ = metadata.version 6 | __author__ = metadata.authors[0] 7 | __license__ = metadata.license 8 | __copyright__ = metadata.copyright 9 | 10 | from ._common import ( 11 | GncNoActiveSession, 12 | GnucashException, 13 | GncValidationError, 14 | GncImbalanceError, 15 | Recurrence, 16 | ) 17 | from .core import ( 18 | Book, 19 | Account, 20 | ACCOUNT_TYPES, 21 | AccountType, 22 | Transaction, 23 | Split, 24 | ScheduledTransaction, 25 | Lot, 26 | Commodity, 27 | Price, 28 | create_book, 29 | open_book, 30 | factories, 31 | ) 32 | from .business import Vendor, Customer, Employee, Address 33 | from .business import Invoice, Job 34 | from .business import Taxtable, TaxtableEntry 35 | from .budget import Budget, BudgetAmount 36 | from .kvp import slot 37 | from .ledger import ledger 38 | -------------------------------------------------------------------------------- /piecash/_common.py: -------------------------------------------------------------------------------- 1 | import locale 2 | 3 | from decimal import Decimal 4 | from sqlalchemy import Column, VARCHAR, INTEGER, cast, Float 5 | from sqlalchemy.ext.hybrid import hybrid_property 6 | 7 | from .sa_extra import DeclarativeBase, _Date 8 | 9 | 10 | class GnucashException(Exception): 11 | pass 12 | 13 | 14 | class GncNoActiveSession(GnucashException): 15 | pass 16 | 17 | 18 | class GncValidationError(GnucashException): 19 | pass 20 | 21 | 22 | class GncImbalanceError(GncValidationError): 23 | pass 24 | 25 | 26 | class GncConversionError(GnucashException): 27 | pass 28 | 29 | 30 | class Recurrence(DeclarativeBase): 31 | """ 32 | Recurrence information for scheduled transactions 33 | 34 | Attributes: 35 | obj_guid (str): link to the parent ScheduledTransaction record. 36 | recurrence_mult (int): Multiplier for the period type. Describes how many times 37 | the period repeats for the next occurrence. 38 | recurrence_period_type (str): type or recurrence (monthly, daily). 39 | recurrence_period_start (date): the date the recurrence starts. 40 | recurrence_weekend_adjust (str): adjustment to be made if the next occurrence 41 | falls on weekend / non-working day. 42 | """ 43 | 44 | __tablename__ = "recurrences" 45 | 46 | __table_args__ = {"sqlite_autoincrement": True} 47 | 48 | # column definitions 49 | id = Column("id", INTEGER(), primary_key=True, nullable=False, autoincrement=True) 50 | obj_guid = Column("obj_guid", VARCHAR(length=32), nullable=False) 51 | recurrence_mult = Column("recurrence_mult", INTEGER(), nullable=False) 52 | recurrence_period_type = Column( 53 | "recurrence_period_type", VARCHAR(length=2048), nullable=False 54 | ) 55 | recurrence_period_start = Column("recurrence_period_start", _Date(), nullable=False) 56 | recurrence_weekend_adjust = Column( 57 | "recurrence_weekend_adjust", VARCHAR(length=2048), nullable=False 58 | ) 59 | 60 | # relation definitions 61 | # added from the DeclarativeBaseGUID object (as linked from different objects like the slots) 62 | def __str__(self): 63 | return "{}*{} from {} [{}]".format( 64 | self.recurrence_period_type, 65 | self.recurrence_mult, 66 | self.recurrence_period_start, 67 | self.recurrence_weekend_adjust, 68 | ) 69 | 70 | 71 | MAX_NUMBER = 2 ** 63 - 1 72 | 73 | 74 | def hybrid_property_gncnumeric(num_col, denom_col): 75 | """Return an hybrid_property handling a Decimal represented by a numerator and a 76 | denominator column. 77 | It assumes the python field related to the sqlcolumn is named as _sqlcolumn. 78 | 79 | :type num_col: sqlalchemy.sql.schema.Column 80 | :type denom_col: sqlalchemy.sql.schema.Column 81 | :return: sqlalchemy.ext.hybrid.hybrid_property 82 | """ 83 | num_name, denom_name = "_{}".format(num_col.name), "_{}".format(denom_col.name) 84 | name = num_col.name.split("_")[0] 85 | 86 | def fset(self, d): 87 | if d is None: 88 | num, denom = None, None 89 | else: 90 | if isinstance(d, tuple): 91 | d = Decimal(d[0]) / d[1] 92 | elif isinstance(d, (int, int, str)): 93 | d = Decimal(d) 94 | elif isinstance(d, float): 95 | raise TypeError( 96 | ( 97 | "Received a floating-point number {} where a decimal is expected. " 98 | + "Use a Decimal, str, or int instead" 99 | ).format(d) 100 | ) 101 | elif not isinstance(d, Decimal): 102 | raise TypeError( 103 | ( 104 | "Received an unknown type {} where a decimal is expected. " 105 | + "Use a Decimal, str, or int instead" 106 | ).format(type(d).__name__) 107 | ) 108 | 109 | sign, digits, exp = d.as_tuple() 110 | denom = 10 ** max(-exp, 0) 111 | 112 | denom_basis = getattr(self, "{}_basis".format(denom_name), None) 113 | if denom_basis is not None: 114 | denom = denom_basis 115 | 116 | num = int(d * denom) 117 | if not ( 118 | (-MAX_NUMBER < num < MAX_NUMBER) and (-MAX_NUMBER < denom < MAX_NUMBER) 119 | ): 120 | raise ValueError( 121 | ( 122 | "The amount '{}' cannot be represented in GnuCash. " 123 | + "Either it is too large or it has too many decimals" 124 | ).format(d) 125 | ) 126 | 127 | setattr(self, num_name, num) 128 | setattr(self, denom_name, denom) 129 | 130 | def fget(self): 131 | num, denom = getattr(self, num_name), getattr(self, denom_name) 132 | if num is None: 133 | return 134 | else: 135 | return Decimal(num) / denom 136 | 137 | def expr(cls): 138 | # todo: cast into Decimal for postgres and for sqlite (for the latter, use sqlite3.register_converter ?) 139 | return (cast(num_col, Float) / denom_col).label(name) 140 | 141 | return hybrid_property( 142 | fget=fget, 143 | fset=fset, 144 | expr=expr, 145 | ) 146 | 147 | 148 | class CallableList(list): 149 | """ 150 | A simple class (inherited from list) allowing to retrieve a given list element with a filter on an attribute. 151 | 152 | It can be used as the collection_class of a sqlalchemy relationship or to wrap any list (see examples 153 | in :class:`piecash.core.session.GncSession`) 154 | """ 155 | 156 | fallback = None 157 | 158 | def __init__(self, *args): 159 | list.__init__(self, *args) 160 | 161 | def __call__(self, **kwargs): 162 | """ 163 | Return the first element of the list that has attributes matching the kwargs dict. The `get` method is 164 | an alias for this method. 165 | 166 | To be used as:: 167 | 168 | l(mnemonic="EUR", namespace="CURRENCY") 169 | """ 170 | for obj in self: 171 | for k, v in kwargs.items(): 172 | if getattr(obj, k) != v: 173 | break 174 | else: 175 | return obj 176 | else: 177 | if self.fallback: 178 | return self.fallback(**kwargs) 179 | else: 180 | raise KeyError( 181 | "Could not find object with {} in {}".format(kwargs, self) 182 | ) 183 | 184 | get = __call__ 185 | 186 | 187 | def get_system_currency_mnemonic(): 188 | """Returns the mnemonic of the locale currency (and EUR if not defined). 189 | 190 | At the target, it could also look in Gnucash configuration/registry to see if the user 191 | has chosen another default currency. 192 | """ 193 | 194 | if locale.getlocale() == (None, None): 195 | locale.setlocale(locale.LC_ALL, "") 196 | 197 | mnemonic = locale.localeconv()["int_curr_symbol"].strip() or "EUR" 198 | 199 | return mnemonic 200 | -------------------------------------------------------------------------------- /piecash/_declbase.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from sqlalchemy import Column, VARCHAR, event 4 | from sqlalchemy.ext.declarative import declared_attr 5 | from sqlalchemy.orm import relation, foreign, object_session 6 | 7 | from ._common import CallableList 8 | from .kvp import DictWrapper, Slot 9 | from .sa_extra import DeclarativeBase 10 | 11 | 12 | class DeclarativeBaseGuid(DictWrapper, DeclarativeBase): 13 | __abstract__ = True 14 | 15 | #: the unique identifier of the object 16 | guid = Column( 17 | "guid", 18 | VARCHAR(length=32), 19 | primary_key=True, 20 | nullable=False, 21 | default=lambda: uuid.uuid4().hex, 22 | ) 23 | 24 | @declared_attr 25 | def slots(cls): 26 | rel = relation( 27 | "Slot", 28 | primaryjoin=foreign(Slot.obj_guid) == cls.guid, 29 | cascade="all, delete-orphan", 30 | collection_class=CallableList, 31 | ) 32 | 33 | return rel 34 | -------------------------------------------------------------------------------- /piecash/budget.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import uuid 4 | 5 | from sqlalchemy import Column, VARCHAR, INTEGER, BIGINT, ForeignKey 6 | from sqlalchemy.orm import relation, foreign 7 | 8 | from ._common import hybrid_property_gncnumeric, Recurrence, CallableList 9 | from ._declbase import DeclarativeBaseGuid 10 | from .sa_extra import DeclarativeBase 11 | 12 | 13 | class Budget(DeclarativeBaseGuid): 14 | """ 15 | A GnuCash Budget 16 | 17 | Attributes: 18 | name (str): name of the budget 19 | description (str): description of the budget 20 | amounts (list of :class:`piecash.budget.BudgetAmount`): list of amounts per account 21 | """ 22 | 23 | __tablename__ = "budgets" 24 | 25 | __table_args__ = {} 26 | 27 | # column definitions 28 | # keep this line as we reference it in the primaryjoin 29 | guid = Column( 30 | "guid", 31 | VARCHAR(length=32), 32 | primary_key=True, 33 | nullable=False, 34 | default=lambda: uuid.uuid4().hex, 35 | ) 36 | name = Column("name", VARCHAR(length=2048), nullable=False) 37 | description = Column("description", VARCHAR(length=2048)) 38 | num_periods = Column("num_periods", INTEGER(), nullable=False) 39 | 40 | # # relation definitions 41 | recurrence = relation( 42 | Recurrence, 43 | primaryjoin=foreign(Recurrence.obj_guid) == guid, 44 | cascade="all, delete-orphan", 45 | uselist=False, 46 | ) 47 | 48 | amounts = relation( 49 | "BudgetAmount", 50 | back_populates="budget", 51 | cascade="all, delete-orphan", 52 | collection_class=CallableList, 53 | ) 54 | 55 | def __str__(self): 56 | return "Budget<{}({}) for {} periods following pattern '{}' >".format( 57 | self.name, self.description, self.num_periods, self.recurrence 58 | ) 59 | 60 | 61 | class BudgetAmount(DeclarativeBase): 62 | """ 63 | A GnuCash BudgetAmount 64 | 65 | Attributes: 66 | amount (:class:`decimal.Decimal`): the budgeted amount 67 | account (:class:`piecash.core.account.Account`): the budgeted account 68 | budget (:class:`Budget`): the budget of the amount 69 | """ 70 | 71 | __tablename__ = "budget_amounts" 72 | 73 | __table_args__ = {"sqlite_autoincrement": True} 74 | 75 | # column definitions 76 | id = Column("id", INTEGER(), primary_key=True, autoincrement=True, nullable=False) 77 | budget_guid = Column( 78 | "budget_guid", VARCHAR(length=32), ForeignKey("budgets.guid"), nullable=False 79 | ) 80 | account_guid = Column( 81 | "account_guid", VARCHAR(length=32), ForeignKey("accounts.guid"), nullable=False 82 | ) 83 | period_num = Column("period_num", INTEGER(), nullable=False) 84 | _amount_num = Column("amount_num", BIGINT(), nullable=False) 85 | _amount_denom = Column("amount_denom", BIGINT(), nullable=False) 86 | amount = hybrid_property_gncnumeric(_amount_num, _amount_denom) 87 | 88 | # relation definitions 89 | account = relation("Account", back_populates="budget_amounts") 90 | budget = relation("Budget", back_populates="amounts") 91 | 92 | def __str__(self): 93 | return "BudgetAmount<{}={}>".format(self.period_num, self.amount) 94 | -------------------------------------------------------------------------------- /piecash/business/__init__.py: -------------------------------------------------------------------------------- 1 | from .invoice import Billterm, Entry, Invoice, Job, Order 2 | from .tax import Taxtable, TaxtableEntry 3 | from .person import Customer, Employee, Vendor, Address 4 | -------------------------------------------------------------------------------- /piecash/business/tax.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from sqlalchemy import Column, VARCHAR, BIGINT, INTEGER, ForeignKey 3 | from sqlalchemy.orm import relation 4 | 5 | from .._common import hybrid_property_gncnumeric, CallableList 6 | from .._declbase import DeclarativeBaseGuid, DeclarativeBase 7 | from ..sa_extra import ChoiceType 8 | 9 | 10 | class Taxtable(DeclarativeBaseGuid): 11 | __tablename__ = "taxtables" 12 | 13 | __table_args__ = {} 14 | 15 | # column definitions 16 | guid = Column( 17 | "guid", 18 | VARCHAR(length=32), 19 | primary_key=True, 20 | nullable=False, 21 | default=lambda: uuid.uuid4().hex, 22 | ) 23 | name = Column("name", VARCHAR(length=50), nullable=False) 24 | refcount = Column("refcount", BIGINT(), nullable=False) 25 | invisible = Column("invisible", INTEGER(), nullable=False) 26 | parent_guid = Column("parent", VARCHAR(length=32), ForeignKey("taxtables.guid")) 27 | 28 | # relation definitions 29 | entries = relation( 30 | "TaxtableEntry", 31 | back_populates="taxtable", 32 | cascade="all, delete-orphan", 33 | collection_class=CallableList, 34 | ) 35 | children = relation( 36 | "Taxtable", 37 | back_populates="parent", 38 | cascade="all, delete-orphan", 39 | collection_class=CallableList, 40 | ) 41 | parent = relation( 42 | "Taxtable", 43 | back_populates="children", 44 | remote_side=guid, 45 | ) 46 | 47 | def __init__(self, name, entries=None): 48 | self.name = name 49 | self.refcount = 0 50 | self.invisible = 0 51 | if entries is not None: 52 | self.entries[:] = entries 53 | 54 | def __str__(self): 55 | if self.entries: 56 | return "TaxTable<{}:{}>".format( 57 | self.name, [te.__str__() for te in self.entries] 58 | ) 59 | else: 60 | return "TaxTable<{}>".format(self.name) 61 | 62 | 63 | class TaxtableEntry(DeclarativeBase): 64 | __tablename__ = "taxtable_entries" 65 | 66 | __table_args__ = {"sqlite_autoincrement": True} 67 | 68 | # column definitions 69 | id = Column("id", INTEGER(), primary_key=True, nullable=False, autoincrement=True) 70 | taxtable_guid = Column( 71 | "taxtable", VARCHAR(length=32), ForeignKey("taxtables.guid"), nullable=False 72 | ) 73 | account_guid = Column( 74 | "account", VARCHAR(length=32), ForeignKey("accounts.guid"), nullable=False 75 | ) 76 | _amount_num = Column("amount_num", BIGINT(), nullable=False) 77 | _amount_denom = Column("amount_denom", BIGINT(), nullable=False) 78 | amount = hybrid_property_gncnumeric(_amount_num, _amount_denom) 79 | type = Column("type", ChoiceType({1: "value", 2: "percentage"}), nullable=False) 80 | 81 | # relation definitions 82 | taxtable = relation("Taxtable", back_populates="entries") 83 | account = relation("Account") 84 | 85 | def __init__(self, type, amount, account, taxtable=None): 86 | self.type = type 87 | self.amount = amount 88 | self.account = account 89 | if taxtable: 90 | self.taxtable = taxtable 91 | 92 | def __str__(self): 93 | return "TaxEntry<{} {} in {}>".format(self.amount, self.type, self.account.name) 94 | -------------------------------------------------------------------------------- /piecash/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .session import create_book, open_book, Version 2 | from .account import Account, ACCOUNT_TYPES, AccountType 3 | from .book import Book 4 | from .commodity import Commodity, Price 5 | from .transaction import Transaction, Split, ScheduledTransaction, Lot 6 | from . import factories 7 | -------------------------------------------------------------------------------- /piecash/core/_commodity_helper.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | from collections import namedtuple 5 | 6 | __author__ = "sdementen" 7 | 8 | 9 | def quandl_fx(fx_mnemonic, base_mnemonic, start_date): 10 | """Retrieve exchange rate of commodity fx in function of base. 11 | 12 | API KEY will be retrieved from the environment variable QUANDL_API_KEY 13 | """ 14 | import requests 15 | 16 | PUBLIC_API_URL = "https://www.quandl.com/api/v1/datasets/CURRFX/{}{}.json".format( 17 | fx_mnemonic, base_mnemonic 18 | ) 19 | params = { 20 | "request_source": "python", 21 | "request_version": 2, 22 | "trim_start": "{:%Y-%m-%d}".format(start_date), 23 | } 24 | 25 | # adapt quandl parameters with apikey if given in environment variable QUANDL_API_KEY 26 | apikey = os.environ.get("QUANDL_API_KEY") 27 | if apikey: 28 | params["api_key"] = apikey 29 | 30 | text_result = requests.get(PUBLIC_API_URL, params=params).text 31 | try: 32 | query_result = json.loads(text_result) 33 | except ValueError: 34 | logging.error( 35 | "issue when retrieving info from quandl.com : '{}'".format(text_result) 36 | ) 37 | return [] 38 | if "error" in query_result: 39 | logging.error( 40 | "issue when retrieving info from quandl.com : '{}'".format( 41 | query_result["error"] 42 | ) 43 | ) 44 | return [] 45 | if "quandl_error" in query_result: 46 | logging.error( 47 | "issue when retrieving info from quandl.com : '{}'".format( 48 | query_result["quandl_error"] 49 | ) 50 | ) 51 | return [] 52 | if "errors" in query_result and query_result["errors"]: 53 | logging.error( 54 | "issue when retrieving info from quandl.com : '{}'".format( 55 | query_result["errors"] 56 | ) 57 | ) 58 | return [] 59 | 60 | rows = query_result["data"] 61 | 62 | qdl_result = namedtuple("QUANDL", ["date", "rate", "high", "low"]) 63 | 64 | return [qdl_result(*v) for v in rows] 65 | -------------------------------------------------------------------------------- /piecash/core/factories.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .._common import GnucashException 5 | from ..yahoo_client import get_latest_quote 6 | 7 | 8 | def create_stock_accounts( 9 | cdty, broker_account, income_account=None, income_account_types="D/CL/I" 10 | ): 11 | """Create the multiple accounts used to track a single stock, ie: 12 | 13 | - broker_account/stock.mnemonic 14 | 15 | and the following accounts depending on the income_account_types argument 16 | 17 | - D = Income/Dividend Income/stock.mnemonic 18 | - CL = Income/Cap Gain (Long)/stock.mnemonic 19 | - CS = Income/Cap Gain (Short)/stock.mnemonic 20 | - I = Income/Interest Income/stock.mnemonic 21 | 22 | Args: 23 | broker_account (:class:`piecash.core.account.Account`): the broker account where the account holding 24 | the stock is to be created 25 | income_account (:class:`piecash.core.account.Account`): the income account where the accounts holding 26 | the income related to the stock are to be created 27 | income_account_types (str): "/" separated codes to drive the creation of income accounts 28 | 29 | Returns: 30 | :class:`piecash.core.account.Account`: a tuple with the account under the broker_account where the stock is held 31 | and the list of income accounts. 32 | """ 33 | if cdty.is_currency(): 34 | raise GnucashException( 35 | "{} is a currency ! You can't create stock_accounts for currencies".format( 36 | cdty 37 | ) 38 | ) 39 | 40 | from .account import Account 41 | 42 | symbol = cdty.mnemonic 43 | try: 44 | acc = broker_account.children(name=symbol) 45 | except KeyError: 46 | acc = Account(symbol, "STOCK", cdty, broker_account) 47 | 48 | inc_accounts = [] 49 | if income_account: 50 | cur = cdty.base_currency 51 | 52 | for inc_acc in income_account_types.split("/"): 53 | sub_account_name = { 54 | "D": "Dividend Income", 55 | "CL": "Cap Gain (Long)", 56 | "CS": "Cap Gain (Short)", 57 | "I": "Interest Income", 58 | }[inc_acc] 59 | try: 60 | sub_acc = income_account.children(name=sub_account_name) 61 | except KeyError: 62 | sub_acc = Account( 63 | sub_account_name, "INCOME", cur.base_currency, income_account 64 | ) 65 | try: 66 | cdty_acc = sub_acc.children(name=symbol) 67 | except KeyError: 68 | cdty_acc = Account(symbol, "INCOME", cur, sub_acc) 69 | inc_accounts.append(cdty_acc) 70 | 71 | return acc, inc_accounts 72 | 73 | 74 | def create_currency_from_ISO(isocode): 75 | """ 76 | Factory function to create a new currency from its ISO code 77 | 78 | Args: 79 | isocode (str): the ISO code of the currency (e.g. EUR for the euro) 80 | 81 | Returns: 82 | :class:`Commodity`: the currency as a commodity object 83 | """ 84 | from .commodity import Commodity 85 | 86 | # if self.get_session().query(Commodity).filter_by(isocode=isocode).first(): 87 | # raise GncCommodityError("Currency '{}' already exists".format(isocode)) 88 | 89 | from .currency_ISO import ISO_currencies 90 | 91 | cur = ISO_currencies.get(isocode) 92 | 93 | if cur is None: 94 | raise ValueError( 95 | "Could not find the ISO code '{}' in the ISO table".format(isocode) 96 | ) 97 | 98 | # create the currency 99 | cdty = Commodity( 100 | mnemonic=cur.mnemonic, 101 | fullname=cur.currency, 102 | fraction=10 ** int(cur.fraction), 103 | cusip=cur.cusip, 104 | namespace="CURRENCY", 105 | quote_flag=1, 106 | ) 107 | 108 | # self.gnc_session.add(cdty) 109 | return cdty 110 | 111 | 112 | def create_stock_from_symbol(symbol, book=None): 113 | """ 114 | Factory function to create a new stock from its symbol. The ISO code of the quoted currency of the stock is 115 | stored in the slot "quoted_currency". 116 | 117 | Args: 118 | symbol (str): the symbol for the stock (e.g. YHOO for the Yahoo! stock) 119 | 120 | Returns: 121 | :class:`Commodity`: the stock as a commodity object 122 | 123 | .. note:: 124 | The information is gathered from the yahoo-finance package 125 | The default currency in which the quote is traded is stored in a slot 'quoted_currency' 126 | 127 | .. todo:: 128 | use 'select * from yahoo.finance.sectors' and 'select * from yahoo.finance.industry where id ="sector_id"' 129 | to retrieve name of stocks and allow therefore the creation of a stock by giving its "stock name" (or part of it). 130 | This could also be used to retrieve all symbols related to the same company 131 | """ 132 | from .commodity import Commodity 133 | 134 | share = get_latest_quote(symbol) 135 | 136 | stock = Commodity( 137 | mnemonic=symbol, 138 | fullname=share.name, 139 | fraction=10000, 140 | namespace=share.exchange, 141 | quote_flag=1, 142 | quote_source="yahoo", 143 | quote_tz=share.timezone, 144 | ) 145 | 146 | if book: 147 | book.add(stock) 148 | book.flush() 149 | 150 | return stock 151 | 152 | 153 | def single_transaction( 154 | post_date, enter_date, description, value, from_account, to_account 155 | ): 156 | from . import Transaction, Split 157 | 158 | # currency is derived from "from_account" (as in GUI) 159 | currency = from_account.commodity 160 | # currency of other destination account should be identical (as only one value given) 161 | assert ( 162 | currency == to_account.commodity 163 | ), "Commodities of accounts should be the same" 164 | tx = Transaction( 165 | currency=currency, 166 | post_date=post_date, 167 | enter_date=enter_date, 168 | description=description, 169 | splits=[ 170 | Split(account=from_account, value=-value), 171 | Split(account=to_account, value=value), 172 | ], 173 | ) 174 | return tx 175 | -------------------------------------------------------------------------------- /piecash/ledger.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import json 4 | import re 5 | from functools import singledispatch 6 | from locale import getdefaultlocale 7 | 8 | from .core import Account, Book, Commodity, Price, Transaction 9 | 10 | """original script from https://github.com/MatzeB/pygnucash/blob/master/gnucash2ledger.py by Matthias Braun matze@braunis.de 11 | adapted for: 12 | - python 3 support 13 | - new string formatting 14 | """ 15 | 16 | try: 17 | import babel 18 | import babel.numbers 19 | 20 | BABEL_AVAILABLE = True 21 | except ImportError: 22 | BABEL_AVAILABLE = False 23 | 24 | 25 | @singledispatch 26 | def ledger(obj, **kwargs): 27 | raise NotImplemented 28 | 29 | 30 | CURRENCY_RE = re.compile("^[A-Z]{3}$") 31 | NUMBER_RE = re.compile(r"(^|\s+)[-+]?([0-9]*(\.|,)[0-9]+|[0-9]+)($|\s+)") # regexp to identify a float with blank spaces 32 | NUMERIC_SPACE = re.compile(r"[0-9\s]") 33 | CHARS_ONLY = re.compile(r"[A-Za-z]+") 34 | 35 | 36 | def quote_commodity(mnemonic): 37 | if not CHARS_ONLY.fullmatch(mnemonic): 38 | return json.dumps(mnemonic) 39 | else: 40 | return mnemonic 41 | 42 | 43 | def format_commodity(mnemonic, locale): 44 | if CURRENCY_RE.match(mnemonic) or True: 45 | # format the currency via BABEL if available and then remove the number/amount 46 | s = format_currency(0, 0, mnemonic, locale) 47 | return NUMBER_RE.sub("", s) 48 | else: 49 | # just quote the commodity 50 | return quote_commodity(mnemonic) 51 | 52 | 53 | def format_currency(amount, decimals, currency, locale=False, decimal_quantization=True): 54 | currency = quote_commodity(currency) 55 | if locale is True: 56 | locale = getdefaultlocale()[0] 57 | if BABEL_AVAILABLE is False: 58 | raise ValueError(f"You must install babel ('pip install babel') to export to ledger in your locale '{locale}'") 59 | else: 60 | return babel.numbers.format_currency( 61 | amount, 62 | currency, 63 | format=None, 64 | locale=locale, 65 | currency_digits=True, 66 | format_type="standard", 67 | decimal_quantization=decimal_quantization, 68 | ) 69 | else: 70 | # local hand made version 71 | if decimal_quantization: 72 | digits = decimals 73 | else: 74 | digits = max(decimals, len((str(amount) + ".").split(".")[1].rstrip("0"))) 75 | return "{} {:,.{}f}".format(currency, amount, digits) 76 | 77 | 78 | @ledger.register(Transaction) 79 | def _(tr, locale=False, **kwargs): 80 | """Return a ledger-cli alike representation of the transaction""" 81 | s = [ 82 | "{:%Y-%m-%d} {}{}\n".format( 83 | tr.post_date, 84 | "({}) ".format(tr.num.replace(")", "")) if tr.num else "", 85 | tr.description, 86 | ) 87 | ] 88 | if tr.notes: 89 | s.append("\t;{}\n".format(tr.notes)) 90 | for split in sorted( 91 | tr.splits, 92 | key=lambda split: (split.value, split.transaction_guid, split.account_guid), 93 | ): 94 | if split.account.commodity.mnemonic == "template": 95 | return "" 96 | if split.reconcile_state in ["c", "y"]: 97 | s.append("\t* {:38} ".format(split.account.fullname)) 98 | else: 99 | s.append("\t{:40} ".format(split.account.fullname)) 100 | if split.account.commodity != tr.currency: 101 | s.append( 102 | "{quantity} @@ {amount}".format( 103 | quantity=format_currency( 104 | split.quantity, 105 | split.account.commodity.precision, 106 | split.account.commodity.mnemonic, 107 | locale, 108 | decimal_quantization=False, 109 | ), 110 | amount=format_currency( 111 | abs(split.value), 112 | tr.currency.precision, 113 | tr.currency.mnemonic, 114 | locale, 115 | ), 116 | ) 117 | ) 118 | else: 119 | s.append(format_currency(split.value, tr.currency.precision, tr.currency.mnemonic, locale)) 120 | 121 | if split.memo: 122 | s.append(" ; {:20}".format(split.memo)) 123 | s.append("\n") 124 | 125 | return "".join(s) 126 | 127 | 128 | @ledger.register(Commodity) 129 | def _(cdty, locale=False, commodity_notes=False, **kwargs): 130 | """Return a ledger-cli alike representation of the commodity""" 131 | if cdty.mnemonic in ["", "template"]: 132 | return "" 133 | res = "commodity {}\n".format(format_commodity(cdty.mnemonic, locale)) 134 | if cdty.fullname != "" and commodity_notes: 135 | res += "\tnote {}\n".format(cdty.fullname, locale) 136 | res += "\n" 137 | return res 138 | 139 | 140 | @ledger.register(Account) 141 | def _(acc, short_account_names=False, locale=False, **kwargs): 142 | """Return a ledger-cli alike representation of the account""" 143 | # ignore "dummy" accounts 144 | if acc.type is None or acc.parent is None: 145 | return "" 146 | if acc.commodity.mnemonic == "template": 147 | return "" 148 | 149 | if short_account_names: 150 | res = "account {}\n".format(acc.name) 151 | else: 152 | res = "account {}\n".format(acc.fullname) 153 | 154 | if acc.description != "": 155 | res += "\tnote {}\n".format(acc.description) 156 | 157 | if acc.commodity.is_currency(): 158 | res += '\tcheck commodity == "{}"\n'.format(format_commodity(acc.commodity.mnemonic,locale)) # .replace('"', '\\"')) 159 | return res 160 | 161 | 162 | @ledger.register(Price) 163 | def _(price, locale=False, **kwargs): 164 | """Return a ledger-cli alike representation of the price""" 165 | return "P {:%Y-%m-%d %H:%M:%S} {} {}\n".format( 166 | price.date, 167 | format_commodity(price.commodity.mnemonic, locale), 168 | format_currency( 169 | price.value, 170 | price.currency.precision, 171 | price.currency.mnemonic, 172 | locale, 173 | decimal_quantization=False, 174 | ), 175 | ) 176 | 177 | 178 | @ledger.register(Book) 179 | def _(book, **kwargs): 180 | """Return a ledger-cli alike representation of the book""" 181 | res = [] 182 | 183 | # Commodities 184 | for commodity in sorted(book.commodities, key=lambda cdty: cdty.mnemonic): 185 | res.append(ledger(commodity, **kwargs)) 186 | 187 | # Accounts 188 | if kwargs.get("short_account_names"): # check that no ambiguity in account names 189 | accounts = [acc.name for acc in book.accounts] 190 | if len(accounts) != len(set(accounts)): 191 | raise ValueError("You have duplicate short names in your book. " "You cannot use the 'short_account_names' option.") 192 | for acc in book.accounts: 193 | res.append(ledger(acc, **kwargs)) 194 | res.append("\n") 195 | 196 | # Prices 197 | for price in sorted(book.prices, key=lambda x: (x.commodity_guid, x.currency_guid, x.date)): 198 | res.append(ledger(price, **kwargs)) 199 | res.append("\n") 200 | 201 | for trans in sorted(book.transactions, key=lambda x: x.post_date): 202 | res.append(ledger(trans, **kwargs)) 203 | res.append("\n") 204 | 205 | return "".join(res) 206 | -------------------------------------------------------------------------------- /piecash/metadata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Project metadata 3 | 4 | Information describing the project. 5 | """ 6 | 7 | # The package name, which is also the "UNIX name" for the project. 8 | package = "piecash" 9 | project = "Python interface to GnuCash documents" 10 | project_no_spaces = project.replace(" ", "") 11 | version = "1.2.1" 12 | description = "A pythonic interface to GnuCash SQL documents." 13 | authors = ["sdementen"] 14 | authors_string = ", ".join(authors) 15 | emails = ["sdementen@gmail.com"] 16 | license = "MIT" 17 | copyright = "2014 " + authors_string 18 | url = "https://github.com/sdementen/piecash" 19 | -------------------------------------------------------------------------------- /piecash/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | """Scripts with basic utilities for gnucash (import and export of data)""" 2 | 3 | from . import export, ledger, cli, qif_export, sql_helper 4 | -------------------------------------------------------------------------------- /piecash/scripts/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) 4 | 5 | 6 | @click.group(context_settings=CONTEXT_SETTINGS) 7 | def cli(): 8 | pass 9 | -------------------------------------------------------------------------------- /piecash/scripts/export.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | from piecash.scripts.cli import cli 6 | 7 | 8 | @cli.command() 9 | @click.argument("book", type=click.Path(exists=True)) 10 | @click.argument("entities", type=click.Choice(["customers", "vendors", "prices"])) 11 | @click.option( 12 | "--output", 13 | type=click.File("w"), 14 | default="-", 15 | help="File to which to export the data (default=stdout)", 16 | ) 17 | @click.option( 18 | "--inactive", 19 | is_flag=True, 20 | default=False, 21 | help="Include inactive entities (for vendors and customers)", 22 | ) 23 | def export(book, entities, output, inactive): 24 | """Exports GnuCash ENTITIES. 25 | 26 | This scripts export ENTITIES from the BOOK in a CSV format. 27 | When possible, it exports in a format that can be used to import the data into GnuCash. 28 | 29 | \b 30 | Remarks: 31 | - for customers and vendors, the format does not include an header 32 | - for prices, the format can be used with the `piecash import` command. 33 | """ 34 | from piecash import open_book 35 | 36 | with open_book(book, open_if_lock=True) as book: 37 | if entities == "prices": 38 | output.write( 39 | "date,type,value,value_num, value_denom, currency,commodity,source\n" 40 | ) 41 | output.writelines( 42 | "{p.date:%Y-%m-%d},{p.type},{p.value},{p._value_num},{p._value_denom},{p.currency.mnemonic},{p.commodity.mnemonic},{p.source}\n".format( 43 | p=p 44 | ) 45 | for p in book.prices 46 | ) 47 | 48 | elif entities in ["customers", "vendors"]: 49 | columns = ( 50 | "id, name, addr_name, addr_addr1, addr_addr2, addr_addr3, addr_addr4, " 51 | "addr_phone, addr_fax, addr_email, notes, shipaddr_name, " 52 | "shipaddr_addr1, shipaddr_addr2, shipaddr_addr3, shipaddr_addr4, " 53 | "shipaddr_phone, shipaddr_fax, shipaddr_email".split(", ") 54 | ) 55 | separator = ";" 56 | 57 | filter_entity = (lambda e: True) if inactive else (lambda e: e.active) 58 | 59 | # open the book 60 | res = "\n".join( 61 | [ 62 | separator.join(getattr(v, fld, "") for fld in columns) 63 | for v in getattr(book, entities) 64 | if filter_entity(v) 65 | ] 66 | ) 67 | 68 | output.write(res) 69 | 70 | if __name__ == '__main__': 71 | cli() -------------------------------------------------------------------------------- /piecash/scripts/ledger.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | """original script from https://github.com/MatzeB/pygnucash/blob/master/gnucash2ledger.py by Matthias Braun matze@braunis.de 3 | adapted for: 4 | - python 3 support 5 | - new string formatting 6 | """ 7 | 8 | import click 9 | 10 | import piecash 11 | from piecash.scripts.cli import cli 12 | 13 | 14 | @cli.command() 15 | @click.argument("book", type=click.Path(exists=True)) 16 | @click.option( 17 | "--locale/--no-locale", 18 | default=False, 19 | help="Export currency amounts using locale for currencies format", 20 | ) 21 | @click.option( 22 | "--commodity-notes/--no-commodity-notes", 23 | default=True, 24 | help="Include the commodity_notes for the commodity (hledger does not support commodity commodity_notes", 25 | ) 26 | @click.option( 27 | "--short-account-names/--no-short-account-names", 28 | default=False, 29 | help="Use the short name for the accounts instead of the full hierarchical name.", 30 | ) 31 | @click.option( 32 | "--output", 33 | type=click.File("w", encoding="UTF-8"), 34 | default="-", 35 | help="File to which to export the data (default=stdout)", 36 | ) 37 | def ledger(book, output, locale, commodity_notes, short_account_names): 38 | """Export to ledger-cli format. 39 | 40 | This scripts export a GnuCash BOOK to the ledget-cli format. 41 | """ 42 | with piecash.open_book(book, open_if_lock=True) as data: 43 | output.write( 44 | piecash.ledger( 45 | data, 46 | locale=locale, 47 | commodity_notes=commodity_notes, 48 | short_account_names=short_account_names, 49 | ) 50 | ) 51 | -------------------------------------------------------------------------------- /piecash/scripts/piecash_prices.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | """original script from https://github.com/MatzeB/pygnucash/blob/master/gnucash2ledger.py by Matthias Braun matze@braunis.de 3 | adapted for: 4 | - python 3 support 5 | - new string formatting 6 | 7 | The security must exist in GnuCash database. 8 | Example of a valid CSV file: 9 | currency,commodity,value,date 10 | AUD,YMAX,9.06,"2017-11-10" 11 | 12 | """ 13 | import argparse 14 | import csv 15 | import sys 16 | from datetime import datetime 17 | from decimal import Decimal 18 | 19 | import piecash 20 | from piecash import Price 21 | 22 | parser = argparse.ArgumentParser( 23 | description=""" 24 | Import and export prices to and from a gnucash book. 25 | 26 | Per default, it exports the full prices list from the gnucash book to the standard output in a CSV format. 27 | To import, add the argument "--import file_to_import.csv". 28 | 29 | The format used is a standard CSV (comma as separator) with the following columns: 30 | - date (YYYY-MM-DD) 31 | - commodity (gnucash mnemonic) 32 | - currency (gnucash mnemonic) 33 | - type (string, optional) 34 | - value (float) 35 | - type (string, optional) 36 | """, 37 | formatter_class=argparse.RawTextHelpFormatter, 38 | ) 39 | parser.add_argument("gnucash_filename", help="the name of the gnucash file") 40 | parser.add_argument( 41 | "--import", 42 | dest="operation", 43 | help="to import the prices from a csv file (default export)", 44 | ) 45 | args = parser.parse_args() 46 | 47 | # args.operation is the name of the import file (.csv). 48 | 49 | if args.operation is None: 50 | # export the prices 51 | sys.stdout.write( 52 | "date,type,value,value_num, value_denom, currency,commodity,source\n" 53 | ) 54 | with piecash.open_book(args.gnucash_filename, open_if_lock=True) as book: 55 | sys.stdout.writelines( 56 | "{p.date:%Y-%m-%d},{p.type},{p.value},{p._value_num},{p._value_denom},{p.currency.mnemonic},{p.commodity.mnemonic},{p.source}\n".format( 57 | p=p 58 | ) 59 | for p in book.prices 60 | ) 61 | else: 62 | # import the prices 63 | with piecash.open_book( 64 | args.gnucash_filename, open_if_lock=True, readonly=False 65 | ) as book: 66 | cdty = book.commodities 67 | importFile = open(args.operation, "r") 68 | 69 | for l in csv.DictReader(importFile): 70 | cur = cdty(mnemonic=l["currency"]) 71 | com = cdty(mnemonic=l["commodity"]) 72 | type = l.get("type", None) 73 | date = datetime.strptime(l["date"], "%Y-%m-%d") 74 | v = Decimal(l["value"]) 75 | Price( 76 | currency=cur, 77 | commodity=com, 78 | date=date, 79 | value=v, 80 | source="piecash-importer", 81 | ) 82 | book.save() 83 | -------------------------------------------------------------------------------- /piecash/scripts/qif_export.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | """Basic script to export QIF. Heavily untested ...""" 3 | import sys 4 | 5 | # https://github.com/jemmyw/Qif/blob/master/QIF_references 6 | 7 | import click 8 | 9 | from piecash.scripts.cli import cli 10 | 11 | 12 | @cli.command() 13 | @click.argument("book", type=click.Path(exists=True)) 14 | @click.option( 15 | "--output", 16 | type=click.File("w"), 17 | default="-", 18 | help="File to which to export the data (default=stdout)", 19 | ) 20 | def qif(book, output): 21 | """Export to QIF format. 22 | 23 | This scripts export a GnuCash BOOK to the QIF format. 24 | """ 25 | try: 26 | import qifparse.qif as _qif 27 | except ImportError: 28 | _qif = None 29 | print("You need to install the qifparse module ('pip install qifparse')") 30 | sys.exit() 31 | 32 | import piecash 33 | 34 | map_gnc2qif = { 35 | "CASH": "Cash", 36 | "BANK": "Bank", 37 | "RECEIVABLE": "Bank", 38 | "PAYABLE": "Ccard", 39 | "MUTUAL": "Bank", 40 | "CREDIT": "Ccard", 41 | "ASSET": "Oth A", 42 | "LIABILITY": "Oth L", 43 | "TRADING": "Oth L", 44 | # 'Invoice', # Quicken for business only 45 | "STOCK": "Invst", 46 | } 47 | 48 | with piecash.open_book(book, open_if_lock=True) as s: 49 | b = _qif.Qif() 50 | map = {} 51 | for acc in s.accounts: 52 | if acc.parent == s.book.root_template: 53 | continue 54 | elif acc.type in ["INCOME", "EXPENSE", "EQUITY"]: 55 | item = _qif.Category( 56 | name=acc.fullname, 57 | description=acc.description, 58 | expense=acc.type == "EXPENSE", 59 | income=acc.type == "INCOME" or acc.type == "EQUITY", 60 | ) 61 | b.add_category(item) 62 | elif acc.type in map_gnc2qif: 63 | actype = map_gnc2qif[acc.type] 64 | if actype == "Invst": 65 | # take parent account 66 | item = _qif.Account(name=acc.fullname, account_type=actype) 67 | else: 68 | item = _qif.Account(name=acc.fullname, account_type=actype) 69 | b.add_account(item) 70 | else: 71 | print("unknow {} for {}".format(acc.type, acc.fullname)) 72 | 73 | map[acc.fullname] = item 74 | 75 | # print(str(b)) 76 | def split_type(sp): 77 | qif_obj = map[sp.account.fullname] 78 | if isinstance(qif_obj, _qif.Account): 79 | return qif_obj.account_type 80 | else: 81 | return "Expense" if qif_obj.expense else "Income" 82 | 83 | def sort_split(sp): 84 | type = split_type(sp) 85 | if type == "Invst": 86 | return 1 87 | elif type in ["Expense", "Income"]: 88 | return 2 89 | else: 90 | return 0 91 | 92 | tpl = s.book.root_template 93 | 94 | for tr in s.transactions: 95 | if not tr.splits or len(tr.splits) < 2: 96 | continue # split empty transactions 97 | if tr.splits[0].account.parent == tpl: 98 | continue # skip template transactions 99 | 100 | splits = sorted(tr.splits, key=sort_split) 101 | if all(sp.account.commodity.is_currency() for sp in splits): 102 | 103 | sp1, sp2 = splits[:2] 104 | item = _qif.Transaction( 105 | date=tr.post_date, 106 | num=tr.num, 107 | payee=tr.description, 108 | amount=sp1.value, 109 | memo=sp1.memo, 110 | ) 111 | if isinstance(map[sp2.account.fullname], _qif.Account): 112 | item.to_account = sp2.account.fullname 113 | else: 114 | item.category = sp2.account.fullname 115 | 116 | if len(splits) > 2: 117 | for sp in splits[1:]: 118 | if isinstance(map[sp.account.fullname], _qif.Account): 119 | asp = _qif.AmountSplit( 120 | amount=-sp.value, 121 | memo=sp.memo, 122 | to_account=sp.account.fullname, 123 | ) 124 | else: 125 | asp = _qif.AmountSplit( 126 | amount=-sp.value, 127 | memo=sp.memo, 128 | category=sp.account.fullname, 129 | ) 130 | item.splits.append(asp) 131 | map[sp1.account.fullname].add_transaction( 132 | item, 133 | header="!Type:{}".format(map[sp1.account.fullname].account_type), 134 | ) 135 | else: 136 | # match pattern of splits for an investment 137 | 138 | sp_account, sp_security, sp_others = splits[0], splits[1], splits[2:] 139 | 140 | assert split_type(sp_account) in ["Bank", "Cash"] 141 | assert split_type(sp_security) in ["Invst"] 142 | assert all(split_type(sp) == "Expense" for sp in sp_others) 143 | assert ( 144 | sp_security.account.parent.type == "BANK" 145 | ), "Security account has no parent STOCK account (aka a Brokerage account)" 146 | item = _qif.Investment( 147 | date=tr.post_date, 148 | action="Buy" if sp_security.quantity > 0 else "Sell", 149 | security=sp_security.account.commodity.mnemonic, 150 | price=sp_security.value / sp_security.quantity, 151 | quantity=sp_security.quantity, 152 | amount=sp_security.value, 153 | commission=sum(sp.value for sp in sp_others), 154 | first_line=tr.description, 155 | to_account=sp_account.account.fullname, 156 | amount_transfer=abs(sp_account.value), 157 | ) 158 | map[sp_security.account.fullname].add_transaction( 159 | item, header="!Type:{}".format(split_type(sp_security)) 160 | ) 161 | 162 | output.write(str(b)) 163 | -------------------------------------------------------------------------------- /piecash/scripts/sql_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sqlite3 3 | 4 | import click 5 | 6 | import piecash 7 | from piecash.scripts.cli import cli 8 | 9 | 10 | @cli.command() 11 | @click.argument("book", type=click.Path(exists=True)) 12 | @click.option( 13 | "--output", 14 | type=click.File("w"), 15 | default=None, 16 | help="File to which to dump the sql schema (default=stdout)", 17 | ) 18 | def sql_dump(book, output): 19 | """Dump SQL schema of the gnucash sqlite book""" 20 | 21 | if output is None: 22 | output = open(book + ".sql", "w", encoding="UTF-8") 23 | 24 | con = sqlite3.connect(book) 25 | for line in con.iterdump(): 26 | output.write("%s\n" % line.replace("VARCHAR(", "TEXT(")) 27 | 28 | output.close() 29 | 30 | 31 | @cli.command() 32 | @click.argument("book", type=click.Path(exists=False)) 33 | def sql_create(book): 34 | """Create an empty book with gnucash""" 35 | with piecash.create_book(book, overwrite=True, keep_foreign_keys=False) as b: 36 | b.save() 37 | -------------------------------------------------------------------------------- /piecash/yahoo_client.py: -------------------------------------------------------------------------------- 1 | """Wrapper around Yahoo Finance API 2 | https://query1.finance.yahoo.com/v7/finance/download/aapl?period1=1487685205&period2=1495457605&interval=1d&events=history&crumb=dU2hYSfAy9E 3 | https://query1.finance.yahoo.com/v7/finance/quote?symbols=TLS.AX,MUS.AX 4 | https://help.yahoo.com/kb/SLN2310.html 5 | """ 6 | import csv 7 | import datetime 8 | import logging 9 | from collections import namedtuple 10 | from decimal import Decimal 11 | from time import sleep 12 | 13 | import pytz 14 | 15 | MAX_ATTEMPT = 5 16 | 17 | YAHOO_BASE_URL = "https://query1.finance.yahoo.com/v7/finance" 18 | 19 | YahooSymbol = namedtuple( 20 | "YahooSymbol", "name,symbol,exchange,timezone,currency,date,price" 21 | ) 22 | YahooQuote = namedtuple("YahooQuote", "date,open,high,low,close,adj_close,volume") 23 | 24 | 25 | def get_latest_quote(symbol): 26 | import requests 27 | 28 | resp = requests.get("{}/quote".format(YAHOO_BASE_URL), 29 | params={"symbols": symbol}, 30 | headers={"user-agent": ""}) 31 | resp.raise_for_status() 32 | 33 | try: 34 | data = resp.json()["quoteResponse"]["result"][0] 35 | except IndexError: 36 | from .core.commodity import GncCommodityError 37 | 38 | raise GncCommodityError( 39 | "Can't find information on symbol '{}' on yahoo".format(symbol) 40 | ) 41 | 42 | tz = data["exchangeTimezoneName"] 43 | 44 | return YahooSymbol( 45 | data["longName"], 46 | data["symbol"], 47 | data["exchange"], 48 | tz, 49 | data["currency"], 50 | datetime.datetime.fromtimestamp(data["regularMarketTime"]).astimezone( 51 | pytz.timezone(tz) 52 | ), 53 | data["regularMarketPrice"], 54 | ) 55 | 56 | 57 | quote_link = "https://query1.finance.yahoo.com/v7/finance/download/{}?period1={}&period2={}&interval=1d&events=history" 58 | 59 | 60 | def download_quote(symbol, date_from, date_to, tz=None): 61 | import requests 62 | 63 | def normalize(d): 64 | if isinstance(d, datetime.datetime): 65 | pass 66 | elif isinstance(d, datetime.date): 67 | d = datetime.datetime.combine(d, datetime.time(0)) 68 | else: 69 | d = datetime.datetime.strptime(d, "%Y-%m-%d") 70 | if not d.tzinfo: 71 | assert tz 72 | # todo: understand yahoo behavior as even in the browser, I get 73 | # weird results ... 74 | # d = d.replace(tzinfo=tz) 75 | return d 76 | 77 | date_from = normalize(date_from) 78 | date_to = normalize(date_to) 79 | time_stamp_from = int(date_from.timestamp()) 80 | time_stamp_to = int(date_to.timestamp()) 81 | 82 | for i in range(MAX_ATTEMPT): 83 | logging.info( 84 | "{} attempt to download quotes for symbol {} from yahoo".format(i, symbol) 85 | ) 86 | 87 | link = quote_link.format(symbol, time_stamp_from, time_stamp_to) 88 | 89 | resp = requests.get(link, headers={"user-agent": ""}) 90 | try: 91 | resp.raise_for_status() 92 | except Exception as e: 93 | sleep(2) 94 | else: 95 | break 96 | else: 97 | raise e # noqa: F821 98 | 99 | csv_data = list(csv.reader(resp.text.strip().split("\n"))) 100 | for n in csv_data: 101 | if n[1:].count('null') == len(n) - 1: csv_data.remove(n) 102 | 103 | return [ 104 | yq 105 | for data in csv_data[1:] 106 | for yq in [ 107 | YahooQuote( 108 | datetime.datetime.strptime(data[0], "%Y-%m-%d").date(), 109 | *[(0 if f=='null' else Decimal(f)) for f in data[1:]] 110 | ) 111 | ] 112 | if date_from.date() <= yq.date <= date_to.date() 113 | ] 114 | 115 | 116 | if __name__ == "__main__": 117 | print(get_latest_quote("KO")) 118 | 119 | print( 120 | download_quote("ENGI.PA", "2018-02-26", "2018-03-01", tz=pytz.timezone("CET")) 121 | ) 122 | 123 | if __name__ == "__main__": 124 | print(get_latest_quote("ENGI.PA")) 125 | print(get_latest_quote("AAPL")) 126 | -------------------------------------------------------------------------------- /piecash_interpreter/piecash_interpreter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import piecash 3 | 4 | 5 | if sys.version_info.major == 3: 6 | 7 | def run_file(fname): 8 | with open(fname) as f: 9 | code = compile(f.read(), fname, "exec") 10 | exec(code, {}) 11 | 12 | 13 | else: 14 | 15 | def run_file(fname): 16 | return execfile(fname, {}) 17 | 18 | 19 | if len(sys.argv) == 1: 20 | print("Specify as argument the path to the script to run") 21 | sys.exit() 22 | 23 | file = sys.argv.pop(1) 24 | run_file(file) 25 | -------------------------------------------------------------------------------- /piecash_interpreter/piecash_interpreter.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | a = Analysis(['piecash_interpreter.py'], 3 | pathex=['C:\\Users\\sdementen\\Projects\\piecash-dev'], 4 | hiddenimports=['piecash'], 5 | hookspath=None, 6 | runtime_hooks=None) 7 | pyz = PYZ(a.pure) 8 | exe = EXE(pyz, 9 | a.scripts, 10 | a.binaries, 11 | a.zipfiles, 12 | a.datas, 13 | name='piecash_interpreter.exe', 14 | debug=False, 15 | strip=None, 16 | upx=True, 17 | console=True ) 18 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | appdirs==1.4.4 3 | atomicwrites==1.4.0 4 | attrs==20.2.0 5 | Babel==2.8.0 6 | backcall==0.2.0 7 | certifi==2023.7.22 8 | chardet==3.0.4 9 | click==7.1.2 10 | colorama==0.4.4 11 | coverage==5.3 12 | decorator==4.4.2 13 | distlib==0.3.1 14 | docutils==0.16 15 | filelock==3.0.12 16 | idna==3.7 17 | imagesize==1.2.0 18 | importlib-metadata==2.0.0 19 | importlib-resources==3.0.0 20 | iniconfig==1.1.1 21 | ipython==7.16.1 22 | ipython-genutils==0.2.0 23 | jedi==0.17.2 24 | Jinja2==3.1.4 25 | MarkupSafe==1.1.1 26 | numpy==1.22.0 27 | packaging==20.4 28 | pandas==1.1.3 29 | parso==0.7.1 30 | pickleshare==0.7.5 31 | pipenv==2020.8.13 32 | pluggy==0.13.1 33 | pockets==0.9.1 34 | prompt-toolkit==3.0.8 35 | psycopg2==2.8.6 36 | py==1.9.0 37 | Pygments==2.15.0 38 | PyMySQL==1.1.1 39 | pyparsing==2.4.7 40 | pytest==6.1.1 41 | pytest-cov==2.10.1 42 | python-dateutil==2.8.1 43 | pytz==2020.1 44 | qifparse==0.5 45 | requests==2.32.2 46 | six==1.15.0 47 | snowballstemmer==2.0.0 48 | Sphinx==3.2.1 49 | sphinx-rtd-theme==0.5.0 50 | sphinxcontrib-applehelp==1.0.2 51 | sphinxcontrib-devhelp==1.0.2 52 | sphinxcontrib-htmlhelp==1.0.3 53 | sphinxcontrib-jsmath==1.0.1 54 | sphinxcontrib-programoutput==0.16 55 | sphinxcontrib-qthelp==1.0.3 56 | sphinxcontrib-serializinghtml==1.1.4 57 | SQLAlchemy==1.3.20 58 | SQLAlchemy-Utils==0.36.8 59 | toml==0.10.1 60 | tox==3.20.1 61 | tox-pipenv==1.10.1 62 | traitlets==4.3.3 63 | tzlocal==2.1 64 | urllib3==1.26.19 65 | virtualenv==20.0.35 66 | virtualenv-clone==0.5.4 67 | wcwidth==0.2.5 68 | zipp==3.3.1 69 | -------------------------------------------------------------------------------- /tests/books/all-accounts.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/all-accounts.gnucash -------------------------------------------------------------------------------- /tests/books/complex_sample.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/complex_sample.gnucash -------------------------------------------------------------------------------- /tests/books/default_3_0_0_basic.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/default_3_0_0_basic.gnucash -------------------------------------------------------------------------------- /tests/books/default_3_0_0_full_options.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/default_3_0_0_full_options.gnucash -------------------------------------------------------------------------------- /tests/books/example_file.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/example_file.gnucash -------------------------------------------------------------------------------- /tests/books/foo.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/foo.gnucash -------------------------------------------------------------------------------- /tests/books/ghost_kvp_scheduled_transaction.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/ghost_kvp_scheduled_transaction.gnucash -------------------------------------------------------------------------------- /tests/books/investment.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/investment.gnucash -------------------------------------------------------------------------------- /tests/books/invoices.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/invoices.gnucash -------------------------------------------------------------------------------- /tests/books/simple_sample.272.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/simple_sample.272.gnucash -------------------------------------------------------------------------------- /tests/books/simple_sample.gnucash: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/books/simple_sample.gnucash -------------------------------------------------------------------------------- /tests/references/file_template_full.commodity_notes_True.ledger: -------------------------------------------------------------------------------- 1 | commodity ADF 2 | note Andorran Franc 3 | 4 | commodity AED 5 | note UAE Dirham 6 | 7 | commodity EUR 8 | note Euro 9 | 10 | commodity FOO 11 | note Foo Inc 12 | 13 | commodity USD 14 | note US Dollar 15 | 16 | account Asset 17 | check commodity == "EUR" 18 | 19 | account Asset:Current 20 | check commodity == "EUR" 21 | 22 | account Asset:Current:Cash 23 | check commodity == "EUR" 24 | 25 | account Asset:Current:Savings 26 | check commodity == "EUR" 27 | 28 | account Asset:Current:Checking 29 | check commodity == "EUR" 30 | 31 | account Asset:Fixed 32 | check commodity == "EUR" 33 | 34 | account Asset:Fixed:House 35 | check commodity == "EUR" 36 | 37 | account Asset:Broker 38 | check commodity == "AED" 39 | 40 | account Asset:Broker:Foo stock 41 | 42 | account Liability 43 | check commodity == "EUR" 44 | 45 | account Income 46 | check commodity == "EUR" 47 | 48 | account Expense 49 | check commodity == "EUR" 50 | 51 | account Equity 52 | check commodity == "EUR" 53 | 54 | account Equity:Opening Balances - EUR 55 | check commodity == "EUR" 56 | 57 | account Mouvements 58 | 59 | account Mouvements:NASDAQ 60 | 61 | account Mouvements:NASDAQ:FOO 62 | 63 | account Mouvements:CURRENCY 64 | check commodity == "EUR" 65 | 66 | account Mouvements:CURRENCY:EUR5 67 | check commodity == "EUR" 68 | 69 | P 2018-02-21 00:00:00 FOO AED 0.90 70 | P 2018-02-21 00:00:00 USD EUR 1.40 71 | P 2018-02-21 00:00:00 EUR AED 1.111111111111111111111111111 72 | P 2018-02-21 00:00:00 ADF AED 1.50 73 | 74 | 2014-12-24 income 1 75 | Income EUR -150.00 76 | Asset:Current:Cash EUR 150.00 77 | 78 | 2014-12-24 initial load 79 | Liability EUR -1,000.00 80 | Asset:Current:Checking EUR 1,000.00 81 | 82 | 2014-12-24 expense 1 83 | Asset:Current:Checking EUR -200.00 84 | Expense EUR 200.00 85 | 86 | 2014-12-24 loan payment 87 | Asset:Current:Checking EUR -130.00 ; monthly payment 88 | Expense EUR 30.00 ; interest 89 | Liability EUR 100.00 ; capital 90 | 91 | 2018-02-20 Transfer current 92 | Asset:Current:Checking EUR -100.00 93 | Asset:Current:Cash EUR 100.00 94 | 95 | 2018-02-20 Purchase 96 | Asset:Current:Cash EUR -30.00 97 | Expense EUR 30.00 98 | 99 | 2018-02-20 transfer intra 100 | Asset:Current:Savings EUR -250.00 101 | Asset:Current:Checking EUR 250.00 102 | 103 | 2018-02-20 Opening Balance 104 | Equity:Opening Balances - EUR EUR -2,500.00 105 | Asset:Current:Savings EUR 2,500.00 106 | 107 | 2018-02-20 house load 108 | Liability EUR -20,000.00 109 | Asset:Fixed:House EUR 20,000.00 110 | 111 | 2018-02-21 buy foo 112 | Mouvements:NASDAQ:FOO FOO -130.0000 @@ EUR 1,200.00 113 | Asset:Current:Savings EUR -1,200.00 114 | Asset:Broker:Foo stock FOO 130.0000 @@ EUR 1,200.00 115 | Mouvements:CURRENCY:EUR5 EUR 1,200.00 116 | 117 | 2018-02-21 Opening Balance 118 | Equity:Opening Balances - EUR EUR -2,500.00 119 | Asset:Current:Savings EUR 2,500.00 120 | 121 | -------------------------------------------------------------------------------- /tests/references/file_template_full.ledger: -------------------------------------------------------------------------------- 1 | commodity ADF 2 | 3 | commodity AED 4 | 5 | commodity EUR 6 | 7 | commodity FOO 8 | 9 | commodity USD 10 | 11 | account Asset 12 | check commodity == "EUR" 13 | 14 | account Asset:Current 15 | check commodity == "EUR" 16 | 17 | account Asset:Current:Cash 18 | check commodity == "EUR" 19 | 20 | account Asset:Current:Savings 21 | check commodity == "EUR" 22 | 23 | account Asset:Current:Checking 24 | check commodity == "EUR" 25 | 26 | account Asset:Fixed 27 | check commodity == "EUR" 28 | 29 | account Asset:Fixed:House 30 | check commodity == "EUR" 31 | 32 | account Asset:Broker 33 | check commodity == "AED" 34 | 35 | account Asset:Broker:Foo stock 36 | 37 | account Liability 38 | check commodity == "EUR" 39 | 40 | account Income 41 | check commodity == "EUR" 42 | 43 | account Expense 44 | check commodity == "EUR" 45 | 46 | account Equity 47 | check commodity == "EUR" 48 | 49 | account Equity:Opening Balances - EUR 50 | check commodity == "EUR" 51 | 52 | account Mouvements 53 | 54 | account Mouvements:NASDAQ 55 | 56 | account Mouvements:NASDAQ:FOO 57 | 58 | account Mouvements:CURRENCY 59 | check commodity == "EUR" 60 | 61 | account Mouvements:CURRENCY:EUR5 62 | check commodity == "EUR" 63 | 64 | P 2018-02-21 00:00:00 FOO AED 0.90 65 | P 2018-02-21 00:00:00 USD EUR 1.40 66 | P 2018-02-21 00:00:00 EUR AED 1.111111111111111111111111111 67 | P 2018-02-21 00:00:00 ADF AED 1.50 68 | 69 | 2014-12-24 income 1 70 | Income EUR -150.00 71 | Asset:Current:Cash EUR 150.00 72 | 73 | 2014-12-24 initial load 74 | Liability EUR -1,000.00 75 | Asset:Current:Checking EUR 1,000.00 76 | 77 | 2014-12-24 expense 1 78 | Asset:Current:Checking EUR -200.00 79 | Expense EUR 200.00 80 | 81 | 2014-12-24 loan payment 82 | Asset:Current:Checking EUR -130.00 ; monthly payment 83 | Expense EUR 30.00 ; interest 84 | Liability EUR 100.00 ; capital 85 | 86 | 2018-02-20 Transfer current 87 | Asset:Current:Checking EUR -100.00 88 | Asset:Current:Cash EUR 100.00 89 | 90 | 2018-02-20 Purchase 91 | Asset:Current:Cash EUR -30.00 92 | Expense EUR 30.00 93 | 94 | 2018-02-20 transfer intra 95 | Asset:Current:Savings EUR -250.00 96 | Asset:Current:Checking EUR 250.00 97 | 98 | 2018-02-20 Opening Balance 99 | Equity:Opening Balances - EUR EUR -2,500.00 100 | Asset:Current:Savings EUR 2,500.00 101 | 102 | 2018-02-20 house load 103 | Liability EUR -20,000.00 104 | Asset:Fixed:House EUR 20,000.00 105 | 106 | 2018-02-21 buy foo 107 | Mouvements:NASDAQ:FOO FOO -130.0000 @@ EUR 1,200.00 108 | Asset:Current:Savings EUR -1,200.00 109 | Asset:Broker:Foo stock FOO 130.0000 @@ EUR 1,200.00 110 | Mouvements:CURRENCY:EUR5 EUR 1,200.00 111 | 112 | 2018-02-21 Opening Balance 113 | Equity:Opening Balances - EUR EUR -2,500.00 114 | Asset:Current:Savings EUR 2,500.00 115 | 116 | -------------------------------------------------------------------------------- /tests/references/file_template_full.locale_True.ledger: -------------------------------------------------------------------------------- 1 | commodity ADF 2 | 3 | commodity AED 4 | 5 | commodity € 6 | 7 | commodity FOO 8 | 9 | commodity US$ 10 | 11 | account Asset 12 | check commodity == "€" 13 | 14 | account Asset:Current 15 | check commodity == "€" 16 | 17 | account Asset:Current:Cash 18 | check commodity == "€" 19 | 20 | account Asset:Current:Savings 21 | check commodity == "€" 22 | 23 | account Asset:Current:Checking 24 | check commodity == "€" 25 | 26 | account Asset:Fixed 27 | check commodity == "€" 28 | 29 | account Asset:Fixed:House 30 | check commodity == "€" 31 | 32 | account Asset:Broker 33 | check commodity == "AED" 34 | 35 | account Asset:Broker:Foo stock 36 | 37 | account Liability 38 | check commodity == "€" 39 | 40 | account Income 41 | check commodity == "€" 42 | 43 | account Expense 44 | check commodity == "€" 45 | 46 | account Equity 47 | check commodity == "€" 48 | 49 | account Equity:Opening Balances - EUR 50 | check commodity == "€" 51 | 52 | account Mouvements 53 | 54 | account Mouvements:NASDAQ 55 | 56 | account Mouvements:NASDAQ:FOO 57 | 58 | account Mouvements:CURRENCY 59 | check commodity == "€" 60 | 61 | account Mouvements:CURRENCY:EUR5 62 | check commodity == "€" 63 | 64 | P 2018-02-21 00:00:00 FOO 0,90 AED 65 | P 2018-02-21 00:00:00 US$ 1,40 € 66 | P 2018-02-21 00:00:00 € 1,111111111111111111111111111 AED 67 | P 2018-02-21 00:00:00 ADF 1,50 AED 68 | 69 | 2014-12-24 income 1 70 | Income -150,00 € 71 | Asset:Current:Cash 150,00 € 72 | 73 | 2014-12-24 initial load 74 | Liability -1.000,00 € 75 | Asset:Current:Checking 1.000,00 € 76 | 77 | 2014-12-24 expense 1 78 | Asset:Current:Checking -200,00 € 79 | Expense 200,00 € 80 | 81 | 2014-12-24 loan payment 82 | Asset:Current:Checking -130,00 € ; monthly payment 83 | Expense 30,00 € ; interest 84 | Liability 100,00 € ; capital 85 | 86 | 2018-02-20 Transfer current 87 | Asset:Current:Checking -100,00 € 88 | Asset:Current:Cash 100,00 € 89 | 90 | 2018-02-20 Purchase 91 | Asset:Current:Cash -30,00 € 92 | Expense 30,00 € 93 | 94 | 2018-02-20 transfer intra 95 | Asset:Current:Savings -250,00 € 96 | Asset:Current:Checking 250,00 € 97 | 98 | 2018-02-20 Opening Balance 99 | Equity:Opening Balances - EUR -2.500,00 € 100 | Asset:Current:Savings 2.500,00 € 101 | 102 | 2018-02-20 house load 103 | Liability -20.000,00 € 104 | Asset:Fixed:House 20.000,00 € 105 | 106 | 2018-02-21 buy foo 107 | Mouvements:NASDAQ:FOO -130,00 FOO @@ 1.200,00 € 108 | Asset:Current:Savings -1.200,00 € 109 | Asset:Broker:Foo stock 130,00 FOO @@ 1.200,00 € 110 | Mouvements:CURRENCY:EUR5 1.200,00 € 111 | 112 | 2018-02-21 Opening Balance 113 | Equity:Opening Balances - EUR -2.500,00 € 114 | Asset:Current:Savings 2.500,00 € 115 | 116 | -------------------------------------------------------------------------------- /tests/references/file_template_full.short_account_names_True.ledger: -------------------------------------------------------------------------------- 1 | commodity ADF 2 | 3 | commodity AED 4 | 5 | commodity EUR 6 | 7 | commodity FOO 8 | 9 | commodity USD 10 | 11 | account Asset 12 | check commodity == "EUR" 13 | 14 | account Current 15 | check commodity == "EUR" 16 | 17 | account Cash 18 | check commodity == "EUR" 19 | 20 | account Savings 21 | check commodity == "EUR" 22 | 23 | account Checking 24 | check commodity == "EUR" 25 | 26 | account Fixed 27 | check commodity == "EUR" 28 | 29 | account House 30 | check commodity == "EUR" 31 | 32 | account Broker 33 | check commodity == "AED" 34 | 35 | account Foo stock 36 | 37 | account Liability 38 | check commodity == "EUR" 39 | 40 | account Income 41 | check commodity == "EUR" 42 | 43 | account Expense 44 | check commodity == "EUR" 45 | 46 | account Equity 47 | check commodity == "EUR" 48 | 49 | account Opening Balances - EUR 50 | check commodity == "EUR" 51 | 52 | account Mouvements 53 | 54 | account NASDAQ 55 | 56 | account FOO 57 | 58 | account CURRENCY 59 | check commodity == "EUR" 60 | 61 | account EUR5 62 | check commodity == "EUR" 63 | 64 | P 2018-02-21 00:00:00 FOO AED 0.90 65 | P 2018-02-21 00:00:00 USD EUR 1.40 66 | P 2018-02-21 00:00:00 EUR AED 1.111111111111111111111111111 67 | P 2018-02-21 00:00:00 ADF AED 1.50 68 | 69 | 2014-12-24 income 1 70 | Income EUR -150.00 71 | Asset:Current:Cash EUR 150.00 72 | 73 | 2014-12-24 initial load 74 | Liability EUR -1,000.00 75 | Asset:Current:Checking EUR 1,000.00 76 | 77 | 2014-12-24 expense 1 78 | Asset:Current:Checking EUR -200.00 79 | Expense EUR 200.00 80 | 81 | 2014-12-24 loan payment 82 | Asset:Current:Checking EUR -130.00 ; monthly payment 83 | Expense EUR 30.00 ; interest 84 | Liability EUR 100.00 ; capital 85 | 86 | 2018-02-20 Transfer current 87 | Asset:Current:Checking EUR -100.00 88 | Asset:Current:Cash EUR 100.00 89 | 90 | 2018-02-20 Purchase 91 | Asset:Current:Cash EUR -30.00 92 | Expense EUR 30.00 93 | 94 | 2018-02-20 transfer intra 95 | Asset:Current:Savings EUR -250.00 96 | Asset:Current:Checking EUR 250.00 97 | 98 | 2018-02-20 Opening Balance 99 | Equity:Opening Balances - EUR EUR -2,500.00 100 | Asset:Current:Savings EUR 2,500.00 101 | 102 | 2018-02-20 house load 103 | Liability EUR -20,000.00 104 | Asset:Fixed:House EUR 20,000.00 105 | 106 | 2018-02-21 buy foo 107 | Mouvements:NASDAQ:FOO FOO -130.0000 @@ EUR 1,200.00 108 | Asset:Current:Savings EUR -1,200.00 109 | Asset:Broker:Foo stock FOO 130.0000 @@ EUR 1,200.00 110 | Mouvements:CURRENCY:EUR5 EUR 1,200.00 111 | 112 | 2018-02-21 Opening Balance 113 | Equity:Opening Balances - EUR EUR -2,500.00 114 | Asset:Current:Savings EUR 2,500.00 115 | 116 | -------------------------------------------------------------------------------- /tests/test_account.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/test_account.py -------------------------------------------------------------------------------- /tests/test_balance.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from piecash import ledger 4 | from test_helper import ( 5 | db_sqlite_uri, 6 | db_sqlite, 7 | new_book, 8 | new_book_USD, 9 | book_uri, 10 | book_transactions, 11 | book_complex, 12 | ) 13 | 14 | # dummy line to avoid removing unused symbols 15 | a = ( 16 | db_sqlite_uri, 17 | db_sqlite, 18 | new_book, 19 | new_book_USD, 20 | book_uri, 21 | book_transactions, 22 | book_complex, 23 | ) 24 | 25 | 26 | def test_get_balance(book_complex): 27 | """ 28 | Tests listing the commodity quantity in the account. 29 | """ 30 | 31 | asset = book_complex.accounts.get(name="Asset") 32 | broker = book_complex.accounts.get(name="Broker") 33 | foo_stock = book_complex.accounts.get(name="Foo stock") 34 | expense = book_complex.accounts.get(name="Expense") 35 | income = book_complex.accounts.get(name="Income") 36 | assert foo_stock.get_balance(recurse=True) == Decimal("130") 37 | assert broker.get_balance(recurse=True) == Decimal("117") 38 | assert asset.get_balance(recurse=False) == Decimal("0") 39 | assert asset.get_balance() == Decimal("24695.3") 40 | assert expense.get_balance() == Decimal("260") 41 | assert income.get_balance() == Decimal("150") 42 | assert income.get_balance(natural_sign=False) == Decimal("-150") 43 | -------------------------------------------------------------------------------- /tests/test_book.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/test_book.py -------------------------------------------------------------------------------- /tests/test_business_owner.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import unicode_literals 3 | 4 | # dummy line to avoid removing unused symbols 5 | from piecash import Employee, Job 6 | from test_helper import ( 7 | db_sqlite_uri, 8 | db_sqlite, 9 | new_book, 10 | new_book_USD, 11 | book_uri, 12 | book_basic, 13 | Person, 14 | ) 15 | 16 | a = db_sqlite_uri, db_sqlite, new_book, new_book_USD, book_uri, book_basic, Person 17 | 18 | 19 | class TestBusinessPerson_create_Person(object): 20 | """ 21 | Person is a parameter taking values in [Customer, Vendor, Employee] 22 | """ 23 | 24 | def test_create_customer_job(self, book_basic, Person): 25 | EUR = book_basic.commodities(namespace="CURRENCY") 26 | 27 | # create detached person 28 | c = Person(name="John Föo", currency=EUR) 29 | book_basic.add(c) 30 | 31 | if Person != Employee: 32 | j = Job(name="my job", owner=c) 33 | 34 | book_basic.validate() 35 | book_basic.flush() 36 | if Person != Employee: 37 | print(c.jobs) 38 | -------------------------------------------------------------------------------- /tests/test_business_person.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import unicode_literals 3 | 4 | from decimal import Decimal 5 | 6 | # dummy line to avoid removing unused symbols 7 | from piecash import Address, Employee, Account, Vendor, Customer 8 | from piecash.business import Taxtable, TaxtableEntry 9 | from test_helper import ( 10 | db_sqlite_uri, 11 | db_sqlite, 12 | new_book, 13 | new_book_USD, 14 | book_uri, 15 | book_basic, 16 | Person, 17 | ) 18 | 19 | a = db_sqlite_uri, db_sqlite, new_book, new_book_USD, book_uri, book_basic, Person 20 | 21 | 22 | class TestBusinessPerson_create_Person(object): 23 | """ 24 | Person is a parameter taking values in [Customer, Vendor, Employee] 25 | """ 26 | 27 | def test_create_person_noid_nobook(self, book_basic, Person): 28 | EUR = book_basic.commodities(namespace="CURRENCY") 29 | 30 | # create detached person 31 | c = Person(name="John Föo", currency=EUR) 32 | # id should not be set 33 | assert c.id is None 34 | 35 | # flushing should not set the id as person not added to book 36 | book_basic.flush() 37 | assert c.id is None 38 | 39 | # adding the person to the book does not per se set the id 40 | book_basic.add(c) 41 | assert c.id == "000001" 42 | # but validation sets the id if still to None 43 | assert getattr(book_basic, Person._counter_name) == 1 44 | 45 | book_basic.flush() 46 | assert c.id == "000001" 47 | assert getattr(book_basic, Person._counter_name) == 1 48 | 49 | def test_create_person_noid_inbook(self, book_basic, Person): 50 | EUR = book_basic.commodities(namespace="CURRENCY") 51 | 52 | # create person attached to book 53 | c = Person(name="John Föo", currency=EUR, book=book_basic) 54 | # id should have already been set 55 | assert c.id == "000001" 56 | 57 | # flushing should not change the id 58 | book_basic.flush() 59 | assert c.id == "000001" 60 | 61 | def test_create_person_id_inbook(self, book_basic, Person): 62 | EUR = book_basic.commodities(namespace="CURRENCY") 63 | 64 | # create person attached to book with a specific id 65 | cust_id = "éyO903" 66 | c = Person(name="John Föo", currency=EUR, book=book_basic, id=cust_id) 67 | # id should have already been set 68 | assert c.id == cust_id 69 | 70 | # flushing should not change the id 71 | book_basic.flush() 72 | assert c.id == cust_id 73 | 74 | def test_create_person_id_nobook(self, book_basic, Person): 75 | EUR = book_basic.commodities(namespace="CURRENCY") 76 | 77 | # create person detached from book with a specific id 78 | cust_id = "éyO903" 79 | c = Person(name="John Föo", currency=EUR, id=cust_id) 80 | # id should have already been set 81 | assert c.id == cust_id 82 | 83 | # flushing should not change the id (as the person is not yet attached to book) 84 | book_basic.flush() 85 | assert c.id == cust_id 86 | 87 | # adding the person to the book and flushing should not change the id 88 | book_basic.add(c) 89 | assert c.id == cust_id 90 | book_basic.flush() 91 | assert c.id == cust_id 92 | 93 | def test_create_person_address(self, book_basic, Person): 94 | EUR = book_basic.commodities(namespace="CURRENCY") 95 | 96 | # create person detached from book with a specific id 97 | addr = Address(name="Héllo", addr1="kap", email="foo@example.com") 98 | c = Person(name="John Föo", currency=EUR, address=addr, book=book_basic) 99 | 100 | assert c.addr_addr1 == "kap" 101 | assert c.address.addr1 == "kap" 102 | 103 | addr.addr1 = "pok" 104 | c2 = Person(name="Jané Döo", currency=EUR, address=addr, book=book_basic) 105 | book_basic.flush() 106 | 107 | assert c.addr_addr1 == "kap" 108 | assert c.address.addr1 == "kap" 109 | assert c2.addr_addr1 == "pok" 110 | assert c2.address.addr1 == "pok" 111 | 112 | def test_create_person_taxtabme(self, book_basic, Person): 113 | if Person is Employee: 114 | return 115 | 116 | EUR = book_basic.commodities(namespace="CURRENCY") 117 | 118 | # create person detached from book with a specific id 119 | taxtable = Taxtable( 120 | name="Local tax", 121 | entries=[ 122 | TaxtableEntry( 123 | type="percentage", 124 | amount=Decimal("6.5"), 125 | account=Account( 126 | name="MyAcc", 127 | parent=book_basic.root_account, 128 | commodity=EUR, 129 | type="ASSET", 130 | ), 131 | ) 132 | ], 133 | ) 134 | te = TaxtableEntry( 135 | type="percentage", 136 | amount=Decimal("6.5"), 137 | account=Account( 138 | name="MyOtherAcc", 139 | parent=book_basic.root_account, 140 | commodity=EUR, 141 | type="ASSET", 142 | ), 143 | taxtable=taxtable, 144 | ) 145 | 146 | c = Person(name="John Föo", currency=EUR, taxtable=taxtable, book=book_basic) 147 | assert len(taxtable.entries) == 2 148 | assert taxtable.entries[0].account.parent == book_basic.root_account 149 | 150 | book_basic.flush() 151 | 152 | assert book_basic.taxtables == [taxtable] 153 | -------------------------------------------------------------------------------- /tests/test_factories.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import unicode_literals 3 | 4 | from datetime import datetime 5 | from decimal import Decimal 6 | 7 | import pytest 8 | import pytz 9 | import tzlocal 10 | 11 | from piecash import GnucashException, Commodity 12 | from piecash.core import factories 13 | from test_helper import ( 14 | db_sqlite_uri, 15 | db_sqlite, 16 | new_book, 17 | new_book_USD, 18 | book_uri, 19 | book_basic, 20 | needweb, 21 | ) 22 | 23 | # dummy line to avoid removing unused symbols 24 | 25 | a = db_sqlite_uri, db_sqlite, new_book, new_book_USD, book_uri, book_basic 26 | 27 | 28 | class TestFactoriesCommodities(object): 29 | def test_create_stock_accounts_simple(self, book_basic): 30 | with pytest.raises(GnucashException): 31 | factories.create_stock_accounts( 32 | book_basic.default_currency, 33 | broker_account=book_basic.accounts(name="broker"), 34 | ) 35 | 36 | broker = book_basic.accounts(name="broker") 37 | appl = Commodity(namespace="NMS", mnemonic="AAPL", fullname="Apple") 38 | acc, inc_accounts = factories.create_stock_accounts(appl, broker_account=broker) 39 | 40 | assert inc_accounts == [] 41 | assert broker.children == [acc] 42 | 43 | def test_create_stock_accounts_incomeaccounts(self, book_basic): 44 | broker = book_basic.accounts(name="broker") 45 | income = book_basic.accounts(name="inc") 46 | 47 | appl = Commodity(namespace="NMS", mnemonic="AAPL", fullname="Apple") 48 | appl["quoted_currency"] = "USD" 49 | acc, inc_accounts = factories.create_stock_accounts( 50 | appl, broker_account=broker, income_account=income, income_account_types="D" 51 | ) 52 | assert len(inc_accounts) == 1 53 | 54 | acc, inc_accounts = factories.create_stock_accounts( 55 | appl, 56 | broker_account=broker, 57 | income_account=income, 58 | income_account_types="CL", 59 | ) 60 | assert len(inc_accounts) == 1 61 | acc, inc_accounts = factories.create_stock_accounts( 62 | appl, 63 | broker_account=broker, 64 | income_account=income, 65 | income_account_types="CS", 66 | ) 67 | assert len(inc_accounts) == 1 68 | acc, inc_accounts = factories.create_stock_accounts( 69 | appl, broker_account=broker, income_account=income, income_account_types="I" 70 | ) 71 | assert len(inc_accounts) == 1 72 | acc, inc_accounts = factories.create_stock_accounts( 73 | appl, 74 | broker_account=broker, 75 | income_account=income, 76 | income_account_types="D/CL/CS/I", 77 | ) 78 | assert len(income.children) == 4 79 | book_basic.flush() 80 | assert sorted(income.children, key=lambda x: x.guid) == sorted( 81 | [_acc.parent for _acc in inc_accounts], key=lambda x: x.guid 82 | ) 83 | assert broker.children == [acc] 84 | 85 | @needweb 86 | def test_create_stock_from_symbol(self, book_basic): 87 | assert len(book_basic.commodities) == 2 88 | 89 | factories.create_stock_from_symbol("AAPL", book_basic) 90 | 91 | assert len(book_basic.commodities) == 3 92 | 93 | cdty = book_basic.commodities(mnemonic="AAPL") 94 | 95 | assert cdty.namespace == "NMS" 96 | assert cdty.quote_tz == "America/New_York" 97 | assert cdty.quote_source == "yahoo" 98 | assert cdty.mnemonic == "AAPL" 99 | assert cdty.fullname == "Apple Inc." 100 | 101 | def test_create_currency_from_ISO(self, book_basic): 102 | assert factories.create_currency_from_ISO("CAD").fullname == "Canadian Dollar" 103 | 104 | with pytest.raises(ValueError): 105 | factories.create_currency_from_ISO("EFR").fullname 106 | 107 | 108 | class TestFactoriesTransactions(object): 109 | def test_single_transaction(self, book_basic): 110 | today = datetime.today() 111 | print("today=", today) 112 | factories.single_transaction( 113 | today.date(), 114 | today, 115 | "my test", 116 | Decimal(100), 117 | from_account=book_basic.accounts(name="inc"), 118 | to_account=book_basic.accounts(name="asset"), 119 | ) 120 | book_basic.save() 121 | tr = book_basic.transactions(description="my test") 122 | assert len(tr.splits) == 2 123 | sp1, sp2 = tr.splits 124 | if sp1.value > 0: 125 | sp2, sp1 = sp1, sp2 126 | # sp1 has negative value 127 | assert sp1.account == book_basic.accounts(name="inc") 128 | assert sp2.account == book_basic.accounts(name="asset") 129 | assert sp1.value == -sp2.value 130 | assert sp1.quantity == sp1.value 131 | assert tr.enter_date == pytz.timezone(str(tzlocal.get_localzone())).localize( 132 | today.replace(microsecond=0) 133 | ) 134 | assert tr.post_date == pytz.timezone(str(tzlocal.get_localzone())).localize(today).date() 135 | 136 | def test_single_transaction_tz(self, book_basic): 137 | today = pytz.timezone(str(tzlocal.get_localzone())).localize(datetime.today()) 138 | tr = factories.single_transaction( 139 | today.date(), 140 | today, 141 | "my test", 142 | Decimal(100), 143 | from_account=book_basic.accounts(name="inc"), 144 | to_account=book_basic.accounts(name="asset"), 145 | ) 146 | book_basic.save() 147 | tr = book_basic.transactions(description="my test") 148 | assert tr.post_date == today.date() 149 | assert tr.enter_date == today.replace(microsecond=0) 150 | 151 | def test_single_transaction_rollback(self, book_basic): 152 | today = pytz.timezone(str(tzlocal.get_localzone())).localize(datetime.today()) 153 | factories.single_transaction( 154 | today.date(), 155 | today, 156 | "my test", 157 | Decimal(100), 158 | from_account=book_basic.accounts(name="inc"), 159 | to_account=book_basic.accounts(name="asset"), 160 | ) 161 | book_basic.validate() 162 | assert len(book_basic.transactions) == 1 163 | book_basic.cancel() 164 | assert len(book_basic.transactions) == 0 165 | -------------------------------------------------------------------------------- /tests/test_helper.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdementen/piecash/4f2142ce401cfc8a59ef32ed8efd9e7124333f60/tests/test_helper.py -------------------------------------------------------------------------------- /tests/test_invoice.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import unicode_literals 3 | 4 | from decimal import Decimal 5 | 6 | from test_helper import ( 7 | db_sqlite_uri, 8 | db_sqlite, 9 | new_book, 10 | new_book_USD, 11 | book_uri, 12 | book_invoices, 13 | Person, 14 | ) 15 | 16 | # dummy line to avoid removing unused symbols 17 | from piecash import Address, Employee, Account, Invoice 18 | from piecash.business import Taxtable, TaxtableEntry 19 | 20 | a = db_sqlite_uri, db_sqlite, new_book, new_book_USD, book_uri, book_invoices, Person 21 | 22 | 23 | class TestInvoice(object): 24 | """ 25 | Person is a parameter taking values in [Customer, Vendor, Employee] 26 | """ 27 | 28 | def test_create_person_noid_nobook(self, book_invoices): 29 | assert len(book_invoices.invoices) == 1 30 | invoice = book_invoices.invoices[0] 31 | assert invoice.charge_amt == 0 32 | assert len(invoice.entries) == 2 33 | entry = invoice.entries[0] 34 | assert entry.quantity == Decimal("25") 35 | assert entry.invoice == invoice 36 | -------------------------------------------------------------------------------- /tests/test_ledger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | import piecash 8 | from test_helper import book_complex 9 | 10 | # dummy line to avoid removing unused symbols 11 | a = book_complex 12 | 13 | REFERENCE = Path(__file__).parent / "references" 14 | 15 | 16 | @pytest.mark.parametrize( 17 | "options", 18 | [ 19 | dict(), 20 | dict(locale=True), 21 | dict(commodity_notes=True), 22 | dict(short_account_names=True), 23 | ], 24 | ) 25 | def test_out_write(book_complex, options): 26 | ledger_output = piecash.ledger(book_complex, **options) 27 | 28 | file_name = ( 29 | "file_template_full" 30 | + "".join(f".{k}_{v}" for k, v in options.items()) 31 | + ".ledger" 32 | ) 33 | 34 | # to generate the first time the expected output of the test 35 | (REFERENCE / file_name).write_text(ledger_output, encoding="utf-8") 36 | 37 | assert ledger_output == (REFERENCE / file_name).read_text(encoding="utf-8") 38 | 39 | 40 | def test_short_account_names_raise_error_when_duplicate_account_names(book_complex): 41 | # no exception 42 | piecash.ledger(book_complex, short_account_names=True) 43 | 44 | # exception as two accounts have the same short name 45 | book_complex.accounts[0].name = book_complex.accounts[1].name 46 | book_complex.flush() 47 | with pytest.raises( 48 | ValueError, 49 | match="You have duplicate short names in your book. " 50 | "You cannot use the 'short_account_names' option.", 51 | ): 52 | piecash.ledger(book_complex, short_account_names=True) 53 | -------------------------------------------------------------------------------- /tests/test_model_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import collections 3 | import datetime 4 | 5 | import pytest 6 | import pytz 7 | from sqlalchemy import create_engine, Column, TEXT 8 | from sqlalchemy.orm import sessionmaker, composite 9 | 10 | import piecash._common as mc 11 | from piecash._declbase import DeclarativeBaseGuid 12 | from piecash.business.person import Address 13 | from piecash.sa_extra import _Date, _DateTime 14 | 15 | 16 | def session(): 17 | engine = create_engine("sqlite://") 18 | 19 | metadata = mc.DeclarativeBase.metadata 20 | metadata.bind = engine 21 | metadata.create_all() 22 | 23 | s = sessionmaker(bind=engine)() 24 | 25 | return s 26 | 27 | 28 | class TestModelCommon(object): 29 | # @parametrize('helparg', ['-h', '--help']) 30 | def test_guid_on_declarativebase(self): 31 | class A(DeclarativeBaseGuid): 32 | __tablename__ = "a_table" 33 | 34 | def __init__(self): 35 | pass 36 | 37 | s = session() 38 | a = A() 39 | s.add(a) 40 | assert a.guid is None 41 | s.flush() 42 | assert a.guid 43 | 44 | def test_addr_composite(self): 45 | flds = "name addr1 addr2 addr3 addr4 email fax phone".split() 46 | 47 | class B(DeclarativeBaseGuid): 48 | __tablename__ = "b_table" 49 | 50 | def __init__(self, **kwargs): 51 | for k, v in kwargs.items(): 52 | setattr(self, k, v) 53 | 54 | l = [] 55 | for fld in flds: 56 | col = Column(fld, TEXT()) 57 | setattr(B, fld, col) 58 | l.append(col) 59 | B.addr = composite(Address, *l) 60 | 61 | s = session() 62 | a = B(addr1="foo") 63 | assert a.addr 64 | a.addr.fax = "baz" 65 | assert a.addr1 == "foo" 66 | assert a.addr.addr1 == "foo" 67 | assert a.addr.fax == "baz" 68 | s.add(a) 69 | s.flush() 70 | 71 | def test_date(self): 72 | class C(DeclarativeBaseGuid): 73 | __tablename__ = "c_table" 74 | day = Column(_Date) 75 | 76 | def __init__(self, day): 77 | self.day = day 78 | 79 | s = session() 80 | a = C(day=datetime.date(2010, 4, 12)) 81 | s.add(a) 82 | s.flush() 83 | assert a.day 84 | 85 | assert str(list(s.bind.execute("select day from c_table"))[0][0]) == "20100412" 86 | 87 | def test_datetime(self): 88 | class D(DeclarativeBaseGuid): 89 | __tablename__ = "d_table" 90 | time = Column(_DateTime) 91 | 92 | def __init__(self, time): 93 | self.time = time 94 | 95 | s = session() 96 | a = D(time=datetime.datetime(2010, 4, 12, 3, 4, 5, tzinfo=pytz.utc)) 97 | s.add(a) 98 | s.flush() 99 | assert a.time 100 | 101 | assert ( 102 | str(list(s.bind.execute("select time from d_table"))[0][0]) 103 | == "2010-04-12 03:04:05" 104 | ) 105 | 106 | def test_float_in_gncnumeric(self): 107 | Mock = collections.namedtuple("Mock", "name") 108 | sqlcolumn_mock = Mock("") 109 | numeric = mc.hybrid_property_gncnumeric(sqlcolumn_mock, sqlcolumn_mock) 110 | with pytest.raises(TypeError) as excinfo: 111 | numeric.fset(None, 4020.19) 112 | assert ( 113 | "Received a floating-point number 4020.19 where a decimal is expected. " 114 | + "Use a Decimal, str, or int instead" 115 | ) == str(excinfo.value) 116 | 117 | def test_weird_type_in_gncnumeric(self): 118 | Mock = collections.namedtuple("Mock", "name") 119 | sqlcolumn_mock = Mock("") 120 | numeric = mc.hybrid_property_gncnumeric(sqlcolumn_mock, sqlcolumn_mock) 121 | with pytest.raises(TypeError) as excinfo: 122 | numeric.fset(None, dict()) 123 | assert ( 124 | "Received an unknown type dict where a decimal is expected. " 125 | + "Use a Decimal, str, or int instead" 126 | ) == str(excinfo.value) 127 | -------------------------------------------------------------------------------- /tests/test_model_core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import shutil 4 | 5 | import pytest 6 | 7 | from piecash import Transaction, Commodity, open_book, create_book, Account 8 | from piecash._common import GnucashException 9 | from piecash.core.session import Version, gnclock 10 | from piecash.kvp import Slot 11 | from test_helper import file_template, file_for_test 12 | 13 | 14 | @pytest.fixture 15 | def session(request): 16 | s = create_book() 17 | return s.session 18 | 19 | 20 | @pytest.fixture 21 | def session_readonly(request): 22 | shutil.copyfile(str(file_template), str(file_for_test)) 23 | 24 | # default session is readonly 25 | s = open_book(file_for_test) 26 | 27 | @request.addfinalizer 28 | def close_s(): 29 | s.close() 30 | file_for_test.unlink() 31 | 32 | return s 33 | 34 | 35 | @pytest.fixture 36 | def book_readonly_lock(request): 37 | shutil.copyfile(str(file_template), str(file_for_test)) 38 | 39 | # default session is readonly 40 | book = open_book(file_for_test) 41 | 42 | @request.addfinalizer 43 | def close_s(): 44 | book.close() 45 | file_for_test.unlink() 46 | 47 | return book 48 | 49 | 50 | class TestModelCore_EmptyBook(object): 51 | def test_accounts(self, session): 52 | # two accounts in an empty gnucash file 53 | account_names = session.query(Account.name).all() 54 | 55 | assert set(account_names) == { 56 | (u"Template Root",), 57 | (u"Root Account",), 58 | } 59 | 60 | def test_transactions(self, session): 61 | # no transactions in an empty gnucash file 62 | transactions = session.query(Transaction).all() 63 | assert transactions == [] 64 | 65 | def test_commodities(self, session): 66 | # no commodities in an empty gnucash file 67 | commodities = session.query(Commodity.mnemonic).all() 68 | assert commodities == [("EUR",)] 69 | 70 | def test_slots(self, session): 71 | # no slots in an empty gnucash file but the default_currency 72 | slots = session.query(Slot._name).all() 73 | assert slots == [] 74 | 75 | def test_versions(self, session): 76 | # confirm versions of tables 77 | versions = session.query(Version.table_name, Version.table_version).all() 78 | assert set(versions) == { 79 | (u"Gnucash", 3000000), 80 | (u"Gnucash-Resave", 19920), 81 | (u"accounts", 1), 82 | (u"books", 1), 83 | (u"budgets", 1), 84 | (u"budget_amounts", 1), 85 | ("jobs", 1), 86 | (u"orders", 1), 87 | (u"taxtables", 2), 88 | (u"taxtable_entries", 3), 89 | (u"vendors", 1), 90 | (u"recurrences", 2), 91 | (u"slots", 4), 92 | (u"transactions", 4), 93 | (u"splits", 4), 94 | (u"lots", 2), 95 | (u"entries", 4), 96 | (u"billterms", 2), 97 | (u"invoices", 4), 98 | (u"commodities", 1), 99 | (u"schedxactions", 1), 100 | (u"prices", 3), 101 | (u"customers", 2), 102 | (u"employees", 2), 103 | } 104 | 105 | def test_readonly_true(self, session_readonly): 106 | # control exception when adding object to readonly gnucash db 107 | v = Version(table_name="sample", table_version="other sample") 108 | sa_session_readonly = session_readonly.session 109 | sa_session_readonly.add(v) 110 | with pytest.raises(GnucashException): 111 | sa_session_readonly.commit() 112 | 113 | # control exception when deleting object to readonly gnucash db 114 | sa_session_readonly.delete(session_readonly.query(Account).first()) 115 | with pytest.raises(GnucashException): 116 | sa_session_readonly.commit() 117 | 118 | # control exception when modifying object to readonly gnucash db 119 | sa_session_readonly.query(Account).first().name = "foo" 120 | with pytest.raises(GnucashException): 121 | sa_session_readonly.commit() 122 | 123 | def test_readonly_false(self, session): 124 | v = Version(table_name="fo", table_version="ok") 125 | session.add(v) 126 | assert session.flush() is None 127 | 128 | def test_lock(self, book_readonly_lock): 129 | # test that lock is not taken in readonly session 130 | locks = list(book_readonly_lock.session.execute(gnclock.select())) 131 | assert len(locks) == 0 132 | -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import unicode_literals 3 | 4 | import locale 5 | import sys 6 | from contextlib import contextmanager 7 | 8 | import pytest 9 | 10 | 11 | from piecash import create_book, Account, open_book 12 | from piecash._common import get_system_currency_mnemonic 13 | from piecash.core.session import build_uri 14 | from test_helper import ( 15 | db_sqlite_uri, 16 | db_sqlite, 17 | new_book, 18 | new_book_USD, 19 | book_uri, 20 | book_db_config, 21 | ) 22 | 23 | # dummy line to avoid removing unused symbols 24 | a = db_sqlite_uri, db_sqlite, new_book, new_book_USD, book_uri, book_db_config 25 | 26 | 27 | class TestSession_create_book(object): 28 | def test_create_default(self, book_db_config): 29 | with create_book(keep_foreign_keys=False, **book_db_config) as b: 30 | a = Account( 31 | commodity=b.currencies(mnemonic="SEK"), 32 | parent=b.root_account, 33 | name="léviö", 34 | type="ASSET", 35 | ) 36 | assert str(b.uri) == build_uri(**book_db_config) 37 | b.save() 38 | 39 | # reopen the DB except if sqlite_file is None 40 | if book_db_config.get("sqlite_file", True): 41 | with open_book(**book_db_config) as b: 42 | assert b.accounts(name="léviö").commodity == b.currencies( 43 | mnemonic="SEK" 44 | ) 45 | 46 | def test_build_uri(self): 47 | assert build_uri() == "sqlite:///:memory:" 48 | assert build_uri(sqlite_file="foo") == "sqlite:///foo" 49 | assert build_uri(uri_conn="sqlite:///foo") == "sqlite:///foo" 50 | with pytest.raises(ValueError): 51 | build_uri(db_name="f") 52 | 53 | with pytest.raises(KeyError): 54 | build_uri( 55 | db_type="pg", 56 | db_user="foo", 57 | db_password="pp", 58 | db_name="pqsd", 59 | db_host="qsdqs", 60 | db_port=3434, 61 | ) 62 | 63 | assert ( 64 | build_uri( 65 | db_type="postgres", 66 | db_user="foo", 67 | db_password="pp", 68 | db_name="pqsd", 69 | db_host="qsdqs", 70 | db_port=3434, 71 | ) 72 | == "postgresql://foo:pp@qsdqs:3434/pqsd" 73 | ) 74 | 75 | assert ( 76 | build_uri( 77 | db_type="mysql", 78 | db_user="foo", 79 | db_password="pp", 80 | db_name="pqsd", 81 | db_host="qsdqs", 82 | db_port=3434, 83 | ) 84 | == "mysql+pymysql://foo:pp@qsdqs:3434/pqsd?charset=utf8" 85 | ) 86 | 87 | ### Test duplicate protocol spec. This happens when the open_book is called 88 | ### from GnuCash reports (.scm), gnucash-utilities. 89 | sqlite_uri = "sqlite:///some_file" 90 | uri = "sqlite:///some_file" 91 | assert build_uri(sqlite_file=uri) == sqlite_uri 92 | # When run with just the name (without sqlite:// prefix): 93 | uri = "some_file" 94 | assert build_uri(sqlite_file=uri) == sqlite_uri 95 | 96 | 97 | @contextmanager 98 | def locale_ctx(l): 99 | _l = locale.getlocale() 100 | 101 | locale.setlocale(locale.LC_ALL, l) 102 | 103 | yield 104 | 105 | locale.setlocale(locale.LC_ALL, _l) 106 | 107 | 108 | if sys.platform == "win32": 109 | locales = { 110 | "English_United States.1252": "USD", 111 | "French_France.1252": "EUR", 112 | } 113 | else: 114 | locales = { 115 | "en_US.UTF-8": "USD", 116 | "fr_FR.UTF-8": "EUR", 117 | } 118 | 119 | 120 | @pytest.fixture(params=locales) 121 | def locale_set(request): 122 | yield request.param 123 | 124 | 125 | def test_get_system_currency_mnemonic(locale_set): 126 | result = locales[locale_set] 127 | with locale_ctx(locale_set): 128 | assert get_system_currency_mnemonic() == result 129 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests in 2 | # multiple virtualenvs. This configuration file will run the test 3 | # suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | # 6 | # To run tox faster, check out Detox 7 | # (https://pypi.python.org/pypi/detox), which runs your tox runs in 8 | # parallel. To use it, "pip install detox" and then run "detox" from 9 | # this directory. 10 | 11 | [tox] 12 | envlist = py36, py37, py38 13 | 14 | [testenv] 15 | DOGOONWEB = True 16 | basepython= 17 | py36: {env:LOCALAPPDATA}\Programs\Python\Python36\python.exe 18 | py37: {env:LOCALAPPDATA}\Programs\Python\Python37\python.exe 19 | py38: {env:LOCALAPPDATA}\Programs\Python\Python38\python.exe 20 | 21 | 22 | commands = pip install pipenv 23 | pip install tox-pipenv 24 | python setup.py test 25 | 26 | [testenv:docs] 27 | basepython = python 28 | commands = paver doc_html 29 | --------------------------------------------------------------------------------