
NetSuite SuiteTalk API Ruby Gem
- This gem will act as a wrapper around the NetSuite SuiteTalk Web Services API.
- The gem does not cover the entire API, only the subset contributors have used so far. Please submit a PR for any functionality that's missing!
- NetSuite is a complex system. There's a lot to learn and sparse resources available to learn from. Here's a list of NetSuite Development Resources.
Help & Support
Join the Slack channel for help with any NetSuite issues. Please do not post usage questions as issues in GitHub.
There is some additional helpful resources for NetSuite development listed here.
Testing
Before contributing a patch make sure all existing tests pass.
git clone git://github.com/NetSweet/netsuite.git
cd netsuite
bundle
bundle exec rspec
Installation
Add this line to your application's Gemfile:
gem 'netsuite'
If you'd like more accurate time conversion support, include the tzinfo
gem.
This gem is built for Ruby 2.6.x+, but should work on older versions down to 1.9. There's a 1-8-stable branch for Ruby 1.8.x support.
Configuration
The most important thing you'll need is your NetSuite account ID. Not sure how to find your account ID? Here's a guide.
How you connect to NetSuite has changed a lot over the years and differs between API versions. For instance:
- Older API versions (~2015) allowed authentication via username and password
- Newer API versions (> 2016) still allowed for username and password authentication, but required an application ID
- "OAuth", which requires four separate keys to be manually generated, was supported sometime after 2015
- API versions greater than 2018_2 require
endpoint
to be set directly (more info)
Here's an example connection configuration. You don't want to actually use username + password config; Token Based Authentication is detailed in a separate section:
NetSuite.configure do
reset!
account 'TSTDRV1576318'
api_version '2018_2'
email 'email@example.com'
password 'password'
role 10
wsdl_domain 'tstdrv1576318.suitetalk.api.netsuite.com'
endpoint "https://#{wsdl_domain}/services/NetSuitePort_#{api_version}"
end
The wsdl_domain
configuration is most important. Note that if you use wsdl
or other configuration options below, you'll want to look at the configuration source to understand more about how the different options interact with each other. Some of the configuration options will mutate the state of other options.
Here's the various options that are are available for configuration:
NetSuite.configure do
reset!
api_version '2018_2'
wsdl "https://webservices.sandbox.netsuite.com/wsdl/v#{api_version}_0/netsuite.wsdl"
wsdl "https://webservices.na2.netsuite.com/wsdl/v#{api_version}_0/netsuite.wsdl"
wsdl_domain "webservices.na2.netsuite.com"
read_timeout 100_000
log File.join(Rails.root, 'log/netsuite.log')
email 'email@domain.com'
password 'password'
account '12345'
role 1111
soap_header 'platformMsgs:preferences' => {
'platformMsgs:ignoreReadOnlyFields' => true,
}
end
If you are using username + password authentication (which you shouldn't be!) and you'd like to use an API endpoint greater than 2015_1, you'll need to specify an application ID:
NetSuite::Configuration.soap_header = {
'platformMsgs:ApplicationInfo' => {
'platformMsgs:applicationId' => 'your-netsuite-app-id'
}
}
Token Based Authentication
OAuth credentials are supported and the recommended authentication approach. Learn more about how to set up Token Based Authentication here.
NetSuite.configure do
reset!
account ENV['NETSUITE_ACCOUNT']
consumer_key ENV['NETSUITE_CONSUMER_KEY']
consumer_secret ENV['NETSUITE_CONSUMER_SECRET']
token_id ENV['NETSUITE_TOKEN_ID']
token_secret ENV['NETSUITE_TOKEN_SECRET']
api_version '2016_2'
endpoint "https://#{wsdl_domain}/services/NetSuitePort_#{api_version}"
end
Multi-Tenancy
If you're interacting with multiple NetSuite accounts, each in separate threads, you can enable multi-tenancy to prevent your configuration and caches from being shared between threads.
From your main thread, you'd want to enable multi-tenancy:
NetSuite.configure do
multi_tentant!
end
Note that multi_tenant!
is a special configuration option which is not effected by reset!
.
Then in each child thread, you'd perform any configuration specific to the NetSuite account you're interacting with for that thread, all of which will be specific to that thread only:
NetSuite.configure do
reset!
account ENV['NETSUITE_ACCOUNT']
end
Usage
CRUD Operations
customer = NetSuite::Records::Customer.get(:internal_id => 4)
customer.is_person
NetSuite::Records::Customer.get(4)
customers = NetSuite::Records::Customer.get_list(:list => [4, 5, 6])
customer_support_reps = [12345, 12346]
task = NetSuite::Records::Task.new(
:title => 'Take Care of a Customer',
:assigned => NetSuite::Records::RecordRef.new(internal_id: customer_support_reps.sample),
:due_date => DateTime.now + 1,
:message => "Take care of this"
)
task.add
`open https://system.sandbox.netsuite.com/app/crm/calendar/task.nl?id=#{invoice.internal_id}`
task.update(message: 'New Message')
task.delete
task.refresh
NetSuite::Records::BaseRefList.get_select_value(
recordType: 'serviceSaleItem',
field: 'taxSchedule'
)
NetSuite::Records::BaseRefList.get_select_value(
field: 'custcol69_2',
sublist: 'itemList',
recordType: 'salesOrder'
)
options = NetSuite::Records::BaseRefList.get_select_value(
field: 'custbody_order_source',
recordType: 'invoice'
)
options.base_refs.map(&:name)
Uploading/Attaching Files
file = NetSuite::Records::File.new(
content: Base64.encode64(File.read('/path/to/file')),
name: 'Invoice.pdf',
)
file.add
invoice = NetSuite::Records::Invoice.get(internal_id: 1)
invoice.attach_file(NetSuite::Records::RecordRef.new(internal_id: file.internal_id))
Custom Records & Fields
contact = NetSuite::Records::Contact.get(12345)
contact.custom_field_list.custentity_alistfield = { internal_id: 1 }
contact.custom_field_list.custentity_abooleanfield = true
contact.update(custom_field_list: contact.custom_field_list)
record = NetSuite::Records::CustomRecord.get(
type_id: 10,
internal_id: 100
)
records = NetSuite::Records::CustomRecord.get_list(
list: [1,2,3],
type_id: 1234
)
record = NetSuite::Records::CustomRecord.new
record.rec_type = NetSuite::Records::CustomRecord.new(internal_id: 10)
record.custom_field_list.custrecord_locationstate = "New Jersey"
record.add
record = NetSuite::Records::CustomRecord.new(internal_id: 100)
record.custom_field_list.custrecord_locationstate = "New Jersey"
record.update(custom_field_list: record.custom_field_list, rec_type: NetSuite::Records::CustomRecord.new(internal_id: 10))
NetSuite::Records::BaseRefList.get_select_value(
field: 'custrecord_something',
customRecordType: {
'@internalId' => 10,
'@xsi:type' => 'customRecord'
}
)
Null Fields
invoice = NetSuite::Records::Invoice.get(12345)
invoice.update(null_field_list: 'shipMethod')
invoice.update(null_field_list: ['shipAddressList', 'shipMethod'])
invoice.update(null_field_list: 'custBody9')
Searching
search = NetSuite::Records::Customer.search({
basic: [
{
field: 'companyName',
operator: 'contains',
value: company_name
}
]
})
`open https://system.netsuite.com/app/common/entity/custjob.nl?id=#{search.results.first.internal_id}`
all_sales_taxes = NetSuite::Utilities.backoff { NetSuite::Records::SalesTaxItem.get_all }
ns_tax_code = all_sales_taxes.detect { |st| st.item_id == 'AVATAX' }
NetSuite::Records::CustomRecord.search(
basic: [
{
field: 'recType',
operator: 'is',
value: NetSuite::Records::CustomRecordRef.new(:internal_id => 10),
}
]
).results
NetSuite::Records::Customer.search({
saved: 500,
basic: [
{
field: 'entityId',
operator: 'hasKeywords',
value: 'Assumption',
},
{
field: 'stage',
operator: 'anyOf',
type: 'SearchMultiSelectCustomField',
value: [
'_lead', '_customer'
]
},
{
field: 'customFieldList',
value: [
{
field: 'custentity_acustomfield',
operator: 'anyOf',
type: 'SearchMultiSelectCustomField',
value: [
NetSuite::Records::CustomRecordRef.new(:internal_id => 1),
NetSuite::Records::CustomRecordRef.new(:internal_id => 2),
]
},
{
field: 'custbody_internetorder',
type: 'SearchBooleanCustomField',
value: true
}
]
}
]
}).results
NetSuite::Records::SalesOrder.search({
criteria: {
basic: [
{
field: 'type',
operator: 'anyOf',
value: [ "_invoice"]
},
{
field: 'tranDate',
operator: 'within',
type: 'SearchDateField',
value: [
Time.parse("01/01/2012").iso8601,
Time.parse("30/07/2013").iso8601,
]
}
],
accountJoin: [
{
field: 'internalId',
operator: 'noneOf',
value: [ NetSuite::Records::Account.new(:internal_id => 215) ]
}
],
itemJoin: [
{
field: 'customFieldList',
value: [
{
field: 'custitem_apcategoryforsales',
operator: 'anyOf',
type: 'SearchMultiSelectCustomField',
value: [
NetSuite::Records::Customer.new(:internal_id => 1),
NetSuite::Records::Customer.new(:internal_id => 2),
]
}
]
}
]
},
columns: {
'tranSales:basic' => [
'platformCommon:internalId/' => {},
'platformCommon:email/' => {},
'platformCommon:tranDate/' => {},
'platformCommon:closeDate/' => {}
],
'tranSales:accountJoin' => [
'platformCommon:internalId/' => {}
],
'tranSales:contactPrimaryJoin' => [
'platformCommon:internalId/' => {}
],
'tranSales:customerJoin' => [
'platformCommon:internalId/' => {}
],
'tranSales:itemJoin' => [
'platformCommon:customFieldList' => [
'platformCore:customField/' => {
'@scriptId' => 'custitem_apcategoryforsales',
'@xsi:type' => "platformCore:SearchColumnSelectCustomField"
}
]
]
},
preferences: {
page_size: 10,
body_fields_only: true
}
}).results
NetSuite::Records::SalesOrder.search(
criteria: {
basic: [
{
field: 'type',
operator: 'anyOf',
value: ['_salesOrder'],
},
{
field: 'status',
operator: 'anyOf',
value: ['_salesOrderPendingApproval'],
},
],
},
)
NetSuite::Records::ItemFulfillment.search({
criteria: {
basic: [
{
field: 'type',
operator: 'anyOf',
type: 'SearchEnumMultiSelectField',
value: ["_itemFulfillment"]
},
{
field: 'lastModifiedDate',
type: 'SearchDateField',
operator: 'within',
value: [
DateTime.now - 2.hours,
DateTime.now
]
}
],
createdFromJoin: [
{
field: 'type',
operator: 'anyOf',
value: [ '_salesOrder' ]
},
{
field: 'internalIdNumber',
operator: 'notEmpty'
}
]
},
preferences: {
pageSize: 1000,
bodyFieldsOnly: false
}
}).results
search = NetSuite::Records::Customer.search(
criteria: {
basic: [
{
field: 'isInactive',
value: false,
},
]
},
preferences: {
page_size: 10,
}
)
search.results_in_batches do |batch|
puts batch.map(&:internal_id)
end
NetSuite::Records::InventoryItem.search({
criteria: {
basic: [
{
field: 'type',
operator: 'anyOf',
type: 'SearchEnumMultiSelectField',
value: [
'_inventoryItem',
'_assembly'
]
},
{
field: 'isInactive',
value: false
}
]
}
}).results.first
search = NetSuite::Records::File.search({
preferences: {
body_fields_only: false,
page_size: 20
}
})
search.results_in_batches do |batch|
batch.each do |file|
next unless file.file_type == "_JAVASCRIPT"
puts Base64.decode64(file.content)
end
end
NetSuite::Records::CustomRecord.get_list(
list: [1,2,3],
type_id: 1234,
allow_incomplete: true
).each do |record|
end
deposit = CustomerDeposit.new
deposit.sales_order = RecordRef.new(internal_id: 7279)
deposit.payment = 20
deposit.add
Getting Deleted Records
response = NetSuite::Records::LotNumberedInventoryItem.get_deleted({
criteria: [
{
field: 'type',
operator: 'anyOf',
value: 'lotNumberedInventoryItem',
}
],
})
Array(response.body.fetch(:deleted_record_list)).first
page = 1
begin
response = NetSuite::Records::LotNumberedInventoryItem.get_deleted({
criteria: [
],
page: page,
})
page += 1
end until page > Integer(response.fetch(:total_pages))
Non-standard Operations
NetSuite::Configuration.connection.call :get_customization_id, message: {
'platformMsgs:customizationType' => { '@getCustomizationType' => 'customRecordType'},
'platformMsgs:includeInactives' => 'false'
}
server_time_response = NetSuite::Configuration.connection.call :get_server_time
server_time_response.body[:get_server_time_response][:get_server_time_result][:server_time]
states = NetSuite::Configuration.connection.call(:get_all, message: {
'platformCore:record' => {
'@recordType' => 'state'
}
})
states.to_array.first[:get_all_response][:get_all_result][:record_list][:record].map { |r| { country: r[:country], abbr: r[:shortname], name: r[:full_name] } }