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.
@@ -84,7 +84,16 @@ describe PseudoResource do
84
84
  }
85
85
 
86
86
  @p.firstname = 'name'
87
- @p.notes << @p.new_notes_item(:line => 'line1')
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' => 'line1'}
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
@@ -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
@@ -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