Odoo Community Association

EDI SPS Commerce

Alpha License: LGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

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:

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:

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

Configuration

Dependencies

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).

Shipped configuration

  • Backend type sps.

  • Backend SPS Commerce Backend with the SPS directory layout:

    ::
    

    input_dir_pending = out # SPS → you (live) input_dir_done = archive # you clean “out” yourself input_dir_error = archive output_dir_pending = in # you → SPS (live) output_dir_done = (empty) # SPS deletes “in” files; unused output_dir_error = (empty)

  • Exchange types (all handler roles → edi.sps.handler):

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 SFTP map

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.

Test mode

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 and product matching

  • 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.

Customizing a partner

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.

Usage

Inbound Orders — manual drop (no storage)

  1. Open EDI → Exchanges → Import SPS RSX.
  2. Pick the exchange type, upload the RSX Orders file.
  3. 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.

Inbound Orders — via SPS SFTP

  1. SPS drops a PO* file into out/testout.
  2. The storage poller creates an input_pending record; the input sync cron receives, validates, and processes it; processed files move to archive.

Outbound Shipments (ASN)

  • 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.

Outbound Invoices

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.

Extension points

  • 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.

Known issues / Roadmap

Status: in progress

Decisions

  • 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.

Open questions

  • 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?

Plan

  • 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.

Tasks

  • ☒ 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

Bug Tracker

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.

Credits

Authors

  • Gray Matter Logic

Maintainers

This module is maintained by the OCA.

Odoo Community Association

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.