business-central 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +120 -0
- data/lib/business_central.rb +22 -0
- data/lib/business_central/client.rb +94 -0
- data/lib/business_central/exceptions.rb +55 -0
- data/lib/business_central/object/account.rb +11 -0
- data/lib/business_central/object/base.rb +105 -0
- data/lib/business_central/object/company.rb +11 -0
- data/lib/business_central/object/helper.rb +15 -0
- data/lib/business_central/object/item.rb +50 -0
- data/lib/business_central/object/purchase_invoice.rb +77 -0
- data/lib/business_central/object/purchase_invoice_line.rb +41 -0
- data/lib/business_central/object/request.rb +80 -0
- data/lib/business_central/object/response.rb +60 -0
- data/lib/business_central/object/validation.rb +54 -0
- data/lib/business_central/object/vendor.rb +45 -0
- data/lib/business_central/version.rb +3 -0
- data/lib/core_ext/string.rb +25 -0
- data/test/business_central/client_test.rb +77 -0
- data/test/business_central/object/account_test.rb +48 -0
- data/test/business_central/object/company_test.rb +47 -0
- data/test/business_central/object/item_test.rb +128 -0
- data/test/business_central/object/purchase_invoice_line_test.rb +129 -0
- data/test/business_central/object/purchase_invoice_test.rb +125 -0
- data/test/business_central/object/request_test.rb +82 -0
- data/test/business_central/object/response_test.rb +26 -0
- data/test/business_central/object/validation_test.rb +61 -0
- data/test/business_central/object/vendor_test.rb +125 -0
- data/test/business_central_test.rb +7 -0
- data/test/test_helper.rb +11 -0
- metadata +190 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module BusinessCentral
|
2
|
+
module Object
|
3
|
+
module Helper
|
4
|
+
def object(object_name, *params)
|
5
|
+
define_method(object_name) do |argument=nil|
|
6
|
+
object = "@#{object_name}_cache".to_sym
|
7
|
+
if !instance_variable_defined?(object)
|
8
|
+
instance_variable_set(object, BusinessCentral::Object.const_get("#{object_name.to_s.to_camel_case(true)}".to_sym).new(self, argument))
|
9
|
+
end
|
10
|
+
instance_variable_get(object)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module BusinessCentral
|
2
|
+
module Object
|
3
|
+
class Item < Base
|
4
|
+
OBJECT = 'items'.freeze
|
5
|
+
|
6
|
+
OBJECT_VALIDATION = {
|
7
|
+
number: {
|
8
|
+
maximum_length: 20
|
9
|
+
},
|
10
|
+
display_name: {
|
11
|
+
maximum_length: 100
|
12
|
+
},
|
13
|
+
type: {
|
14
|
+
required: true,
|
15
|
+
inclusive_of: [
|
16
|
+
'Inventory',
|
17
|
+
'Service',
|
18
|
+
'Non-Inventory'
|
19
|
+
]
|
20
|
+
},
|
21
|
+
item_category_code: {
|
22
|
+
maximum_length: 20
|
23
|
+
},
|
24
|
+
gtin: {
|
25
|
+
maximum_length: 14
|
26
|
+
},
|
27
|
+
tax_group_code: {
|
28
|
+
maximum_length: 20
|
29
|
+
}
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
OBJECT_METHODS = [
|
33
|
+
:get,
|
34
|
+
:post,
|
35
|
+
:patch,
|
36
|
+
:delete
|
37
|
+
].freeze
|
38
|
+
|
39
|
+
def initialize(client, company_id:)
|
40
|
+
super(client, company_id: company_id)
|
41
|
+
@parent_path = [
|
42
|
+
{
|
43
|
+
path: 'companies',
|
44
|
+
id: company_id
|
45
|
+
}
|
46
|
+
]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module BusinessCentral
|
2
|
+
module Object
|
3
|
+
class PurchaseInvoice < Base
|
4
|
+
OBJECT = 'purchaseInvoices'.freeze
|
5
|
+
|
6
|
+
OBJECT_VALIDATION = {
|
7
|
+
number: {
|
8
|
+
maximum_length: 20
|
9
|
+
},
|
10
|
+
invoice_date: {
|
11
|
+
date: true
|
12
|
+
},
|
13
|
+
vendor_invoice_number: {
|
14
|
+
maximum_length: 35
|
15
|
+
},
|
16
|
+
vendor_number: {
|
17
|
+
maximum_length: 20
|
18
|
+
},
|
19
|
+
vendor_name: {
|
20
|
+
maximum_length: 50
|
21
|
+
},
|
22
|
+
currency_code: {
|
23
|
+
maximum_length: 10
|
24
|
+
},
|
25
|
+
status: {
|
26
|
+
maximum_length: 20,
|
27
|
+
inclusion_of: [
|
28
|
+
'Draft',
|
29
|
+
'In Review',
|
30
|
+
'Open',
|
31
|
+
'Paid',
|
32
|
+
'Canceled',
|
33
|
+
'Corrective. Read-Only'
|
34
|
+
]
|
35
|
+
},
|
36
|
+
payment_terms: {
|
37
|
+
maximum_length: 10
|
38
|
+
},
|
39
|
+
shipment_method: {
|
40
|
+
maximum_length: 10
|
41
|
+
},
|
42
|
+
pay_to_name: {
|
43
|
+
maximum_length: 100
|
44
|
+
},
|
45
|
+
pay_to_contact: {
|
46
|
+
maximum_length: 100
|
47
|
+
},
|
48
|
+
pay_to_vendor_number: {
|
49
|
+
maximum_length: 20
|
50
|
+
},
|
51
|
+
ship_to_name: {
|
52
|
+
maximum_length: 100
|
53
|
+
},
|
54
|
+
ship_to_contact: {
|
55
|
+
maximum_length: 100
|
56
|
+
}
|
57
|
+
}.freeze
|
58
|
+
|
59
|
+
OBJECT_METHODS = [
|
60
|
+
:get,
|
61
|
+
:post,
|
62
|
+
:patch,
|
63
|
+
:delete
|
64
|
+
].freeze
|
65
|
+
|
66
|
+
def initialize(client, company_id:)
|
67
|
+
super(client, company_id: company_id)
|
68
|
+
@parent_path = [
|
69
|
+
{
|
70
|
+
path: 'companies',
|
71
|
+
id: company_id
|
72
|
+
}
|
73
|
+
]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module BusinessCentral
|
2
|
+
module Object
|
3
|
+
class PurchaseInvoiceLine < Base
|
4
|
+
OBJECT = 'purchaseInvoiceLines'.freeze
|
5
|
+
|
6
|
+
OBJECT_VALIDATION = {
|
7
|
+
line_type: {
|
8
|
+
inclusion_of: [
|
9
|
+
'Comment',
|
10
|
+
'Account',
|
11
|
+
'Item',
|
12
|
+
'Resource',
|
13
|
+
'Fixed Asset',
|
14
|
+
'Charge'
|
15
|
+
]
|
16
|
+
}
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
OBJECT_METHODS = [
|
20
|
+
:get,
|
21
|
+
:post,
|
22
|
+
:patch,
|
23
|
+
:delete
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
def initialize(client, company_id:, purchase_invoice_id:)
|
27
|
+
super(client, company_id: company_id)
|
28
|
+
@parent_path = [
|
29
|
+
{
|
30
|
+
path: 'companies',
|
31
|
+
id: company_id
|
32
|
+
},
|
33
|
+
{
|
34
|
+
path: 'purchaseInvoices',
|
35
|
+
id: purchase_invoice_id
|
36
|
+
}
|
37
|
+
]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module BusinessCentral
|
2
|
+
module Object
|
3
|
+
class Request
|
4
|
+
|
5
|
+
def self.get(client, url)
|
6
|
+
request(:get, client, url)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.post(client, url, params)
|
10
|
+
request(:post, client, url, params: params)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.patch(client, url, etag, params)
|
14
|
+
request(:patch, client, url, etag: etag, params: params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.delete(client, url, etag)
|
18
|
+
request(:delete, client, url, etag: etag)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def self.request(method, client, url, etag: '', params: {})
|
24
|
+
send do
|
25
|
+
uri = URI(url)
|
26
|
+
https = Net::HTTP.new(uri.host, uri.port);
|
27
|
+
https.use_ssl = true
|
28
|
+
request = Object.const_get("Net::HTTP::#{method.to_s.capitalize}").new(uri)
|
29
|
+
request['Content-Type'] = 'application/json'
|
30
|
+
request['Accept'] = 'application/json'
|
31
|
+
request['If-Match'] = etag if !etag.blank?
|
32
|
+
request.body = convert(params) if method == :post || method == :patch
|
33
|
+
if client.access_token
|
34
|
+
request['Authorization'] = "Bearer #{client.access_token.token}"
|
35
|
+
else
|
36
|
+
request.basic_auth(client.username, client.password)
|
37
|
+
end
|
38
|
+
https.request(request)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.convert(request = {})
|
43
|
+
result = {}
|
44
|
+
request.each do |key, value|
|
45
|
+
result[key.to_s.to_camel_case] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
return result.to_json
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.send
|
52
|
+
begin
|
53
|
+
request = yield
|
54
|
+
response = Response.new(request.read_body.to_s).results
|
55
|
+
|
56
|
+
if Response.success?(request.code.to_i)
|
57
|
+
return response
|
58
|
+
elsif Response.deleted?(request.code.to_i)
|
59
|
+
return true
|
60
|
+
else
|
61
|
+
if Response.unauthorized?(request.code.to_i)
|
62
|
+
raise UnauthorizedException.new
|
63
|
+
else
|
64
|
+
if response[:error][:code].present?
|
65
|
+
case response[:error][:code]
|
66
|
+
when 'Internal_CompanyNotFound'
|
67
|
+
raise CompanyNotFoundException.new
|
68
|
+
else
|
69
|
+
raise ApiException.new("#{request.code} - #{response[:error][:code]} #{response[:error][:message]}")
|
70
|
+
end
|
71
|
+
else
|
72
|
+
raise ApiException.new("#{request.code} - API call failed")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module BusinessCentral
|
2
|
+
module Object
|
3
|
+
class Response
|
4
|
+
attr_reader :results
|
5
|
+
|
6
|
+
def initialize(response)
|
7
|
+
@results = nil
|
8
|
+
if !response.blank?
|
9
|
+
@response = JSON.parse(response)
|
10
|
+
if @response.has_key?('value')
|
11
|
+
@response = @response['value']
|
12
|
+
end
|
13
|
+
process
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.success?(status)
|
18
|
+
status == 200 || status == 201
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.deleted?(status)
|
22
|
+
status == 204
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.unauthorized?(status)
|
26
|
+
status == 401
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def process
|
32
|
+
if @response.is_a?(Array)
|
33
|
+
@results = []
|
34
|
+
@response.each do |data|
|
35
|
+
@results << convert(data)
|
36
|
+
end
|
37
|
+
elsif @response.is_a?(Hash)
|
38
|
+
@results = convert(@response)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def convert(data)
|
43
|
+
result = {}
|
44
|
+
data.each do |key, value|
|
45
|
+
if key == "@odata.etag"
|
46
|
+
result[:etag] = value
|
47
|
+
elsif key == "@odata.context"
|
48
|
+
result[:context] = value
|
49
|
+
elsif value.is_a?(Hash)
|
50
|
+
result[key.to_snake_case.to_sym] = convert(value)
|
51
|
+
else
|
52
|
+
result[key.to_snake_case.to_sym] = value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
return result
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module BusinessCentral
|
2
|
+
module Object
|
3
|
+
class Validation
|
4
|
+
def initialize(validation_rules, object_params)
|
5
|
+
@validation_rules = validation_rules
|
6
|
+
@object_params = object_params
|
7
|
+
@errors = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
@validation_rules.each do |rules_key, rules_value|
|
12
|
+
rules_value.each do |validation_key, validation_value|
|
13
|
+
if required?(validation_key, validation_value, @object_params[rules_key].to_s)
|
14
|
+
@errors << { field: rules_key, message: 'is a required field'}
|
15
|
+
end
|
16
|
+
|
17
|
+
if exceeds_maximum_length?(validation_key, validation_value, @object_params[rules_key].to_s)
|
18
|
+
@errors << { field: rules_key, message: "has exceeded the maximum length #{validation_value}"}
|
19
|
+
end
|
20
|
+
|
21
|
+
if not_inclusive_of?(validation_key, validation_value, @object_params[rules_key].to_s)
|
22
|
+
@errors << { field: rules_key, message: "is not one of #{validation_value.join(', ')}"}
|
23
|
+
end
|
24
|
+
|
25
|
+
if date_type?(validation_key, validation_value, @object_params[rules_key])
|
26
|
+
@errors << { field: rules_key, message: 'is not a date'}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
raise InvalidObjectException.new(@errors) if @errors.any?
|
32
|
+
return true
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def required?(validation_rule, validation_value, value)
|
38
|
+
validation_rule == :required && validation_value == true && value.blank?
|
39
|
+
end
|
40
|
+
|
41
|
+
def exceeds_maximum_length?(validation_rule, validation_value, value)
|
42
|
+
validation_rule == :maximum_length && value.length > validation_value
|
43
|
+
end
|
44
|
+
|
45
|
+
def not_inclusive_of?(validation_rule, validation_value, value)
|
46
|
+
validation_rule == :inclusion_of && !validation_value.include?(value) && !value.blank?
|
47
|
+
end
|
48
|
+
|
49
|
+
def date_type?(validation_rule, validation_value, value)
|
50
|
+
validation_rule == :date && validation_value == true && !value.is_a?(Date) && !value.nil?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module BusinessCentral
|
2
|
+
module Object
|
3
|
+
class Vendor < Base
|
4
|
+
OBJECT = 'vendors'.freeze
|
5
|
+
|
6
|
+
OBJECT_VALIDATION = {
|
7
|
+
number: {
|
8
|
+
maximum_length: 20
|
9
|
+
},
|
10
|
+
display_name: {
|
11
|
+
maximum_length: 100
|
12
|
+
},
|
13
|
+
phone_number: {
|
14
|
+
maximum_length: 30
|
15
|
+
},
|
16
|
+
email: {
|
17
|
+
maximum_length: 80
|
18
|
+
},
|
19
|
+
website: {
|
20
|
+
maximum_length: 80
|
21
|
+
},
|
22
|
+
tax_registration_number: {
|
23
|
+
maximum_length: 20
|
24
|
+
}
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
OBJECT_METHODS = [
|
28
|
+
:get,
|
29
|
+
:post,
|
30
|
+
:patch,
|
31
|
+
:delete
|
32
|
+
].freeze
|
33
|
+
|
34
|
+
def initialize(client, company_id:)
|
35
|
+
super(client, company_id: company_id)
|
36
|
+
@parent_path = [
|
37
|
+
{
|
38
|
+
path: 'companies',
|
39
|
+
id: company_id
|
40
|
+
}
|
41
|
+
]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class String
|
2
|
+
def blank?
|
3
|
+
empty? || /\A[[:space:]]*\z/.match?(self)
|
4
|
+
end
|
5
|
+
|
6
|
+
# Convert string to CamelCase
|
7
|
+
def to_camel_case(uppercase_first_letter = false)
|
8
|
+
string = self
|
9
|
+
if uppercase_first_letter
|
10
|
+
string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
|
11
|
+
else
|
12
|
+
string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
|
13
|
+
end
|
14
|
+
string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub("/", "::")
|
15
|
+
end
|
16
|
+
|
17
|
+
# Convert string to snake_case
|
18
|
+
def to_snake_case
|
19
|
+
self.gsub(/::/, '/').
|
20
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
21
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
22
|
+
tr("-", "_").
|
23
|
+
downcase
|
24
|
+
end
|
25
|
+
end
|