1#!/usr/bin/env python3
2
3'''
4
5gnucash_rest.py -- A Flask app which responds to REST requests
6with JSON responses
7
8Copyright (C) 2013 Tom Lofts <dev@loftx.co.uk>
9
10This program is free software; you can redistribute it and/or
11modify it under the terms of the GNU General Public License as
12published by the Free Software Foundation; either version 2 of
13the License, or (at your option) any later version.
14
15This program is distributed in the hope that it will be useful,
16but WITHOUT ANY WARRANTY; without even the implied warranty of
17MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18GNU General Public License for more details.
19
20You should have received a copy of the GNU General Public License
21along with this program; if not, contact:
22
23Free Software Foundation Voice: +1-617-542-5942
2451 Franklin Street, Fifth Floor Fax: +1-617-542-2652
25Boston, MA 02110-1301, USA gnu@gnu.org
26
27@author Tom Lofts <dev@loftx.co.uk>
28
29'''
30
31import gnucash
32import gnucash_simple
33import json
34import atexit
35from flask import Flask, abort, request, Response
36import sys
37import getopt
38
39from decimal import Decimal
40
41from gnucash.gnucash_business import Vendor, Bill, Entry, GncNumeric, \
42    Customer, Invoice, Split, Account, Transaction
43
44import datetime
45
46from gnucash import \
47    QOF_QUERY_AND, \
48    QOF_QUERY_OR, \
49    QOF_QUERY_NAND, \
50    QOF_QUERY_NOR, \
51    QOF_QUERY_XOR
52
53from gnucash import \
54    QOF_STRING_MATCH_NORMAL, \
55    QOF_STRING_MATCH_CASEINSENSITIVE
56
57from gnucash import \
58    QOF_COMPARE_LT, \
59    QOF_COMPARE_LTE, \
60    QOF_COMPARE_EQUAL, \
61    QOF_COMPARE_GT, \
62    QOF_COMPARE_GTE, \
63    QOF_COMPARE_NEQ
64
65from gnucash import \
66    INVOICE_TYPE
67
68from gnucash import \
69    INVOICE_IS_PAID
70
71from gnucash import SessionOpenMode
72
73app = Flask(__name__)
74app.debug = True
75
76@app.route('/accounts', methods=['GET', 'POST'])
77def api_accounts():
78
79    if request.method == 'GET':
80
81        accounts = getAccounts(session.book)
82
83        return Response(json.dumps(accounts), mimetype='application/json')
84
85    elif request.method == 'POST':
86
87        try:
88            account = addAccount(session.books)
89        except Error as error:
90            return Response(json.dumps({'errors': [{'type' : error.type,
91                'message': error.message, 'data': error.data}]}), status=400,
92                mimetype='application/json')
93        else:
94            return Response(json.dumps(account), status=201,
95                mimetype='application/json')
96
97    else:
98        abort(405)
99
100@app.route('/accounts/<guid>', methods=['GET'])
101def api_account(guid):
102
103    account = getAccount(session.book, guid)
104
105    if account is None:
106        abort(404)
107    else:
108        return Response(json.dumps(account), mimetype='application/json')
109
110@app.route('/accounts/<guid>/splits', methods=['GET'])
111def api_account_splits(guid):
112
113    date_posted_from = request.args.get('date_posted_from', None)
114    date_posted_to = request.args.get('date_posted_to', None)
115
116    # check account exists
117    account = getAccount(session.book, guid)
118
119    if account is None:
120        abort(404)
121
122    splits = getAccountSplits(session.book, guid, date_posted_from,
123        date_posted_to)
124
125    return Response(json.dumps(splits), mimetype='application/json')
126
127
128@app.route('/transactions', methods=['POST'])
129def api_transactions():
130
131    if request.method == 'POST':
132
133        currency = str(request.form.get('currency', ''))
134        description = str(request.form.get('description', ''))
135        num = str(request.form.get('num', ''))
136        date_posted = str(request.form.get('date_posted', ''))
137
138        splitvalue1 = int(request.form.get('splitvalue1', ''))
139        splitaccount1 = str(request.form.get('splitaccount1', ''))
140        splitvalue2 = int(request.form.get('splitvalue2', ''))
141        splitaccount2 = str(request.form.get('splitaccount2', ''))
142
143        splits = [
144            {'value': splitvalue1, 'account_guid': splitaccount1},
145            {'value': splitvalue2, 'account_guid': splitaccount2}]
146
147        try:
148            transaction = addTransaction(session.book, num, description,
149                date_posted, currency, splits)
150        except Error as error:
151            return Response(json.dumps({'errors': [{'type' : error.type,
152                'message': error.message, 'data': error.data}]}), status=400,
153                mimetype='application/json')
154        else:
155            return Response(json.dumps(transaction), status=201,
156                mimetype='application/json')
157
158    else:
159        abort(405)
160
161@app.route('/transactions/<guid>', methods=['GET', 'POST', 'DELETE'])
162def api_transaction(guid):
163
164    if request.method == 'GET':
165
166        transaction = getTransaction(session.book, guid)
167
168        if transaction is None:
169            abort(404)
170
171        return Response(json.dumps(transaction), mimetype='application/json')
172
173    elif request.method == 'POST':
174
175        currency = str(request.form.get('currency', ''))
176        description = str(request.form.get('description', ''))
177        num = str(request.form.get('num', ''))
178        date_posted = str(request.form.get('date_posted', ''))
179
180        splitguid1 = str(request.form.get('splitguid1', ''))
181        splitvalue1 = int(request.form.get('splitvalue1', ''))
182        splitaccount1 = str(request.form.get('splitaccount1', ''))
183        splitguid2 = str(request.form.get('splitguid2', ''))
184        splitvalue2 = int(request.form.get('splitvalue2', ''))
185        splitaccount2 = str(request.form.get('splitaccount2', ''))
186
187        splits = [
188            {'guid': splitguid1,
189            'value': splitvalue1,
190            'account_guid': splitaccount1},
191            {'guid': splitguid2,
192            'value': splitvalue2,
193            'account_guid': splitaccount2}
194        ]
195
196        try:
197            transaction = editTransaction(session.book, guid, num, description,
198                date_posted, currency, splits)
199        except Error as error:
200            return Response(json.dumps({'errors': [{'type' : error.type,
201                'message': error.message, 'data': error.data}]}), status=400, mimetype='application/json')
202        else:
203            return Response(json.dumps(transaction), status=200,
204                mimetype='application/json')
205
206    elif request.method == 'DELETE':
207
208        deleteTransaction(session.book, guid)
209
210        return Response('', status=200, mimetype='application/json')
211
212    else:
213        abort(405)
214
215@app.route('/bills', methods=['GET', 'POST'])
216def api_bills():
217
218    if request.method == 'GET':
219
220        is_paid = request.args.get('is_paid', None)
221        is_active = request.args.get('is_active', None)
222        date_opened_to = request.args.get('date_opened_to', None)
223        date_opened_from = request.args.get('date_opened_from', None)
224
225        if is_paid == '1':
226            is_paid = 1
227        elif is_paid == '0':
228            is_paid = 0
229        else:
230            is_paid = None
231
232        if is_active == '1':
233            is_active = 1
234        elif is_active == '0':
235            is_active = 0
236        else:
237            is_active = None
238
239        bills = getBills(session.book, None, is_paid, is_active,
240            date_opened_from, date_opened_to)
241
242        return Response(json.dumps(bills), mimetype='application/json')
243
244    elif request.method == 'POST':
245
246        id = str(request.form.get('id', None))
247
248        if id == '':
249            id = None
250        elif id != None:
251            id = str(id)
252
253        vendor_id = str(request.form.get('vendor_id', ''))
254        currency = str(request.form.get('currency', ''))
255        date_opened = str(request.form.get('date_opened', ''))
256        notes = str(request.form.get('notes', ''))
257
258        try:
259            bill = addBill(session.book, id, vendor_id, currency, date_opened,
260                notes)
261        except Error as error:
262            # handle incorrect parameter errors
263            return Response(json.dumps({'errors': [{'type' : error.type,
264                'message': error.message, 'data': error.data}]}), status=400, mimetype='application/json')
265        else:
266            return Response(json.dumps(bill), status=201,
267                mimetype='application/json')
268
269    else:
270        abort(405)
271
272@app.route('/bills/<id>', methods=['GET', 'POST', 'PAY'])
273def api_bill(id):
274
275    if request.method == 'GET':
276
277        bill = getBill(session.book, id)
278
279        if bill is None:
280            abort(404)
281        else:
282            return Response(json.dumps(bill), mimetype='application/json')
283
284    elif request.method == 'POST':
285
286        vendor_id = str(request.form.get('vendor_id', ''))
287        currency = str(request.form.get('currency', ''))
288        date_opened = request.form.get('date_opened', None)
289        notes = str(request.form.get('notes', ''))
290        posted = request.form.get('posted', None)
291        posted_account_guid = str(request.form.get('posted_account_guid', ''))
292        posted_date = request.form.get('posted_date', '')
293        due_date = request.form.get('due_date', '')
294        posted_memo = str(request.form.get('posted_memo', ''))
295        posted_accumulatesplits = request.form.get('posted_accumulatesplits',
296            '')
297        posted_autopay = request.form.get('posted_autopay', '')
298
299        if posted == '1':
300            posted = 1
301        else:
302            posted = 0
303
304        if (posted_accumulatesplits == '1'
305            or posted_accumulatesplits == 'true'
306            or posted_accumulatesplits == 'True'
307            or posted_accumulatesplits == True):
308            posted_accumulatesplits = True
309        else:
310            posted_accumulatesplits = False
311
312        if posted_autopay == '1':
313            posted_autopay = True
314        else:
315            posted_autopay = False
316        try:
317            bill = updateBill(session.book, id, vendor_id, currency,
318                date_opened, notes, posted, posted_account_guid, posted_date,
319                due_date, posted_memo, posted_accumulatesplits, posted_autopay)
320        except Error as error:
321            return Response(json.dumps({'errors': [{'type' : error.type,
322                'message': error.message, 'data': error.data}]}), status=400,
323                mimetype='application/json')
324        else:
325            return Response(json.dumps(bill), status=200,
326                mimetype='application/json')
327
328        if bill is None:
329            abort(404)
330        else:
331            return Response(json.dumps(bill),
332                mimetype='application/json')
333
334    elif request.method == 'PAY':
335
336        posted_account_guid = str(request.form.get('posted_account_guid', ''))
337        transfer_account_guid = str(request.form.get('transfer_account_guid',
338            ''))
339        payment_date = request.form.get('payment_date', '')
340        num = str(request.form.get('num', ''))
341        memo = str(request.form.get('posted_memo', ''))
342        auto_pay = request.form.get('auto_pay', '')
343
344        try:
345            bill = payBill(session.book, id, posted_account_guid,
346                transfer_account_guid, payment_date, memo, num, auto_pay)
347        except Error as error:
348            return Response(json.dumps({'errors': [{'type' : error.type,
349                'message': error.message, 'data': error.data}]}), status=400,
350            mimetype='application/json')
351        else:
352            return Response(json.dumps(bill), status=200,
353                mimetype='application/json')
354
355    else:
356        abort(405)
357
358@app.route('/bills/<id>/entries', methods=['GET', 'POST'])
359def api_bill_entries(id):
360
361    bill = getBill(session.book, id)
362
363    if bill is None:
364        abort(404)
365    else:
366        if request.method == 'GET':
367            return Response(json.dumps(bill['entries']), mimetype='application/json')
368        elif request.method == 'POST':
369
370            date = str(request.form.get('date', ''))
371            description = str(request.form.get('description', ''))
372            account_guid = str(request.form.get('account_guid', ''))
373            quantity = str(request.form.get('quantity', ''))
374            price = str(request.form.get('price', ''))
375
376            try:
377                entry = addBillEntry(session.book, id, date, description,
378                    account_guid, quantity, price)
379            except Error as error:
380                return Response(json.dumps({'errors': [{'type' : error.type,
381                    'message': error.message, 'data': error.data}]}),
382                    status=400, mimetype='application/json')
383            else:
384                return Response(json.dumps(entry), status=201,
385                    mimetype='application/json')
386
387        else:
388            abort(405)
389
390@app.route('/invoices', methods=['GET', 'POST'])
391def api_invoices():
392
393    if request.method == 'GET':
394
395        is_paid = request.args.get('is_paid', None)
396        is_active = request.args.get('is_active', None)
397        date_due_to = request.args.get('date_due_to', None)
398        date_due_from = request.args.get('date_due_from', None)
399
400        if is_paid == '1':
401            is_paid = 1
402        elif is_paid == '0':
403            is_paid = 0
404        else:
405            is_paid = None
406
407        if is_active == '1':
408            is_active = 1
409        elif is_active == '0':
410            is_active = 0
411        else:
412            is_active = None
413
414        invoices = getInvoices(session.book, None, is_paid, is_active,
415            date_due_from, date_due_to)
416
417        return Response(json.dumps(invoices), mimetype='application/json')
418
419    elif request.method == 'POST':
420
421        id = str(request.form.get('id', None))
422
423        if id == '':
424            id = None
425        elif id != None:
426            id = str(id)
427
428        customer_id = str(request.form.get('customer_id', ''))
429        currency = str(request.form.get('currency', ''))
430        date_opened = str(request.form.get('date_opened', ''))
431        notes = str(request.form.get('notes', ''))
432
433        try:
434            invoice = addInvoice(session.book, id, customer_id, currency,
435                date_opened, notes)
436        except Error as error:
437            return Response(json.dumps({'errors': [{'type' : error.type,
438                'message': error.message, 'data': error.data}]}), status=400,
439                mimetype='application/json')
440        else:
441            return Response(json.dumps(invoice), status=201,
442                mimetype='application/json')
443
444    else:
445        abort(405)
446
447@app.route('/invoices/<id>', methods=['GET', 'POST', 'PAY'])
448def api_invoice(id):
449
450    if request.method == 'GET':
451
452        invoice = getInvoice(session.book, id)
453
454        if invoice is None:
455            abort(404)
456        else:
457            return Response(json.dumps(invoice), mimetype='application/json')
458
459    elif request.method == 'POST':
460
461        customer_id = str(request.form.get('customer_id', ''))
462        currency = str(request.form.get('currency', ''))
463        date_opened = request.form.get('date_opened', None)
464        notes = str(request.form.get('notes', ''))
465        posted = request.form.get('posted', None)
466        posted_account_guid = str(request.form.get('posted_account_guid', ''))
467        posted_date = request.form.get('posted_date', '')
468        due_date = request.form.get('due_date', '')
469        posted_memo = str(request.form.get('posted_memo', ''))
470        posted_accumulatesplits = request.form.get('posted_accumulatesplits',
471            '')
472        posted_autopay = request.form.get('posted_autopay', '')
473
474        if posted == '1':
475            posted = 1
476        else:
477            posted = 0
478
479        if (posted_accumulatesplits == '1'
480            or posted_accumulatesplits == 'true'
481            or posted_accumulatesplits == 'True'
482            or posted_accumulatesplits == True):
483            posted_accumulatesplits = True
484        else:
485            posted_accumulatesplits = False
486
487        if posted_autopay == '1':
488            posted_autopay = True
489        else:
490            posted_autopay = False
491        try:
492            invoice = updateInvoice(session.book, id, customer_id, currency,
493                date_opened, notes, posted, posted_account_guid, posted_date,
494                due_date, posted_memo, posted_accumulatesplits, posted_autopay)
495        except Error as error:
496            return Response(json.dumps({'errors': [{'type' : error.type,
497                'message': error.message, 'data': error.data}]}), status=400,
498                mimetype='application/json')
499        else:
500            return Response(json.dumps(invoice), status=200,
501                mimetype='application/json')
502
503        if invoice is None:
504            abort(404)
505        else:
506            return Response(json.dumps(invoice), mimetype='application/json')
507
508    elif request.method == 'PAY':
509
510        posted_account_guid = str(request.form.get('posted_account_guid', ''))
511        transfer_account_guid = str(request.form.get('transfer_account_guid',
512            ''))
513        payment_date = request.form.get('payment_date', '')
514        num = str(request.form.get('num', ''))
515        memo = str(request.form.get('posted_memo', ''))
516        auto_pay = request.form.get('auto_pay', '')
517
518        try:
519            invoice = payInvoice(session.book, id, posted_account_guid,
520                transfer_account_guid, payment_date, memo, num, auto_pay)
521        except Error as error:
522            return Response(json.dumps({'errors': [{'type' : error.type,
523                'message': error.message, 'data': error.data}]}), status=400,
524            mimetype='application/json')
525        else:
526            return Response(json.dumps(invoice), status=200,
527                mimetype='application/json')
528
529    else:
530        abort(405)
531
532@app.route('/invoices/<id>/entries', methods=['GET', 'POST'])
533def api_invoice_entries(id):
534
535    invoice = getInvoice(session.book, id)
536
537    if invoice is None:
538        abort(404)
539    else:
540        if request.method == 'GET':
541            return Response(json.dumps(invoice['entries']),
542                mimetype='application/json')
543        elif request.method == 'POST':
544
545            date = str(request.form.get('date', ''))
546            description = str(request.form.get('description', ''))
547            account_guid = str(request.form.get('account_guid', ''))
548            quantity = str(request.form.get('quantity', ''))
549            price = str(request.form.get('price', ''))
550
551            try:
552                entry = addEntry(session.book, id, date, description,
553                    account_guid, quantity, price)
554            except Error as error:
555                return Response(json.dumps({'errors': [{'type' : error.type,
556                    'message': error.message, 'data': error.data}]}),
557                    status=400, mimetype='application/json')
558            else:
559                return Response(json.dumps(entry), status=201,
560                    mimetype='application/json')
561
562        else:
563            abort(405)
564
565@app.route('/entries/<guid>', methods=['GET', 'POST', 'DELETE'])
566def api_entry(guid):
567
568    entry = getEntry(session.book, guid)
569
570    if entry is None:
571        abort(404)
572    else:
573        if request.method == 'GET':
574            return Response(json.dumps(entry), mimetype='application/json')
575        elif request.method == 'POST':
576
577            date = str(request.form.get('date', ''))
578            description = str(request.form.get('description', ''))
579            account_guid = str(request.form.get('account_guid', ''))
580            quantity = str(request.form.get('quantity', ''))
581            price = str(request.form.get('price', ''))
582
583            try:
584                entry = updateEntry(session.book, guid, date, description,
585                    account_guid, quantity, price)
586            except Error as error:
587                return Response(json.dumps({'errors': [{'type' : error.type,
588                    'message': error.message, 'data': error.data}]}),
589                    status=400, mimetype='application/json')
590            else:
591                return Response(json.dumps(entry), status=200,
592                    mimetype='application/json')
593
594        elif request.method == 'DELETE':
595
596            deleteEntry(session.book, guid)
597
598            return Response('', status=201, mimetype='application/json')
599
600        else:
601            abort(405)
602
603@app.route('/customers', methods=['GET', 'POST'])
604def api_customers():
605
606    if request.method == 'GET':
607        customers = getCustomers(session.book)
608        return Response(json.dumps(customers), mimetype='application/json')
609    elif request.method == 'POST':
610
611        id = str(request.form.get('id', None))
612
613        if id == '':
614            id = None
615        elif id != None:
616            id = str(id)
617
618        currency = str(request.form.get('currency', ''))
619        name = str(request.form.get('name', ''))
620        contact = str(request.form.get('contact', ''))
621        address_line_1 = str(request.form.get('address_line_1', ''))
622        address_line_2 = str(request.form.get('address_line_2', ''))
623        address_line_3 = str(request.form.get('address_line_3', ''))
624        address_line_4 = str(request.form.get('address_line_4', ''))
625        phone = str(request.form.get('phone', ''))
626        fax = str(request.form.get('fax', ''))
627        email = str(request.form.get('email', ''))
628
629        try:
630            customer = addCustomer(session.book, id, currency, name, contact,
631                address_line_1, address_line_2, address_line_3, address_line_4,
632                phone, fax, email)
633        except Error as error:
634            return Response(json.dumps({'errors': [{'type' : error.type,
635                'message': error.message, 'data': error.data}]}), status=400,
636                mimetype='application/json')
637        else:
638            return Response(json.dumps(customer), status=201,
639                mimetype='application/json')
640
641    else:
642        abort(405)
643
644@app.route('/customers/<id>', methods=['GET', 'POST'])
645def api_customer(id):
646
647    if request.method == 'GET':
648
649        customer = getCustomer(session.book, id)
650
651        if customer is None:
652            abort(404)
653        else:
654            return Response(json.dumps(customer), mimetype='application/json')
655
656    elif request.method == 'POST':
657
658        id = str(request.form.get('id', None))
659
660        name = str(request.form.get('name', ''))
661        contact = str(request.form.get('contact', ''))
662        address_line_1 = str(request.form.get('address_line_1', ''))
663        address_line_2 = str(request.form.get('address_line_2', ''))
664        address_line_3 = str(request.form.get('address_line_3', ''))
665        address_line_4 = str(request.form.get('address_line_4', ''))
666        phone = str(request.form.get('phone', ''))
667        fax = str(request.form.get('fax', ''))
668        email = str(request.form.get('email', ''))
669
670        try:
671            customer = updateCustomer(session.book, id, name, contact,
672                address_line_1, address_line_2, address_line_3, address_line_4,
673                phone, fax, email)
674        except Error as error:
675            if error.type == 'NoCustomer':
676                return Response(json.dumps({'errors': [{'type' : error.type,
677                    'message': error.message, 'data': error.data}]}),
678                    status=404, mimetype='application/json')
679            else:
680                return Response(json.dumps({'errors': [{'type' : error.type,
681                    'message': error.message, 'data': error.data}]}),
682                    status=400, mimetype='application/json')
683        else:
684            return Response(json.dumps(customer), status=200,
685                mimetype='application/json')
686
687    else:
688        abort(405)
689
690@app.route('/customers/<id>/invoices', methods=['GET'])
691def api_customer_invoices(id):
692
693    customer = getCustomer(session.book, id)
694
695    if customer is None:
696        abort(404)
697
698    invoices = getInvoices(session.book, customer['guid'], None, None, None,
699        None)
700
701    return Response(json.dumps(invoices), mimetype='application/json')
702
703@app.route('/vendors', methods=['GET', 'POST'])
704def api_vendors():
705
706    if request.method == 'GET':
707        vendors = getVendors(session.book)
708        return Response(json.dumps(vendors), mimetype='application/json')
709    elif request.method == 'POST':
710
711        id = str(request.form.get('id', None))
712
713        if id == '':
714            id = None
715        elif id != None:
716            id = str(id)
717
718        currency = str(request.form.get('currency', ''))
719        name = str(request.form.get('name', ''))
720        contact = str(request.form.get('contact', ''))
721        address_line_1 = str(request.form.get('address_line_1', ''))
722        address_line_2 = str(request.form.get('address_line_2', ''))
723        address_line_3 = str(request.form.get('address_line_3', ''))
724        address_line_4 = str(request.form.get('address_line_4', ''))
725        phone = str(request.form.get('phone', ''))
726        fax = str(request.form.get('fax', ''))
727        email = str(request.form.get('email', ''))
728
729        try:
730            vendor = addVendor(session.book, id, currency, name, contact,
731                address_line_1, address_line_2, address_line_3, address_line_4,
732                phone, fax, email)
733        except Error as error:
734            return Response(json.dumps({'errors': [{'type' : error.type,
735                'message': error.message, 'data': error.data}]}), status=400,
736                mimetype='application/json')
737        else:
738            return Response(json.dumps(vendor), status=201,
739                mimetype='application/json')
740
741    else:
742        abort(405)
743
744@app.route('/vendors/<id>', methods=['GET', 'POST'])
745def api_vendor(id):
746
747    if request.method == 'GET':
748
749        vendor = getVendor(session.book, id)
750
751        if vendor is None:
752            abort(404)
753        else:
754            return Response(json.dumps(vendor), mimetype='application/json')
755    else:
756        abort(405)
757
758@app.route('/vendors/<id>/bills', methods=['GET'])
759def api_vendor_bills(id):
760
761    vendor = getVendor(session.book, id)
762
763    if vendor is None:
764        abort(404)
765
766    bills = getBills(session.book, vendor['guid'], None, None, None, None)
767
768    return Response(json.dumps(bills), mimetype='application/json')
769
770def getCustomers(book):
771
772    query = gnucash.Query()
773    query.search_for('gncCustomer')
774    query.set_book(book)
775    customers = []
776
777    for result in query.run():
778        customers.append(gnucash_simple.customerToDict(
779            gnucash.gnucash_business.Customer(instance=result)))
780
781    query.destroy()
782
783    return customers
784
785def getCustomer(book, id):
786
787    customer = book.CustomerLookupByID(id)
788
789    if customer is None:
790        return None
791    else:
792        return gnucash_simple.customerToDict(customer)
793
794def getVendors(book):
795
796    query = gnucash.Query()
797    query.search_for('gncVendor')
798    query.set_book(book)
799    vendors = []
800
801    for result in query.run():
802        vendors.append(gnucash_simple.vendorToDict(
803            gnucash.gnucash_business.Vendor(instance=result)))
804
805    query.destroy()
806
807    return vendors
808
809def getVendor(book, id):
810
811    vendor = book.VendorLookupByID(id)
812
813    if vendor is None:
814        return None
815    else:
816        return gnucash_simple.vendorToDict(vendor)
817
818def getAccounts(book):
819
820    accounts = gnucash_simple.accountToDict(book.get_root_account())
821
822    return accounts
823
824def getAccountsFlat(book):
825
826    accounts = gnucash_simple.accountToDict(book.get_root_account())
827
828    flat_accounts = getSubAccounts(accounts)
829
830    for n, account in enumerate(flat_accounts):
831        account.pop('subaccounts')
832
833    filtered_flat_account = []
834
835    type_ids = [9]
836
837    for n, account in enumerate(flat_accounts):
838        if account['type_id'] in type_ids:
839            filtered_flat_account.append(account)
840            print(account['name'] + ' ' + str(account['type_id']))
841
842    return filtered_flat_account
843
844def getSubAccounts(account):
845
846    flat_accounts = []
847
848    if 'subaccounts' in list(account.keys()):
849        for n, subaccount in enumerate(account['subaccounts']):
850            flat_accounts.append(subaccount)
851            flat_accounts = flat_accounts + getSubAccounts(subaccount)
852
853    return flat_accounts
854
855def getAccount(book, guid):
856
857    account_guid = gnucash.gnucash_core.GUID()
858    gnucash.gnucash_core.GUIDString(guid, account_guid)
859
860    account = account_guid.AccountLookup(book)
861
862    if account is None:
863        return None
864
865    account = gnucash_simple.accountToDict(account)
866
867    if account is None:
868        return None
869    else:
870        return account
871
872
873def getTransaction(book, guid):
874
875    transaction_guid = gnucash.gnucash_core.GUID()
876    gnucash.gnucash_core.GUIDString(guid, transaction_guid)
877
878    transaction = transaction_guid.TransactionLookup(book)
879
880    if transaction is None:
881        return None
882
883    transaction = gnucash_simple.transactionToDict(transaction, ['splits'])
884
885    if transaction is None:
886        return None
887    else:
888        return transaction
889
890def getTransactions(book, account_guid, date_posted_from, date_posted_to):
891
892    query = gnucash.Query()
893
894    query.search_for('Trans')
895    query.set_book(book)
896
897    transactions = []
898
899    for transaction in query.run():
900        transactions.append(gnucash_simple.transactionToDict(
901            gnucash.gnucash_business.Transaction(instance=transaction)))
902
903    query.destroy()
904
905    return transactions
906
907def getAccountSplits(book, guid, date_posted_from, date_posted_to):
908
909    account_guid = gnucash.gnucash_core.GUID()
910    gnucash.gnucash_core.GUIDString(guid, account_guid)
911
912    query = gnucash.Query()
913    query.search_for('Split')
914    query.set_book(book)
915
916    SPLIT_TRANS= 'trans'
917
918    QOF_DATE_MATCH_NORMAL = 1
919
920    TRANS_DATE_POSTED = 'date-posted'
921
922    if date_posted_from != None:
923        pred_data = gnucash.gnucash_core.QueryDatePredicate(
924            QOF_COMPARE_GTE, QOF_DATE_MATCH_NORMAL, datetime.datetime.strptime(
925                date_posted_from, "%Y-%m-%d").date())
926        param_list = [SPLIT_TRANS, TRANS_DATE_POSTED]
927        query.add_term(param_list, pred_data, QOF_QUERY_AND)
928
929    if date_posted_to != None:
930        pred_data = gnucash.gnucash_core.QueryDatePredicate(
931            QOF_COMPARE_LTE, QOF_DATE_MATCH_NORMAL, datetime.datetime.strptime(
932                date_posted_to, "%Y-%m-%d").date())
933        param_list = [SPLIT_TRANS, TRANS_DATE_POSTED]
934        query.add_term(param_list, pred_data, QOF_QUERY_AND)
935
936    SPLIT_ACCOUNT = 'account'
937    QOF_PARAM_GUID = 'guid'
938
939    if guid != None:
940        gnucash.gnucash_core.GUIDString(guid, account_guid)
941        query.add_guid_match(
942            [SPLIT_ACCOUNT, QOF_PARAM_GUID], account_guid, QOF_QUERY_AND)
943
944    splits = []
945
946    for split in query.run():
947        splits.append(gnucash_simple.splitToDict(
948            gnucash.gnucash_business.Split(instance=split),
949            ['account', 'transaction', 'other_split']))
950
951    query.destroy()
952
953    return splits
954
955def getInvoices(book, customer, is_paid, is_active, date_due_from,
956    date_due_to):
957
958    query = gnucash.Query()
959    query.search_for('gncInvoice')
960    query.set_book(book)
961
962    if is_paid == 0:
963        query.add_boolean_match([INVOICE_IS_PAID], False, QOF_QUERY_AND)
964    elif is_paid == 1:
965        query.add_boolean_match([INVOICE_IS_PAID], True, QOF_QUERY_AND)
966
967    # active = JOB_IS_ACTIVE
968    if is_active == 0:
969        query.add_boolean_match(['active'], False, QOF_QUERY_AND)
970    elif is_active == 1:
971        query.add_boolean_match(['active'], True, QOF_QUERY_AND)
972
973    QOF_PARAM_GUID = 'guid'
974    INVOICE_OWNER = 'owner'
975
976    if customer != None:
977        customer_guid = gnucash.gnucash_core.GUID()
978        gnucash.gnucash_core.GUIDString(customer, customer_guid)
979        query.add_guid_match(
980            [INVOICE_OWNER, QOF_PARAM_GUID], customer_guid, QOF_QUERY_AND)
981
982    if date_due_from != None:
983        pred_data = gnucash.gnucash_core.QueryDatePredicate(
984            QOF_COMPARE_GTE, 2, datetime.datetime.strptime(
985                date_due_from, "%Y-%m-%d").date())
986        query.add_term(['date_due'], pred_data, QOF_QUERY_AND)
987
988    if date_due_to != None:
989        pred_data = gnucash.gnucash_core.QueryDatePredicate(
990            QOF_COMPARE_LTE, 2, datetime.datetime.strptime(
991                date_due_to, "%Y-%m-%d").date())
992        query.add_term(['date_due'], pred_data, QOF_QUERY_AND)
993
994    # return only invoices (1 = invoices)
995    pred_data = gnucash.gnucash_core.QueryInt32Predicate(QOF_COMPARE_EQUAL, 1)
996    query.add_term([INVOICE_TYPE], pred_data, QOF_QUERY_AND)
997
998    invoices = []
999
1000    for result in query.run():
1001        invoices.append(gnucash_simple.invoiceToDict(
1002            gnucash.gnucash_business.Invoice(instance=result)))
1003
1004    query.destroy()
1005
1006    return invoices
1007
1008def getBills(book, customer, is_paid, is_active, date_opened_from,
1009    date_opened_to):
1010
1011    query = gnucash.Query()
1012    query.search_for('gncInvoice')
1013    query.set_book(book)
1014
1015    if is_paid == 0:
1016        query.add_boolean_match([INVOICE_IS_PAID], False, QOF_QUERY_AND)
1017    elif is_paid == 1:
1018        query.add_boolean_match([INVOICE_IS_PAID], True, QOF_QUERY_AND)
1019
1020    # active = JOB_IS_ACTIVE
1021    if is_active == 0:
1022        query.add_boolean_match(['active'], False, QOF_QUERY_AND)
1023    elif is_active == 1:
1024        query.add_boolean_match(['active'], True, QOF_QUERY_AND)
1025
1026    QOF_PARAM_GUID = 'guid'
1027    INVOICE_OWNER = 'owner'
1028
1029    if customer != None:
1030        customer_guid = gnucash.gnucash_core.GUID()
1031        gnucash.gnucash_core.GUIDString(customer, customer_guid)
1032        query.add_guid_match(
1033            [INVOICE_OWNER, QOF_PARAM_GUID], customer_guid, QOF_QUERY_AND)
1034
1035    if date_opened_from != None:
1036        pred_data = gnucash.gnucash_core.QueryDatePredicate(
1037            QOF_COMPARE_GTE, 2, datetime.datetime.strptime(
1038                date_opened_from, "%Y-%m-%d").date())
1039        query.add_term(['date_opened'], pred_data, QOF_QUERY_AND)
1040
1041    if date_opened_to != None:
1042        pred_data = gnucash.gnucash_core.QueryDatePredicate(
1043            QOF_COMPARE_LTE, 2, datetime.datetime.strptime(
1044                date_opened_to, "%Y-%m-%d").date())
1045        query.add_term(['date_opened'], pred_data, QOF_QUERY_AND)
1046
1047    # return only bills (2 = bills)
1048    pred_data = gnucash.gnucash_core.QueryInt32Predicate(QOF_COMPARE_EQUAL, 2)
1049    query.add_term([INVOICE_TYPE], pred_data, QOF_QUERY_AND)
1050
1051    bills = []
1052
1053    for result in query.run():
1054        bills.append(gnucash_simple.billToDict(
1055            gnucash.gnucash_business.Bill(instance=result)))
1056
1057    query.destroy()
1058
1059    return bills
1060
1061def getGnuCashInvoice(book ,id):
1062
1063    # we don't use book.InvoicelLookupByID(id) as this is identical to
1064    # book.BillLookupByID(id) so can return the same object if they share IDs
1065
1066    query = gnucash.Query()
1067    query.search_for('gncInvoice')
1068    query.set_book(book)
1069
1070    # return only invoices (1 = invoices)
1071    pred_data = gnucash.gnucash_core.QueryInt32Predicate(QOF_COMPARE_EQUAL, 1)
1072    query.add_term([INVOICE_TYPE], pred_data, QOF_QUERY_AND)
1073
1074    INVOICE_ID = 'id'
1075
1076    pred_data = gnucash.gnucash_core.QueryStringPredicate(
1077        QOF_COMPARE_EQUAL, id, QOF_STRING_MATCH_NORMAL, False)
1078    query.add_term([INVOICE_ID], pred_data, QOF_QUERY_AND)
1079
1080    invoice = None
1081
1082    for result in query.run():
1083        invoice = gnucash.gnucash_business.Invoice(instance=result)
1084
1085    query.destroy()
1086
1087    return invoice
1088
1089def getGnuCashBill(book ,id):
1090
1091    # we don't use book.InvoicelLookupByID(id) as this is identical to
1092    # book.BillLookupByID(id) so can return the same object if they share IDs
1093
1094    query = gnucash.Query()
1095    query.search_for('gncInvoice')
1096    query.set_book(book)
1097
1098    # return only bills (2 = bills)
1099    pred_data = gnucash.gnucash_core.QueryInt32Predicate(QOF_COMPARE_EQUAL, 2)
1100    query.add_term([INVOICE_TYPE], pred_data, QOF_QUERY_AND)
1101
1102    INVOICE_ID = 'id'
1103
1104    pred_data = gnucash.gnucash_core.QueryStringPredicate(
1105        QOF_COMPARE_EQUAL, id, QOF_STRING_MATCH_NORMAL, False)
1106    query.add_term([INVOICE_ID], pred_data, QOF_QUERY_AND)
1107
1108    bill = None
1109
1110    for result in query.run():
1111        bill = gnucash.gnucash_business.Bill(instance=result)
1112
1113    query.destroy()
1114
1115    return bill
1116
1117def getInvoice(book, id):
1118
1119    return gnucash_simple.invoiceToDict(getGnuCashInvoice(book, id))
1120
1121def payInvoice(book, id, posted_account_guid, transfer_account_guid,
1122    payment_date, memo, num, auto_pay):
1123
1124    invoice = getGnuCashInvoice(book, id)
1125
1126    account_guid2 = gnucash.gnucash_core.GUID()
1127    gnucash.gnucash_core.GUIDString(transfer_account_guid, account_guid2)
1128
1129    xfer_acc = account_guid2.AccountLookup(session.book)
1130
1131    invoice.ApplyPayment(None, xfer_acc, invoice.GetTotal(), GncNumeric(0),
1132        datetime.datetime.strptime(payment_date, '%Y-%m-%d'), memo, num)
1133
1134    return gnucash_simple.invoiceToDict(invoice)
1135
1136def payBill(book, id, posted_account_guid, transfer_account_guid, payment_date,
1137    memo, num, auto_pay):
1138
1139    bill = getGnuCashBill(book, id)
1140
1141    account_guid = gnucash.gnucash_core.GUID()
1142    gnucash.gnucash_core.GUIDString(transfer_account_guid, account_guid)
1143
1144    xfer_acc = account_guid.AccountLookup(session.book)
1145
1146    # We pay the negative total as the bill as this seemed to cause issues
1147    # with the split not being set correctly and not being marked as paid
1148    bill.ApplyPayment(None, xfer_acc, bill.GetTotal().neg(), GncNumeric(0),
1149        datetime.datetime.strptime(payment_date, '%Y-%m-%d'), memo, num)
1150
1151    return gnucash_simple.billToDict(bill)
1152
1153def getBill(book, id):
1154
1155    return gnucash_simple.billToDict(getGnuCashBill(book, id))
1156
1157def addVendor(book, id, currency_mnumonic, name, contact, address_line_1,
1158    address_line_2, address_line_3, address_line_4, phone, fax, email):
1159
1160    if name == '':
1161        raise Error('NoVendorName', 'A name must be entered for this company',
1162            {'field': 'name'})
1163
1164    if (address_line_1 == ''
1165        and address_line_2 == ''
1166        and address_line_3 == ''
1167        and address_line_4 == ''):
1168        raise Error('NoVendorAddress',
1169            'An address must be entered for this company',
1170            {'field': 'address'})
1171
1172    commod_table = book.get_table()
1173    currency = commod_table.lookup('CURRENCY', currency_mnumonic)
1174
1175    if currency is None:
1176        raise Error('InvalidVendorCurrency',
1177            'A valid currency must be supplied for this vendor',
1178            {'field': 'currency'})
1179
1180    if id is None:
1181        id = book.VendorNextID()
1182
1183    vendor = Vendor(session.book, id, currency, name)
1184
1185    address = vendor.GetAddr()
1186    address.SetName(contact)
1187    address.SetAddr1(address_line_1)
1188    address.SetAddr2(address_line_2)
1189    address.SetAddr3(address_line_3)
1190    address.SetAddr4(address_line_4)
1191    address.SetPhone(phone)
1192    address.SetFax(fax)
1193    address.SetEmail(email)
1194
1195    return gnucash_simple.vendorToDict(vendor)
1196
1197def addCustomer(book, id, currency_mnumonic, name, contact, address_line_1,
1198    address_line_2, address_line_3, address_line_4, phone, fax, email):
1199
1200    if name == '':
1201        raise Error('NoCustomerName',
1202            'A name must be entered for this company', {'field': 'name'})
1203
1204    if (address_line_1 == ''
1205        and address_line_2 == ''
1206        and address_line_3 == ''
1207        and address_line_4 == ''):
1208        raise Error('NoCustomerAddress',
1209            'An address must be entered for this company',
1210            {'field': 'address'})
1211
1212    commod_table = book.get_table()
1213    currency = commod_table.lookup('CURRENCY', currency_mnumonic)
1214
1215    if currency is None:
1216        raise Error('InvalidCustomerCurrency',
1217            'A valid currency must be supplied for this customer',
1218            {'field': 'currency'})
1219
1220    if id is None:
1221        id = book.CustomerNextID()
1222
1223    customer = Customer(session.book, id, currency, name)
1224
1225    address = customer.GetAddr()
1226    address.SetName(contact)
1227    address.SetAddr1(address_line_1)
1228    address.SetAddr2(address_line_2)
1229    address.SetAddr3(address_line_3)
1230    address.SetAddr4(address_line_4)
1231    address.SetPhone(phone)
1232    address.SetFax(fax)
1233    address.SetEmail(email)
1234
1235    return gnucash_simple.customerToDict(customer)
1236
1237def updateCustomer(book, id, name, contact, address_line_1, address_line_2,
1238    address_line_3, address_line_4, phone, fax, email):
1239
1240    customer = book.CustomerLookupByID(id)
1241
1242    if customer is None:
1243        raise Error('NoCustomer', 'A customer with this ID does not exist',
1244            {'field': 'id'})
1245
1246    if name == '':
1247        raise Error('NoCustomerName',
1248            'A name must be entered for this company', {'field': 'name'})
1249
1250    if (address_line_1 == ''
1251        and address_line_2 == ''
1252        and address_line_3 == ''
1253        and address_line_4 == ''):
1254        raise Error('NoCustomerAddress',
1255            'An address must be entered for this company',
1256            {'field': 'address'})
1257
1258    customer.SetName(name)
1259
1260    address = customer.GetAddr()
1261    address.SetName(contact)
1262    address.SetAddr1(address_line_1)
1263    address.SetAddr2(address_line_2)
1264    address.SetAddr3(address_line_3)
1265    address.SetAddr4(address_line_4)
1266    address.SetPhone(phone)
1267    address.SetFax(fax)
1268    address.SetEmail(email)
1269
1270    return gnucash_simple.customerToDict(customer)
1271
1272def addInvoice(book, id, customer_id, currency_mnumonic, date_opened, notes):
1273
1274    customer = book.CustomerLookupByID(customer_id)
1275
1276    if customer is None:
1277        raise Error('NoCustomer',
1278            'A customer with this ID does not exist', {'field': 'id'})
1279
1280    if id is None:
1281        id = book.InvoiceNextID(customer)
1282
1283    try:
1284        date_opened = datetime.datetime.strptime(date_opened, "%Y-%m-%d")
1285    except ValueError:
1286        raise Error('InvalidDateOpened',
1287            'The date opened must be provided in the form YYYY-MM-DD',
1288            {'field': 'date_opened'})
1289
1290    if currency_mnumonic is None:
1291        currency_mnumonic = customer.GetCurrency().get_mnemonic()
1292
1293    commod_table = book.get_table()
1294    currency = commod_table.lookup('CURRENCY', currency_mnumonic)
1295
1296    if currency is None:
1297        raise Error('InvalidCustomerCurrency',
1298            'A valid currency must be supplied for this customer',
1299            {'field': 'currency'})
1300
1301    invoice = Invoice(book, id, currency, customer, date_opened.date())
1302
1303    invoice.SetNotes(notes)
1304
1305    return gnucash_simple.invoiceToDict(invoice)
1306
1307def updateInvoice(book, id, customer_id, currency_mnumonic, date_opened,
1308    notes, posted, posted_account_guid, posted_date, due_date, posted_memo,
1309    posted_accumulatesplits, posted_autopay):
1310
1311    invoice = getGnuCashInvoice(book, id)
1312
1313    if invoice is None:
1314        raise Error('NoInvoice',
1315            'An invoice with this ID does not exist',
1316            {'field': 'id'})
1317
1318    customer = book.CustomerLookupByID(customer_id)
1319
1320    if customer is None:
1321        raise Error('NoCustomer', 'A customer with this ID does not exist',
1322            {'field': 'customer_id'})
1323
1324    try:
1325        date_opened = datetime.datetime.strptime(date_opened, "%Y-%m-%d")
1326    except ValueError:
1327        raise Error('InvalidDateOpened',
1328            'The date opened must be provided in the form YYYY-MM-DD',
1329            {'field': 'date_opened'})
1330
1331    if posted_date == '':
1332        if posted == 1:
1333            raise Error('NoDatePosted',
1334                'The date posted must be supplied when posted=1',
1335                {'field': 'date_posted'})
1336    else:
1337        try:
1338            posted_date = datetime.datetime.strptime(posted_date, "%Y-%m-%d")
1339        except ValueError:
1340            raise Error('InvalidDatePosted',
1341                'The date posted must be provided in the form YYYY-MM-DD',
1342                {'field': 'posted_date'})
1343
1344    if due_date == '':
1345        if posted == 1:
1346            raise Error('NoDatePosted',
1347                'The due date must be supplied when posted=1',
1348                {'field': 'date_posted'})
1349    else:
1350        try:
1351            due_date = datetime.datetime.strptime(due_date, "%Y-%m-%d")
1352        except ValueError:
1353            raise Error('InvalidDatePosted',
1354                'The due date must be provided in the form YYYY-MM-DD',
1355                {'field': 'due_date'})
1356
1357    if posted_account_guid == '':
1358        if posted == 1:
1359            raise Error('NoPostedAccountGuid',
1360                'The posted account GUID must be supplied when posted=1',
1361                {'field': 'posted_account_guid'})
1362    else:
1363        guid = gnucash.gnucash_core.GUID()
1364        gnucash.gnucash_core.GUIDString(posted_account_guid, guid)
1365
1366        posted_account = guid.AccountLookup(book)
1367
1368        if posted_account is None:
1369            raise Error('NoAccount',
1370                'No account exists with the posted account GUID',
1371                {'field': 'posted_account_guid'})
1372
1373    invoice.SetOwner(customer)
1374    invoice.SetDateOpened(date_opened)
1375    invoice.SetNotes(notes)
1376
1377    # post if currently unposted and posted=1
1378    if (invoice.GetDatePosted().strftime('%Y-%m-%d') == '1970-01-01'
1379        and posted == 1):
1380        invoice.PostToAccount(posted_account, posted_date, due_date,
1381            posted_memo, posted_accumulatesplits, posted_autopay)
1382
1383    return gnucash_simple.invoiceToDict(invoice)
1384
1385def updateBill(book, id, vendor_id, currency_mnumonic, date_opened, notes,
1386    posted, posted_account_guid, posted_date, due_date, posted_memo,
1387    posted_accumulatesplits, posted_autopay):
1388
1389    bill = getGnuCashBill(book, id)
1390
1391    if bill is None:
1392        raise Error('NoBill', 'A bill with this ID does not exist',
1393            {'field': 'id'})
1394
1395    vendor = book.VendorLookupByID(vendor_id)
1396
1397    if vendor is None:
1398        raise Error('NoVendor',
1399            'A vendor with this ID does not exist',
1400            {'field': 'vendor_id'})
1401
1402    try:
1403        date_opened = datetime.datetime.strptime(date_opened, "%Y-%m-%d")
1404    except ValueError:
1405        raise Error('InvalidDateOpened',
1406            'The date opened must be provided in the form YYYY-MM-DD',
1407            {'field': 'date_opened'})
1408
1409    if posted_date == '':
1410        if posted == 1:
1411            raise Error('NoDatePosted',
1412                'The date posted must be supplied when posted=1',
1413                {'field': 'date_posted'})
1414    else:
1415        try:
1416            posted_date = datetime.datetime.strptime(posted_date, "%Y-%m-%d")
1417        except ValueError:
1418            raise Error('InvalidDatePosted',
1419                'The date posted must be provided in the form YYYY-MM-DD',
1420                {'field': 'posted_date'})
1421
1422    if due_date == '':
1423        if posted == 1:
1424            raise Error('NoDatePosted',
1425                'The due date must be supplied when posted=1',
1426                {'field': 'date_posted'})
1427    else:
1428        try:
1429            due_date = datetime.datetime.strptime(due_date, "%Y-%m-%d")
1430        except ValueError:
1431            raise Error('InvalidDatePosted',
1432                'The due date must be provided in the form YYYY-MM-DD',
1433                {'field': 'due_date'})
1434
1435    if posted_account_guid == '':
1436        if posted == 1:
1437            raise Error('NoPostedAccountGuid',
1438                'The posted account GUID must be supplied when posted=1',
1439                {'field': 'posted_account_guid'})
1440    else:
1441        guid = gnucash.gnucash_core.GUID()
1442        gnucash.gnucash_core.GUIDString(posted_account_guid, guid)
1443
1444        posted_account = guid.AccountLookup(book)
1445
1446        if posted_account is None:
1447            raise Error('NoAccount',
1448                'No account exists with the posted account GUID',
1449                {'field': 'posted_account_guid'})
1450
1451    bill.SetOwner(vendor)
1452    bill.SetDateOpened(date_opened)
1453    bill.SetNotes(notes)
1454
1455    # post if currently unposted and posted=1
1456    if bill.GetDatePosted().strftime('%Y-%m-%d') == '1970-01-01' and posted == 1:
1457        bill.PostToAccount(posted_account, posted_date, due_date, posted_memo,
1458            posted_accumulatesplits, posted_autopay)
1459
1460    return gnucash_simple.billToDict(bill)
1461
1462def addEntry(book, invoice_id, date, description, account_guid, quantity, price):
1463
1464    invoice = getGnuCashInvoice(book, invoice_id)
1465
1466    if invoice is None:
1467        raise Error('NoInvoice',
1468            'No invoice exists with this ID', {'field': 'invoice_id'})
1469
1470    try:
1471        date = datetime.datetime.strptime(date, "%Y-%m-%d")
1472    except ValueError:
1473        raise Error('InvalidDateOpened',
1474            'The date opened must be provided in the form YYYY-MM-DD',
1475            {'field': 'date'})
1476
1477    guid = gnucash.gnucash_core.GUID()
1478    gnucash.gnucash_core.GUIDString(account_guid, guid)
1479
1480    account = guid.AccountLookup(book)
1481
1482    if account is None:
1483        raise Error('NoAccount', 'No account exists with this GUID',
1484            {'field': 'account_guid'})
1485
1486    try:
1487        quantity = Decimal(quantity).quantize(Decimal('.01'))
1488    except ArithmeticError:
1489        raise Error('InvalidQuantity', 'This quantity is not valid',
1490            {'field': 'quantity'})
1491
1492    try:
1493        price = Decimal(price).quantize(Decimal('.01'))
1494    except ArithmeticError:
1495        raise Error('InvalidPrice', 'This price is not valid',
1496            {'field': 'price'})
1497
1498    entry = Entry(book, invoice, date.date())
1499    entry.SetDateEntered(datetime.datetime.now())
1500    entry.SetDescription(description)
1501    entry.SetInvAccount(account)
1502    entry.SetQuantity(gnc_numeric_from_decimal(quantity))
1503    entry.SetInvPrice(gnc_numeric_from_decimal(price))
1504
1505    return gnucash_simple.entryToDict(entry)
1506
1507def addBillEntry(book, bill_id, date, description, account_guid, quantity,
1508    price):
1509
1510    bill = getGnuCashBill(book,bill_id)
1511
1512    if bill is None:
1513        raise Error('NoBill', 'No bill exists with this ID',
1514            {'field': 'bill_id'})
1515
1516    try:
1517        date = datetime.datetime.strptime(date, "%Y-%m-%d")
1518    except ValueError:
1519        raise Error('InvalidDateOpened',
1520            'The date opened must be provided in the form YYYY-MM-DD',
1521            {'field': 'date'})
1522
1523    guid = gnucash.gnucash_core.GUID()
1524    gnucash.gnucash_core.GUIDString(account_guid, guid)
1525
1526    account = guid.AccountLookup(book)
1527
1528    if account is None:
1529        raise Error('NoAccount', 'No account exists with this GUID',
1530            {'field': 'account_guid'})
1531
1532    try:
1533        quantity = Decimal(quantity).quantize(Decimal('.01'))
1534    except ArithmeticError:
1535        raise Error('InvalidQuantity', 'This quantity is not valid',
1536            {'field': 'quantity'})
1537
1538    try:
1539        price = Decimal(price).quantize(Decimal('.01'))
1540    except ArithmeticError:
1541        raise Error('InvalidPrice', 'This price is not valid',
1542            {'field': 'price'})
1543
1544    entry = Entry(book, bill, date.date())
1545    entry.SetDateEntered(datetime.datetime.now())
1546    entry.SetDescription(description)
1547    entry.SetBillAccount(account)
1548    entry.SetQuantity(gnc_numeric_from_decimal(quantity))
1549    entry.SetBillPrice(gnc_numeric_from_decimal(price))
1550
1551    return gnucash_simple.entryToDict(entry)
1552
1553def getEntry(book, entry_guid):
1554
1555    guid = gnucash.gnucash_core.GUID()
1556    gnucash.gnucash_core.GUIDString(entry_guid, guid)
1557
1558    entry = book.EntryLookup(guid)
1559
1560    if entry is None:
1561        return None
1562    else:
1563        return gnucash_simple.entryToDict(entry)
1564
1565def updateEntry(book, entry_guid, date, description, account_guid, quantity,
1566    price):
1567
1568    guid = gnucash.gnucash_core.GUID()
1569    gnucash.gnucash_core.GUIDString(entry_guid, guid)
1570
1571    entry = book.EntryLookup(guid)
1572
1573    if entry is None:
1574        raise Error('NoEntry', 'No entry exists with this GUID',
1575            {'field': 'entry_guid'})
1576
1577    try:
1578        date = datetime.datetime.strptime(date, "%Y-%m-%d")
1579    except ValueError:
1580        raise Error('InvalidDateOpened',
1581            'The date opened must be provided in the form YYYY-MM-DD',
1582            {'field': 'date'})
1583
1584    gnucash.gnucash_core.GUIDString(account_guid, guid)
1585
1586    account = guid.AccountLookup(book)
1587
1588    if account is None:
1589        raise Error('NoAccount', 'No account exists with this GUID',
1590            {'field': 'account_guid'})
1591
1592    entry.SetDate(date.date())
1593    entry.SetDateEntered(datetime.datetime.now())
1594    entry.SetDescription(description)
1595    entry.SetInvAccount(account)
1596    entry.SetQuantity(
1597        gnc_numeric_from_decimal(Decimal(quantity).quantize(Decimal('.01'))))
1598    entry.SetInvPrice(
1599        gnc_numeric_from_decimal(Decimal(price).quantize(Decimal('.01'))))
1600
1601    return gnucash_simple.entryToDict(entry)
1602
1603def deleteEntry(book, entry_guid):
1604
1605    guid = gnucash.gnucash_core.GUID()
1606    gnucash.gnucash_core.GUIDString(entry_guid, guid)
1607
1608    entry = book.EntryLookup(guid)
1609
1610    invoice = entry.GetInvoice()
1611    bill = entry.GetBill()
1612
1613    if invoice != None and entry != None:
1614        invoice.RemoveEntry(entry)
1615    elif bill != None and entry != None:
1616        bill.RemoveEntry(entry)
1617
1618    if entry != None:
1619        entry.Destroy()
1620
1621def deleteTransaction(book, transaction_guid):
1622
1623    guid = gnucash.gnucash_core.GUID()
1624    gnucash.gnucash_core.GUIDString(transaction_guid, guid)
1625
1626    transaction = guid.TransLookup(book)
1627
1628    if transaction != None :
1629        transaction.Destroy()
1630
1631def addBill(book, id, vendor_id, currency_mnumonic, date_opened, notes):
1632
1633    vendor = book.VendorLookupByID(vendor_id)
1634
1635    if vendor is None:
1636        raise Error('NoVendor', 'A vendor with this ID does not exist',
1637            {'field': 'id'})
1638
1639    if id is None:
1640        id = book.BillNextID(vendor)
1641
1642    try:
1643        date_opened = datetime.datetime.strptime(date_opened, "%Y-%m-%d")
1644    except ValueError:
1645        raise Error('InvalidVendorDateOpened',
1646            'The date opened must be provided in the form YYYY-MM-DD',
1647            {'field': 'date_opened'})
1648
1649    if currency_mnumonic is None:
1650        currency_mnumonic = vendor.GetCurrency().get_mnemonic()
1651
1652    commod_table = book.get_table()
1653    currency = commod_table.lookup('CURRENCY', currency_mnumonic)
1654
1655    if currency is None:
1656        raise Error('InvalidVendorCurrency',
1657            'A valid currency must be supplied for this vendor',
1658            {'field': 'currency'})
1659
1660    bill = Bill(book, id, currency, vendor, date_opened.date())
1661
1662    bill.SetNotes(notes)
1663
1664    return gnucash_simple.billToDict(bill)
1665
1666def addAccount(book, name, currency_mnumonic, account_guid):
1667
1668    from gnucash.gnucash_core_c import \
1669    ACCT_TYPE_ASSET, ACCT_TYPE_RECEIVABLE, ACCT_TYPE_INCOME, \
1670    GNC_OWNER_CUSTOMER, ACCT_TYPE_LIABILITY
1671
1672    root_account = book.get_root_account()
1673
1674    commod_table = book.get_table()
1675    currency = commod_table.lookup('CURRENCY', currency_mnumonic)
1676
1677    if currency is None:
1678        raise Error('InvalidCustomerCurrency',
1679            'A valid currency must be supplied for this customer',
1680            {'field': 'currency'})
1681
1682    account = Account(book)
1683    root_account.append_child(root_account)
1684    account.SetName(name)
1685    account.SetType(ACCT_TYPE_ASSET)
1686    account.SetCommodity(currency)
1687
1688def addTransaction(book, num, description, date_posted, currency_mnumonic, splits):
1689
1690    transaction = Transaction(book)
1691
1692    transaction.BeginEdit()
1693
1694    commod_table = book.get_table()
1695    currency = commod_table.lookup('CURRENCY', currency_mnumonic)
1696
1697    if currency is None:
1698        raise Error('InvalidTransactionCurrency',
1699            'A valid currency must be supplied for this transaction',
1700            {'field': 'currency'})
1701
1702    try:
1703        date_posted = datetime.datetime.strptime(date_posted, "%Y-%m-%d")
1704    except ValueError:
1705        raise Error('InvalidDatePosted',
1706            'The date posted must be provided in the form YYYY-MM-DD',
1707            {'field': 'date_posted'})
1708
1709
1710    for split_values in splits:
1711        account_guid = gnucash.gnucash_core.GUID()
1712        gnucash.gnucash_core.GUIDString(split_values['account_guid'], account_guid)
1713
1714        account = account_guid.AccountLookup(book)
1715
1716        if account is None:
1717            raise Error('InvalidSplitAccount',
1718                'A valid account must be supplied for this split',
1719                {'field': 'account'})
1720
1721        split = Split(book)
1722        split.SetValue(GncNumeric(split_values['value'], 100))
1723        split.SetAccount(account)
1724        split.SetParent(transaction)
1725
1726    transaction.SetCurrency(currency)
1727    transaction.SetDescription(description)
1728    transaction.SetNum(num)
1729
1730    transaction.SetDatePostedTS(date_posted)
1731
1732    transaction.CommitEdit()
1733
1734    return gnucash_simple.transactionToDict(transaction, ['splits'])
1735
1736def getTransaction(book, transaction_guid):
1737
1738    guid = gnucash.gnucash_core.GUID()
1739    gnucash.gnucash_core.GUIDString(transaction_guid, guid)
1740
1741    transaction = guid.TransLookup(book)
1742
1743    if transaction is None:
1744        return None
1745    else:
1746        return gnucash_simple.transactionToDict(transaction, ['splits'])
1747
1748def editTransaction(book, transaction_guid, num, description, date_posted,
1749    currency_mnumonic, splits):
1750
1751    guid = gnucash.gnucash_core.GUID()
1752    gnucash.gnucash_core.GUIDString(transaction_guid, guid)
1753
1754    transaction = guid.TransLookup(book)
1755
1756    if transaction is None:
1757        raise Error('NoCustomer',
1758            'A transaction with this GUID does not exist',
1759            {'field': 'guid'})
1760
1761    transaction.BeginEdit()
1762
1763    commod_table = book.get_table()
1764    currency = commod_table.lookup('CURRENCY', currency_mnumonic)
1765
1766    if currency is None:
1767        raise Error('InvalidTransactionCurrency',
1768            'A valid currency must be supplied for this transaction',
1769            {'field': 'currency'})
1770
1771
1772    try:
1773        date_posted = datetime.datetime.strptime(date_posted, "%Y-%m-%d")
1774    except ValueError:
1775        raise Error('InvalidDatePosted',
1776            'The date posted must be provided in the form YYYY-MM-DD',
1777            {'field': 'date_posted'})
1778
1779    for split_values in splits:
1780
1781        split_guid = gnucash.gnucash_core.GUID()
1782        gnucash.gnucash_core.GUIDString(split_values['guid'], split_guid)
1783
1784        split = split_guid.SplitLookup(book)
1785
1786        if split is None:
1787            raise Error('InvalidSplitGuid',
1788                'A valid guid must be supplied for this split',
1789                {'field': 'guid'})
1790
1791        account_guid = gnucash.gnucash_core.GUID()
1792        gnucash.gnucash_core.GUIDString(
1793            split_values['account_guid'], account_guid)
1794
1795        account = account_guid.AccountLookup(book)
1796
1797        if account is None:
1798            raise Error('InvalidSplitAccount',
1799                'A valid account must be supplied for this split',
1800                {'field': 'account'})
1801
1802        split.SetValue(GncNumeric(split_values['value'], 100))
1803        split.SetAccount(account)
1804        split.SetParent(transaction)
1805
1806    transaction.SetCurrency(currency)
1807    transaction.SetDescription(description)
1808    transaction.SetNum(num)
1809
1810    transaction.SetDatePostedTS(date_posted)
1811
1812    transaction.CommitEdit()
1813
1814    return gnucash_simple.transactionToDict(transaction, ['splits'])
1815
1816def gnc_numeric_from_decimal(decimal_value):
1817    sign, digits, exponent = decimal_value.as_tuple()
1818
1819    # convert decimal digits to a fractional numerator
1820    # equivlent to
1821    # numerator = int(''.join(digits))
1822    # but without the wated conversion to string and back,
1823    # this is probably the same algorithm int() uses
1824    numerator = 0
1825    TEN = int(Decimal(0).radix()) # this is always 10
1826    numerator_place_value = 1
1827    # add each digit to the final value multiplied by the place value
1828    # from least significant to most sigificant
1829    for i in range(len(digits)-1,-1,-1):
1830        numerator += digits[i] * numerator_place_value
1831        numerator_place_value *= TEN
1832
1833    if decimal_value.is_signed():
1834        numerator = -numerator
1835
1836    # if the exponent is negative, we use it to set the denominator
1837    if exponent < 0 :
1838        denominator = TEN ** (-exponent)
1839    # if the exponent isn't negative, we bump up the numerator
1840    # and set the denominator to 1
1841    else:
1842        numerator *= TEN ** exponent
1843        denominator = 1
1844
1845    return GncNumeric(numerator, denominator)
1846
1847def shutdown():
1848    session.save()
1849    session.end()
1850    session.destroy()
1851    print('Shutdown')
1852
1853class Error(Exception):
1854    """Base class for exceptions in this module."""
1855    def __init__(self, type, message, data):
1856        self.type = type
1857        self.message = message
1858        self.data = data
1859
1860try:
1861    options, arguments = getopt.getopt(sys.argv[1:], 'nh:', ['host=', 'new='])
1862except getopt.GetoptError as err:
1863    print(str(err)) # will print something like "option -a not recognized"
1864    print('Usage: python-rest.py <connection string>')
1865    sys.exit(2)
1866
1867if len(arguments) == 0:
1868    print('Usage: python-rest.py <connection string>')
1869    sys.exit(2)
1870
1871#set default host for Flask
1872host = '127.0.0.1'
1873
1874#allow host option to be changed
1875for option, value in options:
1876    if option in ("-h", "--host"):
1877        host = value
1878
1879is_new = False
1880
1881# allow a new database to be used
1882for option, value in options:
1883    if option in ("-n", "--new"):
1884        is_new = True
1885
1886
1887#start gnucash session base on connection string argument
1888if is_new:
1889    session = gnucash.Session(arguments[0], SessionOpenMode.SESSION_NEW_STORE)
1890
1891    # seem to get errors if we use the session directly, so save it and
1892    #destroy it so it's no longer new
1893
1894    session.save()
1895    session.end()
1896    session.destroy()
1897
1898# unsure about SESSION_BREAK_LOCK - it used to be ignore_lock=True
1899session = gnucash.Session(arguments[0], SessionOpenMode.SESSION_BREAK_LOCK)
1900
1901# register method to close gnucash connection gracefully
1902atexit.register(shutdown)
1903
1904app.debug = False
1905
1906# log to console
1907if not app.debug:
1908    import logging
1909    from logging import StreamHandler
1910    stream_handler = StreamHandler()
1911    stream_handler.setLevel(logging.ERROR)
1912    app.logger.addHandler(stream_handler)
1913
1914# start Flask server
1915app.run(host=host)
1916