netsuite 0.8.4 → 0.8.8

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.
Files changed (87) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/main.yml +20 -0
  3. data/.gitignore +1 -0
  4. data/.ruby-version +1 -1
  5. data/.tool-versions +1 -0
  6. data/Gemfile +2 -5
  7. data/HISTORY.md +26 -0
  8. data/README.md +72 -29
  9. data/Rakefile +1 -1
  10. data/lib/netsuite.rb +13 -19
  11. data/lib/netsuite/actions/login.rb +10 -2
  12. data/lib/netsuite/actions/upsert.rb +2 -0
  13. data/lib/netsuite/configuration.rb +34 -4
  14. data/lib/netsuite/records/accounting_period.rb +2 -2
  15. data/lib/netsuite/records/classification.rb +4 -1
  16. data/lib/netsuite/records/contact.rb +1 -1
  17. data/lib/netsuite/records/cost_category.rb +28 -0
  18. data/lib/netsuite/records/credit_memo.rb +1 -1
  19. data/lib/netsuite/records/custom_field_list.rb +9 -3
  20. data/lib/netsuite/records/custom_record.rb +1 -1
  21. data/lib/netsuite/records/customer.rb +2 -1
  22. data/lib/netsuite/records/customer_credit_cards.rb +36 -0
  23. data/lib/netsuite/records/customer_credit_cards_list.rb +10 -0
  24. data/lib/netsuite/records/customer_deposit.rb +5 -5
  25. data/lib/netsuite/records/customer_payment.rb +6 -2
  26. data/lib/netsuite/records/customer_payment_credit.rb +17 -0
  27. data/lib/netsuite/records/customer_payment_credit_list.rb +12 -0
  28. data/lib/netsuite/records/employee.rb +1 -1
  29. data/lib/netsuite/records/estimate.rb +42 -0
  30. data/lib/netsuite/records/estimate_item.rb +40 -0
  31. data/lib/netsuite/records/estimate_item_list.rb +11 -0
  32. data/lib/netsuite/records/inventory_item.rb +62 -1
  33. data/lib/netsuite/records/invoice.rb +94 -1
  34. data/lib/netsuite/records/item_fulfillment.rb +1 -1
  35. data/lib/netsuite/records/lot_numbered_inventory_item.rb +116 -0
  36. data/lib/netsuite/records/matrix_option_list.rb +12 -4
  37. data/lib/netsuite/records/message.rb +30 -0
  38. data/lib/netsuite/records/non_inventory_resale_item.rb +1 -0
  39. data/lib/netsuite/records/non_inventory_sale_item.rb +1 -1
  40. data/lib/netsuite/records/other_charge_sale_item.rb +2 -2
  41. data/lib/netsuite/records/partner.rb +7 -5
  42. data/lib/netsuite/records/serialized_assembly_item.rb +3 -1
  43. data/lib/netsuite/records/service_resale_item.rb +1 -1
  44. data/lib/netsuite/records/support_case_type.rb +26 -0
  45. data/lib/netsuite/records/tax_group.rb +2 -2
  46. data/lib/netsuite/records/vendor.rb +2 -1
  47. data/lib/netsuite/records/vendor_currency.rb +26 -0
  48. data/lib/netsuite/records/vendor_currency_list.rb +9 -0
  49. data/lib/netsuite/support/fields.rb +16 -0
  50. data/lib/netsuite/support/records.rb +1 -1
  51. data/lib/netsuite/support/search_result.rb +36 -6
  52. data/lib/netsuite/utilities.rb +18 -6
  53. data/lib/netsuite/version.rb +1 -1
  54. data/netsuite.gemspec +5 -3
  55. data/spec/netsuite/actions/search_spec.rb +22 -0
  56. data/spec/netsuite/configuration_spec.rb +111 -6
  57. data/spec/netsuite/records/basic_record_spec.rb +9 -1
  58. data/spec/netsuite/records/classification_spec.rb +10 -1
  59. data/spec/netsuite/records/cost_category_spec.rb +105 -0
  60. data/spec/netsuite/records/custom_field_list_spec.rb +46 -6
  61. data/spec/netsuite/records/custom_record_spec.rb +1 -1
  62. data/spec/netsuite/records/customer_credit_cards_list_spec.rb +23 -0
  63. data/spec/netsuite/records/customer_payment_credit_list_spec.rb +26 -0
  64. data/spec/netsuite/records/customer_payment_spec.rb +1 -6
  65. data/spec/netsuite/records/customer_spec.rb +22 -1
  66. data/spec/netsuite/records/employee_spec.rb +2 -2
  67. data/spec/netsuite/records/estimate_item_list_spec.rb +26 -0
  68. data/spec/netsuite/records/estimate_item_spec.rb +40 -0
  69. data/spec/netsuite/records/estimate_spec.rb +216 -0
  70. data/spec/netsuite/records/inventory_item_spec.rb +65 -0
  71. data/spec/netsuite/records/invoice_spec.rb +94 -0
  72. data/spec/netsuite/records/matrix_option_list_spec.rb +15 -5
  73. data/spec/netsuite/records/message_spec.rb +49 -0
  74. data/spec/netsuite/records/non_inventory_resale_item_spec.rb +165 -0
  75. data/spec/netsuite/records/non_inventory_sale_item_spec.rb +1 -1
  76. data/spec/netsuite/records/partner_spec.rb +143 -0
  77. data/spec/netsuite/records/service_resale_item_spec.rb +134 -0
  78. data/spec/netsuite/records/support_case_type_spec.rb +22 -0
  79. data/spec/netsuite/records/vendor_spec.rb +1 -1
  80. data/spec/netsuite/support/search_result_spec.rb +24 -0
  81. data/spec/netsuite/utilities_spec.rb +20 -15
  82. data/spec/support/fixtures/custom_fields/multi_select.xml +47 -0
  83. data/spec/support/fixtures/search/saved_search_item.xml +55 -0
  84. data/spec/support/fixtures/search/saved_search_joined_custom_customer.xml +15 -1
  85. data/spec/support/search_only_field_matcher.rb +7 -0
  86. metadata +77 -12
  87. data/circle.yml +0 -17
@@ -78,7 +78,7 @@ module NetSuite
78
78
  begin
79
79
  count += 1
80
80
  yield
81
- rescue Exception => e
81
+ rescue StandardError => e
82
82
  exceptions_to_retry = [
83
83
  Errno::ECONNRESET,
84
84
  Errno::ETIMEDOUT,
@@ -115,6 +115,7 @@ module NetSuite
115
115
  # https://github.com/stripe/stripe-netsuite/issues/815
116
116
  if !e.message.include?("Only one request may be made against a session at a time") &&
117
117
  !e.message.include?('java.util.ConcurrentModificationException') &&
118
+ !e.message.include?('java.lang.NullPointerException') &&
118
119
  !e.message.include?('java.lang.IllegalStateException') &&
119
120
  !e.message.include?('java.lang.reflect.InvocationTargetException') &&
120
121
  !e.message.include?('com.netledger.common.exceptions.NLDatabaseOfflineException') &&
@@ -172,7 +173,9 @@ module NetSuite
172
173
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::GiftCertificateItem, ns_item_internal_id, opts)
173
174
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::KitItem, ns_item_internal_id, opts)
174
175
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::SerializedInventoryItem, ns_item_internal_id, opts)
176
+ ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::SerializedAssemblyItem, ns_item_internal_id, opts)
175
177
  ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::LotNumberedAssemblyItem, ns_item_internal_id, opts)
178
+ ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::LotNumberedInventoryItem, ns_item_internal_id, opts)
176
179
 
177
180
  if ns_item.nil?
178
181
  fail NetSuite::RecordNotFound, "item with ID #{ns_item_internal_id} not found"
@@ -277,16 +280,25 @@ module NetSuite
277
280
  # NetSuite requires that the time be passed to the API with the PDT TZ offset
278
281
  # of the time passed in (i.e. not the current TZ offset of PDT)
279
282
 
280
- offset = Rational(-7, 24)
281
-
282
283
  if defined?(TZInfo)
283
- time = TZInfo::Timezone.get("America/Los_Angeles").utc_to_local(time)
284
- offset = time.offset
284
+ # if no version is defined, less than 2.0
285
+ # https://github.com/tzinfo/tzinfo/blob/master/CHANGES.md#added
286
+ if !defined?(TZInfo::VERSION)
287
+ # https://stackoverflow.com/questions/2927111/ruby-get-time-in-given-timezone
288
+ offset = TZInfo::Timezone.get("America/Los_Angeles").period_for_utc(time).utc_total_offset_rational
289
+ time = time.new_offset(offset)
290
+ else
291
+ time = TZInfo::Timezone.get("America/Los_Angeles").utc_to_local(time)
292
+ offset = time.offset
293
+ end
285
294
  else
295
+ # if tzinfo is not installed, let's give it our best guess: -7
296
+ offset = Rational(-7, 24)
286
297
  time = time.new_offset("-07:00")
287
298
  end
288
299
 
289
- (time + (offset * -1)).iso8601
300
+ time = (time + (offset * -1))
301
+ time.iso8601
290
302
  end
291
303
 
292
304
  end
@@ -1,3 +1,3 @@
1
1
  module NetSuite
2
- VERSION = '0.8.4'
2
+ VERSION = '0.8.8'
3
3
  end
data/netsuite.gemspec CHANGED
@@ -2,8 +2,9 @@
2
2
  require File.expand_path('../lib/netsuite/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
+ gem.licenses = ['MIT']
5
6
  gem.authors = ['Ryan Moran', 'Michael Bianco']
6
- gem.email = ['ryan.moran@gmail.com', 'mike@cliffsidemedia.com']
7
+ gem.email = ['ryan.moran@gmail.com', 'mike@mikebian.co']
7
8
  gem.description = %q{NetSuite SuiteTalk API Wrapper}
8
9
  gem.summary = %q{NetSuite SuiteTalk API (SOAP) Wrapper}
9
10
  gem.homepage = 'https://github.com/NetSweet/netsuite'
@@ -15,7 +16,8 @@ Gem::Specification.new do |gem|
15
16
  gem.require_paths = ['lib']
16
17
  gem.version = NetSuite::VERSION
17
18
 
18
- gem.add_dependency 'savon', '>= 2.3.0'
19
+ gem.add_dependency 'savon', '>= 2.3.0', '<= 2.11.1'
19
20
 
20
- gem.add_development_dependency 'rspec', '~> 3.1.0'
21
+ gem.add_development_dependency 'rspec', '~> 3.10.0'
22
+ gem.add_development_dependency 'rake'
21
23
  end
@@ -168,9 +168,31 @@ describe NetSuite::Actions::Search do
168
168
 
169
169
  expect(search.results.size).to eq(2)
170
170
  expect(search.current_page).to eq(1)
171
+ expect(search.results.first.internal_id).to eq('123')
172
+ expect(search.results.first.external_id).to eq('456')
171
173
  expect(search.results.first.alt_name).to eq('A Awesome Name')
174
+ expect(search.results.first.custom_field_list.custitem_stringfield.value).to eq('sample string value')
175
+ expect(search.results.first.custom_field_list.custitem_apcategoryforsales.value.internal_id).to eq('4')
172
176
  expect(search.results.last.email).to eq('alessawesome@gmail.com')
173
177
  end
178
+
179
+ it "should handle an ID search with basic search only field result columns" do
180
+ response = File.read('spec/support/fixtures/search/saved_search_item.xml')
181
+ savon.expects(:search)
182
+ .with(message: {
183
+ "searchRecord"=>{
184
+ "@xsi:type" =>"listAcct:ItemSearchAdvanced",
185
+ "@savedSearchId" =>42,
186
+ :content! =>{"listAcct:criteria"=>{}},
187
+ }
188
+ }).returns(response)
189
+
190
+ search = NetSuite::Records::InventoryItem.search(saved: 42)
191
+
192
+ expect(search.results.first.location_quantity_available).to eq('3307.0')
193
+ expect(search.results.first.location_re_order_point).to eq('2565.0')
194
+ expect(search.results.first.location_quantity_on_order).to eq('40000.0')
195
+ end
174
196
  end
175
197
 
176
198
  context "advanced search" do
@@ -29,10 +29,12 @@ describe NetSuite::Configuration do
29
29
  end
30
30
 
31
31
  describe '#connection' do
32
+ EXAMPLE_ENDPOINT = 'https://1023.suitetalk.api.netsuite.com/services/NetSuitePort_2020_2'
32
33
  before(:each) do
33
34
  # reset clears out the password info
34
35
  config.email 'me@example.com'
35
36
  config.password 'me@example.com'
37
+ config.endpoint EXAMPLE_ENDPOINT
36
38
  config.account 1023
37
39
  config.wsdl "my_wsdl"
38
40
  config.api_version "2012_2"
@@ -57,6 +59,19 @@ describe NetSuite::Configuration do
57
59
 
58
60
  expect(config).to have_received(:cached_wsdl)
59
61
  end
62
+
63
+ it 'sets the endpoint on the Savon client' do
64
+ # this is ugly/brittle, but it's hard to see how else to test this
65
+ savon_configs = config.connection.globals.instance_eval {@options}
66
+ expect(savon_configs.fetch(:endpoint)).to eq(EXAMPLE_ENDPOINT)
67
+ end
68
+
69
+ it 'handles a nil endpoint' do
70
+ config.endpoint = nil
71
+ # this is ugly/brittle, but it's hard to see how else to test this
72
+ savon_configs = config.connection.globals.instance_eval {@options}
73
+ expect(savon_configs.fetch(:endpoint)).to eq(nil)
74
+ end
60
75
  end
61
76
 
62
77
  describe '#wsdl' do
@@ -166,6 +181,23 @@ describe NetSuite::Configuration do
166
181
  end
167
182
  end
168
183
 
184
+ describe '#endpoint' do
185
+ it 'can be set with endpoint=' do
186
+ config.endpoint = 42
187
+ expect(config.endpoint).to eq(42)
188
+ end
189
+
190
+ it 'can be set with just endpoint(value)' do
191
+ config.endpoint(42)
192
+ expect(config.endpoint).to eq(42)
193
+ end
194
+
195
+ it 'supports nil endpoints' do
196
+ config.endpoint = nil
197
+ expect(config.endpoint).to eq(nil)
198
+ end
199
+ end
200
+
169
201
  describe '#auth_header' do
170
202
  context 'when doing user authentication' do
171
203
  before do
@@ -326,15 +358,11 @@ describe NetSuite::Configuration do
326
358
 
327
359
  describe "#credentials" do
328
360
  context "when none are defined" do
329
- skip "should properly create the auth credentials" do
330
-
331
- end
361
+ skip "should properly create the auth credentials"
332
362
  end
333
363
 
334
364
  context "when they are defined" do
335
- it "should properly replace the default auth credentials" do
336
-
337
- end
365
+ skip "should properly replace the default auth credentials"
338
366
  end
339
367
  end
340
368
 
@@ -371,4 +399,81 @@ describe NetSuite::Configuration do
371
399
  end
372
400
  end
373
401
 
402
+ describe "#log" do
403
+ it 'allows a file path to be set as the log destination' do
404
+ file_path = Tempfile.new('tmplog').path
405
+ config.log = file_path
406
+ config.logger.info "foo"
407
+
408
+ log_contents = open(file_path).read
409
+ expect(log_contents).to include("foo")
410
+ end
411
+
412
+ it 'allows an IO device to bet set as the log destination' do
413
+ stream = StringIO.new
414
+ config.log = stream
415
+ config.logger.info "foo"
416
+
417
+ expect(stream.string).to include("foo")
418
+ end
419
+ end
420
+
421
+ describe '#log_level' do
422
+ it 'defaults to :debug' do
423
+ expect(config.log_level).to eq(:debug)
424
+ end
425
+
426
+ it 'can be initially set to any log level' do
427
+ config.log_level(:info)
428
+
429
+ expect(config.log_level).to eq(:info)
430
+ end
431
+
432
+ it 'can override itself' do
433
+ config.log_level = :info
434
+
435
+ expect(config.log_level).to eq(:info)
436
+
437
+ config.log_level(:debug)
438
+
439
+ expect(config.log_level).to eq(:debug)
440
+ end
441
+ end
442
+
443
+ describe '#log_level=' do
444
+ it 'can set the initial log_level' do
445
+ config.log_level = :info
446
+
447
+ expect(config.log_level).to eq(:info)
448
+ end
449
+
450
+ it 'can override a previously set log level' do
451
+ config.log_level = :info
452
+
453
+ expect(config.log_level).to eq(:info)
454
+
455
+ config.log_level = :debug
456
+
457
+ expect(config.log_level).to eq(:debug)
458
+ end
459
+ end
460
+
461
+ describe 'timeouts' do
462
+ it 'has defaults' do
463
+ expect(config.read_timeout).to eql(60)
464
+ expect(config.open_timeout).to be_nil
465
+ end
466
+
467
+ it 'sets timeouts' do
468
+ config.read_timeout = 100
469
+ config.open_timeout = 60
470
+
471
+ expect(config.read_timeout).to eql(100)
472
+ expect(config.open_timeout).to eql(60)
473
+
474
+ # ensure no exception is raised
475
+ config.connection
476
+ end
477
+ end
478
+
374
479
  end
@@ -1,6 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe 'basic records' do
4
+ # all records with internal IDs should be added to this list
4
5
  let(:basic_record_list) {
5
6
  [
6
7
  NetSuite::Records::Currency,
@@ -25,6 +26,7 @@ describe 'basic records' do
25
26
  NetSuite::Records::CustomerDeposit,
26
27
  NetSuite::Records::NonInventoryPurchaseItem,
27
28
  NetSuite::Records::NonInventoryResaleItem,
29
+ NetSuite::Records::LotNumberedInventoryItem,
28
30
  NetSuite::Records::TaxGroup,
29
31
  NetSuite::Records::Folder,
30
32
  NetSuite::Records::CustomerCategory,
@@ -48,6 +50,7 @@ describe 'basic records' do
48
50
  NetSuite::Records::SerializedInventoryItem,
49
51
  NetSuite::Records::DepositApplication,
50
52
  NetSuite::Records::InventoryAdjustment,
53
+ NetSuite::Records::Vendor,
51
54
  NetSuite::Records::VendorReturnAuthorization,
52
55
  NetSuite::Records::AssemblyBuild,
53
56
  NetSuite::Records::AssemblyUnbuild,
@@ -60,6 +63,7 @@ describe 'basic records' do
60
63
  NetSuite::Records::BinTransfer,
61
64
  NetSuite::Records::SerializedAssemblyItem,
62
65
  NetSuite::Records::CustomerStatus,
66
+ NetSuite::Records::CustomerPayment,
63
67
  NetSuite::Records::TransactionBodyCustomField,
64
68
  NetSuite::Records::TransactionColumnCustomField,
65
69
  NetSuite::Records::EntityCustomField
@@ -107,8 +111,12 @@ describe 'basic records' do
107
111
 
108
112
  if !sublist_fields.empty?
109
113
  sublist_fields.each do |sublist_field|
114
+ sublist = record_instance.send(sublist_field)
115
+
110
116
  # TODO make a sublist entry with some fields valid for that sublist item
111
- record_instance.send(sublist_field) << {}
117
+ sublist << {}
118
+
119
+ expect(sublist.send(sublist.sublist_key).count).to be(1)
112
120
  end
113
121
  end
114
122
 
@@ -5,12 +5,21 @@ describe NetSuite::Records::Classification do
5
5
 
6
6
  it 'has all the right fields' do
7
7
  [
8
- :name, :include_children, :is_inactive, :class_translation_list, :custom_field_list
8
+ :name, :include_children, :is_inactive, :class_translation_list
9
9
  ].each do |field|
10
10
  expect(classification).to have_field(field)
11
11
  end
12
12
 
13
13
  expect(classification.subsidiary_list.class).to eq(NetSuite::Records::RecordRefList)
14
+ expect(classification.custom_field_list.class).to eq(NetSuite::Records::CustomFieldList)
15
+ end
16
+
17
+ it 'has all the right record refs' do
18
+ [
19
+ :parent
20
+ ].each do |record_ref|
21
+ expect(classification).to have_record_ref(record_ref)
22
+ end
14
23
  end
15
24
 
16
25
  describe '.get' do
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe NetSuite::Records::CostCategory do
4
+ let(:cost_category) { described_class.new }
5
+
6
+ it 'has all the right fields' do
7
+ [
8
+ :is_inactive,
9
+ :item_cost_type,
10
+ :name,
11
+ ].each do |field|
12
+ expect(cost_category).to have_field(field)
13
+ end
14
+ end
15
+
16
+ it 'has all the right record refs' do
17
+ [
18
+ :account,
19
+ ].each do |record_ref|
20
+ expect(cost_category).to have_record_ref(record_ref)
21
+ end
22
+ end
23
+
24
+ describe '.get' do
25
+ context 'when the response is successful' do
26
+ let(:response) { NetSuite::Response.new(:success => true, :body => { :name => 'CostCategory 1' }) }
27
+
28
+ it 'returns a CostCategory instance populated with the data from the response object' do
29
+ expect(NetSuite::Actions::Get).to receive(:call).with([described_class, {:external_id => 1}], {}).and_return(response)
30
+ cost_category = described_class.get(:external_id => 1)
31
+ expect(cost_category).to be_kind_of(described_class)
32
+ expect(cost_category.name).to eql('CostCategory 1')
33
+ end
34
+ end
35
+
36
+ context 'when the response is unsuccessful' do
37
+ let(:response) { NetSuite::Response.new(:success => false, :body => {}) }
38
+
39
+ it 'raises a RecordNotFound exception' do
40
+ expect(NetSuite::Actions::Get).to receive(:call).with([described_class, {:external_id => 1}], {}).and_return(response)
41
+ expect {
42
+ described_class.get(:external_id => 1)
43
+ }.to raise_error(NetSuite::RecordNotFound,
44
+ /NetSuite::Records::CostCategory with OPTIONS=(.*) could not be found/)
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '#add' do
50
+ let(:test_data) { { :name => 'Test CostCategory' } }
51
+
52
+ context 'when the response is successful' do
53
+ let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) }
54
+
55
+ it 'returns true' do
56
+ cost_category = described_class.new(test_data)
57
+ expect(NetSuite::Actions::Add).to receive(:call).
58
+ with([cost_category], {}).
59
+ and_return(response)
60
+ expect(cost_category.add).to be_truthy
61
+ end
62
+ end
63
+
64
+ context 'when the response is unsuccessful' do
65
+ let(:response) { NetSuite::Response.new(:success => false, :body => {}) }
66
+
67
+ it 'returns false' do
68
+ cost_category = described_class.new(test_data)
69
+ expect(NetSuite::Actions::Add).to receive(:call).
70
+ with([cost_category], {}).
71
+ and_return(response)
72
+ expect(cost_category.add).to be_falsey
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '#delete' do
78
+ let(:test_data) { { :internal_id => '1' } }
79
+
80
+ context 'when the response is successful' do
81
+ let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) }
82
+
83
+ it 'returns true' do
84
+ cost_category = described_class.new(test_data)
85
+ expect(NetSuite::Actions::Delete).to receive(:call).
86
+ with([cost_category], {}).
87
+ and_return(response)
88
+ expect(cost_category.delete).to be_truthy
89
+ end
90
+ end
91
+
92
+ context 'when the response is unsuccessful' do
93
+ let(:response) { NetSuite::Response.new(:success => false, :body => {}) }
94
+
95
+ it 'returns false' do
96
+ cost_category = described_class.new(test_data)
97
+ expect(NetSuite::Actions::Delete).to receive(:call).
98
+ with([cost_category], {}).
99
+ and_return(response)
100
+ expect(cost_category.delete).to be_falsey
101
+ end
102
+ end
103
+ end
104
+
105
+ end