netsuite_rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +57 -0
- data/Rakefile +2 -0
- data/lib/generators/netsuite_rails/install_generator.rb +25 -0
- data/lib/generators/netsuite_rails/templates/create_netsuite_poll_timestamps.rb +12 -0
- data/lib/netsuite_rails/configuration.rb +54 -0
- data/lib/netsuite_rails/list_sync/pull_manager.rb +33 -0
- data/lib/netsuite_rails/list_sync.rb +25 -0
- data/lib/netsuite_rails/netsuite_rails.rb +22 -0
- data/lib/netsuite_rails/poll_manager.rb +62 -0
- data/lib/netsuite_rails/poll_timestamp.rb +11 -0
- data/lib/netsuite_rails/record_sync/pull_manager.rb +147 -0
- data/lib/netsuite_rails/record_sync/push_manager.rb +186 -0
- data/lib/netsuite_rails/record_sync.rb +235 -0
- data/lib/netsuite_rails/spec/spec_helper.rb +56 -0
- data/lib/netsuite_rails/sub_list_sync.rb +31 -0
- data/lib/netsuite_rails/sync_trigger.rb +104 -0
- data/lib/netsuite_rails/tasks/netsuite.rb +52 -0
- data/lib/netsuite_rails/transformations.rb +44 -0
- data/lib/netsuite_rails/url_helper.rb +31 -0
- data/lib/netsuite_rails.rb +1 -0
- data/netsuite_rails.gemspec +25 -0
- data/spec/spec_helper.rb +9 -0
- metadata +152 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Michael Bianco
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# NetSuite Rails
|
2
|
+
|
3
|
+
**Note:** Documentation is horrible... look at the code for details.
|
4
|
+
|
5
|
+
Build custom rails application that sync to NetSuite.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'netsuite_rails'
|
11
|
+
```
|
12
|
+
|
13
|
+
Install the database migration for poll timestamps
|
14
|
+
|
15
|
+
```bash
|
16
|
+
rails g netsuite_rails:install
|
17
|
+
```
|
18
|
+
|
19
|
+
### Date & Time
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
NetSuiteRails.configure do
|
23
|
+
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
modes: :read, :read_write, :aggressive
|
30
|
+
|
31
|
+
When using a proc in a NS mapping, you are responsible for setting local and remote values
|
32
|
+
|
33
|
+
for pushing tasks to DJ https://github.com/collectiveidea/delayed_job/wiki/Rake-Task-as-a-Delayed-Job
|
34
|
+
|
35
|
+
### Syncing
|
36
|
+
|
37
|
+
```bash
|
38
|
+
rake netsuite:sync
|
39
|
+
|
40
|
+
rake netsuite:fresh_sync
|
41
|
+
```
|
42
|
+
|
43
|
+
Caveats:
|
44
|
+
|
45
|
+
* If you have date time fields, or custom fields that will trigger `changed_attributes` this might cause issues when pulling an existing record
|
46
|
+
* `changed_attributes` doesn't work well with store
|
47
|
+
|
48
|
+
## Testing
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# in spec_helper.rb
|
52
|
+
require 'netsuite_rails/spec/spec_helper'
|
53
|
+
```
|
54
|
+
|
55
|
+
## Author
|
56
|
+
|
57
|
+
* Michael Bianco @iloveitaly
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
require 'rails/generators/active_record'
|
4
|
+
|
5
|
+
module NetsuiteRails
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
# http://stackoverflow.com/questions/4141739/generators-and-migrations-in-plugins-rails-3
|
9
|
+
|
10
|
+
if Rails::VERSION::STRING.start_with?('3.2')
|
11
|
+
include Rails::Generators::Migration
|
12
|
+
extend ActiveRecord::Generators::Migration
|
13
|
+
else
|
14
|
+
include ActiveRecord::Generators::Migration
|
15
|
+
end
|
16
|
+
|
17
|
+
source_root File.expand_path('../templates', __FILE__)
|
18
|
+
|
19
|
+
def copy_migration
|
20
|
+
migration_template "create_netsuite_poll_timestamps.rb", "db/migrate/create_netsuite_poll_timestamps.rb"
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateNetsuitePollTimestamps < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :netsuite_poll_timestamps do |t|
|
4
|
+
t.string :name, :limit => 100
|
5
|
+
t.text :value
|
6
|
+
t.string :key
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :netsuite_poll_timestamps, [:key], :name => 'index_netsuite_poll_timestamps_on_key', :unique => true
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
module Configuration
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def reset!
|
6
|
+
attributes.clear
|
7
|
+
end
|
8
|
+
|
9
|
+
def attributes
|
10
|
+
@attributes ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def netsuite_sync_mode(mode = nil)
|
14
|
+
if mode.nil?
|
15
|
+
attributes[:sync_mode] ||= :none
|
16
|
+
else
|
17
|
+
attributes[:sync_mode] = mode
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def netsuite_push_disabled(flag = nil)
|
22
|
+
if flag.nil?
|
23
|
+
attributes[:flag] ||= false
|
24
|
+
else
|
25
|
+
attributes[:flag] = flag
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def netsuite_pull_disabled(flag = nil)
|
30
|
+
if flag.nil?
|
31
|
+
attributes[:flag] ||= false
|
32
|
+
else
|
33
|
+
attributes[:flag] = flag
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def netsuite_instance_time_zone_offset(zone_offset = nil)
|
38
|
+
if zone_offset.nil?
|
39
|
+
attributes[:zone_offset] ||= -8
|
40
|
+
else
|
41
|
+
attributes[:zone_offset] = zone_offset
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def polling_page_size(size = nil)
|
46
|
+
if size.nil?
|
47
|
+
attributes[:size] ||= 1000
|
48
|
+
else
|
49
|
+
attributes[:size] = size
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
module ListSync
|
3
|
+
|
4
|
+
class PullManager
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def poll(klass, opts = {})
|
8
|
+
custom_list = NetSuite::Records::CustomList.get(klass.netsuite_list_id)
|
9
|
+
|
10
|
+
process_results(custom_list.custom_value_list.custom_value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_results(klass, opts, list)
|
14
|
+
list.each do |custom_value|
|
15
|
+
local_record = klass.where(netsuite_id: custom_value.attributes[:value_id]).first_or_initialize
|
16
|
+
|
17
|
+
if local_record.respond_to?(:value=)
|
18
|
+
local_record.value = custom_value.attributes[:value]
|
19
|
+
end
|
20
|
+
|
21
|
+
if local_record.respond_to?(:inactive=)
|
22
|
+
local_record.inactive = custom_value.attributes[:is_inactive]
|
23
|
+
end
|
24
|
+
|
25
|
+
local_record.save!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
module ListSync
|
3
|
+
|
4
|
+
def self.included(klass)
|
5
|
+
klass.send(:extend, ClassMethods)
|
6
|
+
|
7
|
+
PollManager.attach(klass)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def netsuite_list_id(internal_id = nil)
|
12
|
+
if internal_id.nil?
|
13
|
+
@netsuite_list_id
|
14
|
+
else
|
15
|
+
@netsuite_list_id = internal_id
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def netsuite_poll(opts = {})
|
20
|
+
NetSuiteRails::ListSync::PullManager.poll(self, opts)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'netsuite_rails/configuration'
|
2
|
+
require 'netsuite_rails/poll_timestamp'
|
3
|
+
require 'netsuite_rails/transformations'
|
4
|
+
require 'netsuite_rails/poll_manager'
|
5
|
+
require 'netsuite_rails/sync_trigger'
|
6
|
+
require 'netsuite_rails/sub_list_sync'
|
7
|
+
require 'netsuite_rails/record_sync'
|
8
|
+
require 'netsuite_rails/record_sync/pull_manager'
|
9
|
+
require 'netsuite_rails/record_sync/push_manager'
|
10
|
+
require 'netsuite_rails/list_sync'
|
11
|
+
require 'netsuite_rails/list_sync/pull_manager'
|
12
|
+
require 'netsuite_rails/url_helper'
|
13
|
+
|
14
|
+
module NetSuiteRails
|
15
|
+
|
16
|
+
class Railtie < ::Rails::Railtie
|
17
|
+
rake_tasks do
|
18
|
+
load 'netsuite_rails/tasks/netsuite.rb'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
class PollManager
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def attach(klass)
|
7
|
+
@record_models ||= []
|
8
|
+
@list_models ||= []
|
9
|
+
|
10
|
+
if klass.include? RecordSync
|
11
|
+
@record_models << klass
|
12
|
+
elsif klass.include? ListSync
|
13
|
+
@list_models << klass
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def sync(opts = {})
|
18
|
+
record_models = opts[:record_models] || @record_models
|
19
|
+
list_models = opts[:list_models] || @list_models
|
20
|
+
|
21
|
+
list_models.each do |klass|
|
22
|
+
Rails.logger.info "NetSuite: Syncing #{klass}"
|
23
|
+
klass.netsuite_poll
|
24
|
+
end
|
25
|
+
|
26
|
+
record_models.each do |klass|
|
27
|
+
sync_frequency = klass.netsuite_sync_options[:frequency] || 1.day
|
28
|
+
|
29
|
+
if sync_frequency == :never
|
30
|
+
Rails.logger.info "Not syncing #{klass.to_s}"
|
31
|
+
next
|
32
|
+
end
|
33
|
+
|
34
|
+
Rails.logger.info "NetSuite: Syncing #{klass.to_s}"
|
35
|
+
|
36
|
+
preference = PollTimestamp.where(key: "netsuite_poll_#{klass.to_s.downcase}timestamp").first_or_initialize
|
37
|
+
|
38
|
+
# check if we've never synced before
|
39
|
+
if preference.new_record?
|
40
|
+
klass.netsuite_poll({ import_all: true }.merge(opts))
|
41
|
+
else
|
42
|
+
# TODO look into removing the conditional parsing; I don't think this is needed
|
43
|
+
last_poll_date = preference.value
|
44
|
+
last_poll_date = DateTime.parse(last_poll_date) unless last_poll_date.is_a?(DateTime)
|
45
|
+
|
46
|
+
if DateTime.now - last_poll_date > sync_frequency
|
47
|
+
Rails.logger.info "NetSuite: Syncing #{klass} modified since #{last_poll_date}"
|
48
|
+
klass.netsuite_poll({ last_poll: last_poll_date }.merge(opts))
|
49
|
+
else
|
50
|
+
Rails.logger.info "NetSuite: Skipping #{klass} because of syncing frequency"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
preference.value = DateTime.now
|
55
|
+
preference.save!
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
module RecordSync
|
3
|
+
|
4
|
+
class PullManager
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def poll(klass, opts = {})
|
8
|
+
opts = {
|
9
|
+
import_all: false,
|
10
|
+
page_size: NetSuiteRails::Configuration.polling_page_size,
|
11
|
+
}.merge(opts)
|
12
|
+
|
13
|
+
opts[:netsuite_record_class] ||= klass.netsuite_record_class
|
14
|
+
|
15
|
+
search = opts[:netsuite_record_class].search(
|
16
|
+
poll_criteria(klass, opts).merge({
|
17
|
+
preferences: {
|
18
|
+
body_fields_only: false,
|
19
|
+
page_size: opts[:page_size]
|
20
|
+
}
|
21
|
+
})
|
22
|
+
)
|
23
|
+
|
24
|
+
# TODO more robust error reporting
|
25
|
+
unless search
|
26
|
+
raise 'error running netsuite sync'
|
27
|
+
end
|
28
|
+
|
29
|
+
process_search_results(klass, opts, search)
|
30
|
+
end
|
31
|
+
|
32
|
+
def poll_criteria(klass, opts)
|
33
|
+
search_criteria = {
|
34
|
+
criteria: {
|
35
|
+
basic: poll_basic_criteria(klass, opts)
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
saved_search_id = opts[:saved_search_id] || klass.netsuite_sync_options[:saved_search_id]
|
40
|
+
|
41
|
+
if saved_search_id
|
42
|
+
search_criteria[:criteria][:saved] = saved_search_id
|
43
|
+
end
|
44
|
+
|
45
|
+
if needs_get_list?(opts)
|
46
|
+
search_criteria[:columns] = {
|
47
|
+
'listRel:basic' => [
|
48
|
+
'platformCommon:internalId/' => {},
|
49
|
+
],
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
search_criteria
|
54
|
+
end
|
55
|
+
|
56
|
+
def poll_basic_criteria(klass, opts)
|
57
|
+
opts = {
|
58
|
+
criteria: [],
|
59
|
+
# last_poll: DateTime
|
60
|
+
}.merge(opts)
|
61
|
+
|
62
|
+
# allow custom criteria to be passed directly to the sync call
|
63
|
+
criteria = opts[:criteria] || []
|
64
|
+
|
65
|
+
# allow custom criteria from the model level
|
66
|
+
criteria += klass.netsuite_sync_options[:criteria] || []
|
67
|
+
|
68
|
+
if opts[:netsuite_record_class] == NetSuite::Records::CustomRecord
|
69
|
+
opts[:netsuite_custom_record_type_id] ||= klass.netsuite_custom_record_type_id
|
70
|
+
|
71
|
+
criteria << {
|
72
|
+
field: 'recType',
|
73
|
+
operator: 'is',
|
74
|
+
value: NetSuite::Records::CustomRecordRef.new(internal_id: opts[:netsuite_custom_record_type_id])
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
unless opts[:import_all]
|
79
|
+
criteria << {
|
80
|
+
# CustomRecordSearchBasic uses lastModified instead of the standard lastModifiedDate
|
81
|
+
field: (klass.netsuite_custom_record?) ? 'lastModified' : 'lastModifiedDate',
|
82
|
+
operator: 'after',
|
83
|
+
value: opts[:last_poll]
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
criteria
|
88
|
+
end
|
89
|
+
|
90
|
+
def process_search_results(klass, opts, search)
|
91
|
+
opts = {
|
92
|
+
skip_existing: false,
|
93
|
+
full_record_data: -1,
|
94
|
+
}.merge(opts)
|
95
|
+
|
96
|
+
Rails.logger.info "NetSuite: Processing #{search.total_records} over #{search.total_pages} pages"
|
97
|
+
|
98
|
+
# TODO need to improve the conditional here to match the get_list call conditional belo
|
99
|
+
if opts[:import_all] && opts[:skip_existing]
|
100
|
+
synced_netsuite_list = klass.pluck(:netsuite_id)
|
101
|
+
end
|
102
|
+
|
103
|
+
search.results_in_batches do |batch|
|
104
|
+
# a saved search is processed as a advanced search; advanced search often does not allow you to retrieve
|
105
|
+
# all of the fields (ex: addressbooklist on customer) that a normal search does
|
106
|
+
# the only way to get those fields is to pull down the full record again using getAll
|
107
|
+
|
108
|
+
if needs_get_list?(opts)
|
109
|
+
filtered_netsuite_id_list = batch.map(&:internal_id)
|
110
|
+
|
111
|
+
if opts[:skip_existing] == true
|
112
|
+
filtered_netsuite_id_list.reject! { |netsuite_id| synced_netsuite_list.include?(netsuite_id) }
|
113
|
+
end
|
114
|
+
|
115
|
+
opts[:netsuite_record_class].get_list(list: batch.map(&:internal_id))
|
116
|
+
else
|
117
|
+
batch
|
118
|
+
end.each do |netsuite_record|
|
119
|
+
self.process_search_result_item(klass, opts, netsuite_record)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def process_search_result_item(klass, opts, netsuite_record)
|
125
|
+
local_record = klass.where(netsuite_id: netsuite_record.internal_id).first_or_initialize
|
126
|
+
|
127
|
+
# when importing lots of records during an import_all skipping imported records is important
|
128
|
+
return if opts[:skip_existing] == true && !local_record.new_record?
|
129
|
+
|
130
|
+
local_record.netsuite_extract_from_record(netsuite_record)
|
131
|
+
|
132
|
+
# TODO optionally throw fatal errors; we want to skip fatal errors on intial import
|
133
|
+
|
134
|
+
unless local_record.save
|
135
|
+
Rails.logger.error "NetSuite: Error pulling record #{klass} NS ID #{netsuite_record.internal_id} #{local_record.errors.full_messages}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def needs_get_list?(opts)
|
140
|
+
(opts[:saved_search_id].present? && opts[:full_record_data] != false) || opts[:full_record_data] == true
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
module RecordSync
|
3
|
+
|
4
|
+
class PushManager
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def push(local_record, opts)
|
8
|
+
# TODO check to see if anything is changed before moving forward
|
9
|
+
# if changes_keys.blank? && local_record.netsuite_manual_fields
|
10
|
+
|
11
|
+
netsuite_record = build_netsuite_record(local_record)
|
12
|
+
|
13
|
+
local_record.netsuite_execute_callbacks(local_record.class.before_netsuite_push, netsuite_record)
|
14
|
+
|
15
|
+
if !local_record.new_netsuite_record?
|
16
|
+
push_update(local_record, netsuite_record)
|
17
|
+
else
|
18
|
+
push_add(local_record, netsuite_record)
|
19
|
+
end
|
20
|
+
|
21
|
+
# :aggressive is for custom fields which are based on input – need pull updated values after
|
22
|
+
# the push to netsuite to retrieve the calculated values
|
23
|
+
|
24
|
+
if local_record.netsuite_sync == :aggressive
|
25
|
+
local_record.netsuite_pull
|
26
|
+
end
|
27
|
+
|
28
|
+
local_record.netsuite_execute_callbacks(local_record.class.after_netsuite_push, netsuite_record)
|
29
|
+
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def push_add(local_record, netsuite_record)
|
34
|
+
if netsuite_record.add
|
35
|
+
# update_column to avoid triggering another save
|
36
|
+
local_record.update_column(:netsuite_id, netsuite_record.internal_id)
|
37
|
+
else
|
38
|
+
# TODO use NS error class
|
39
|
+
raise "NetSuite: error creating record #{netsuite_record.errors}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def push_update(local_record, netsuite_record)
|
44
|
+
# build change hash to limit the number of fields pushed to NS on change
|
45
|
+
# NS could have logic which could change field functionality depending on
|
46
|
+
# input data; it's safest to limit the number of field changes pushed to NS
|
47
|
+
|
48
|
+
custom_field_list = local_record.netsuite_field_map[:custom_field_list] || {}
|
49
|
+
all_field_list = eligible_local_fields(local_record)
|
50
|
+
|
51
|
+
update_list = {}
|
52
|
+
|
53
|
+
all_field_list.each do |local_field, netsuite_field|
|
54
|
+
if custom_field_list.keys.include?(local_field)
|
55
|
+
# if custom field has changed, mark and copy over customFieldList later
|
56
|
+
update_list[:custom_field_list] = true
|
57
|
+
else
|
58
|
+
update_list[netsuite_field] = netsuite_record.send(netsuite_field)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# manual field list is for fields manually defined on the NS record
|
63
|
+
# outside the context of ActiveRecord (e.g. in a before_netsuite_push)
|
64
|
+
|
65
|
+
(local_record.netsuite_manual_fields || []).each do |netsuite_field|
|
66
|
+
if netsuite_field == :custom_field_list
|
67
|
+
update_list[:custom_field_list] = true
|
68
|
+
else
|
69
|
+
update_list[netsuite_field] = netsuite_record.send(netsuite_field)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
if update_list[:custom_field_list]
|
74
|
+
update_list[:custom_field_list] = netsuite_record.custom_field_list
|
75
|
+
end
|
76
|
+
|
77
|
+
if local_record.netsuite_custom_record?
|
78
|
+
update_list[:rec_type] = netsuite_record.rec_type
|
79
|
+
end
|
80
|
+
|
81
|
+
# TODO consider using upsert here
|
82
|
+
|
83
|
+
if netsuite_record.update(update_list)
|
84
|
+
true
|
85
|
+
else
|
86
|
+
raise "NetSuite: error updating record #{netsuite_record.errors}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_netsuite_record(local_record)
|
91
|
+
netsuite_record = build_netsuite_record_reference(local_record)
|
92
|
+
|
93
|
+
# TODO need to normalize datetime fields
|
94
|
+
|
95
|
+
all_field_list = eligible_local_fields(local_record)
|
96
|
+
custom_field_list = local_record.netsuite_field_map[:custom_field_list] || {}
|
97
|
+
field_hints = local_record.netsuite_field_hints
|
98
|
+
|
99
|
+
all_field_list.each do |local_field, netsuite_field|
|
100
|
+
# allow Procs as field mapping in the record definition for custom mapping
|
101
|
+
if netsuite_field.is_a?(Proc)
|
102
|
+
netsuite_field.call(local_record, netsuite_record, :push)
|
103
|
+
next
|
104
|
+
end
|
105
|
+
|
106
|
+
# TODO pretty sure this will break if we are dealing with has_many
|
107
|
+
|
108
|
+
netsuite_field_value = if local_record.reflections.has_key?(local_field)
|
109
|
+
if (remote_internal_id = local_record.send(local_field).try(:netsuite_id)).present?
|
110
|
+
{ internal_id: remote_internal_id }
|
111
|
+
else
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
else
|
115
|
+
local_record.send(local_field)
|
116
|
+
end
|
117
|
+
|
118
|
+
if field_hints.has_key?(local_field) && netsuite_field_value.present?
|
119
|
+
netsuite_field_value = NetSuiteRails::Transformations.transform(field_hints[local_field], netsuite_field_value)
|
120
|
+
end
|
121
|
+
|
122
|
+
# TODO should we skip setting nil values completely? What if we want to nil out fields on update?
|
123
|
+
|
124
|
+
# be wary of API version issues: https://github.com/NetSweet/netsuite/issues/61
|
125
|
+
|
126
|
+
if custom_field_list.keys.include?(local_field)
|
127
|
+
netsuite_record.custom_field_list.send(:"#{netsuite_field}=", netsuite_field_value)
|
128
|
+
else
|
129
|
+
netsuite_record.send(:"#{netsuite_field}=", netsuite_field_value)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
netsuite_record
|
134
|
+
end
|
135
|
+
|
136
|
+
def build_netsuite_record_reference(local_record)
|
137
|
+
# must set internal_id for records on new; will be set to nil if new record
|
138
|
+
|
139
|
+
netsuite_record = local_record.netsuite_record_class.new(internal_id: local_record.netsuite_id)
|
140
|
+
|
141
|
+
if local_record.netsuite_custom_record?
|
142
|
+
netsuite_record.rec_type = NetSuite::Records::CustomRecord.new(internal_id: local_record.class.netsuite_custom_record_type_id)
|
143
|
+
end
|
144
|
+
|
145
|
+
netsuite_record
|
146
|
+
end
|
147
|
+
|
148
|
+
def eligible_local_fields(local_record)
|
149
|
+
custom_field_list = local_record.netsuite_field_map[:custom_field_list] || {}
|
150
|
+
all_field_list = local_record.netsuite_field_map.except(:custom_field_list) || {}
|
151
|
+
|
152
|
+
all_field_list.merge!(custom_field_list)
|
153
|
+
|
154
|
+
changed_keys = changed_attributes(local_record)
|
155
|
+
|
156
|
+
# filter out unchanged keys when updating record
|
157
|
+
unless local_record.new_netsuite_record?
|
158
|
+
all_field_list.select! { |k,v| changed_keys.include?(k) }
|
159
|
+
end
|
160
|
+
|
161
|
+
all_field_list
|
162
|
+
end
|
163
|
+
|
164
|
+
def changed_attributes(local_record)
|
165
|
+
# otherwise filter only by attributes that have been changed
|
166
|
+
# limiting the delta sent to NS will reduce hitting edge cases
|
167
|
+
|
168
|
+
# TODO think about has_many / join table changes
|
169
|
+
|
170
|
+
association_field_key_mapping = local_record.reflections.values.reject(&:collection?).inject({}) do |h, a|
|
171
|
+
h[a.association_foreign_key.to_sym] = a.name
|
172
|
+
h
|
173
|
+
end
|
174
|
+
|
175
|
+
# convert relationship symbols from :object_id to :object
|
176
|
+
local_record.changed_attributes.keys.map do |k|
|
177
|
+
association_field_key_mapping[k.to_sym] || k.to_sym
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|