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
@@ -0,0 +1,235 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
module RecordSync
|
3
|
+
|
4
|
+
def self.included(klass)
|
5
|
+
klass.send(:extend, ClassMethods)
|
6
|
+
|
7
|
+
SyncTrigger.attach(klass)
|
8
|
+
PollManager.attach(klass)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def netsuite_poll(opts = {})
|
13
|
+
RecordSync::PullManager.poll(self, opts)
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :netsuite_custom_record_type_id
|
17
|
+
attr_accessor :netsuite_sync_options
|
18
|
+
attr_accessor :netsuite_credentials
|
19
|
+
|
20
|
+
# TODO is there a better way to implement callback chains?
|
21
|
+
# https://github.com/rails/rails/blob/0c0f278ab20f3042cdb69604166e18a61f8605ad/activesupport/lib/active_support/callbacks.rb#L491
|
22
|
+
|
23
|
+
def before_netsuite_push(callback = nil, &block)
|
24
|
+
@before_netsuite_push ||= []
|
25
|
+
@before_netsuite_push << (callback || block) if callback || block
|
26
|
+
@before_netsuite_push
|
27
|
+
end
|
28
|
+
|
29
|
+
def after_netsuite_push(callback = nil, &block)
|
30
|
+
@after_netsuite_push ||= []
|
31
|
+
@after_netsuite_push << (callback || block) if callback || block
|
32
|
+
@after_netsuite_push
|
33
|
+
end
|
34
|
+
|
35
|
+
def after_netsuite_pull(callback = nil, &block)
|
36
|
+
@after_netsuite_pull ||= []
|
37
|
+
@after_netsuite_pull << (callback || block) if callback || block
|
38
|
+
@after_netsuite_pull
|
39
|
+
end
|
40
|
+
|
41
|
+
def netsuite_field_map(field_mapping = nil)
|
42
|
+
if field_mapping.nil?
|
43
|
+
@netsuite_field_map ||= {}
|
44
|
+
else
|
45
|
+
@netsuite_field_map = field_mapping
|
46
|
+
end
|
47
|
+
|
48
|
+
@netsuite_field_map
|
49
|
+
end
|
50
|
+
|
51
|
+
def netsuite_local_fields
|
52
|
+
@netsuite_field_map.except(:custom_field_list).keys + (@netsuite_field_map[:custom_field_list] || {}).keys
|
53
|
+
end
|
54
|
+
|
55
|
+
def netsuite_field_hints(list = nil)
|
56
|
+
if list.nil?
|
57
|
+
@netsuite_field_hints ||= {}
|
58
|
+
else
|
59
|
+
@netsuite_field_hints = list
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# TODO persist type for CustomRecordRef
|
64
|
+
def netsuite_record_class(record_class = nil, custom_record_type_id = nil)
|
65
|
+
if record_class.nil?
|
66
|
+
@netsuite_record_class
|
67
|
+
else
|
68
|
+
@netsuite_record_class = record_class
|
69
|
+
@netsuite_custom_record_type_id = custom_record_type_id
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# there is a model level of this method in order to be based on the model level record class
|
74
|
+
def netsuite_custom_record?
|
75
|
+
self.netsuite_record_class == NetSuite::Records::CustomRecord
|
76
|
+
end
|
77
|
+
|
78
|
+
# :read_only, :aggressive (push & update on save), :write_only, :read_write
|
79
|
+
def netsuite_sync(flag = nil, opts = {})
|
80
|
+
if flag.nil?
|
81
|
+
@netsuite_sync_options ||= {}
|
82
|
+
@netsuite_sync ||= :read_only
|
83
|
+
else
|
84
|
+
@netsuite_sync = flag
|
85
|
+
@netsuite_sync_options = opts
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_accessor :netsuite_manual_fields
|
91
|
+
|
92
|
+
# these methods are here for easy model override
|
93
|
+
|
94
|
+
def netsuite_sync_options
|
95
|
+
self.class.netsuite_sync_options
|
96
|
+
end
|
97
|
+
|
98
|
+
def netsuite_sync
|
99
|
+
self.class.netsuite_sync
|
100
|
+
end
|
101
|
+
|
102
|
+
def netsuite_record_class
|
103
|
+
self.class.netsuite_record_class
|
104
|
+
end
|
105
|
+
|
106
|
+
def netsuite_field_map
|
107
|
+
self.class.netsuite_field_map
|
108
|
+
end
|
109
|
+
|
110
|
+
def netsuite_field_hints
|
111
|
+
self.class.netsuite_field_hints
|
112
|
+
end
|
113
|
+
|
114
|
+
# assumes netsuite_id field on activerecord
|
115
|
+
|
116
|
+
def netsuite_pulling?
|
117
|
+
@netsuite_pulling ||= false
|
118
|
+
end
|
119
|
+
|
120
|
+
def netsuite_pulled?
|
121
|
+
@netsuite_pulled ||= false
|
122
|
+
end
|
123
|
+
|
124
|
+
def netsuite_pull
|
125
|
+
netsuite_extract_from_record(netsuite_pull_record)
|
126
|
+
end
|
127
|
+
|
128
|
+
def netsuite_pull_record
|
129
|
+
if netsuite_custom_record?
|
130
|
+
NetSuite::Records::CustomRecord.get(
|
131
|
+
internal_id: self.netsuite_id,
|
132
|
+
type_id: self.class.netsuite_custom_record_type_id
|
133
|
+
)
|
134
|
+
else
|
135
|
+
self.netsuite_record_class.get(self.netsuite_id)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def netsuite_push(opts = {})
|
140
|
+
NetSuiteRails::RecordSync::PushManager.push(self, opts)
|
141
|
+
end
|
142
|
+
|
143
|
+
def netsuite_extract_from_record(netsuite_record)
|
144
|
+
@netsuite_pulling = true
|
145
|
+
|
146
|
+
field_hints = self.netsuite_field_hints
|
147
|
+
|
148
|
+
custom_field_list = self.netsuite_field_map[:custom_field_list] || {}
|
149
|
+
|
150
|
+
all_field_list = self.netsuite_field_map.except(:custom_field_list) || {}
|
151
|
+
all_field_list.merge!(custom_field_list)
|
152
|
+
|
153
|
+
# self.netsuite_normalize_datetimes(:pull)
|
154
|
+
|
155
|
+
# handle non-collection associations
|
156
|
+
association_keys = self.reflections.values.reject(&:collection?).map(&:name)
|
157
|
+
|
158
|
+
all_field_list.each do |local_field, netsuite_field|
|
159
|
+
is_custom_field = custom_field_list.keys.include?(local_field)
|
160
|
+
|
161
|
+
if netsuite_field.is_a?(Proc)
|
162
|
+
netsuite_field.call(self, netsuite_record, :pull)
|
163
|
+
next
|
164
|
+
end
|
165
|
+
|
166
|
+
field_value = if is_custom_field
|
167
|
+
netsuite_record.custom_field_list.send(netsuite_field).value rescue ""
|
168
|
+
else
|
169
|
+
netsuite_record.send(netsuite_field)
|
170
|
+
end
|
171
|
+
|
172
|
+
if field_value.blank?
|
173
|
+
# TODO possibly nil out the local value?
|
174
|
+
next
|
175
|
+
end
|
176
|
+
|
177
|
+
if association_keys.include?(local_field)
|
178
|
+
field_value = self.reflections[local_field].klass.where(netsuite_id: field_value.internal_id).first_or_initialize
|
179
|
+
elsif is_custom_field
|
180
|
+
# TODO I believe this only handles a subset of all the possibly CustomField values
|
181
|
+
if field_value.present? && field_value.is_a?(Hash) && field_value.has_key?(:name)
|
182
|
+
field_value = field_value[:name]
|
183
|
+
end
|
184
|
+
|
185
|
+
if field_value.present? && field_value.is_a?(NetSuite::Records::CustomRecordRef)
|
186
|
+
field_value = field_value.attributes[:name]
|
187
|
+
end
|
188
|
+
else
|
189
|
+
# then it's not a custom field
|
190
|
+
end
|
191
|
+
|
192
|
+
# TODO should we just check for nil? vs present?
|
193
|
+
|
194
|
+
# TODO should be moved to Transformations with a direction flag
|
195
|
+
if field_hints.has_key?(local_field) && field_value.present?
|
196
|
+
case field_hints[local_field]
|
197
|
+
when :datetime
|
198
|
+
field_value = field_value.change(offset: "00:00") - (Time.zone.utc_offset / 3600).hours + (8 + NetSuiteRails::Configuration.netsuite_instance_time_zone_offset).hours
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
self.send(:"#{local_field}=", field_value)
|
203
|
+
end
|
204
|
+
|
205
|
+
netsuite_execute_callbacks(self.class.after_netsuite_pull, netsuite_record)
|
206
|
+
|
207
|
+
@netsuite_pulling = false
|
208
|
+
@netsuite_pulled = true
|
209
|
+
|
210
|
+
# return netsuite record for debugging
|
211
|
+
netsuite_record
|
212
|
+
end
|
213
|
+
|
214
|
+
def new_netsuite_record?
|
215
|
+
self.netsuite_id.blank?
|
216
|
+
end
|
217
|
+
|
218
|
+
def netsuite_custom_record?
|
219
|
+
self.netsuite_record_class == NetSuite::Records::CustomRecord
|
220
|
+
end
|
221
|
+
|
222
|
+
# TODO this should be protected; it needs to be pushed down to the Push/Pull manager level
|
223
|
+
|
224
|
+
def netsuite_execute_callbacks(list, record)
|
225
|
+
list.each do |callback|
|
226
|
+
if callback.is_a?(Symbol)
|
227
|
+
self.send(callback, record)
|
228
|
+
else
|
229
|
+
instance_exec(record, &callback)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'netsuite'
|
2
|
+
|
3
|
+
module NetSuiteRails::TestHelpers
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.before { netsuite_timestamp(DateTime.now) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def netsuite_timestamp(stamp = nil)
|
10
|
+
if stamp.nil?
|
11
|
+
@netsuite_timestamp ||= (Time.now - (60 * 2)).to_datetime
|
12
|
+
else
|
13
|
+
@netsuite_timestamp ||= stamp
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_last_netsuite_object(record)
|
18
|
+
search = record.netsuite_record_class.search({
|
19
|
+
criteria: {
|
20
|
+
basic:
|
21
|
+
if record.netsuite_custom_record?
|
22
|
+
[
|
23
|
+
{
|
24
|
+
field: 'recType',
|
25
|
+
operator: 'is',
|
26
|
+
value: NetSuite::Records::CustomRecordRef.new(internal_id: record.class.netsuite_custom_record_type_id)
|
27
|
+
},
|
28
|
+
{
|
29
|
+
field: 'lastModified',
|
30
|
+
operator: 'after',
|
31
|
+
value: netsuite_timestamp
|
32
|
+
}
|
33
|
+
]
|
34
|
+
else
|
35
|
+
[
|
36
|
+
{
|
37
|
+
field: 'lastModifiedDate',
|
38
|
+
operator: 'after',
|
39
|
+
value: netsuite_timestamp
|
40
|
+
}
|
41
|
+
]
|
42
|
+
end
|
43
|
+
}
|
44
|
+
})
|
45
|
+
|
46
|
+
if record.netsuite_custom_record?
|
47
|
+
NetSuite::Records::CustomRecord.get(
|
48
|
+
internal_id: search.results.first.internal_id.to_i,
|
49
|
+
type_id: record.class.netsuite_custom_record_type_id
|
50
|
+
)
|
51
|
+
else
|
52
|
+
record.netsuite_record_class.get(search.results.first.internal_id.to_i)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
module SubListSync
|
3
|
+
|
4
|
+
def self.included(klass)
|
5
|
+
klass.send(:extend, ClassMethods)
|
6
|
+
|
7
|
+
NetSuiteRails::SyncTrigger.attach(klass)
|
8
|
+
end
|
9
|
+
|
10
|
+
# one issue here is that sublist items dont' have an internal ID until
|
11
|
+
# they are created, but they are created in the context of a parent record
|
12
|
+
|
13
|
+
# some sublists don't have an internal ID at all, from the docs:
|
14
|
+
# "...non-keyed sublists contain no referencing keys (or handles)"
|
15
|
+
# "...Instead, you must interact with the sublist as a whole.
|
16
|
+
# In non-keyed sublists, the replaceAll attribute is ignored and behaves as if
|
17
|
+
# it were set to TRUE for all requests. Consequently, an update operation is
|
18
|
+
# similar to the add operation with respect to non-keyed sublists."
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def netsuite_sublist_parent(parent = nil)
|
22
|
+
if parent.nil?
|
23
|
+
@netsuite_sublist_parent
|
24
|
+
else
|
25
|
+
@netsuite_sublist_parent = parent
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
class SyncTrigger
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# TODO think about a flag to push to NS on after_validation vs after_commit
|
6
|
+
# TODO think about background async record syncing (re: multiple sales order updates)
|
7
|
+
# TODO need to add hook for custom proc to determine if data should be pushed to netsuite
|
8
|
+
# if a model has a pending/complete state we might want to only push on complete
|
9
|
+
|
10
|
+
def attach(klass)
|
11
|
+
if klass.include?(SubListSync)
|
12
|
+
klass.after_save { SyncTrigger.sublist_trigger(self) }
|
13
|
+
klass.after_destroy { SyncTrigger.sublist_trigger(self) }
|
14
|
+
elsif klass.include?(RecordSync)
|
15
|
+
|
16
|
+
# during the initial pull we don't want to push changes up
|
17
|
+
klass.before_save do
|
18
|
+
@netsuite_sync_record_import = self.new_record? && self.netsuite_id.present?
|
19
|
+
|
20
|
+
# if false record will not save
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
klass.after_save do
|
25
|
+
# need to implement this conditional on the save hook level
|
26
|
+
# because the coordination class doesn't know about model persistence state
|
27
|
+
|
28
|
+
if @netsuite_sync_record_import
|
29
|
+
# pull the record down if it has't been pulled yet
|
30
|
+
# this is useful when this is triggered by a save on a parent record which has this
|
31
|
+
# record as a related record
|
32
|
+
|
33
|
+
unless self.netsuite_pulled?
|
34
|
+
SyncTrigger.record_pull_trigger(self)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
SyncTrigger.record_push_trigger(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
@netsuite_sync_record_import = false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO think on NetSuiteRails::ListSync
|
45
|
+
end
|
46
|
+
|
47
|
+
def record_pull_trigger(local)
|
48
|
+
return if NetSuiteRails::Configuration.netsuite_pull_disabled
|
49
|
+
|
50
|
+
record_trigger_action(local, :netsuite_pull)
|
51
|
+
end
|
52
|
+
|
53
|
+
def record_push_trigger(netsuite_record_rep)
|
54
|
+
# don't update when fields are updated because of a netsuite_pull
|
55
|
+
return if netsuite_record_rep.netsuite_pulling?
|
56
|
+
|
57
|
+
return if NetSuiteRails::Configuration.netsuite_push_disabled
|
58
|
+
|
59
|
+
# don't update if a read only record
|
60
|
+
return if netsuite_record_rep.netsuite_sync == :read
|
61
|
+
|
62
|
+
sync_options = netsuite_record_rep.netsuite_sync_options
|
63
|
+
|
64
|
+
# :if option is a block that returns a boolean
|
65
|
+
return if sync_options.has_key?(:if) && !netsuite_record_rep.instance_exec(&sync_options[:if])
|
66
|
+
|
67
|
+
record_trigger_action(netsuite_record_rep, :netsuite_push)
|
68
|
+
end
|
69
|
+
|
70
|
+
def record_trigger_action(local, action)
|
71
|
+
sync_options = local.netsuite_sync_options
|
72
|
+
|
73
|
+
credentials = if sync_options.has_key?(:credentials)
|
74
|
+
local.instance_exec(&sync_options[:credentials])
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO need to pass off the credentials to the NS push command
|
78
|
+
|
79
|
+
# You can force sync mode in different envoirnments with the global configuration variables
|
80
|
+
|
81
|
+
if sync_options[:mode] == :sync || NetSuiteRails::Configuration.netsuite_sync_mode == :sync
|
82
|
+
local.send(action)
|
83
|
+
else
|
84
|
+
# TODO support the rails4 DJ implementation
|
85
|
+
|
86
|
+
if local.respond_to?(:delay)
|
87
|
+
local.delay.send(action)
|
88
|
+
else
|
89
|
+
raise 'no supported delayed job method found'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def sublist_trigger(sublist_item_rep)
|
95
|
+
parent = sublist_item_rep.send(sublist_item_rep.class.netsuite_sublist_parent)
|
96
|
+
|
97
|
+
if parent.class.include?(RecordSync)
|
98
|
+
record_push_trigger(parent)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
namespace :netsuite do
|
2
|
+
|
3
|
+
desc "Sync all NetSuite records using import_all"
|
4
|
+
task :fresh_sync => :environment do
|
5
|
+
NetSuiteRails::PollTimestamp.delete_all
|
6
|
+
|
7
|
+
ENV['SKIP_EXISTING'] = "true"
|
8
|
+
|
9
|
+
Rake::Task["netsuite:sync"].invoke
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "sync all netsuite records"
|
13
|
+
task :sync => :environment do
|
14
|
+
# need to eager load to ensure that all classes are loaded into the poll manager
|
15
|
+
Rails.application.eager_load!
|
16
|
+
|
17
|
+
opts = {
|
18
|
+
skip_existing: ENV['SKIP_EXISTING'].present?
|
19
|
+
}
|
20
|
+
|
21
|
+
if ENV['RECORD_MODELS'].present?
|
22
|
+
opts[:record_models] = ENV['RECORD_MODELS'].split(',').map(&:constantize)
|
23
|
+
end
|
24
|
+
|
25
|
+
if ENV['LIST_MODELS'].present?
|
26
|
+
opts[:list_models] = ENV['LIST_MODELS'].split(',').map(&:constantize)
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO make push disabled configurable
|
30
|
+
|
31
|
+
# field values might change on import because of remote data structure changes
|
32
|
+
# stop all pushes on sync & fresh_sync to avoid pushing up data that really hasn't
|
33
|
+
# changed for each record
|
34
|
+
|
35
|
+
NetSuiteRails::Configuration.netsuite_push_disabled true
|
36
|
+
|
37
|
+
NetSuiteRails::PollManager.sync(opts)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
# TODO could use this for a "updates local records with a netsuite_id with remote NS data"
|
43
|
+
# Model.select([:netsuite_id, :id]).find_in_batches do |batch|
|
44
|
+
# NetSuite::Records::CustomRecord.get_list(
|
45
|
+
# list: batch.map(&:netsuite_id),
|
46
|
+
# type_id: Model::NETSUITE_RECORD_TYPE_ID
|
47
|
+
# ).each do |record|
|
48
|
+
# model = Model.find_by_netsuite_id(record.internal_id)
|
49
|
+
# model.extract_from_netsuite_record(record)
|
50
|
+
# model.save!
|
51
|
+
# end
|
52
|
+
# end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
module Transformations
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def transform(type, value)
|
6
|
+
self.send(type, value)
|
7
|
+
end
|
8
|
+
|
9
|
+
# NS limits firstname fields to 33 characters
|
10
|
+
def firstname(firstname)
|
11
|
+
firstname[0..33]
|
12
|
+
end
|
13
|
+
|
14
|
+
def phone(phone)
|
15
|
+
formatted_phone = phone.strip
|
16
|
+
.gsub(/ext(ension)?/, 'x')
|
17
|
+
.gsub(/[^0-9x ]/, '')
|
18
|
+
.gsub(/[ ]{2,}/m, ' ')
|
19
|
+
|
20
|
+
formatted_phone.gsub!(/x.*$/, '') if formatted_phone.size > 22
|
21
|
+
|
22
|
+
formatted_phone
|
23
|
+
end
|
24
|
+
|
25
|
+
# NS will throw an error if whitespace bumpers the email string
|
26
|
+
def email(email)
|
27
|
+
email.strip
|
28
|
+
end
|
29
|
+
|
30
|
+
# https://www.reinteractive.net/posts/168-dealing-with-timezones-effectively-in-rails
|
31
|
+
# http://stackoverflow.com/questions/16818180/ruby-rails-how-do-i-change-the-timezone-of-a-time-without-changing-the-time
|
32
|
+
# http://alwayscoding.ca/momentos/2013/08/22/handling-dates-and-timezones-in-ruby-and-rails/
|
33
|
+
|
34
|
+
def date(date)
|
35
|
+
date.change(offset: "-07:00", hour: 24 - (8 + NetSuiteRails::Configuration.netsuite_instance_time_zone_offset))
|
36
|
+
end
|
37
|
+
|
38
|
+
def datetime(datetime)
|
39
|
+
datetime.change(offset: "-08:00") - (8 + NetSuiteRails::Configuration.netsuite_instance_time_zone_offset).hours
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module NetSuiteRails
|
2
|
+
module UrlHelper
|
3
|
+
|
4
|
+
# TODO create a xxx_netsuite_url helper generator
|
5
|
+
|
6
|
+
def self.netsuite_url(record = self)
|
7
|
+
prefix = "https://system#{".sandbox" if NetSuite::Configuration.sandbox}.netsuite.com/app"
|
8
|
+
|
9
|
+
record_class = record.netsuite_record_class
|
10
|
+
internal_id = record.netsuite_id
|
11
|
+
|
12
|
+
# https://system.sandbox.netsuite.com/app/common/scripting/scriptrecordlist.nl
|
13
|
+
# https://system.sandbox.netsuite.com/app/common/scripting/script.nl
|
14
|
+
|
15
|
+
if record.netsuite_custom_record?
|
16
|
+
"#{prefix}/common/custom/custrecordentry.nl?id=#{internal_id}&rectype=#{record.class.netsuite_custom_record_type_id}"
|
17
|
+
elsif [ NetSuite::Records::InventoryItem, NetSuite::Records::NonInventorySaleItem, NetSuite::Records::AssemblyItem].include?(record_class)
|
18
|
+
"#{prefix}/common/item/item.nl?id=#{internal_id}"
|
19
|
+
elsif record_class == NetSuite::Records::Task
|
20
|
+
"#{prefix}/crm/calendar/task.nl?id=#{internal_id}"
|
21
|
+
elsif record_class == NetSuite::Records::Customer
|
22
|
+
"#{prefix}/common/entity/custjob.nl?id=#{internal_id}"
|
23
|
+
elsif record_class == NetSuite::Records::Contact
|
24
|
+
"#{prefix}/common/entity/contact.nl?id=#{internal_id}"
|
25
|
+
elsif [ NetSuite::Records::SalesOrder, NetSuite::Records::Invoice, NetSuite::Records::CustomerRefund ].include?(record_class)
|
26
|
+
"#{prefix}/accounting/transactions/transaction.nl?id=#{internal_id}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'netsuite_rails/netsuite_rails'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "netsuite_rails"
|
7
|
+
s.version = "0.1.0"
|
8
|
+
s.authors = ["Michael Bianco"]
|
9
|
+
s.email = ["mike@cliffsidemedia.com"]
|
10
|
+
s.summary = %q{Write Rails applications that integrate with NetSuite}
|
11
|
+
s.homepage = "http://github.com/netsweet/netsuite_rails"
|
12
|
+
s.license = "MIT"
|
13
|
+
|
14
|
+
s.files = `git ls-files -z`.split("\x0")
|
15
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
16
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_dependency 'netsuite', '~> 0.3'
|
20
|
+
s.add_dependency 'rails', '>= 3.2.16'
|
21
|
+
|
22
|
+
s.add_development_dependency "bundler", "~> 1.6"
|
23
|
+
s.add_development_dependency "rake"
|
24
|
+
s.add_development_dependency "rspec"
|
25
|
+
end
|
data/spec/spec_helper.rb
ADDED