ns_connector 0.0.14 → 0.0.15
Sign up to get free protection for your applications and to get access to all the features.
- 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
|