elmas 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +52 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Guardfile +11 -0
- data/README.md +149 -0
- data/Rakefile +11 -0
- data/bin/console +16 -0
- data/bin/setup +7 -0
- data/elmas.gemspec +33 -0
- data/jenkins.sh +33 -0
- data/lib/elmas.rb +33 -0
- data/lib/elmas/api.rb +30 -0
- data/lib/elmas/client.rb +11 -0
- data/lib/elmas/config.rb +96 -0
- data/lib/elmas/exception.rb +19 -0
- data/lib/elmas/log.rb +17 -0
- data/lib/elmas/oauth.rb +109 -0
- data/lib/elmas/parser.rb +15 -0
- data/lib/elmas/request.rb +58 -0
- data/lib/elmas/resource.rb +112 -0
- data/lib/elmas/resources/account.rb +40 -0
- data/lib/elmas/resources/contact.rb +18 -0
- data/lib/elmas/resources/invoice.rb +19 -0
- data/lib/elmas/resources/invoice_line.rb +18 -0
- data/lib/elmas/resources/item.rb +17 -0
- data/lib/elmas/resources/journal.rb +17 -0
- data/lib/elmas/response.rb +79 -0
- data/lib/elmas/uri.rb +50 -0
- data/lib/elmas/utils.rb +48 -0
- data/lib/elmas/version.rb +13 -0
- metadata +264 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Elmas
|
2
|
+
class BadRequestException < Exception
|
3
|
+
def initialize(object)
|
4
|
+
super(message)
|
5
|
+
@object = object
|
6
|
+
end
|
7
|
+
|
8
|
+
def message
|
9
|
+
case @object
|
10
|
+
when 500
|
11
|
+
"Server error"
|
12
|
+
else
|
13
|
+
"Something went wrong"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class UnauthorizedException < Exception; end
|
19
|
+
end
|
data/lib/elmas/log.rb
ADDED
data/lib/elmas/oauth.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require "mechanize"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
require File.expand_path("../utils", __FILE__)
|
5
|
+
require File.expand_path("../response", __FILE__)
|
6
|
+
|
7
|
+
# from https://developers.exactonline.com/#Example retrieve access token.html
|
8
|
+
module Elmas
|
9
|
+
module OAuth
|
10
|
+
def authorize(user_name, password, options = {})
|
11
|
+
agent = Mechanize.new
|
12
|
+
|
13
|
+
login(agent, user_name, password, options)
|
14
|
+
allow_access(agent)
|
15
|
+
|
16
|
+
code = URI.unescape(agent.page.uri.query.split("=").last)
|
17
|
+
OauthResponse.new(get_access_token(code))
|
18
|
+
end
|
19
|
+
|
20
|
+
def authorized?
|
21
|
+
response = get("/Current/Me", no_division: true)
|
22
|
+
!response.unauthorized?
|
23
|
+
# Do a test call, return false if 401 or any error code
|
24
|
+
end
|
25
|
+
|
26
|
+
def authorize_division
|
27
|
+
get("/Current/Me", no_division: true).first.current_division
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return URL for OAuth authorization
|
31
|
+
def authorize_url(options = {})
|
32
|
+
options[:response_type] ||= "code"
|
33
|
+
options[:redirect_uri] ||= redirect_uri
|
34
|
+
params = authorization_params.merge(options)
|
35
|
+
uri = URI("https://start.exactonline.nl/api/oauth2/auth/")
|
36
|
+
uri.query = URI.encode_www_form(params)
|
37
|
+
uri.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return an access token from authorization
|
41
|
+
def get_access_token(code, _options = {})
|
42
|
+
conn = Faraday.new(url: "https://start.exactonline.nl") do |faraday|
|
43
|
+
faraday.request :url_encoded
|
44
|
+
faraday.adapter Faraday.default_adapter
|
45
|
+
end
|
46
|
+
params = access_token_params(code)
|
47
|
+
conn.post do |req|
|
48
|
+
req.url "/api/oauth2/token"
|
49
|
+
req.body = params
|
50
|
+
req.headers["Accept"] = "application/json"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def login(agent, user_name, password, options)
|
57
|
+
# Login
|
58
|
+
agent.get(authorize_url(options)) do |page|
|
59
|
+
form = page.forms.first
|
60
|
+
form["UserNameField"] = user_name
|
61
|
+
form["PasswordField"] = password
|
62
|
+
form.click_button
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def allow_access(agent)
|
67
|
+
return if agent.page.uri.to_s.include?("getpostman")
|
68
|
+
form = agent.page.form_with(id: "PublicOAuth2Form")
|
69
|
+
button = form.button_with(id: "AllowButton")
|
70
|
+
agent.submit(form, button)
|
71
|
+
end
|
72
|
+
|
73
|
+
def authorization_params
|
74
|
+
{
|
75
|
+
client_id: client_id
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def access_token_params(code)
|
80
|
+
{
|
81
|
+
client_id: client_id,
|
82
|
+
client_secret: client_secret,
|
83
|
+
grant_type: "authorization_code",
|
84
|
+
code: code,
|
85
|
+
redirect_uri: redirect_uri
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
module Elmas
|
92
|
+
class OauthResponse < Response
|
93
|
+
def body
|
94
|
+
JSON.parse(@response.body)
|
95
|
+
end
|
96
|
+
|
97
|
+
def access_token
|
98
|
+
body["access_token"]
|
99
|
+
end
|
100
|
+
|
101
|
+
def division
|
102
|
+
body["division"]
|
103
|
+
end
|
104
|
+
|
105
|
+
def refresh_token
|
106
|
+
body["refresh_token"]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/elmas/parser.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Elmas
|
2
|
+
# Defines HTTP request methods
|
3
|
+
module Request
|
4
|
+
# Perform an HTTP GET request
|
5
|
+
def get(path, options = {})
|
6
|
+
request(:get, path, options)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Perform an HTTP POST request
|
10
|
+
def post(path, options = {})
|
11
|
+
request(:post, path, options)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Perform an HTTP PUT request
|
15
|
+
def put(path, options = {})
|
16
|
+
request(:put, path, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Perform an HTTP DELETE request
|
20
|
+
def delete(path, options = {})
|
21
|
+
request(:delete, path, options)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def build_path(path, options)
|
27
|
+
path = "#{division}/#{path}" unless options[:no_division]
|
28
|
+
path = "#{endpoint}/#{path}" unless options[:no_endpoint]
|
29
|
+
path = "#{options[:url] || base_url}/#{path}"
|
30
|
+
path
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_headers
|
34
|
+
headers = {}
|
35
|
+
headers["Content-Type"] = "application/#{response_format}"
|
36
|
+
headers["Accept"] = "application/#{response_format}"
|
37
|
+
headers["Authorization"] = "Bearer #{access_token}" if access_token
|
38
|
+
headers
|
39
|
+
end
|
40
|
+
|
41
|
+
# Perform an HTTP request
|
42
|
+
def request(method, path, options = {})
|
43
|
+
path = build_path(path, options)
|
44
|
+
|
45
|
+
response = connection.send(method) do |request|
|
46
|
+
case method
|
47
|
+
when :post, :put
|
48
|
+
request.url path
|
49
|
+
request.body = options[:params].to_json
|
50
|
+
when :get, :delete
|
51
|
+
request.url path
|
52
|
+
end
|
53
|
+
request.headers = add_headers
|
54
|
+
end
|
55
|
+
Response.new(response)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require File.expand_path("../utils", __FILE__)
|
2
|
+
require File.expand_path("../exception", __FILE__)
|
3
|
+
require File.expand_path("../uri", __FILE__)
|
4
|
+
|
5
|
+
module Elmas
|
6
|
+
module Resource
|
7
|
+
include UriMethods
|
8
|
+
|
9
|
+
STANDARD_FILTERS = [:id].freeze
|
10
|
+
|
11
|
+
attr_accessor :attributes, :url
|
12
|
+
attr_reader :response
|
13
|
+
|
14
|
+
def initialize(attributes = {})
|
15
|
+
@attributes = Utils.normalize_hash(attributes)
|
16
|
+
@filters = STANDARD_FILTERS
|
17
|
+
@query = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_all(options = {})
|
21
|
+
@order_by = options[:order_by]
|
22
|
+
@select = options[:select]
|
23
|
+
get(uri([:order, :select]))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Pass filters in an array, for example 'filters: [:id, :name]'
|
27
|
+
def find_by(options = {})
|
28
|
+
@filters = options[:filters]
|
29
|
+
@order_by = options[:order_by]
|
30
|
+
@select = options[:select]
|
31
|
+
get(uri([:order, :select, :filters]))
|
32
|
+
end
|
33
|
+
|
34
|
+
def find
|
35
|
+
return nil unless id?
|
36
|
+
get(uri([:filters]))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Normally use the url method (which applies the filters) but sometimes you only want to use the base path or other paths
|
40
|
+
def get(uri = self.uri)
|
41
|
+
@response = Elmas.get(URI.unescape(uri.to_s))
|
42
|
+
end
|
43
|
+
|
44
|
+
def valid?
|
45
|
+
valid = true
|
46
|
+
mandatory_attributes.each do |attribute|
|
47
|
+
valid = @attributes.key? attribute
|
48
|
+
end
|
49
|
+
valid
|
50
|
+
end
|
51
|
+
|
52
|
+
def id?
|
53
|
+
!@attributes[:id].nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
def save
|
57
|
+
attributes_to_submit = sanitize
|
58
|
+
if valid?
|
59
|
+
if id?
|
60
|
+
return @response = Elmas.put(basic_identifier_uri, params: attributes_to_submit)
|
61
|
+
else
|
62
|
+
return @response = Elmas.post(base_path, params: attributes_to_submit)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
Elmas.error("Invalid Resource #{self.class.name}, attributes: #{@attributes.inspect}")
|
66
|
+
Elmas::Response.new(Faraday::Response.new(status: 400, body: "Invalid Request"))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def delete
|
71
|
+
return nil unless id?
|
72
|
+
Elmas.delete(basic_identifier_uri)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parse the attributes for to post to the API
|
76
|
+
def sanitize
|
77
|
+
to_submit = {}
|
78
|
+
@attributes.each do |key, value|
|
79
|
+
next if key == :id || !valid_attribute?(key)
|
80
|
+
key = Utils.parse_key(key)
|
81
|
+
value.is_a?(Elmas::Resource) ? submit_value = value.id : submit_value = value # Turn relation into ID
|
82
|
+
to_submit[key] = submit_value
|
83
|
+
end
|
84
|
+
to_submit
|
85
|
+
end
|
86
|
+
|
87
|
+
# Getter/Setter for resource
|
88
|
+
def method_missing(method, *args, &block)
|
89
|
+
yield if block
|
90
|
+
if /^(\w+)=$/ =~ method
|
91
|
+
set_attribute($1, args[0])
|
92
|
+
else
|
93
|
+
nil unless @attributes[method.to_sym]
|
94
|
+
end
|
95
|
+
@attributes[method.to_sym]
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def set_attribute(attribute, value)
|
101
|
+
@attributes[attribute.to_sym] = value if valid_attribute?(attribute)
|
102
|
+
end
|
103
|
+
|
104
|
+
def valid_attribute?(attribute)
|
105
|
+
valid_attributes.include?(attribute.to_sym)
|
106
|
+
end
|
107
|
+
|
108
|
+
def valid_attributes
|
109
|
+
@valid_attributes ||= mandatory_attributes.inject(other_attributes, :<<)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Elmas
|
2
|
+
class Account
|
3
|
+
# An account needs a name
|
4
|
+
include Elmas::Resource
|
5
|
+
|
6
|
+
def base_path
|
7
|
+
"crm/Accounts"
|
8
|
+
end
|
9
|
+
|
10
|
+
def mandatory_attributes
|
11
|
+
[:name]
|
12
|
+
end
|
13
|
+
|
14
|
+
# https://start.exactonline.nl/docs/HlpRestAPIResourcesDetails.aspx?id=9
|
15
|
+
def other_attributes # rubocop:disable Metrics/MethodLength
|
16
|
+
[
|
17
|
+
:accountant, :account_manager, :activity_sector,
|
18
|
+
:activity_sub_sector, :address_line1, :address_line2,
|
19
|
+
:address_line3, :blocked, :business_type, :can_drop_ship,
|
20
|
+
:chamber_of_commerce, :city, :code, :code_at_supplier,
|
21
|
+
:company_size, :consolidation_scenario, :controlled_date,
|
22
|
+
:cost_paid, :country, :credit_line_purchase, :credit_line_sales,
|
23
|
+
:discount_purchase, :discount_sales, :email, :end_date, :fax,
|
24
|
+
:intra_stat_area, :intra_stat_delivery_term, :intra_stat_system,
|
25
|
+
:intra_stat_transaction_a, :intra_stat_transaction_b,
|
26
|
+
:intra_stat_transport_method, :invoice_acount, :invoice_attachment_type,
|
27
|
+
:invoicing_method, :is_accountant, :is_agency, :is_competitor, :is_mailing,
|
28
|
+
:is_pilot, :is_reseller, :is_sales, :is_supplier, :language, :latitude,
|
29
|
+
:lead_source, :logo, :logo_file_name, :longitude, :main_contact,
|
30
|
+
:payment_condition_purchase, :payment_condition_sales, :phone,
|
31
|
+
:phone_extension, :postcode, :price_list, :purchase_currency,
|
32
|
+
:purchase_lead_days, :purchase_VAT_code, :recipient_of_commissions,
|
33
|
+
:remarks, :reseller, :sales_currency, :sales_tax_schedule, :sales_vat_code,
|
34
|
+
:search_code, :security_level, :seperate_inv_per_project, :seperate_inv_per_subscription,
|
35
|
+
:shipping_lead_days, :shipping_method, :start_date, :state, :status,
|
36
|
+
:VAT_liability, :VAT_number, :website
|
37
|
+
]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Elmas
|
2
|
+
class Contact
|
3
|
+
# A contact needs a First and Last name and a reference to an account
|
4
|
+
include Elmas::Resource
|
5
|
+
|
6
|
+
def base_path
|
7
|
+
"crm/Contacts"
|
8
|
+
end
|
9
|
+
|
10
|
+
def mandatory_attributes
|
11
|
+
[:first_name, :last_name, :account]
|
12
|
+
end
|
13
|
+
|
14
|
+
def other_attributes
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Elmas
|
2
|
+
class Invoice
|
3
|
+
# An invoice usually has multiple invoice lines
|
4
|
+
# It should also have a journal id and a contact id who ordered it
|
5
|
+
include Elmas::Resource
|
6
|
+
|
7
|
+
def base_path
|
8
|
+
"salesinvoice/SalesInvoices"
|
9
|
+
end
|
10
|
+
|
11
|
+
def mandatory_attributes
|
12
|
+
[:journal, :ordered_by]
|
13
|
+
end
|
14
|
+
|
15
|
+
def other_attributes
|
16
|
+
[:sales_invoice_lines, :type]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Elmas
|
2
|
+
class InvoiceLine
|
3
|
+
# An invoice_line should always have a reference to an item and to an invoice.
|
4
|
+
include Elmas::Resource
|
5
|
+
|
6
|
+
def base_path
|
7
|
+
"salesinvoice/SalesInvoiceLines"
|
8
|
+
end
|
9
|
+
|
10
|
+
def mandatory_attributes
|
11
|
+
[:item]
|
12
|
+
end
|
13
|
+
|
14
|
+
def other_attributes
|
15
|
+
[:discount, :quantity, :amount_FC, :description, :vat_code]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|