REST API Example for Python Bindings

Initial version of REST API allowing minimal information about accounts, invoices and customers to be accessed in JSON format. Includes modifications to gnucash_core.py  to add additional functions.

Author: Tom Lofts <dev@loftx.co.uk>

git-svn-id: svn+ssh://svn.gnucash.org/repo/gnucash/trunk@22936 57a11ea4-9604-0410-9ed3-97b8803252fd
This commit is contained in:
Geert Janssens
2013-05-02 16:34:03 +00:00
parent 216bb3263c
commit 48cceb51e6
5 changed files with 474 additions and 1 deletions

View File

@@ -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

View File

@@ -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: <http://localhost:5000/accounts>
Individual accounr: <http://localhost:5000/accounts/XXXX>
Invoices: <http://localhost:5000/invoices>
Individual invoice: <http://localhost:5000/invoices/XXXX>
Customers: <http://localhost:5000/customers>
Individual customer: <http://localhost:5000/customers/XXXX>
Invoices can be filtered via the following parameters:
is_active 1 or 0
is_paid 1 or 0
e.g. <http://localhost:5000/invoices?is_active=1&is_paid=0>
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 <connection string>` to start the server on <http://localhost:5000> e.g `python ./gnucash_rest mysql://user:password@127.0.0.1/gnucash_test`
Navigate to <http://localhost:5000/invoices> 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.

View File

@@ -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/<guid>')
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/<id>')
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/<id>')
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 <connection string>'
sys.exit(2)
if len(arguments) != 1:
print 'Usage: python-rest.py <connection string>'
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)

View File

@@ -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

View File

@@ -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')