EDI SPS Commerce

EDI integration with SPS Commerce via their canonical RSX XML
format (namespace http://www.spscommerce.com/RSX, schema v7.7.0), on
top of the OCA EDI framework (edi_core_oca), targeting Odoo
19.0.
RSX is SPS’s proprietary normalized schema: one document type covers
every trading partner — SPS maps each partner’s native X12/EDIFACT/API
behind it, so we integrate once instead of per-partner segment maps.
Documents covered:
- Orders inbound — RSX Orders → draft sale.order
- Shipments outbound (ASN) — stock.picking → RSX Shipments
- Invoices outbound — account.move → RSX Invoices
All three share one group vocabulary (Address, Dates, References, Notes,
PaymentTerms, Taxes, ChargesAllowances, CarrierInformation, part-number
id block). Inbound payloads are parsed by services/rsx.py
(xmltodict) into schema-shaped dicts; outbound payloads are rendered
by QWeb edi.exchange.template.output templates that consume the
business records directly.
Transport is pluggable per exchange record:
- SPS-hosted SFTP (sftp.spscommerce.com) via edi_storage_oca
— inbound files polled from out/testout, outbound uploaded to
in/testin;
- Manual drop (fallback) — import wizard for inbound, local
directory for outbound.
This module is a POC in active development: the mappings cover the core
fields and defer the long schema tail (carrier detail, pack levels,
charges/allowances, taxes detail) to the partner implementation guide.
Not affiliated with or endorsed by SPS Commerce.
Important
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
More details on development status
Table of contents
Odoo modules:
- edi_core_oca — exchange framework (backends, exchange types,
records).
- edi_storage_oca — storage-backed transport (pulls fs_storage).
- edi_exchange_template_oca — QWeb output templates.
- sale_stock, account — sales/delivery orders and invoices.
Python packages:
- xmltodict — inbound parser (manifest external_dependencies).
- fsspec + paramiko — for the SFTP transport (via
fs_storage; paramiko must be installed manually).
| Code |
Direction |
Document |
Filename |
| sps_orders_in |
input |
rsx.document: orders |
^PO |
| sps_shipments_out |
output |
rsx.document: shipments |
SH-{dt}.xml |
| sps_invoices_out |
output |
rsx.document: invoices |
IN-{dt}.xml |
- Output templates sps_shipments / sps_invoices
(edi.exchange.template.output records + QWeb views, incl. shared
fragments sps_meta/sps_address/sps_item_ids), bound via
output_template_id on the outbound types.
- Exchange type rules: Send SPS ASN button on done pickings, Send
SPS Invoice button on posted customer invoices.
Exchange-type advanced settings (rsx: block):
- document — expected RSX root (orders/shipments/invoices).
- sender_id / receiver_id — RSX Meta envelope ids.
- outbound_dir — local dir for the no-storage fallback.
- xsd_validate — (planned) XSD validation toggle; schemas are
user-supplied, not shipped.
SPS hosts the endpoint at sftp.spscommerce.com with fixed directory
pairs — out/testout inbound, in/testin outbound. Create
archive on the SPS account first (rename fails otherwise); we must
clean out ourselves. SPS deletes in files during processing, so
outbound types have no check handler: output_sent is terminal
until an inbound ack (997/855) arrives.
The directory names carry the environment — same SFTP account, live
files in out/in, test files in testout/testin — set on
the backend’s Storage page (input_dir_pending/output_dir_pending
- their test_* counterparts). Test mode is supported because the
sps backend type ships with test_mode_supported; enable the
Test mode checkbox (edi.backend.test_mode from
edi_core_oca) and get_conn_parameter() returns the live or
test folder accordingly — configured values are never rewritten.
data/neutralize.sql in edi_core_oca sets test mode on all
backends for copied databases. To point test mode at a different SFTP
account entirely, set test_storage_id instead.
- Partner: res.partner.ref = the SPS trading-partner id
(OrderHeader.TradingPartnerId, falling back to the Meta sender
ids). The id is also what outbound templates emit in
TradingPartnerId.
- Product: OrderLine.VendorPartNumber/ApplicationId →
default_code; BuyerPartNumber → product.customer.code when
installed; GTIN/EAN/ConsumerPackageCode → barcode.
- UOM: RSX carries X12 355 codes; a mapping table to uom.uom is
planned — today quantities import in the product’s UoM.
Each SPS relationship still has a mandatory-subset profile. Point the
partner’s own exchange type at a copy of the shipped
edi.exchange.template.output (or extend it via code_snippet) —
no code changes needed.
- Open EDI → Exchanges → Import SPS RSX.
- Pick the exchange type, upload the RSX Orders file.
- The wizard creates an edi.exchange.record in input_pending
and runs receive → validate → process immediately.
Expected result: one draft sale.order per <Order> —
PurchaseOrderNumber → client_order_ref, Dates[002] →
commitment_date, one line per LineItem. The exchange record is
linked as the order’s EDI origin. TsetPurposeCode other than 00
(original) raises — cancel/change documents are handled manually for
now.
- SPS drops a PO* file into out/testout.
- The storage poller creates an input_pending record; the input
sync cron receives, validates, and processes it; processed files move
to archive.
- Manual: open a done delivery order → Send SPS ASN.
- Automatic: stock.picking.button_validate queues a Shipments
exchange for done pickings when the partner is enabled on the type.
- Retrigger: call picking._edi_sps_queue_shipments(force=True)
(server action / shell) — skips pickings with an existing record
unless force.
The picking is rendered by the sps_shipments template and uploaded
to output_dir_pending (SPS: in/testin) as SH-<dt>.xml,
or written to rsx.outbound_dir without storage.
Open a posted customer invoice (or credit note) → Send SPS Invoice.
The move is rendered by sps_invoices (DI for invoices, CR
for credit notes) and delivered the same way.
- services/rsx.py — inbound codec: parse/parse_orders +
FORCE_LIST for repeatable groups.
- edi.exchange.template.output — outbound documents; copy a shipped
template per partner profile, or use code_snippet for extra render
values.
- edi.sps.handler — handler roles; partner/product matching in
_find_partner/_find_product.
- edi.exchange.type.advanced_settings_edit — rsx.* config.
Status: in progress
- SPS RSX XML (schema v7.7.0) is the payload format — normalized
across partners by SPS; proprietary to SPS but standard connector
practice (cf. account_avatax, delivery_sendcloud).
- Rejected: extending the raw-X12 codec in edi_x12_poc —
per-partner segment maps duplicate work SPS already did.
- Rejected: base_business_document_import/UBL — the wire format is
fixed by SPS.
- Module name edi_spscommerce.
- Rejected: edi_rsx — RSX is SPS’s proprietary namespace; a
generic name would overstate portability.
- Transport: SPS-hosted SFTP (sftp.spscommerce.com) via
edi_storage_oca — out/testout in, in/testin out,
archive for processed; no outbound check (SPS deletes in
files, output_sent terminal until an inbound ack).
- Rejected: SPS REST API/AS2 — would need edi_webservice_oca and
loses the file-based audit trail.
- Inbound codec services/rsx.py via xmltodict — schema-shaped
dicts (RSX element names verbatim), FORCE_LIST for [0..*]
groups; the handler does RSX→Odoo translation.
- Outbound generation via QWeb edi.exchange.template.output —
templates consume record directly (mapping lives in data;
per-partner overrides need no code). Handler injects only meta.
- 2026-11 — supersedes programmatic ``etree`` builders.
- 2026-11 — record-direct templates supersede ``_edi_sps_*_values``
dict builders — one mapping surface.
- Batching: <Order>/<Shipment>/<Invoice>[1..*] — plan to split
one file into one exchange record per document (error isolation).
- Rejected: one record per file — a bad document would block the
batch.
- Inbound Orders create draft sale.order; TsetPurposeCode
drives create/cancel/change (only 00 implemented).
- XSD validation behind rsx.xsd_validate for the test phase; schemas
are user-supplied, not shipped.
- OCA publication: license LGPL-3; no vendored SPS materials (tests
use hand-written fixtures); nominative use of the name; confirm the
SPS agreement doesn’t mark RSX confidential before publishing.
- Which address types does the partner send (ST+BY only, or
separate BT)? Drives partner-matching fallback order.
- Do we get TsetPurposeCode 01/04/05 (cancel/change) or originals
only?
- ASN depth: PackLevel/SSCC required, or OrderLevel→ItemLevel
enough?
- Invoice trigger: post of account.move, or batch end-of-day?
- RSX filename convention: same PO/SH/IN prefixes +
.xml?
- Iteration 1 (done): module scaffold, inbound codec, outbound QWeb
templates + skeleton mappings, SFTP/manual transports, tests.
- Iteration 2: Orders full mapping (ship-to/GLN matching, payment terms,
UOM table, currency, notes, dropship refs); split-per-document
receive.
- Iteration 3: Shipments full mapping (carrier/SCAC, tracking,
PackLevel/SSCC); Invoices (terms, taxes, charges/allowances).
- Iteration 4: partner profile config; verify vs SPS
testin/testout.
- Iteration 5: inbound acks (997/855), cancel/change order handling.
- ☒ Scaffold edi_spscommerce (manifest, handler, backend type, three
exchange types, access rules, import wizard, tests)
- ☒ Codec services/rsx.py — xmltodict parse (verified against the
real Orders.xml sample)
- ☒ Outbound generation via edi_exchange_template_oca — QWeb views
consuming record + template records on the outbound types
(skeleton mappings)
- ☒ Split design doc into readme/ fragments (this doc)
- ☐ Split-per-document receive (one exchange record per <Order>)
- ☐ x_sps_* fields — x_sps_location (partner GLN/location id),
x_sps_customer_ref / x_sps_line_ref on sale orders
- ☐ Orders → sale.order full mapping (ship-to match/create, UOM
table, payment terms, currency, notes, dropship refs)
- ☐ UOM map: X12 355 codes → uom.uom (module data + lookup)
- ☐ TsetPurposeCode 01/04/05 — cancel/change order handling
- ☐ Shipments full mapping (carrier/SCAC, tracking, PackLevel/SSCC,
Dates, References)
- ☐ Invoices full mapping (payment terms, taxes, charges/allowances,
BOL/PRO)
- ☐ Partner profile config (trading-partner id, code maps, per-partner
mandatory fields)
- ☐ Implement rsx.xsd_validate (user-supplied schema path)
- ☐ Inbound acks — 997/855 exchange types
- ☐ Confirm with SPS: address types sent, purpose codes used,
PackLevel/SSCC requirement, invoice trigger, RSX filename convention
- ☐ Verify against SPS testin/testout
- ☐ Confirm SPS agreement permits OCA publication
- ☐ Regenerate README.rst/static/description with
oca-gen-addon-readme before publishing
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
feedback.
Do not contact contributors directly about support or help with technical issues.
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the OCA/edi project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.