The Paperless Parts Software Development Kit (SDK) enables developers to easily write custom event listeners that run when objects are created in Paperless Parts. The most common use case is creating orders and other records in an ERP system after an order is placed in Paperless Parts. The SDK uses the Paperless Parts Open API to access your data.
These instructions assume you are running on Windows 10.
First install the latest Python3, which
includes python3
, pip
, and venv
. Add the Python installation folder to
your system path.
Create a virtual environment and activate it:
python -m venv osenv
osenv\Scripts\activate
Install the required Python packages:
cd path\to\core-python
pip install -r requirements.txt
Make sure paperless
is on your Python path. You can do this using an
environment variable like this:
set PYTHONPATH=c:\path\to\core-python
The SDK client is authenticated via an automatically generated token linked to your Paperless Parts account. To generate, revoke, or re-generate this token, go to Settings > Integrations > API Token. This token must be included in the header of all requests using the key as follows:
Authorization
: API-Token <api-token>
The SDK handles this for you when you include this access token when instantiating your
PaperlessClient
object, as shown in the example below. We recommend
structuring your application to read this from a configuration file. Your access
token should never be committed to your version control system (like git).
The SDK provides the paperless.listeners.BaseListener
class, which can be
extended to listen for particular object creation event. The subclass
paperless.listeners.OrderListener
is provided to listen for new order creation
events. You can extend OrderListener
and implement an on_event
method. Similary,
paperless.listeners.QuoteListener
is provided to listen for new quotes.
You will need to handle all exceptions if you intend to have a long-running listener that does not require manual restarts. Alternatively, you can add watchdog or restart logic to your application built on the SDK.
Listeners keep track of which objects they have processed and persist this data in a local file in JSON format.
The first time you run the Paperless SDK you can optionally configure the
listener to start with a later object (for example, an order with number other
than 1) by providing last_record_id
when instantiating the listener.
last_record_id
represents the last record that was processed, and this record
will not be processed. Once resources have been processed and the local JSON
file has been initialized, the last_record_id
will be ignored.
The paperless.main.PaperlessSDK
class provides the event loop to regularly
check for new objects and call any registered listeners. Use the add_listener
to register your listener subclass and run
to start the event loop. You can
customize the polling interval (in seconds) by specifying the delay
argument
when instantiating PaperlessSDK
. The default and recommended delay is 900
seconds (15 minutes).
By default, the event loop will run until the program is terminated. If you
wish to manage these intervals elsewhere in your application, set loop=False
when instantiating PaperlessSDK
, which cause run()
to check for objects one
time and then return.
from paperless.client import PaperlessClient
from paperless.main import PaperlessSDK
from paperless.listeners import OrderListener
class MyOrderListener(OrderListener):
def on_event(self, resource):
print("on event")
print(resource)
class MyQuoteListener(QuoteListener):
def on_event(self, resource):
print("on event")
print(resource)
my_client = PaperlessClient(access_token='', version=PaperlessClient.VERSION_0)
my_order_listener = MyOrderListener(last_record_id=None)
my_quote_listener = MyQuoteListener(last_record_id=None)
my_sdk = PaperlessSDK()
my_sdk.add_listener(my_order_listener)
my_sdk.add_listener(my_quote_listener)
my_sdk.run()
The Paperless Parts Open-API limits users to a rate of 100 requests per minute. In the event that this rate is exceeded, the API will respond with the response code 429 (Too Many Requests). The Paperless Parts SDK will catch this response and automatically retry the request after waiting the amount of time specified in the response body.
The SDK will convert numeric fields from the open API that represent money to a
Money object. The Money object has .raw_amount
and .dollars
properties. The
.raw_amount
value will maintain the original value of the number out to as many
decimal places as it was initialized with. The .dollars
property will always round
the value to two decimal places. These are both decimal objects.
NOTE: Every money field returned from the open api is pre-rounded to 2 decimal places
besides one, the piece_price
field of a purchased component. This number is stored
to 4 decimal places. When working with the piece_price
if you want to maintain the
4 decimal place precision, work with the .raw_amount
and not .dollars
property of
the Money object.
In Paperless Parts, you can upload and manage custom user-defined tables. With custom tables, you can perform table lookups from within your customized pricing logic. This section will demonstrate how to create and manage custom tables from the SDK.
First, import the CustomTable
class:
from paperless.custom_tables.custom_tables import CustomTable
Next, list all of the tables in your account:
CustomTable.get_list()
A custom table is defined by two things: its configuration and its row data. The configuration defines the table's column names and types, and the row data provides the contents of the table's rows.
To create and populate a table, first instantiate a CustomTable
with a
configuration, or a configuration and accompanying row data. The configuration
should be a list of dictionaries with keys 'column_name'
and 'value_type'
,
and optionally 'is_for_unique_key'
(more on this later). The allowed value
types are: string, numeric, boolean. The row data should be a list of
dictionaries with keys corresponding to the column names supplied in the
configuration.
sample_table_config = [
dict(column_name='diameter', value_type='numeric'),
dict(column_name='length', value_type='numeric'),
dict(column_name='requires_prep', value_type='boolean'),
dict(column_name='material', value_type='string'),
]
sample_table_rows = [
dict(diameter=1.0, length=24.0, requires_prep=False, material='6061-T6'),
dict(diameter=2.0, length=48.0, requires_prep=False, material='5052-H32'),
dict(diameter=3.0, length=24.0, requires_prep=True, material='304-2B'),
dict(diameter=4.0, length=48.0, requires_prep=True, material='304-#4'),
dict(diameter=5.0, length=24.0, requires_prep=True, material='Ti6Al4V'),
dict(diameter=6.0, length=48.0, requires_prep=False, material='A2'),
]
table = CustomTable(config=sample_table_config, data=sample_table_rows)
You may supply a configuration without row data, but any row data supplied must be accompanied by a configuration.
In order to create this table you've defined in Paperless Parts, first create
the
table, supplying a table name, and then call update
.
table.create('test_sdk_table_1') # This creates a blank new table
table.update('test_sdk_table_1') # This populates the table with the supplied config and data
NOTE: When you call update
on a table, you will blow away whatever config and data
were there before and replace them with the new config and data you've supplied.
You can also instantiate a table from a configuration CSV file, or a combination of a configuration CSV file and a data CSV file. Here are some example CSV file contents corresponding to the Python examples above:
config.csv
column_name,value_type
diameter,numeric
length,numeric
requires_prep,boolean
material,string
data.csv
diameter,length,requires_prep,material
1,24,FALSE,6061-T6
2,48,FALSE,5052-H32
3,24,TRUE,304-2B
4,48,TRUE,304-#4
5,24,TRUE,Ti6Al4V
6,48,FALSE,A2
To instantiate a CustomTable
from CSV files, do the following:
table = CustomTable()
table.from_csv('config.csv', 'data.csv')
You can also download the CSV files for the table config and data using the
download_csv
method, providing the table name as an argument. This can be useful
if you want to modify the existing data in the table slightly. Supply
config=True
if you want the config file. You can also supply an optional
file_path
argument to specify where to save the file to:
CustomTable.download_csv('test_sdk_table_1') # download the data file
CustomTable.download_csv('test_sdk_table_1', config=True) # download the config file
CustomTable.download_csv('test_sdk_table_1', file_path='renamed_test_sdk_table_1_data.csv') # download the data file and rename it
You can delete a CustomTable
from your Paperless Parts account by calling
the delete
method with the table's name:
CustomTable.delete('test_sdk_table_1')
The PaperlessParts SDK provides functionality for identifying newly sent quotes, pulling all information related to a particular quote, and updating a quote's status.
from paperless.objects.quotes import Quote
new_quotes = Quote.get_new(id=35, revision=1) #Where id is the quote number
This will return a list of newly sent quotes starting after Quote #35 Revision 1.
NOTE: The id and revision parameters are optional, if they are not supplied the all sent quotes will be returned.
quote = Quote.get(id=35, revision=1) #Where id is the quote number
This will return the details for a specific quote.
NOTE: The revision parameter is optional
You can change a quote's status using the set_status
method. The available statuses
are OUTSTANDING
, CANCELLED
, TRASH
LOST
, these statuses are defined in the STATUSES
enum on the Quote
object.
quote = Quote.get(1090)
quote.set_status(Quote.STATUSES.OUTSTANDING)
The PaperlessParts SDK provides functionality for identifying newly placed orders, pulling all information related to a particular order, and also facilitating and order using an existing quote.
orders = Order.list()
order = Order.get(id=35) #Where id is the order number
This will return the details for a specific order.
Paperless Parts includes Customer Relationship Management (CRM) functionality to make it easy to send quotes to new and existing customers, while keeping data consistent with third-party CRM and ERP systems. Typical use cases for these endpoints are to bulk import customers from an existing customer database and to synchronize new customers or changes from another system.
An account represents a single company or account to which you would send quotes. An account has zero or more Contacts, each of which represents a person at that company and is identified uniquely by their email address. An account also has facilities and billing addresses. Facilities represent destinations to which orders will be shipped and BillingAddresses represent the bill to address for and order.
A contact represents an individual at an account. A contact has the following fields:
* account_id: int(optional)
* email: string
* first_name: string
* id: int
* last_name: string
* address: Address(optional)
* created: string
* notes: string
* phone: string
* phone_ext: string
* salesperson: Salesperson(optional)
from paperless.objects.customers import Contact
contacts = Contact.list()
This will return a list of minified Contact objects
contacts = Contact.filter(account_id=id)
Contacts can be filtered by account_id
contacts = Contact.search('[email protected]')
Contacts can be searched by the following fields:
* email
* first_name
* last_name
* notes
* phone
* account id
Searches are case insensitive and can be partial matches
contact = Contact.get(101) #where 101 is the the contact id
This will return the contact object with the given id
contact.first_name = 'Jim'
contact.update()
This will update the contact in Paperless Parts and refresh the local instance
address = Address(address1="137 Portland St.", address2="lower", city="Boston", country="USA", postal_code="02114", state="MA")
contact = Contact(account_id=141, address=address, email='[email protected]', first_name='Jim', last_name='Gordan', notes='Test Account', phone='6175555555', phone_ext='123')
contact.create()
from paperless.objects.common import Salesperson
salesperson = Salesperson(email="[email protected]")
contact.salesperson = salesperson
contact.update()
This will update the contact in Paperless Parts and refresh the local instance.
Note: A salespersons email must correspond to a group member for the user group whose API-Token is being used. If another email is used, the request will return an HTTP 400 Error with a relevant error message.
An account represents a company. An account has the following fields:
* billing_addresses: list of BillingAddress objects
* created: string
* credit_line: Money object(optional)
* id: int
* erp_code: string(optional)
* notes: string(optional)
* phone: string(optional)
* phone_ext: string(optional)
* payment_terms: string(optional)
* payment_terms_period: int(optional)
* purchase_orders_enabled: boolean(optional)
* salesperson: Salesperson(optional)
* sold_to_address: Address object(optional)
* tax_exempt: boolean(optional)
* tax_rate: float(optional)
* url: string(optional)
from paperless.objects.customers import Account
accounts = Account.list()
This will return a list of minified Contact objects
accounts = Account.filter(erp_code='PPI')
Account can be filtered by erp code
accounts = Account.search(name='Paperless Parts, Inc.')
Accounts can be searched by the following fields:
* name
* erp_code
* notes
* id
Searches are case insensitive and can be partial matches
account = Account.get(101) #where 101 is the account id
This will return the account object with the given id
account.name = "Paperless Parts, Inc."
account.update()
This will update the account in Paperless Parts and refresh the local instance
NOTE: Optional fields will be initialized with the value NO_UPDATE by default when no value is provided. Properties with a value of NO_UPDATE are filtered out before being sent to the backend.
address = Address(address1="137 Portland St.", address2="lower", city="Boston", country="USA", postal_code="02114", state="MA")
account = Account(credit_line=10000, erp_code='PPI', name='Paperless Parts', notes='Test account', phone='6175555555', phone_ext='123', payment_terms='Net 30', payment_terms_period=30, purchase_orders_enabled=True, sold_to_address=address, tax_exempt=False, tax_rate=5.25)
account.create()
A billing address represents a billing address for a company. A billing addresss has the following fields:
* address1: string
* address2: string(optional)
* city: string
* country: string - three character country code
* id: int
* state: string - two character state code
* postal_code: string
from paperless.objects.customers import BillingAddress
billing_addresses = BillingAddress.list(account_id=141)
This will return a list of billing addresses
billing_address = BillingAddress.get(101) #where 101 is the billing address id
This will return the BillingAddress object with the given id
billing_address.address2 = "Lower Level"
billing_address.update()
This will update the billing address in Paperless Parts and refresh the local instance
billing_address = BillingAddress(address1="137 Portland St.", address2="lower", city="Boston", country="USA", postal_code="02114", state="MA")
billing_address.create(account_id=141)
A facility represents a location for a company. A facility has the following fields:
* account_id: int
* address: Address object(optional)
* attention: string(optional)
* created: string(optional)
* id: int
* name: string
* salesperson: Salesperson(optional)
from paperless.objects.customers import Facility
facilities = Facility.list(account_id=141)
This will return a list of facilities for the account
facility = Facility.get(101) #where 101 is the billing address id
This will return the Facility object with the given id
facility.name = 'Boston Office'
facility.update()
This will update the Facility in Paperless Parts and refresh the local instance
address = Address(address1="137 Portland St.", address2="lower", city="Boston", country="USA", postal_code="02114", state="MA") billing_address.create(account_id=141)
facility = Facility(name="Boston Office", attention="Jim Gordan", address=address)
facility.create(account_id=141)
A Purchased Component has the following fields:
* id: int
* oem_part_number: string
* piece_price: string
* properties: List(PurchasedComponentCustomProperty)
* internal_part_number: string(optional)
* description: string(optional)
from paperless.objects.purchased_components import PurchasedComponent
pc = PurchasedComponent.get(101) #where 101 is the the purchased component id
This will return the purchased component object with the given id
components = PurchasedComponent.search('AEM')
Purchased Components can be searched by the following fields:
* oem_part_number
* internal_part_number
Searches are case insensitive and can be partial matches
pc.description = 'very cool part'
pc.update()
This will update the purchased component in Paperless Parts and refresh the local instance
pc.delete()
Paperless Parts allows you to setup custom properties for purchased components, thes PurchasedComponent object has a getter and setter to work with these custom properties.
pc.set_property('my_prop', 20)
print(pc.get_property('my_prop'))
Paperless Parts allows you to setup custom properties for purchased components. These are manageable by the SDK as well. A Purchased Component Column has the following fields:
* id: int
* name: string
* code_name: string
* value_type: string
* default_string_value: string(optional)
* default_boolean_value: boolean
* default_numeric_value: int(optional)
* position: int
from paperless.objects.purchased_components import PurchasedComponentColumn
pcc = PurchasedComponentColumn.get(101) #where 101 is the the purchased component column id
This will return the purchased component column object with the given id
pcc.default_string_value = 'THIS IS A DEFAULT'
pcc.update(update_existing_defaults=True) # update existing components default values
This will update the purchased component column in Paperless Parts and refresh the local instance
pcc.delete()