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.
@@ -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