diff --git a/src/optional/python-bindings/Makefile.am b/src/optional/python-bindings/Makefile.am index d68a56ed2f..80a73f1e7c 100644 --- a/src/optional/python-bindings/Makefile.am +++ b/src/optional/python-bindings/Makefile.am @@ -92,7 +92,10 @@ EXTRA_DIST = \ example_scripts/change_tax_code.py \ example_scripts/account_analysis.py \ example_scripts/new_book_with_opening_balances.py \ - example_scripts/test_imbalance_transaction.py + example_scripts/test_imbalance_transaction.py \ + example_scripts/rest-api/gnucash_rest.py \ + example_scripts/rest-api/gnucash_simple.py \ + example_scripts/rest-api/README MAINTAINERCLEANFILES = gnucash_core.c diff --git a/src/optional/python-bindings/example_scripts/rest-api/README b/src/optional/python-bindings/example_scripts/rest-api/README new file mode 100644 index 0000000000..b15d8dd6c1 --- /dev/null +++ b/src/optional/python-bindings/example_scripts/rest-api/README @@ -0,0 +1,48 @@ +A Python based REST framework for the Gnucash accounting application + +**Please note this is a very early work in progress and should not be run against live or production data.** + +This project is designed to allow other applications to easily extract (and hopefully insert and update at some point) data from Gnucash via a RESTful API. + +The API is built on the existing Python bindings and uses Flask to serve responses in JSON. + +Currently only accounts, customers and invoices can be accessed via the framework + +Accounts: +Individual accounr: + +Invoices: +Individual invoice: + +Customers: +Individual customer: + +Invoices can be filtered via the following parameters: + +is_active 1 or 0 +is_paid 1 or 0 + +e.g. + +Usage +----- + +**As this is work in progress you'll need a knowledge of building Gnucash from source and experience with python.** + +Full details to be provided at a later date, but in short you'll need a copy of Gnucash built with Python bindings and the Flask module installed. + +Rrun `python ./gnucash_rest ` to start the server on e.g `python ./gnucash_rest mysql://user:password@127.0.0.1/gnucash_test` + +Navigate to and you should get your invoices in JSON format. + +Files +----- + +`gnucash_rest.py` - The Flask app which responds to REST requests with JSON responses + +`gnucash_simple.py` - A helper file to convert Gnucash objects into dictionaries for easier conversion to JSON. + +Future +------ + +I'm using the API already to integrate Gnucash invoices with other systems, so I'll be adding features as and when I need them. The format of the API is likely to change as development continues. diff --git a/src/optional/python-bindings/example_scripts/rest-api/gnucash_rest.py b/src/optional/python-bindings/example_scripts/rest-api/gnucash_rest.py new file mode 100755 index 0000000000..2e215861f8 --- /dev/null +++ b/src/optional/python-bindings/example_scripts/rest-api/gnucash_rest.py @@ -0,0 +1,220 @@ +#!/usr/bin/python + +import gnucash +import gnucash_simple +import json +import atexit +from flask import Flask, abort, request +import sys +import getopt + +from gnucash import \ + QOF_QUERY_AND, \ + QOF_QUERY_OR, \ + QOF_QUERY_NAND, \ + QOF_QUERY_NOR, \ + QOF_QUERY_XOR + +from gnucash import \ + QOF_STRING_MATCH_NORMAL, \ + QOF_STRING_MATCH_CASEINSENSITIVE + +from gnucash import \ + QOF_COMPARE_LT, \ + QOF_COMPARE_LTE, \ + QOF_COMPARE_EQUAL, \ + QOF_COMPARE_GT, \ + QOF_COMPARE_GTE, \ + QOF_COMPARE_NEQ + +from gnucash import \ + INVOICE_TYPE + +from gnucash import \ + INVOICE_IS_PAID + +app = Flask(__name__) +app.debug = True + +@app.route('/accounts') +def api_accounts(): + + accounts = getAccounts(session.book) + + return accounts + +@app.route('/accounts/') +def api_account(guid): + + account = getAccount(session.book, guid) + + if account is None: + abort(404) + else: + return account + +@app.route('/invoices') +def api_invoices(): + + is_paid = request.args.get('is_paid', None) + is_active = request.args.get('is_active', None) + + if is_paid == '1': + is_paid = 1 + elif is_paid == '0': + is_paid = 0 + else: + is_paid = None + + if is_active == '1': + is_active = 1 + elif is_active == '0': + is_active = 0 + else: + is_active = None + + invoices = getInvoices(session.book, is_paid, is_active) + + return invoices + +@app.route('/invoices/') +def api_invoice(id): + + invoice = getInvoice(session.book, id) + + if invoice is None: + abort(404) + else: + return invoice + + +@app.route('/customers') +def api_customers(): + + customers = getCustomers(session.book) + + return customers + +@app.route('/customers/') +def api_customer(id): + + customer = getCustomer(session.book, id) + + if customer is None: + abort(404) + else: + return customer + +def getCustomers(book): + + query = gnucash.Query() + query.search_for('gncCustomer') + query.set_book(book) + customers = [] + + for result in query.run(): + customers.append(gnucash_simple.customerToDict(gnucash.gnucash_business.Customer(instance=result))) + + query.destroy() + + return json.dumps(customers) + +def getCustomer(book, id): + + customer = book.CustomerLookupByID(id) + + if customer is None: + return None + else: + return json.dumps(gnucash_simple.customerToDict(customer)) + +def getAccounts(book): + + accounts = gnucash_simple.accountToDict(book.get_root_account()) + + return json.dumps(accounts) + +def getAccount(book, guid): + + account_guid = gnucash.gnucash_core.GUID() + gnucash.gnucash_core.GUIDString(guid, account_guid) + + account = gnucash_simple.accountToDict(account_guid.AccountLookup(book)) + + if account is None: + return None + else: + return json.dumps(account) + + +def getInvoices(book, is_paid, is_active): + + query = gnucash.Query() + query.search_for('gncInvoice') + query.set_book(book) + + if is_paid == 0: + query.add_boolean_match([INVOICE_IS_PAID], False, QOF_QUERY_AND) + elif is_paid == 1: + query.add_boolean_match([INVOICE_IS_PAID], True, QOF_QUERY_AND) + + # active = JOB_IS_ACTIVE + if is_active == 0: + query.add_boolean_match(['active'], False, QOF_QUERY_AND) + elif is_active == 1: + query.add_boolean_match(['active'], True, QOF_QUERY_AND) + + # return only invoices (1 = invoices) + pred_data = gnucash.gnucash_core.QueryInt32Predicate(QOF_COMPARE_EQUAL, 1) + query.add_term([INVOICE_TYPE], pred_data, QOF_QUERY_AND) + + invoices = [] + + for result in query.run(): + invoices.append(gnucash_simple.invoiceToDict(gnucash.gnucash_business.Invoice(instance=result))) + + query.destroy() + + return json.dumps(invoices) + +def getInvoice(book, id): + + invoice = book.InvoiceLookupByID(id) + + if invoice is None: + return None + else: + #print invoiceToDict(invoice) + return json.dumps(gnucash_simple.invoiceToDict(invoice)) + +def shutdown(): + session.end() + session.destroy() + +try: + options, arguments = getopt.getopt(sys.argv[1:], 'h:', ['host=']) +except getopt.GetoptError as err: + print str(err) # will print something like "option -a not recognized" + print 'Usage: python-rest.py ' + sys.exit(2) + +if len(arguments) != 1: + print 'Usage: python-rest.py ' + sys.exit(2) + +#set default host for flash +host = '127.0.0.1' + +#allow host option to be changed +for option, value in options: + if option in ("-h", "--host"): + host = value + +#start gnucash session base on connection string argument +session = gnucash.Session(arguments[0], ignore_lock=True) + +# register method to close gnucash connection gracefully +atexit.register(shutdown) + +# start Flask server +app.run(host=host) diff --git a/src/optional/python-bindings/example_scripts/rest-api/gnucash_simple.py b/src/optional/python-bindings/example_scripts/rest-api/gnucash_simple.py new file mode 100644 index 0000000000..51e7632d81 --- /dev/null +++ b/src/optional/python-bindings/example_scripts/rest-api/gnucash_simple.py @@ -0,0 +1,152 @@ +import gnucash +from gnucash.gnucash_business import Entry + +def addressToDict(address): + if address is None: + return None + else: + simple_address = {} + simple_address['name'] = address.GetName(); + simple_address['line_1'] = address.GetAddr1(); + simple_address['line_2'] = address.GetAddr2(); + simple_address['line_3'] = address.GetAddr3(); + simple_address['line_4'] = address.GetAddr4(); + simple_address['phone'] = address.GetPhone(); + simple_address['fax'] = address.GetFax(); + simple_address['email'] = address.GetEmail(); + + return simple_address + +def vendorToDict(vendor): + + if vendor is None: + return None + else: + simple_vendor = {} + simple_vendor['name'] = vendor.GetName() + simple_vendor['id'] = vendor.GetID() + simple_vendor['notes'] = vendor.GetNotes() + simple_vendor['active'] = vendor.GetActive() + simple_vendor['currency'] = vendor.GetCurrency().get_mnemonic() + simple_vendor['tax_table_override'] = vendor.GetTaxTableOverride() + simple_vendor['address'] = addressToDict(vendor.GetAddr()) + simple_vendor['tax_included'] = vendor.GetTaxIncluded() + + return simple_vendor + +def customerToDict(customer): + + if customer is None: + return None + else: + simple_customer = {} + simple_customer['name'] = customer.GetName() + simple_customer['id'] = customer.GetID() + simple_customer['notes'] = customer.GetNotes() + simple_customer['active'] = customer.GetActive() + simple_customer['discount'] = customer.GetDiscount().to_double() + simple_customer['credit'] = customer.GetCredit().to_double() + simple_customer['currency'] = customer.GetCurrency().get_mnemonic() + simple_customer['tax_table_override'] = customer.GetTaxTableOverride() + simple_customer['address'] = addressToDict(customer.GetAddr()) + simple_customer['shipping_address'] = addressToDict(customer.GetShipAddr()) + simple_customer['tax_included'] = customer.GetTaxIncluded() + + return simple_customer + +def transactionToDict(transaction): + if transaction is None: + return None + else: + simple_transaction = {} + simple_transaction['num'] = transaction.GetNum() + simple_transaction['notes'] = transaction.GetNotes() + simple_transaction['is_closing_txn'] = transaction.GetIsClosingTxn() + simple_transaction['count_splits'] = transaction.CountSplits() + simple_transaction['has_reconciled_splits'] = transaction.HasReconciledSplits() + simple_transaction['currency'] = transaction.GetCurrency().get_mnemonic() + simple_transaction['imbalance_value'] = transaction.GetImbalanceValue().to_double() + simple_transaction['is_balanced'] = transaction.IsBalanced() + simple_transaction['date'] = transaction.GetDate() + simple_transaction['date_posted'] = transaction.RetDatePostedTS().strftime('%Y-%m-%d') + simple_transaction['date_entered'] = transaction.RetDateEnteredTS().strftime('%Y-%m-%d') + simple_transaction['date_due'] = transaction.RetDateDueTS().strftime('%Y-%m-%d') + simple_transaction['void_status'] = transaction.GetVoidStatus() + simple_transaction['void_time'] = transaction.GetVoidTime().strftime('%Y-%m-%d') + + return simple_transaction + +def invoiceToDict(invoice): + + if invoice is None: + return None + else: + simple_invoice = {} + simple_invoice['id'] = invoice.GetID() + simple_invoice['type'] = invoice.GetType() + simple_invoice['date_opened'] = invoice.GetDateOpened().strftime('%Y-%m-%d') + simple_invoice['date_posted'] = invoice.GetDatePosted().strftime('%Y-%m-%d') + simple_invoice['date_due'] = invoice.GetDateDue().strftime('%Y-%m-%d') + simple_invoice['notes'] = invoice.GetNotes() + simple_invoice['active'] = invoice.GetActive() + simple_invoice['currency'] = invoice.GetCurrency().get_mnemonic() + simple_invoice['owner'] = vendorToDict(invoice.GetOwner()) + simple_invoice['owner_type'] = invoice.GetOwnerType() + simple_invoice['billing_id'] = invoice.GetBillingID() + simple_invoice['to_charge_amount'] = invoice.GetToChargeAmount().to_double() + simple_invoice['total'] = invoice.GetTotal().to_double() + simple_invoice['total_subtotal'] = invoice.GetTotalSubtotal().to_double() + simple_invoice['total_tax'] = invoice.GetTotalTax().to_double() + simple_invoice['entries'] = {} + for n, entry in enumerate(invoice.GetEntries()): + if type(entry) != Entry: + entry=Entry(instance=entry) + simple_invoice['entries'][n] = entryToDict(entry) + simple_invoice['posted'] = invoice.IsPosted() + simple_invoice['paid'] = invoice.IsPaid() + + return simple_invoice + +def entryToDict(entry): + + if entry is None: + return None + else: + simple_entry = {} + simple_entry['date'] = entry.GetDate().strftime('%Y-%m-%d') + simple_entry['date_entered'] = entry.GetDateEntered().strftime('%Y-%m-%d') + simple_entry['description'] = entry.GetDescription() + simple_entry['action'] = entry.GetAction() + simple_entry['notes'] = entry.GetNotes() + simple_entry['quantity'] = gnucash.GncNumeric(instance=entry.GetQuantity()).to_double() + simple_entry['inv_price'] = gnucash.GncNumeric(instance=entry.GetInvPrice()).to_double() + simple_entry['discount'] = gnucash.GncNumeric(instance=entry.GetInvDiscount()).to_double() + simple_entry['discounted_type'] = entry.GetInvDiscountType() + simple_entry['discounted_how'] = entry.GetInvDiscountHow() + simple_entry['inv_taxable'] = entry.GetInvTaxable() + simple_entry['inv_tax_included'] = entry.GetInvTaxIncluded() + simple_entry['inv_tax_table_override'] = entry.GetInvTaxTable() + simple_entry['bill_price'] = gnucash.GncNumeric(instance=entry.GetBillPrice()).to_double() + simple_entry['bill_taxable'] = entry.GetBillTaxable() + simple_entry['bill_tax_included'] = entry.GetBillTaxIncluded() + simple_entry['bill_tax_table'] = entry.GetBillTaxTable() + simple_entry['billable'] = entry.GetBillable() + simple_entry['bill_payment'] = entry.GetBillPayment() + simple_entry['is_open'] = entry.IsOpen() + + return simple_entry + + +def accountToDict(account): + + if account is None: + return None + else: + simple_account = {} + simple_account['name'] = account.GetName() + simple_account['guid'] = account.GetGUID().to_string() + simple_account['subaccounts'] = [] + for n, subaccount in enumerate(account.get_children_sorted()): + simple_account['subaccounts'].append(accountToDict(subaccount)) + + return simple_account \ No newline at end of file diff --git a/src/optional/python-bindings/gnucash_core.py b/src/optional/python-bindings/gnucash_core.py index be9fe57c18..2f3d9c2cd3 100644 --- a/src/optional/python-bindings/gnucash_core.py +++ b/src/optional/python-bindings/gnucash_core.py @@ -666,6 +666,10 @@ GUID.add_method('xaccAccountLookup', 'AccountLookup') GUID.add_method('xaccTransLookup', 'TransLookup') GUID.add_method('xaccSplitLookup', 'SplitLookup') +## define addition methods for GUID object - do we need these +GUID.add_method('guid_to_string', 'to_string') +#GUID.add_method('string_to_guid', 'string_to_guid') + guid_dict = { 'copy' : GUID, 'TransLookup': Transaction, @@ -674,6 +678,12 @@ guid_dict = { } methods_return_instance(GUID, guid_dict) +#GUIDString +class GUIDString(GnuCashCoreClass): + pass + +GUIDString.add_constructor_and_methods_with_prefix('string_', 'to_guid') + #Query from gnucash_core_c import \ QOF_QUERY_AND, \ @@ -682,7 +692,47 @@ from gnucash_core_c import \ QOF_QUERY_NOR, \ QOF_QUERY_XOR +from gnucash_core_c import \ + QOF_STRING_MATCH_NORMAL, \ + QOF_STRING_MATCH_CASEINSENSITIVE + +from gnucash_core_c import \ + QOF_COMPARE_LT, \ + QOF_COMPARE_LTE, \ + QOF_COMPARE_EQUAL, \ + QOF_COMPARE_GT, \ + QOF_COMPARE_GTE, \ + QOF_COMPARE_NEQ + +from gnucash_core_c import \ + INVOICE_TYPE + +from gnucash_core_c import \ + INVOICE_IS_PAID + class Query(GnuCashCoreClass): pass Query.add_constructor_and_methods_with_prefix('qof_query_', 'create') + +Query.add_method('qof_query_set_book', 'set_book') +Query.add_method('qof_query_search_for', 'search_for') +Query.add_method('qof_query_run', 'run') +Query.add_method('qof_query_add_term', 'add_term') +Query.add_method('qof_query_add_boolean_match', 'add_boolean_match') +Query.add_method('qof_query_destroy', 'destroy') + +class QueryStringPredicate(GnuCashCoreClass): + pass + +QueryStringPredicate.add_constructor_and_methods_with_prefix('qof_query_', 'string_predicate') + +class QueryBooleanPredicate(GnuCashCoreClass): + pass + +QueryBooleanPredicate.add_constructor_and_methods_with_prefix('qof_query_', 'boolean_predicate') + +class QueryInt32Predicate(GnuCashCoreClass): + pass + +QueryInt32Predicate.add_constructor_and_methods_with_prefix('qof_query_', 'int32_predicate') \ No newline at end of file