netsuite_rails 0.1.0
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/.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
|