ns_connector 0.0.14 → 0.0.15
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.
- data/Gemfile +1 -0
- data/Gemfile.lock +93 -48
- data/VERSION +1 -1
- data/lib/ns_connector/resource.rb +22 -1
- data/lib/ns_connector/resources/customer.rb +1 -1
- data/lib/ns_connector/resources/customer_payment.rb +5 -3
- data/lib/ns_connector/resources/discount_item.rb +63 -0
- data/lib/ns_connector/resources/non_inventory_item.rb +489 -0
- data/lib/ns_connector/resources/sales_order.rb +430 -0
- data/lib/ns_connector/sublist.rb +22 -0
- data/spec/resource_spec.rb +25 -2
- data/spec/sublist_spec.rb +29 -0
- data/support/restlet.js +209 -0
- data/support/type_scraper.rb +99 -0
- data/support/type_scraper.tpl.erb +29 -0
- metadata +24 -3
data/spec/resource_spec.rb
CHANGED
@@ -84,7 +84,16 @@ describe PseudoResource do
|
|
84
84
|
}
|
85
85
|
|
86
86
|
@p.firstname = 'name'
|
87
|
-
|
87
|
+
|
88
|
+
note_item1 = @p.new_notes_item(:line => 'line 1')
|
89
|
+
note_item2 = @p.new_notes_item(:line => 'line 2')
|
90
|
+
|
91
|
+
@p.notes << note_item1
|
92
|
+
@p.notes << note_item2
|
93
|
+
|
94
|
+
# Replace them, backways to test.
|
95
|
+
SubList.should_receive(:save!).
|
96
|
+
and_return([note_item2, note_item1])
|
88
97
|
|
89
98
|
Restlet.should_receive(:execute!).
|
90
99
|
with({
|
@@ -94,7 +103,8 @@ describe PseudoResource do
|
|
94
103
|
:data => {'firstname' => 'name'},
|
95
104
|
:sublists => {
|
96
105
|
:notes => [
|
97
|
-
{'line' => '
|
106
|
+
{'line' => 'line 1'},
|
107
|
+
{'line' => 'line 2'}
|
98
108
|
]
|
99
109
|
},
|
100
110
|
}).
|
@@ -106,6 +116,9 @@ describe PseudoResource do
|
|
106
116
|
expect(@p.firstname).to eql('Name')
|
107
117
|
expect(@p.lastname).to eql('nothing')
|
108
118
|
expect(@p.id).to eql('42')
|
119
|
+
|
120
|
+
# Backwards now
|
121
|
+
expect(@p.notes.first.line).to eql('line 2')
|
109
122
|
end
|
110
123
|
|
111
124
|
it 'has a pretty inspect' do
|
@@ -299,6 +312,16 @@ describe PseudoResource do
|
|
299
312
|
p = PseudoResource.new
|
300
313
|
expect(p.notes).to be_empty
|
301
314
|
end
|
315
|
+
|
316
|
+
it 'can append a new list to blank object' do
|
317
|
+
p = PseudoResource.new
|
318
|
+
expect(p.new_notes_item).to be_a(NSConnector::SubListItem)
|
319
|
+
p.notes << p.new_notes_item(:line => 'line 1')
|
320
|
+
p.notes << p.new_notes_item(:line => 'line 2')
|
321
|
+
|
322
|
+
expect(p.notes.pop.line).to eql('line 2')
|
323
|
+
expect(p.notes.pop.line).to eql('line 1')
|
324
|
+
end
|
302
325
|
end
|
303
326
|
|
304
327
|
context 'raw search' do
|
data/spec/sublist_spec.rb
CHANGED
@@ -42,4 +42,33 @@ describe SubList do
|
|
42
42
|
expect(sublist.first.field1).to eql('new')
|
43
43
|
expect(sublist.last.field2).to eql('new2')
|
44
44
|
end
|
45
|
+
|
46
|
+
it 'saves' do
|
47
|
+
Restlet.should_receive(:execute!).
|
48
|
+
with({
|
49
|
+
:action => 'update_sublist',
|
50
|
+
:type_id => 'contact',
|
51
|
+
:parent_id => '42',
|
52
|
+
:sublist_id => 'sublist',
|
53
|
+
:fields => ['field1', 'field2'],
|
54
|
+
:data => [
|
55
|
+
{'field1' => 'data'},
|
56
|
+
{'field2' => 'otherdata'}
|
57
|
+
]
|
58
|
+
}).and_return(
|
59
|
+
'true'
|
60
|
+
)
|
61
|
+
|
62
|
+
SubList.should_receive(:fetch).and_return('hai')
|
63
|
+
# Duplicates should be ignored, I figure? I'll be a little
|
64
|
+
# confused if duplicates ever appear in NetSuite
|
65
|
+
saved = SubList.save!(
|
66
|
+
[@item1, @item1, @item2],
|
67
|
+
@parent,
|
68
|
+
'sublist',
|
69
|
+
['field1', 'field2']
|
70
|
+
)
|
71
|
+
|
72
|
+
expect(saved).to eql('hai')
|
73
|
+
end
|
45
74
|
end
|
data/support/restlet.js
CHANGED
@@ -14,12 +14,29 @@ function apply_constructor(klass, opts) {
|
|
14
14
|
return new applicator();
|
15
15
|
}
|
16
16
|
|
17
|
+
// Asserts that the current request object has a particular property.
|
18
|
+
// If it doesn't, it raises a 400 error.
|
19
|
+
function assert_property(request, property_name)
|
20
|
+
{
|
21
|
+
if(!request.hasOwnProperty(property_name)) {
|
22
|
+
argument_error("Missing mandatory argument: " + property_name);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
17
26
|
// Stops execution, sending a HTTP 400 response
|
18
27
|
function argument_error(message)
|
19
28
|
{
|
20
29
|
throw nlapiCreateError('400', message);
|
21
30
|
}
|
22
31
|
|
32
|
+
// Implements a simple non-prototyped indexOf for objects.
|
33
|
+
function object_index_of(current_obj, value) {
|
34
|
+
for (var key in current_obj) {
|
35
|
+
if (current_obj[key] == value) return key;
|
36
|
+
}
|
37
|
+
return null;
|
38
|
+
}
|
39
|
+
|
23
40
|
|
24
41
|
// Returns a simple record as an associative array
|
25
42
|
function get_record_by_id(type_id, fields, id)
|
@@ -319,6 +336,197 @@ function fetch_sublist(request)
|
|
319
336
|
return(get_sublist(record, request.sublist_id, request.fields));
|
320
337
|
}
|
321
338
|
|
339
|
+
// Before we can update sublists, we must determine first what operations
|
340
|
+
// we are about to perform on it.
|
341
|
+
//
|
342
|
+
// Arguments::
|
343
|
+
// type_id: The type of the parent entity
|
344
|
+
// parent_id: The ID of the entity to get the sublist from
|
345
|
+
// sublist_id: The identifier for the current sublist
|
346
|
+
// data: The data which we are sending to update the sublist with
|
347
|
+
//
|
348
|
+
// Returns:: An object representing the changes that the current request is
|
349
|
+
// about to perform on the sublist. This object contains three keys:
|
350
|
+
// add, update, and del (for delete). Each key points to an array of changes.
|
351
|
+
//
|
352
|
+
// Raises:: A 400 Missing mandatory argument exception if we missed a
|
353
|
+
// required argument.
|
354
|
+
//
|
355
|
+
// Each change item is an object that can contain the following keys:
|
356
|
+
// * downstream_index: The index of the sublist item in the changes that we
|
357
|
+
// are pushing up. You'll only see this on add and update
|
358
|
+
// operations.
|
359
|
+
function get_sublist_changes(request)
|
360
|
+
{
|
361
|
+
assert_property(request, 'parent_id');
|
362
|
+
assert_property(request, 'sublist_id');
|
363
|
+
|
364
|
+
var record = nlapiLoadRecord(request.type_id, request.parent_id);
|
365
|
+
var upstream_item_count = record.getLineItemCount(request.sublist_id);
|
366
|
+
var downstream_items = request.data;
|
367
|
+
|
368
|
+
// Start out by checking downstream data to see if we need to add new records.
|
369
|
+
var downstream_identifiers = {}, add_downstream = [];
|
370
|
+
|
371
|
+
for (var i = 0; i < downstream_items.length; i++) {
|
372
|
+
downstream_identifiers[i] = null;
|
373
|
+
|
374
|
+
if (downstream_items[i].hasOwnProperty('id')) {
|
375
|
+
downstream_identifiers[i] = downstream_items[i].id;
|
376
|
+
}
|
377
|
+
else {
|
378
|
+
add_downstream.push({
|
379
|
+
downstream_index: i
|
380
|
+
});
|
381
|
+
}
|
382
|
+
}
|
383
|
+
|
384
|
+
// Mark existing records that have been changed or deleted.
|
385
|
+
|
386
|
+
var update_upstream = [], delete_upstream = []; // Indices of upstream items to update & delete.
|
387
|
+
|
388
|
+
for (var i = 1; i <= upstream_item_count; i++) {
|
389
|
+
var upstream_id = record.getLineItemValue(
|
390
|
+
request.sublist_id, 'id', i
|
391
|
+
);
|
392
|
+
|
393
|
+
var corresponding_downstream = object_index_of(downstream_identifiers, upstream_id);
|
394
|
+
|
395
|
+
if (corresponding_downstream) {
|
396
|
+
// Mark for update.
|
397
|
+
update_upstream.push({
|
398
|
+
downstream_index: corresponding_downstream,
|
399
|
+
upstream_index: i,
|
400
|
+
id: upstream_id
|
401
|
+
});
|
402
|
+
}
|
403
|
+
else {
|
404
|
+
// Mark for deletion.
|
405
|
+
delete_upstream.push({
|
406
|
+
upstream_index: i,
|
407
|
+
id: upstream_id
|
408
|
+
});
|
409
|
+
}
|
410
|
+
}
|
411
|
+
|
412
|
+
return {
|
413
|
+
add: add_downstream,
|
414
|
+
update: update_upstream,
|
415
|
+
del: delete_upstream
|
416
|
+
}
|
417
|
+
}
|
418
|
+
|
419
|
+
// Commits changes to a given sublist.
|
420
|
+
//
|
421
|
+
// Arguments::
|
422
|
+
// type_id: The type of the parent entity
|
423
|
+
// parent_id: The ID of the entity to get the sublist from
|
424
|
+
// sublist_id: The identifier for the current sublist
|
425
|
+
// data: The data which we are sending to update the sublist with
|
426
|
+
//
|
427
|
+
// Returns:: An empty array. You'll need to re-fetch the sublist yourself
|
428
|
+
// later.
|
429
|
+
//
|
430
|
+
// Raises:: A 400 Missing mandatory argument exception if we missed a
|
431
|
+
// required argument.
|
432
|
+
//
|
433
|
+
// Each change item is an object that can contain the following keys:
|
434
|
+
// * downstream_index: The index of the sublist item in the changes that we
|
435
|
+
// are pushing up. You'll only see this on add and update
|
436
|
+
//
|
437
|
+
function update_sublist(request)
|
438
|
+
{
|
439
|
+
var changes = get_sublist_changes(request);
|
440
|
+
|
441
|
+
var has_changes = (changes.add.length > 0 || changes.update.length > 0 || changes.del.length > 0);
|
442
|
+
|
443
|
+
var change_log = [];
|
444
|
+
|
445
|
+
if (has_changes) {
|
446
|
+
|
447
|
+
var record = nlapiLoadRecord(request.type_id, request.parent_id);
|
448
|
+
var upstream_item_count = record.getLineItemCount(request.sublist_id);
|
449
|
+
var dirty = false;
|
450
|
+
|
451
|
+
// 1. Update
|
452
|
+
|
453
|
+
for (var i = 0; i < changes.update.length; i++) {
|
454
|
+
var change = changes.update[i];
|
455
|
+
var downstream_item = request.data[change.downstream_index];
|
456
|
+
|
457
|
+
for (var field in downstream_item) {
|
458
|
+
var downstream_value = downstream_item[field];
|
459
|
+
|
460
|
+
var upstream_value = record.getLineItemValue(
|
461
|
+
request.sublist_id, field, change.upstream_index
|
462
|
+
);
|
463
|
+
|
464
|
+
if (upstream_value != downstream_value) {
|
465
|
+
dirty = true;
|
466
|
+
|
467
|
+
record.setLineItemValue(
|
468
|
+
request.sublist_id,
|
469
|
+
field,
|
470
|
+
change.upstream_index,
|
471
|
+
downstream_value
|
472
|
+
);
|
473
|
+
}
|
474
|
+
}
|
475
|
+
}
|
476
|
+
|
477
|
+
// 2. Delete
|
478
|
+
|
479
|
+
for (var i = 0; i < changes.del.length; i++) {
|
480
|
+
var change = changes.del[i];
|
481
|
+
|
482
|
+
dirty = true;
|
483
|
+
|
484
|
+
record.removeLineItem(
|
485
|
+
request.sublist_id,
|
486
|
+
change.upstream_index
|
487
|
+
);
|
488
|
+
|
489
|
+
--upstream_item_count;
|
490
|
+
}
|
491
|
+
|
492
|
+
// 3. Add
|
493
|
+
|
494
|
+
for (var i = 0; i < changes.add.length; i++) {
|
495
|
+
var change = changes.add[i];
|
496
|
+
var downstream_item = request.data[change.downstream_index];
|
497
|
+
|
498
|
+
dirty = true;
|
499
|
+
|
500
|
+
var new_upstream_index = ++upstream_item_count;
|
501
|
+
|
502
|
+
record.insertLineItem(request.sublist_id, new_upstream_index);
|
503
|
+
|
504
|
+
for (var field in downstream_item) {
|
505
|
+
var downstream_value = downstream_item[field];
|
506
|
+
|
507
|
+
record.setLineItemValue(
|
508
|
+
request.sublist_id,
|
509
|
+
field,
|
510
|
+
new_upstream_index,
|
511
|
+
downstream_value
|
512
|
+
);
|
513
|
+
}
|
514
|
+
}
|
515
|
+
|
516
|
+
// 4. Save
|
517
|
+
|
518
|
+
// NetSuite explodes if we try to commit a sublist without altering it
|
519
|
+
// in any way, so we have to make sure that the data needs to be saved
|
520
|
+
// before actually trying to save it.
|
521
|
+
if (dirty) {
|
522
|
+
nlapiCommitLineItem(request.sublist_id);
|
523
|
+
nlapiSubmitRecord(record, true);
|
524
|
+
}
|
525
|
+
};
|
526
|
+
|
527
|
+
return([]);
|
528
|
+
}
|
529
|
+
|
322
530
|
// Make sure we have the required arguments in our request object
|
323
531
|
function pre_flight_check(request)
|
324
532
|
{
|
@@ -412,6 +620,7 @@ function main(request)
|
|
412
620
|
'search' : search,
|
413
621
|
'update' : update,
|
414
622
|
'fetch_sublist' : fetch_sublist,
|
623
|
+
'update_sublist' : update_sublist,
|
415
624
|
'invoice_pdf' : invoice_pdf,
|
416
625
|
'attach' : attach,
|
417
626
|
'detach' : detach,
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
require "uri"
|
5
|
+
require "erb"
|
6
|
+
require "ostruct"
|
7
|
+
require "open-uri"
|
8
|
+
|
9
|
+
page = ARGV[0]
|
10
|
+
class_name = ARGV[1]
|
11
|
+
|
12
|
+
# NB: Index of types is at
|
13
|
+
# https://system.netsuite.com/help/helpcenter/en_US/RecordsBrowser/2012_2/index.html
|
14
|
+
|
15
|
+
url = "https://system.netsuite.com/help/helpcenter/en_US/RecordsBrowser/2012_2/Records/#{page}.html"
|
16
|
+
bogus_ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.73.11 (KHTML, like Gecko) Version/6.1.1 Safari/537.73.11"
|
17
|
+
|
18
|
+
raw = []
|
19
|
+
|
20
|
+
open(url, "User-Agent" => bogus_ua) {|f|
|
21
|
+
f.each_line {|line|
|
22
|
+
raw << line
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
html_doc = Nokogiri::HTML(raw.join("\n"))
|
27
|
+
|
28
|
+
record_id = nil
|
29
|
+
fields = [:id]
|
30
|
+
sublists = {}
|
31
|
+
|
32
|
+
record_ident = html_doc.css('.record_id').first.content.gsub('Internal ID: ', '')
|
33
|
+
|
34
|
+
# Fields
|
35
|
+
|
36
|
+
fields_table = html_doc.css('table.record_table').first
|
37
|
+
|
38
|
+
fields_table.css('tr').each_with_index do |row, i|
|
39
|
+
next if i == 0
|
40
|
+
|
41
|
+
record_cell = row.css('td').first
|
42
|
+
fields << record_cell.content.gsub(/^\s+/, '').gsub(/\s+$/, '').to_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
# Record shit
|
46
|
+
|
47
|
+
html_doc.css('.record_item').each do |r|
|
48
|
+
if r.content.downcase == "sublists"
|
49
|
+
row_table = r.parent.parent.parent
|
50
|
+
row_table.next_element.css('tr').each_with_index do |row, i|
|
51
|
+
next if i == 0
|
52
|
+
|
53
|
+
record_cell = row.css('td').first.css('a').first
|
54
|
+
sublist_name = record_cell.content.downcase.to_sym
|
55
|
+
sublists[sublist_name] = []
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
html_doc.css('body > div.sublist').each do |r|
|
61
|
+
if r.content.downcase =~ / sublist fields$/
|
62
|
+
sublist_name = nil
|
63
|
+
|
64
|
+
r.next_element.css('tr').each_with_index do |row, i|
|
65
|
+
next if i == 0
|
66
|
+
|
67
|
+
record_cell = row.css('td').first
|
68
|
+
|
69
|
+
if i == 1
|
70
|
+
sublist_name = row.css('td').first.content.downcase.to_sym
|
71
|
+
record_cell = row.css('td')[1]
|
72
|
+
end
|
73
|
+
|
74
|
+
sublist_value = record_cell.content.downcase.to_sym
|
75
|
+
sublists[sublist_name] << sublist_value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
ns = OpenStruct.new({
|
83
|
+
resource_type: class_name,
|
84
|
+
resource_type_ident: record_ident,
|
85
|
+
fields: fields,
|
86
|
+
sublists: sublists
|
87
|
+
})
|
88
|
+
|
89
|
+
class_content = ERB.new(
|
90
|
+
File.read(File.join(
|
91
|
+
File.dirname(__FILE__), "type_scraper.tpl.erb"
|
92
|
+
)),
|
93
|
+
nil,
|
94
|
+
'-'
|
95
|
+
).result(
|
96
|
+
ns.instance_eval { binding }
|
97
|
+
)
|
98
|
+
|
99
|
+
puts class_content
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ns_connector/resource'
|
2
|
+
|
3
|
+
# == <%= resource_type %> resource
|
4
|
+
# === Fields
|
5
|
+
<% fields.each do |f| -%>
|
6
|
+
# * <%= f.to_s %>
|
7
|
+
<% end -%>
|
8
|
+
# === Sublists
|
9
|
+
<% sublists.each do |k, v| -%>
|
10
|
+
# * <%= k.to_s %>
|
11
|
+
<% end -%>
|
12
|
+
|
13
|
+
class NSConnector::<%= resource_type %> < NSConnector::Resource
|
14
|
+
@type_id = '<%= resource_type_ident %>'
|
15
|
+
@fields = [
|
16
|
+
<% fields.each do |f| -%>
|
17
|
+
:<%= f.to_s %>,
|
18
|
+
<% end -%>
|
19
|
+
]
|
20
|
+
@sublists = {
|
21
|
+
<% sublists.each do |k, v| -%>
|
22
|
+
:<%= k.to_s %> => [
|
23
|
+
<% v.each do |_v| -%>
|
24
|
+
:<%= _v.to_s %>,
|
25
|
+
<% end -%>
|
26
|
+
]<% if k == @sublists.keys.last %>,<% end %>
|
27
|
+
<% end -%>
|
28
|
+
}
|
29
|
+
end
|