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