elmas 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|