Package: exactonline
Exact Online REST API Library in PythonExact Online provides accounting software in a Software-as-a-Service
delivery model. It implements an API through a REST interface. This
library aims to ease its use.Quick jumpUsage by exampleUsing element adaptersSetting up the linkImplemented resourcesOther benefitsLicenseTODOFurther readingUsage by exampleSet up the basics:fromexactonline.apiimportExactApifromexactonline.exceptionsimportObjectDoesNotExistfromexactonline.resourceimportGET,POST,PUT,DELETEfromexactonline.storage.iniimportIniStorage# Create a function to get the api with your own storage backend.defget_api():# NOTE: The IniStorage is really simple does not synchronize with# other instances. You should create your own storage. See below.storage=IniStorage('/path/to/config.ini')returnExactApi(storage=storage)api=get_api()Get an invoice:# Get an invoice by your own invoice number (YourRef).# Returns a dictionary, or raises ObjectDoesNotExist.invoice=api.invoices.get(invoice_number='F0005555')It looks somewhat like this:invoice=={u'AmountDC':50.8,u'AmountFC':50.8,# ...u'SalesEntryLines':[{u'AmountDC':41.98,u'AmountFC':41.98,# ...u'Description':u'Omzet backups',u'VATBaseAmountDC':41.98,u'VATBaseAmountFC':41.98},],# ...u'VATAmountDC':8.82,u'VATAmountFC':8.82,u'YourRef':u'F0005555',u'__metadata':{u'type':u'Exact.Web.Api.Models.SalesEntry',u'uri':u"https://start.exactonline.nl/api/v1/..."},}Get relations:relations_limit_2=api.relations.filter(top=2)# that was cheaper than: api.relations.all()[0:2]relations_limit_2==[{u'Code':u'              1068',u'ID':u'11111111-2222-3333-4444-555555555555',u'Name':u'ACME Corporation',u'__metadata':{u'type':u'Exact.Web.Api.Models.Account',u'uri':u"https://start.exactonline.nl/api/v1/...')"}},{u'Code':u'               555',u'ID':u'22222222-3333-4444-5555-666666666666',u'Name':u'Daffy Duck Ltd.',u'__metadata':{u'type':u'Exact.Web.Api.Models.Account',u'uri':u"https://start.exactonline.nl/api/v1/...')"}}]Update a relation:daffy_duck=api.relations.get(relation_code='555')api.relations.update(daffy_duck['ID'],{'Name':'Daffy Duck and sons'})Delete a relation:daffy_duck=api.relations.get(relation_code='555')api.relations.delete(daffy_duck['ID'])Create an invoice:customer_data=api.relations.get(relation_code='123')# local relation_codecustomer_guid=customer_data['ID']invoice_data={'AmountDC':str(amount_with_vat),# DC = default currency'AmountFC':str(amount_with_vat),# FC = foreign currency'EntryDate':invoice_date.strftime('%Y-%m-%dT%H:%M:%SZ'),# pretend we're in UTC'Customer':customer_guid,'Description':u'Invoice description','Journal':remote_journal,# 70 "Verkoopboek"'ReportingPeriod':invoice_date.month,'ReportingYear':invoice_date.year,'SalesEntryLines':[],'VATAmountDC':str(vat_amount),'VATAmountFC':str(vat_amount),'YourRef':local_invoice_number,# must start uniquely at the start of a year, defaults to:# YYJJ0001 where YY=invoice_date.year, and JJ=remote_journal'InvoiceNumber':'%d%d%04d'%(invoice_date.year,remote_journal,int(local_invoice_number)),}# The SalesEntryLines need to be filled with a bunch of dictionaries# with these keys: AmountDC, AmountFC, Description, GLAccount,# VATCode where GLAccount holds the Journal remote GUID, and the# amounts are without VAT.api.invoices.create(invoice_dict)You may need to play around a bit to find out which fields are
mandatory, and what kind of values the fields need.  TheExact Online
REST resources listisn’t always clear on that.Using element adaptersUsing the above works, but it’s not really object oriented. If
available, you may be better off using one of the adaptable classes inexactonline.elementsand subclassing that.For example, this is how you could create your own interface to an Exact
Online customer.# Assuming you have a MyRelation that looks like this:classMyRelation(object):relcode=12345first_name='John'last_name='Doe'billing_address=None# ...# You could create an adapter subclass of ExactCustomer like this:classMyExactCustomer(ExactCustomer):def__init__(self,my_relation=None,**kwargs):super(MyExactCustomer,self).__init__(**kwargs)self._my_relation=my_relationdefget_code(self):returnstr(self._my_relation.relcode)defget_name(self):return' '.join([self._my_relation.first_name,self._my_relation.last_name])defget_address(self):address=self._my_relation.billing_addressifaddress:return{'AddressLine1':address.street_and_number(),'Postcode':address.zipcode,'City':address.city.name,}return{}If you have the above set up, and have unique customer codes, then
writing/updating an Exact Online relation is as convenient as this:johndoe=MyRelation(...)exactonline_relation=MyExactCustomer(my_relation=johndoe,api=api)ret=exactonline_relation.commit()These adaptable elements are currently implemented for writing customers
(ExactCustomer) and invoices (ExactInvoice). See the files inexactonline/elements/for more info.Setting up the linkYou’ll need a storage backend. The defaultIniStoragecan be taken fromexactonline.storage.fromexactonline.storage.iniimportIniStorageclassMyIniStorage(IniStorage):defget_response_url(self):"Configure your custom response URL."returnself.get_base_url()+'/oauth/success/'storage=MyIniStorage('/path/to/config.ini')NoteThe IniStorage does not synchronize with other instancesThis means that you cannot use multiple IniStorage instances at the same
time. If you want that, or thread-safe/multi-process-safe operations, or
better yet, integration with your project storage (database?), you should
create your custom storage backend. Look in ``exactonline/storage/base.py``
to see how. It’s really easy.You need to set up access to your Exact Online SaaS instance, by creating an
export link. Seecreating Exact Online credentialsfor more info.Take that info, and configure it in yourconfig.ini.[server]auth_url=https://start.exactonline.co.uk/api/oauth2/authrest_url=https://start.exactonline.co.uk/apitoken_url=https://start.exactonline.co.uk/api/oauth2/token[application]base_url=https://example.comclient_id={12345678-abcd-1234-abcd-0123456789ab}client_secret=ZZZ999xxx000; optional config:iteration_limit=50Create an initial URL:api=ExactApi(storage=storage)url=api.create_auth_request_url()The URL will look like this; redirect the user there so he may
authenticate and allow your application access to Exact Online (this is
OAuth):https://start.exactonline.nl/api/oauth2/auth?
  client_id=%7B12345678-abcd-1234-abcd-0123456789ab%7D&
  redirect_uri=https%3A//example.com/oauth/success/&
  response_type=codeAfter authentication he will get redirected back to:https://example.com/oauth/success/?code=...You should implement a view on that URL, that does basically this:api.request_token(code)At this point, you should configure your default division, if you
haven’t already:division_choices,current_division=api.get_divisions()api.set_division(division_choices[0][0])# select ID of first divisionNow you’re all set!Implemented resourcesViewexactonline/api/__init__.pyto see which resource helpers are
implemented.Currently, it looks like this:contacts=Contacts.as_property()invoices=Invoices.as_property()ledgeraccounts=LedgerAccounts.as_property()receivables=Receivables.as_property()relations=Relations.as_property()vatcodes=VatCodes.as_property()But you can call resources which don’t have a helper directly. The
following two three are equivalent:api.relations.all()api.restv1(GET('crm/Accounts'))api.rest(GET('v1/%d/crm/Accounts'%selected_division))As are the following three:api.relations.filter(top=2)api.restv1(GET('crm/Accounts?$top=2'))api.rest(GET('v1/%d/crm/Accounts?$top=2'%selected_division))And these:api.invoices.filter(filter="EntryDate gt datetime'2015-01-01'")api.restv1(GET('salesentry/SalesEntries?'+'$filter=EntryDate%20gt%20datetime%272015-01-01%27'))api.rest(GET('v1/%d/salesentry/SalesEntries?'+'$filter=EntryDate%%20gt%%20datetime%%272015-01-01%%27'%selected_division)# convinced yet that the helpers are useful?See theExact Online REST resources listfor all available resources.Other benefitsThe ExactApi class ensures that:Tokens are refreshed as needed (see:exactonline/api/autorefresh.py).Paginated lists are automatically downloaded in full (see:exactonline/api/unwrap.py).Creating Exact Online credentialsPreviously, one could create an API from the Exact Online interface directly.
This was removed at some point between 2014 and 2015.According to the“how can I create an application key?” FAQ entryyou must now create one through the App Center.Why am I unable to see the Register an API link and how can I
create an application key?All registrations are now configured through the App Center.
Previously you were able to generate an Application Key and/or create an
OAuth registration within your Exact Online.In Exact Online you can create an app registration for private use
(customer account) or an app registration for commercial use (partner
account). Go to Target groups and site maps for more information.If the Register API Key link is not visible in the App Center
menu you do not have the correct rights to view it. To make the
link visible go to, Username > My Exact Online > Rights and
select Manage subscription.Log into theExact Online App Center, click MANAGE APPS (APPS BEHEREN);
it should be a large link visible on the Top Right. Make sure the redirect
URI has the same transport+domainname as the site that you wish to connect.For sites with an internal URI only, you may need to alter the hostname
temporarily when registering. Generate the register URL withapi.create_auth_request_urland alter it as appropriate.After creating the App, you can go back and fetch the theClient IDand theClient secret.LicenseExact Online REST API Library in Python is free software: you can
redistribute it and/or modify it under the terms of the GNU Lesser
General Public License as published by the Free Software Foundation,
version 3 or any later version.TODOFix so file Copyright headers are auto-populated (and date-updated).Further readingExact Online REST API.Exact Online REST resources list.Tips by Bas van Beek.Changesv0.4.0:Added ratelimiter, requested and tested by various people.Added debug logs, by Martijn Hemeryck (@mhemeryck).Cleanup. Also by Eike Dehling (@EikeDehling).v0.3.6:Try refreshing tokensbeforereceiving a 401 per the request of
Alexandar Mechev (@apmechev).v0.3.5:Implement quotations. Thanks Ties (@ties-v).Updated/added assets, tests and docs.v0.3.4:Added bankaccounts manager. Closes #21. Thanks Edwin van de Ven
(@edwinvandeven).Added global configurable iteration limit parameter.Default unpagination was limited to 50 to avoid infinite loops in
case of bugs or bad data. Some users require more pages of output.Set the iteration_limit in your config storage to a higher value if
you run into “Iteration … limit reached!”.Closes #22. Thanks George Viorel Visniuc (@gvisniuc).Move storage/config backends to separate storage subdir. This way we
don’t require the configparse if you’re not using the IniStorage.
Shows a DeprecationWarning so you have time to update your code.Closes #19.v0.3.3:Additions to ExactInvoice element:Add optional “month” to get_ledger_lines() for accrued/deferred
income.Add get_vatcode_for_ledger_line() for customizable VATCodes.v0.3.2:Fix ConfigParser deprecationwarning.Add VatCodes API support. Thanks Stani Michiels (@stanim). Closes
#12.v0.3.1:Add Contacts API support. Thanks @Imperatus. Closes #8 and #11.v0.3.0:Refactor code to use fewer arguments. This makes the BetterCodeHub
checks happy and that makes me happy. This is a BACKWARDS
INCOMPATIBLE CHANGE. All your code that looked like this:ret = self.rest('GET', 'v1/current/Me?$select=CurrentDivision')will have to be changed to this:from exactonline.resource import GET
ret = self.rest(GET('v1/current/Me?$select=CurrentDivision'))Closes #7.Add IniStorage default return value for get_default_url(), returning
get_base_url(). This makes life easier for those who did not read
the README. Closes #6.v0.2.5:Fix set_tokens() bug in Python3; make sure json.loads() gets an
unistr. Reported by @Imperatus, @LordGaav. Closes #9 and #10.v0.2.4:Improve HTTP functions for better error reporting and better Py23
compatibility.v0.2.3:Add api.invoices.filter(invoice_number__in=LIST) filter.Iterate over resultsets instead of using recursion (api.unwrap).Fix python3 compatibility issue: bytestring found on autorefresh.
Thanks @wpxgit.v0.2.2:Correct RST source in CHANGES file. Did not display properly on
PyPI.v0.2.1:Correct example Python code in docs. Thanks @alexBaizeau.Addexactonline.elementsthat were missing in the 0.2.0
distributed package. Thanks @hcwsegers.Add overridableget_ledger_code_to_guid_maps()helper to the
invoices elements class.v0.2.0:Update build/PyPI info: move to Stable, add Python 3.4 and 3.5
version, fix Python 3 compatibility.Fix a few README RST issues. Update OAuth documentation.Add initialexactonline.elementsto use for easier object
oriented data submissions.Add and improve SSL, API and Python3 tests.v0.1.3:Addreceivablesmanager to the API. This manager allows you to
build a list similar to theOutstanding Receivablespage ofFinancial Reporting.Addapi.invoices.map_exact2foreign_invoice_numbersandapi.invoices.map_foreign2exact_invoice_numbersmethods to
quickly get a mapping between our own and the ExactOnline invoice
numbers.Python3 compatibility.Minor fixes.