ns_connector 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +13 -0
- data/Gemfile.lock +80 -0
- data/Guardfile +9 -0
- data/HACKING +31 -0
- data/LICENSE.txt +7 -0
- data/README.rdoc +191 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/lib/ns_connector.rb +4 -0
- data/lib/ns_connector/attaching.rb +42 -0
- data/lib/ns_connector/chunked_searching.rb +111 -0
- data/lib/ns_connector/config.rb +66 -0
- data/lib/ns_connector/errors.rb +79 -0
- data/lib/ns_connector/field_store.rb +19 -0
- data/lib/ns_connector/hash.rb +11 -0
- data/lib/ns_connector/resource.rb +288 -0
- data/lib/ns_connector/resources.rb +3 -0
- data/lib/ns_connector/resources/contact.rb +279 -0
- data/lib/ns_connector/resources/customer.rb +355 -0
- data/lib/ns_connector/resources/invoice.rb +466 -0
- data/lib/ns_connector/restlet.rb +137 -0
- data/lib/ns_connector/sublist.rb +21 -0
- data/lib/ns_connector/sublist_item.rb +25 -0
- data/misc/failed_sublist_saving_patch +547 -0
- data/scripts/run_restlet +25 -0
- data/scripts/test_shell +21 -0
- data/spec/attaching_spec.rb +48 -0
- data/spec/chunked_searching_spec.rb +75 -0
- data/spec/config_spec.rb +43 -0
- data/spec/resource_spec.rb +340 -0
- data/spec/resources/contact_spec.rb +8 -0
- data/spec/resources/customer_spec.rb +8 -0
- data/spec/resources/invoice_spec.rb +20 -0
- data/spec/restlet_spec.rb +135 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/sublist_item_spec.rb +25 -0
- data/spec/sublist_spec.rb +45 -0
- data/spec/support/mock_data.rb +10 -0
- data/support/read_only_test +63 -0
- data/support/restlet.js +384 -0
- data/support/super_dangerous_write_test +85 -0
- metadata +221 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
require 'ns_connector/config'
|
5
|
+
require 'ns_connector/errors'
|
6
|
+
|
7
|
+
# Connects to the RESTlet configured in NetSuite to make generic requests.
|
8
|
+
# Example usage:
|
9
|
+
# NSConnector::Restlet.execute!(
|
10
|
+
# :action => 'retrieve',
|
11
|
+
# :type_id => type_id,
|
12
|
+
# :data => {'_id' => Integer(id)}
|
13
|
+
# )
|
14
|
+
# => parsed json results
|
15
|
+
module NSConnector::Restlet
|
16
|
+
RuntimeError = Class.new(Exception)
|
17
|
+
ArgumentError = Class.new(Exception)
|
18
|
+
|
19
|
+
# Build a HTTP request to connect to NetSuite RESTlets, then request
|
20
|
+
# our magic restlet that can do everything useful.
|
21
|
+
#
|
22
|
+
# Returns:: The parsed JSON response, i.e. JSON.parse(response.body)
|
23
|
+
def self.execute! options
|
24
|
+
NSConnector::Config.check_valid!
|
25
|
+
|
26
|
+
# Build our request up, a bit ugly
|
27
|
+
uri = URI(NSConnector::Config[:restlet_url])
|
28
|
+
|
29
|
+
unless uri.scheme then
|
30
|
+
raise NSConnector::Restlet::ArgumentError,
|
31
|
+
'Configuration value restlet_url must at '\
|
32
|
+
'least contain a scheme (i.e. http://)'
|
33
|
+
end
|
34
|
+
|
35
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
36
|
+
|
37
|
+
if NSConnector::Config[:debug] then
|
38
|
+
http.set_debug_output $stderr
|
39
|
+
end
|
40
|
+
|
41
|
+
http.use_ssl = (uri.scheme == 'https')
|
42
|
+
|
43
|
+
request = Net::HTTP::Post.new("#{uri.path}?#{uri.query}")
|
44
|
+
|
45
|
+
request['Content-Type'] = 'application/json'
|
46
|
+
request['Authorization'] = NSConnector::Restlet.auth_header
|
47
|
+
|
48
|
+
begin
|
49
|
+
options[:code] = restlet_code
|
50
|
+
request.body = JSON.dump(options)
|
51
|
+
rescue JSON::GeneratorError => current
|
52
|
+
exception = NSConnector::Restlet::ArgumentError.new(
|
53
|
+
"Failed to convert options (#{options}) " \
|
54
|
+
"into JSON: #{current.message}"
|
55
|
+
)
|
56
|
+
|
57
|
+
exception.set_backtrace(current.backtrace)
|
58
|
+
raise exception
|
59
|
+
end
|
60
|
+
|
61
|
+
response = http.request(request)
|
62
|
+
|
63
|
+
# Netsuite seems to use HTTP 400 (bad requests) for all runtime
|
64
|
+
# errors. Hah.
|
65
|
+
if response.kind_of? Net::HTTPBadRequest then
|
66
|
+
# So let's try and raise this exception as something
|
67
|
+
# nicer.
|
68
|
+
NSConnector::Errors.try_handle_response!(response)
|
69
|
+
end
|
70
|
+
|
71
|
+
unless response.kind_of? Net::HTTPSuccess then
|
72
|
+
raise NSConnector::Restlet::RuntimeError.new(
|
73
|
+
'Restlet execution failed, expected a '\
|
74
|
+
'HTTP 2xx response, got a HTTP '\
|
75
|
+
"#{response.code}: #{response.body}"
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
if response.body.nil? then
|
80
|
+
raise NSConnector::Restlet::RuntimeError.new(
|
81
|
+
"Recieved a blank response from RESTlet"
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
begin
|
86
|
+
return JSON.parse(response.body)
|
87
|
+
rescue JSON::ParserError => current
|
88
|
+
exception = NSConnector::Restlet::RuntimeError.new(
|
89
|
+
'Failed to parse response ' \
|
90
|
+
'from Restlet as JSON '\
|
91
|
+
"(#{response.body}): " \
|
92
|
+
"#{current.message}"
|
93
|
+
)
|
94
|
+
exception.set_backtrace(current.backtrace)
|
95
|
+
raise exception
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Generate a NetSuite specific Authorization header.
|
100
|
+
#
|
101
|
+
# From the NetSuite documentation:
|
102
|
+
#
|
103
|
+
# NLAuth passes in the following login credentials:
|
104
|
+
# +nlauth_account+:: NetSuite company ID (required)
|
105
|
+
# +nlauth_email+:: NetSuite user name (required)
|
106
|
+
# +nclauth_signature+:: NetSuite password (required)
|
107
|
+
# +nlauth_role+:: internal ID of the role used to log in to
|
108
|
+
# NetSuite (optional)
|
109
|
+
# The Authorization header should be formatted as:
|
110
|
+
# NLAuth<space><comma-separated parameters>
|
111
|
+
#
|
112
|
+
# For example:
|
113
|
+
# Authorization: NLAuth nlauth_account=123456, \
|
114
|
+
# nlauth_email=jsmith@ABC.com, nlauth_signature=xxxxxxxx, \
|
115
|
+
# nlauth_role=41
|
116
|
+
#
|
117
|
+
# Returns:: String with the contents of the header
|
118
|
+
def self.auth_header
|
119
|
+
c = NSConnector::Config
|
120
|
+
c.check_valid!
|
121
|
+
|
122
|
+
"NLAuth nlauth_account=#{c[:account_id]}," \
|
123
|
+
"nlauth_email=#{c[:email]}," \
|
124
|
+
"nlauth_role=#{c[:role]}," \
|
125
|
+
"nlauth_signature=#{URI.escape(c[:password])}"
|
126
|
+
end
|
127
|
+
|
128
|
+
# Retrieve restlet code from support/restlet.js
|
129
|
+
# Returns:: String
|
130
|
+
def self.restlet_code
|
131
|
+
restlet_location = File.join(
|
132
|
+
File.dirname(__FILE__),
|
133
|
+
'..', '..', 'support', 'restlet.js'
|
134
|
+
)
|
135
|
+
File.read(restlet_location)
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Provides a method for grabbing sublists
|
2
|
+
module NSConnector::SubList
|
3
|
+
# Grab sublist_id from NetSuite
|
4
|
+
# Returns:: An array of SubListItems
|
5
|
+
def self.fetch parent, sublist_id, fields
|
6
|
+
NSConnector::Restlet.execute!(
|
7
|
+
:action => 'fetch_sublist',
|
8
|
+
:type_id => parent.type_id,
|
9
|
+
:parent_id => parent.id,
|
10
|
+
:fields => fields,
|
11
|
+
:sublist_id => sublist_id
|
12
|
+
).map do |upstream_store|
|
13
|
+
NSConnector::SubListItem.new(
|
14
|
+
sublist_id,
|
15
|
+
fields,
|
16
|
+
parent,
|
17
|
+
upstream_store
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'ns_connector/field_store'
|
2
|
+
require 'ns_connector/hash'
|
3
|
+
|
4
|
+
# Represents one SubListItem under a Resource
|
5
|
+
class NSConnector::SubListItem
|
6
|
+
include NSConnector::FieldStore
|
7
|
+
attr_accessor :store
|
8
|
+
attr_accessor :parent
|
9
|
+
attr_accessor :name
|
10
|
+
attr_accessor :fields
|
11
|
+
|
12
|
+
def initialize name, fields, parent, upstream_store = nil
|
13
|
+
upstream_store.stringify_keys! if upstream_store
|
14
|
+
@store = (upstream_store || {})
|
15
|
+
@parent = parent
|
16
|
+
@fields = fields
|
17
|
+
@name = name
|
18
|
+
|
19
|
+
create_store_accessors!
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
"#<NSConnector::#{self.class}:#{name}>"
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,547 @@
|
|
1
|
+
Why is this stored here and not in git?
|
2
|
+
|
3
|
+
I'm planning rebase into a single commit, prefer not to lose this as a reference.
|
4
|
+
|
5
|
+
diff --git a/lib/ns_connector/resource.rb b/lib/ns_connector/resource.rb
|
6
|
+
index 92bbeab..3e49d7b 100644
|
7
|
+
--- a/lib/ns_connector/resource.rb
|
8
|
+
+++ b/lib/ns_connector/resource.rb
|
9
|
+
@@ -94,17 +94,6 @@ class NSConnector::Resource
|
10
|
+
# If we got this far, we're definitely in NetSuite
|
11
|
+
@in_netsuite = true
|
12
|
+
|
13
|
+
- # Now we save our sublist(s)
|
14
|
+
- @sublist_store.each do |sublist_id, sublist_items|
|
15
|
+
- # Overwriting the current item
|
16
|
+
- @sublist_store[sublist_id] = NSConnector::SubList.save!(
|
17
|
+
- sublist_items,
|
18
|
+
- self,
|
19
|
+
- sublist_id,
|
20
|
+
- sublists[sublist_id]
|
21
|
+
- )
|
22
|
+
- end
|
23
|
+
-
|
24
|
+
return true
|
25
|
+
end
|
26
|
+
|
27
|
+
@@ -228,12 +217,7 @@ class NSConnector::Resource
|
28
|
+
private
|
29
|
+
# Given a sublist of {:addressbook => ['fields']} we want a method
|
30
|
+
# addressbook that looks up the sublist if we have an ID, otherwise
|
31
|
+
- # returns the empty array.
|
32
|
+
- #
|
33
|
+
- # And finally we need a method to create new sublist objects that is
|
34
|
+
- # generic and not too crazy. So we have new_addressbook that returns a
|
35
|
+
- # SubList object that can be stored and later turned into a hash to
|
36
|
+
- # send to NetSuite.
|
37
|
+
+ # returns an empty array.
|
38
|
+
def create_sublist_accessors!
|
39
|
+
sublists.each do |sublist_name, fields|
|
40
|
+
self.class.class_eval do
|
41
|
+
@@ -250,21 +234,6 @@ class NSConnector::Resource
|
42
|
+
|
43
|
+
@sublist_store[sublist_name] ||= []
|
44
|
+
end
|
45
|
+
-
|
46
|
+
- define_method("#{sublist_name}=") do |value|
|
47
|
+
- @sublist_store[sublist_name] = value
|
48
|
+
- end
|
49
|
+
-
|
50
|
+
- define_method(
|
51
|
+
- "new_#{sublist_name}_item"
|
52
|
+
- ) do |upstream_store = nil|
|
53
|
+
- NSConnector::SubListItem.new(
|
54
|
+
- sublist_name,
|
55
|
+
- fields,
|
56
|
+
- self,
|
57
|
+
- upstream_store
|
58
|
+
- )
|
59
|
+
- end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
diff --git a/lib/ns_connector/sublist.rb b/lib/ns_connector/sublist.rb
|
64
|
+
index b24f0c1..94d9c2f 100644
|
65
|
+
--- a/lib/ns_connector/sublist.rb
|
66
|
+
+++ b/lib/ns_connector/sublist.rb
|
67
|
+
@@ -1,27 +1,5 @@
|
68
|
+
# Provides a method for saving a bunch of SubListItems
|
69
|
+
module NSConnector::SubList
|
70
|
+
- # We save our array of SubListItems in the order in which they appear.
|
71
|
+
- # Arguments:: An array of SubListItem, the parent object and the fields
|
72
|
+
- # Returns:: An array of SubListItem that have been saved
|
73
|
+
- def self.save! sublist_items, parent, sublist_id, fields
|
74
|
+
- data = sublist_items.uniq.map do |item|
|
75
|
+
- item.store
|
76
|
+
- end
|
77
|
+
-
|
78
|
+
- NSConnector::Restlet.execute!(
|
79
|
+
- :action => 'save_sublist',
|
80
|
+
- :type_id => parent.type_id,
|
81
|
+
- :parent_id => parent.id,
|
82
|
+
- :fields => fields,
|
83
|
+
- :sublist_id => sublist_id,
|
84
|
+
- :data => data
|
85
|
+
- )
|
86
|
+
-
|
87
|
+
- # We have to do this in a second request as NetSuite needs a
|
88
|
+
- # short time to think about any added records.
|
89
|
+
- return NSConnector::SubList.fetch(parent, sublist_id, fields)
|
90
|
+
- end
|
91
|
+
-
|
92
|
+
# Grab sublist_id from NetSuite
|
93
|
+
# Returns:: An array of SubListItems
|
94
|
+
def self.fetch parent, sublist_id, fields
|
95
|
+
diff --git a/misc/failed_sublist_saving_patch b/misc/failed_sublist_saving_patch
|
96
|
+
index 5393f12..e69de29 100644
|
97
|
+
--- a/misc/failed_sublist_saving_patch
|
98
|
+
+++ b/misc/failed_sublist_saving_patch
|
99
|
+
@@ -1,214 +0,0 @@
|
100
|
+
-diff --git a/lib/ns_connector/resource.rb b/lib/ns_connector/resource.rb
|
101
|
+
-index 92bbeab..ef0f649 100644
|
102
|
+
---- a/lib/ns_connector/resource.rb
|
103
|
+
-+++ b/lib/ns_connector/resource.rb
|
104
|
+
-@@ -94,17 +94,6 @@ class NSConnector::Resource
|
105
|
+
- # If we got this far, we're definitely in NetSuite
|
106
|
+
- @in_netsuite = true
|
107
|
+
-
|
108
|
+
-- # Now we save our sublist(s)
|
109
|
+
-- @sublist_store.each do |sublist_id, sublist_items|
|
110
|
+
-- # Overwriting the current item
|
111
|
+
-- @sublist_store[sublist_id] = NSConnector::SubList.save!(
|
112
|
+
-- sublist_items,
|
113
|
+
-- self,
|
114
|
+
-- sublist_id,
|
115
|
+
-- sublists[sublist_id]
|
116
|
+
-- )
|
117
|
+
-- end
|
118
|
+
--
|
119
|
+
- return true
|
120
|
+
- end
|
121
|
+
-
|
122
|
+
-@@ -250,21 +239,6 @@ class NSConnector::Resource
|
123
|
+
-
|
124
|
+
- @sublist_store[sublist_name] ||= []
|
125
|
+
- end
|
126
|
+
--
|
127
|
+
-- define_method("#{sublist_name}=") do |value|
|
128
|
+
-- @sublist_store[sublist_name] = value
|
129
|
+
-- end
|
130
|
+
--
|
131
|
+
-- define_method(
|
132
|
+
-- "new_#{sublist_name}_item"
|
133
|
+
-- ) do |upstream_store = nil|
|
134
|
+
-- NSConnector::SubListItem.new(
|
135
|
+
-- sublist_name,
|
136
|
+
-- fields,
|
137
|
+
-- self,
|
138
|
+
-- upstream_store
|
139
|
+
-- )
|
140
|
+
-- end
|
141
|
+
- end
|
142
|
+
- end
|
143
|
+
- end
|
144
|
+
-diff --git a/lib/ns_connector/sublist.rb b/lib/ns_connector/sublist.rb
|
145
|
+
-index b24f0c1..94d9c2f 100644
|
146
|
+
---- a/lib/ns_connector/sublist.rb
|
147
|
+
-+++ b/lib/ns_connector/sublist.rb
|
148
|
+
-@@ -1,27 +1,5 @@
|
149
|
+
- # Provides a method for saving a bunch of SubListItems
|
150
|
+
- module NSConnector::SubList
|
151
|
+
-- # We save our array of SubListItems in the order in which they appear.
|
152
|
+
-- # Arguments:: An array of SubListItem, the parent object and the fields
|
153
|
+
-- # Returns:: An array of SubListItem that have been saved
|
154
|
+
-- def self.save! sublist_items, parent, sublist_id, fields
|
155
|
+
-- data = sublist_items.uniq.map do |item|
|
156
|
+
-- item.store
|
157
|
+
-- end
|
158
|
+
--
|
159
|
+
-- NSConnector::Restlet.execute!(
|
160
|
+
-- :action => 'save_sublist',
|
161
|
+
-- :type_id => parent.type_id,
|
162
|
+
-- :parent_id => parent.id,
|
163
|
+
-- :fields => fields,
|
164
|
+
-- :sublist_id => sublist_id,
|
165
|
+
-- :data => data
|
166
|
+
-- )
|
167
|
+
--
|
168
|
+
-- # We have to do this in a second request as NetSuite needs a
|
169
|
+
-- # short time to think about any added records.
|
170
|
+
-- return NSConnector::SubList.fetch(parent, sublist_id, fields)
|
171
|
+
-- end
|
172
|
+
--
|
173
|
+
- # Grab sublist_id from NetSuite
|
174
|
+
- # Returns:: An array of SubListItems
|
175
|
+
- def self.fetch parent, sublist_id, fields
|
176
|
+
-diff --git a/support/restlet.js b/support/restlet.js
|
177
|
+
-index 4569862..afc8df2 100644
|
178
|
+
---- a/support/restlet.js
|
179
|
+
-+++ b/support/restlet.js
|
180
|
+
-@@ -193,133 +193,6 @@ function create(request)
|
181
|
+
- ));
|
182
|
+
- }
|
183
|
+
-
|
184
|
+
--// Save a sublist, trying to merge changes nicely.
|
185
|
+
--//
|
186
|
+
--// Returns:: True, note that you will need to fetch the updated items yourself,
|
187
|
+
--// as netsuite seems to need a little while to think about any newly added
|
188
|
+
--// records or it freaks out.
|
189
|
+
--//
|
190
|
+
--// Raises:: An invalid sublist operation error, often, as apparently you're not
|
191
|
+
--// allowed to delete certain sublists even though you can append to them. Kind
|
192
|
+
--// of really annoying. Not much I can do about it.
|
193
|
+
--//
|
194
|
+
--// Also seems to raise a 500 on modifying existing items if it doesn't want you
|
195
|
+
--// to.
|
196
|
+
--//
|
197
|
+
--// We have the minor problem here of only being able to modify existing line
|
198
|
+
--// items in place, insert new ones and delete old ones, not rewrite the whole
|
199
|
+
--// list. This problem is easily dealt with if we simply don't worry about
|
200
|
+
--// order. So, new items should be appended to the end, and the existing order
|
201
|
+
--// shouldn't be messed with, however if you wanted to say, pull the whole list
|
202
|
+
--// out and reverse the order, you're out of luck.
|
203
|
+
--//
|
204
|
+
--// It's possible to do, I think. I have other things to work on just now.
|
205
|
+
--//
|
206
|
+
--// So we have three basic steps:
|
207
|
+
--//
|
208
|
+
--// 1. Modify all existing records that have changed, identifying by id.
|
209
|
+
--// 2. If the existing record does not appear in the new list, delete it.
|
210
|
+
--// 3. Append new records to the end.
|
211
|
+
--function save_sublist(request)
|
212
|
+
--{
|
213
|
+
-- if(!request.hasOwnProperty('parent_id')) {
|
214
|
+
-- argument_error("Missing mandatory argument: parent_id");
|
215
|
+
-- }
|
216
|
+
--
|
217
|
+
-- if(!request.hasOwnProperty('sublist_id')) {
|
218
|
+
-- argument_error("Missing mandatory argument: sublist_id");
|
219
|
+
-- }
|
220
|
+
--
|
221
|
+
-- var record = nlapiLoadRecord(request.type_id, request.parent_id);
|
222
|
+
-- var sublist_len = record.getLineItemCount(request.sublist_id);
|
223
|
+
-- var downstream_items = request.data;
|
224
|
+
-- var we_touched_something = false;
|
225
|
+
--
|
226
|
+
-- // 1. Modify all existing records that have been changed.
|
227
|
+
-- for(var i = 1; i <= sublist_len; i++) {
|
228
|
+
-- var upstream_id = record.getLineItemValue(
|
229
|
+
-- request.sublist_id, 'id', i
|
230
|
+
-- );
|
231
|
+
-- // Search for an id within downstream items.
|
232
|
+
-- for(var j = 0; j < downstream_items.length; j++){
|
233
|
+
-- if(!downstream_items[j].hasOwnProperty('id')) {
|
234
|
+
-- // New record, not interested in this yet
|
235
|
+
-- continue;
|
236
|
+
-- }
|
237
|
+
--
|
238
|
+
-- if(downstream_items[j]['id'] == upstream_id){
|
239
|
+
-- // We have a match, we need to merge now
|
240
|
+
-- for(var field in downstream_items[j]) {
|
241
|
+
-- var upstream_value = record.getLineItemValue(
|
242
|
+
-- request.sublist_id, field, i
|
243
|
+
-- )
|
244
|
+
--
|
245
|
+
-- // Set if changed
|
246
|
+
-- if(downstream_items[j][field] != upstream_value) {
|
247
|
+
-- record.setLineItemValue(
|
248
|
+
-- request.sublist_id,
|
249
|
+
-- field,
|
250
|
+
-- i,
|
251
|
+
-- downstream_items[j][field]
|
252
|
+
-- )
|
253
|
+
-- we_touched_something = true;
|
254
|
+
-- }
|
255
|
+
-- }
|
256
|
+
-- }
|
257
|
+
-- }
|
258
|
+
-- }
|
259
|
+
--
|
260
|
+
-- // 2. Delete all existing records that aren't in our new dataset
|
261
|
+
-- for(var i = sublist_len; i > 0; i--) {
|
262
|
+
-- var match = false;
|
263
|
+
-- var upstream_id = record.getLineItemValue(
|
264
|
+
-- request.sublist_id, 'id', i
|
265
|
+
-- );
|
266
|
+
--
|
267
|
+
-- for(var j = 0; j < downstream_items.length; j++){
|
268
|
+
-- if(!downstream_items[j].hasOwnProperty('id')) {
|
269
|
+
-- continue;
|
270
|
+
-- }
|
271
|
+
-- if(downstream_items[j]['id'] == upstream_id) {
|
272
|
+
-- match = true;
|
273
|
+
-- }
|
274
|
+
-- }
|
275
|
+
--
|
276
|
+
-- // Delete this item if it wasn't in the downstream data set
|
277
|
+
-- if(!match) {
|
278
|
+
-- record.removeLineItem(request.sublist_id, i);
|
279
|
+
-- we_touched_something = true;
|
280
|
+
-- sublist_len--;
|
281
|
+
-- }
|
282
|
+
-- }
|
283
|
+
--
|
284
|
+
-- // 3. Append items
|
285
|
+
-- for(var i = 0; i < downstream_items.length; i++) {
|
286
|
+
-- if(downstream_items[i].hasOwnProperty('id')) {
|
287
|
+
-- continue;
|
288
|
+
-- }
|
289
|
+
-- // New item, append it.
|
290
|
+
-- record.insertLineItem(request.sublist_id, ++sublist_len);
|
291
|
+
-- we_touched_something = true;
|
292
|
+
--
|
293
|
+
-- for(var field in downstream_items[i]) {
|
294
|
+
-- value = downstream_items[i][field];
|
295
|
+
-- record.setLineItemValue(
|
296
|
+
-- request.sublist_id, field, i, value
|
297
|
+
-- );
|
298
|
+
-- }
|
299
|
+
-- }
|
300
|
+
--
|
301
|
+
-- // NetSuite explodes if we try to commit a sublist without altering it
|
302
|
+
-- // in any way, so we have to keep this silly flag around.
|
303
|
+
-- if(we_touched_something) {
|
304
|
+
-- record.commitLineItem(request.sublist_id);
|
305
|
+
-- nlapiSubmitRecord(record, true);
|
306
|
+
-- };
|
307
|
+
--
|
308
|
+
-- return([]);
|
309
|
+
--}
|
310
|
+
--
|
311
|
+
- // Given a record, sublist_id and array of fields, retrieve the whole sublist
|
312
|
+
- // as an array of hashes
|
313
|
+
- function get_sublist(record, sublist_id, fields)
|
314
|
+
diff --git a/spec/resource_spec.rb b/spec/resource_spec.rb
|
315
|
+
index 2d95382..7eaf835 100644
|
316
|
+
--- a/spec/resource_spec.rb
|
317
|
+
+++ b/spec/resource_spec.rb
|
318
|
+
@@ -67,16 +67,6 @@ describe PseudoResource do
|
319
|
+
|
320
|
+
@p.firstname = 'name'
|
321
|
+
|
322
|
+
- note_item1 = @p.new_notes_item(:line => 'line 1')
|
323
|
+
- note_item2 = @p.new_notes_item(:line => 'line 2')
|
324
|
+
-
|
325
|
+
- @p.notes << note_item1
|
326
|
+
- @p.notes << note_item2
|
327
|
+
-
|
328
|
+
- # Replace them, backways to test.
|
329
|
+
- SubList.should_receive(:save!).
|
330
|
+
- and_return([note_item2, note_item1])
|
331
|
+
-
|
332
|
+
Restlet.should_receive(:execute!).
|
333
|
+
with({
|
334
|
+
:action => 'create',
|
335
|
+
@@ -91,9 +81,6 @@ describe PseudoResource do
|
336
|
+
|
337
|
+
expect(@p.firstname).to eql('Name')
|
338
|
+
expect(@p.lastname).to eql('nothing')
|
339
|
+
-
|
340
|
+
- # Backwards now
|
341
|
+
- expect(@p.notes.first.line).to eql('line 2')
|
342
|
+
end
|
343
|
+
|
344
|
+
it 'has a pretty inspect' do
|
345
|
+
@@ -266,14 +253,9 @@ describe PseudoResource do
|
346
|
+
end
|
347
|
+
|
348
|
+
context 'sublists on new object' do
|
349
|
+
- it 'can append a new list to blank object' do
|
350
|
+
+ it 'is empty' do
|
351
|
+
p = PseudoResource.new
|
352
|
+
- expect(p.new_notes_item).to be_a(NSConnector::SubListItem)
|
353
|
+
- p.notes << p.new_notes_item(:line => 'line 1')
|
354
|
+
- p.notes << p.new_notes_item(:line => 'line 2')
|
355
|
+
-
|
356
|
+
- expect(p.notes.pop.line).to eql('line 2')
|
357
|
+
- expect(p.notes.pop.line).to eql('line 1')
|
358
|
+
+ expect(p.notes).to be_empty
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
diff --git a/spec/sublist_spec.rb b/spec/sublist_spec.rb
|
363
|
+
index 38b0147..ef2aea3 100644
|
364
|
+
--- a/spec/sublist_spec.rb
|
365
|
+
+++ b/spec/sublist_spec.rb
|
366
|
+
@@ -16,35 +16,6 @@ describe SubList do
|
367
|
+
)
|
368
|
+
end
|
369
|
+
|
370
|
+
- it 'saves' do
|
371
|
+
- Restlet.should_receive(:execute!).
|
372
|
+
- with({
|
373
|
+
- :action => 'save_sublist',
|
374
|
+
- :type_id => 'contact',
|
375
|
+
- :parent_id => '42',
|
376
|
+
- :sublist_id => 'sublist',
|
377
|
+
- :fields => ['field1', 'field2'],
|
378
|
+
- :data => [
|
379
|
+
- {'field1' => 'data'},
|
380
|
+
- {'field2' => 'otherdata'}
|
381
|
+
- ]
|
382
|
+
- }).and_return(
|
383
|
+
- 'true'
|
384
|
+
- )
|
385
|
+
-
|
386
|
+
- SubList.should_receive(:fetch).and_return('hai')
|
387
|
+
- # Duplicates should be ignored, I figure? I'll be a little
|
388
|
+
- # confused if duplicates ever appear in NetSuite
|
389
|
+
- saved = SubList.save!(
|
390
|
+
- [@item1, @item1, @item2],
|
391
|
+
- @parent,
|
392
|
+
- 'sublist',
|
393
|
+
- ['field1', 'field2']
|
394
|
+
- )
|
395
|
+
-
|
396
|
+
- expect(saved).to eql('hai')
|
397
|
+
- end
|
398
|
+
-
|
399
|
+
it 'fetches' do
|
400
|
+
Restlet.should_receive(:execute!).
|
401
|
+
with({
|
402
|
+
diff --git a/support/restlet.js b/support/restlet.js
|
403
|
+
index 4569862..3a7ad1b 100644
|
404
|
+
--- a/support/restlet.js
|
405
|
+
+++ b/support/restlet.js
|
406
|
+
@@ -193,133 +193,6 @@ function create(request)
|
407
|
+
));
|
408
|
+
}
|
409
|
+
|
410
|
+
-// Save a sublist, trying to merge changes nicely.
|
411
|
+
-//
|
412
|
+
-// Returns:: True, note that you will need to fetch the updated items yourself,
|
413
|
+
-// as netsuite seems to need a little while to think about any newly added
|
414
|
+
-// records or it freaks out.
|
415
|
+
-//
|
416
|
+
-// Raises:: An invalid sublist operation error, often, as apparently you're not
|
417
|
+
-// allowed to delete certain sublists even though you can append to them. Kind
|
418
|
+
-// of really annoying. Not much I can do about it.
|
419
|
+
-//
|
420
|
+
-// Also seems to raise a 500 on modifying existing items if it doesn't want you
|
421
|
+
-// to.
|
422
|
+
-//
|
423
|
+
-// We have the minor problem here of only being able to modify existing line
|
424
|
+
-// items in place, insert new ones and delete old ones, not rewrite the whole
|
425
|
+
-// list. This problem is easily dealt with if we simply don't worry about
|
426
|
+
-// order. So, new items should be appended to the end, and the existing order
|
427
|
+
-// shouldn't be messed with, however if you wanted to say, pull the whole list
|
428
|
+
-// out and reverse the order, you're out of luck.
|
429
|
+
-//
|
430
|
+
-// It's possible to do, I think. I have other things to work on just now.
|
431
|
+
-//
|
432
|
+
-// So we have three basic steps:
|
433
|
+
-//
|
434
|
+
-// 1. Modify all existing records that have changed, identifying by id.
|
435
|
+
-// 2. If the existing record does not appear in the new list, delete it.
|
436
|
+
-// 3. Append new records to the end.
|
437
|
+
-function save_sublist(request)
|
438
|
+
-{
|
439
|
+
- if(!request.hasOwnProperty('parent_id')) {
|
440
|
+
- argument_error("Missing mandatory argument: parent_id");
|
441
|
+
- }
|
442
|
+
-
|
443
|
+
- if(!request.hasOwnProperty('sublist_id')) {
|
444
|
+
- argument_error("Missing mandatory argument: sublist_id");
|
445
|
+
- }
|
446
|
+
-
|
447
|
+
- var record = nlapiLoadRecord(request.type_id, request.parent_id);
|
448
|
+
- var sublist_len = record.getLineItemCount(request.sublist_id);
|
449
|
+
- var downstream_items = request.data;
|
450
|
+
- var we_touched_something = false;
|
451
|
+
-
|
452
|
+
- // 1. Modify all existing records that have been changed.
|
453
|
+
- for(var i = 1; i <= sublist_len; i++) {
|
454
|
+
- var upstream_id = record.getLineItemValue(
|
455
|
+
- request.sublist_id, 'id', i
|
456
|
+
- );
|
457
|
+
- // Search for an id within downstream items.
|
458
|
+
- for(var j = 0; j < downstream_items.length; j++){
|
459
|
+
- if(!downstream_items[j].hasOwnProperty('id')) {
|
460
|
+
- // New record, not interested in this yet
|
461
|
+
- continue;
|
462
|
+
- }
|
463
|
+
-
|
464
|
+
- if(downstream_items[j]['id'] == upstream_id){
|
465
|
+
- // We have a match, we need to merge now
|
466
|
+
- for(var field in downstream_items[j]) {
|
467
|
+
- var upstream_value = record.getLineItemValue(
|
468
|
+
- request.sublist_id, field, i
|
469
|
+
- )
|
470
|
+
-
|
471
|
+
- // Set if changed
|
472
|
+
- if(downstream_items[j][field] != upstream_value) {
|
473
|
+
- record.setLineItemValue(
|
474
|
+
- request.sublist_id,
|
475
|
+
- field,
|
476
|
+
- i,
|
477
|
+
- downstream_items[j][field]
|
478
|
+
- )
|
479
|
+
- we_touched_something = true;
|
480
|
+
- }
|
481
|
+
- }
|
482
|
+
- }
|
483
|
+
- }
|
484
|
+
- }
|
485
|
+
-
|
486
|
+
- // 2. Delete all existing records that aren't in our new dataset
|
487
|
+
- for(var i = sublist_len; i > 0; i--) {
|
488
|
+
- var match = false;
|
489
|
+
- var upstream_id = record.getLineItemValue(
|
490
|
+
- request.sublist_id, 'id', i
|
491
|
+
- );
|
492
|
+
-
|
493
|
+
- for(var j = 0; j < downstream_items.length; j++){
|
494
|
+
- if(!downstream_items[j].hasOwnProperty('id')) {
|
495
|
+
- continue;
|
496
|
+
- }
|
497
|
+
- if(downstream_items[j]['id'] == upstream_id) {
|
498
|
+
- match = true;
|
499
|
+
- }
|
500
|
+
- }
|
501
|
+
-
|
502
|
+
- // Delete this item if it wasn't in the downstream data set
|
503
|
+
- if(!match) {
|
504
|
+
- record.removeLineItem(request.sublist_id, i);
|
505
|
+
- we_touched_something = true;
|
506
|
+
- sublist_len--;
|
507
|
+
- }
|
508
|
+
- }
|
509
|
+
-
|
510
|
+
- // 3. Append items
|
511
|
+
- for(var i = 0; i < downstream_items.length; i++) {
|
512
|
+
- if(downstream_items[i].hasOwnProperty('id')) {
|
513
|
+
- continue;
|
514
|
+
- }
|
515
|
+
- // New item, append it.
|
516
|
+
- record.insertLineItem(request.sublist_id, ++sublist_len);
|
517
|
+
- we_touched_something = true;
|
518
|
+
-
|
519
|
+
- for(var field in downstream_items[i]) {
|
520
|
+
- value = downstream_items[i][field];
|
521
|
+
- record.setLineItemValue(
|
522
|
+
- request.sublist_id, field, i, value
|
523
|
+
- );
|
524
|
+
- }
|
525
|
+
- }
|
526
|
+
-
|
527
|
+
- // NetSuite explodes if we try to commit a sublist without altering it
|
528
|
+
- // in any way, so we have to keep this silly flag around.
|
529
|
+
- if(we_touched_something) {
|
530
|
+
- record.commitLineItem(request.sublist_id);
|
531
|
+
- nlapiSubmitRecord(record, true);
|
532
|
+
- };
|
533
|
+
-
|
534
|
+
- return([]);
|
535
|
+
-}
|
536
|
+
-
|
537
|
+
// Given a record, sublist_id and array of fields, retrieve the whole sublist
|
538
|
+
// as an array of hashes
|
539
|
+
function get_sublist(record, sublist_id, fields)
|
540
|
+
@@ -393,7 +266,6 @@ function main(request)
|
541
|
+
'retrieve' : retrieve,
|
542
|
+
'search' : search,
|
543
|
+
'update' : update,
|
544
|
+
- 'save_sublist' : save_sublist,
|
545
|
+
'fetch_sublist' : fetch_sublist
|
546
|
+
}
|
547
|
+
|