business-central 1.0.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/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
|