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 CustomTableNext, 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 dataNOTE: 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 itYou 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 Quotenew_quotes = Quote.get_new(id=35, revision=1) #Where id is the quote numberThis 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 numberThis 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 numberThis 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('support@paperlessparts.com')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 idThis 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='support@paperlessparts.com', 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="sales@paperlessparts.com")
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 idThis 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 idThis 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 idThis 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 idThis 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 idThis 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 valuesThis will update the purchased component column in Paperless Parts and refresh the local instance
pcc.delete()