Skip to main content

Outbound EPCIS

When the outbound DSCSA document of a delivery (or of a return to a supplier) is posted, the modules write an EPCIS 1.2 XML file for the buyer's system, following the GS1 US DSCSA guideline R1.3. This page describes the file and the pure-Python writer that produces it, aglow_rx_tracking.tools.epcis_writer.build_shipping_epcis(data). What each part holds for a real shipment, with an excerpt, is on The outbound EPCIS 1.2 file.

When a document has a file​

The file is written from the document's frozen snapshot, never from live records, and stored as EPCIS-<document number>.xml (the number with every character other than letters, digits, _, . and - replaced by -). Its SHA-256 is part of the document's hash. A document gets no EPCIS file, and still posts with its T3 report:

  • by design, when it is lot level (a customer with the small-dispenser exemption: there are no serials to list) or has no transfer;
  • when data the file needs was missing at posting: a GLN for the company or the buyer, an address field, an NDC or GS1 company prefix length of a product, serials on a line. The document's EPCIS Problem field (epcis_issue) says why, and a note goes to the document and its transfer. A posted document never changes, so fix the master data for the next deliveries (Handle a document posted without an EPCIS file).

The file​

PartContent
Rootepcis:EPCISDocument, schemaVersion="1.2", creationDate = when the document's content was frozen, in UTC
Headeran SBDH (sbdh:StandardBusinessDocumentHeader): sender (the seller's GLN), receiver (the buyer's GLN), InstanceIdentifier = the document number, creation time
Master dataone EPCClass element per product (urn:epc:idpat:sgtin:<company prefix>.<item reference>.*) with the NDC-11 (type FDA_NDC_11), name and, when known, manufacturer, dosage form, strength and container size; one Location element per SGLN used, with name and address
Transaction Statementgs1ushc:guidelineVersion "GS1 US DSCSA R1.3" and gs1ushc:dscsaTransactionStatement with affirmTransactionStatement true and the legalNotice of the document
Commissioning eventsone ObjectEvent (ADD, commissioning, active) per product and lot, listing the units' SGTINs, with the lot number and expiry date as ILMD (cbvmda:lotNumber, cbvmda:itemExpirationDate)
Shipping eventone ObjectEvent (OBSERVE, shipping, in_transit) with every unit's SGTIN, the business transactions, source (owning party = seller, location = ship-from) and destination (owning party = buyer, location = ship-to), then gs1ushc:dropShipment false and, for a direct purchase, gs1ushc:directPurchase true

Details that matter to a receiving system:

  • Units ship loose. The modules record each unit as it leaves, so a case a unit arrived in has been opened: the file lists SGTINs, with no SSCC and no aggregation event. (The writer supports sealed cases for callers that pass an sscc per unit, below.)
  • Event times. The shipping event's time is when the delivery became done, with the warehouse's time-zone offset. The restated commissioning events are dated 3 seconds before it (and, with sealed cases, the case commissioning and packing events 2 and 1 seconds before), so a system that replays events in time order sees commissioning, packing, then shipping. All events of a file of the company's own stock use one read point, the ship-from SGLN; the shipping event has no business location.
  • Business transactions. po: the customer's purchase order number with the buyer's GLN (from the sale order's Customer Reference), or, for a return to a supplier, the company's purchase order with its own GLN; inv: each customer invoice posted before the delivery was done, with the seller's GLN; desadv: the delivery's number, with the ship-from GLN. References are fitted into the GS1 character set for the URN; the exact text stays on the T3 report.
  • Deterministic. The same document always gives the same bytes: products, lots and units are sorted by GTIN, lot and serial; business transactions keep the order above.
  • The Transaction Statement text in legalNotice comes from the company's EPCIS legal notice setting as it was when the document was created. It is configurable text; your procedure decides its wording (Review the Transaction Statement and the EPCIS legal notice).

With the 3PL add-on, a document issued in an owner's name differs: the owner is the seller and owning party, the commissioning is restated at the manufacturer's location, and the owner's profile can make the 3PL the header's sender and add it as the possessing party of the shipment (What an owner document's EPCIS file contains).

The writer​

build_shipping_epcis(data) takes one shipment as a plain dict and returns the XML as bytes. It has no Odoo imports and uses lxml and the modules' GS1 library. Any invalid or missing value raises EpcisWriterError (a ValueError) whose message names the key, for example packages[3].serial: .... Unknown keys in data, shipment and transaction_statement are refused (they are typos); party, product and package dicts may carry extra keys.

Text values are strings (surrounding whitespace is stripped; None, False and "" mean "not given"). Datetimes may be naive (read as UTC) or timezone-aware; dates are datetime.date or "YYYY-MM-DD".

KeyRequiredContent
document_idyesthe SBDH InstanceIdentifier, for example the document number
created_atyesdatetime of creationDate and the SBDH creation time
sender, receiveryesparty: seller and buyer
ship_from, ship_tonoparty: the physical locations; default the sender and the receiver
productsyesnon-empty list of product, one per GTIN; every package's GTIN has to be listed
packagesyesnon-empty list of package, one per unit (a duplicate SGTIN is refused)
shipmentyesevent_time (datetime, required), timezone ("+HH:MM", "-HH:MM" or an IANA name; default the offset of an aware event_time, else +00:00), read_point (SGLN URN; default the ship-from SGLN), biz_transactions (required non-empty list of {"type", "gln", "reference"}; type is a CBV type such as po, inv or desadv, gln the GLN of the party that issued the reference)
transaction_statementyesaffirm (bool, required) and legal_notice (text)
direct_purchasenoTrue writes gs1ushc:directPurchase true on the shipping event; default False (nothing written)
drop_shipmentnoa bool writes gs1ushc:dropShipment; default None (nothing written)
guideline_versionnowritten as gs1ushc:guidelineVersion, for example "GS1 US DSCSA R1.3"
  • party: sgln (SGLN URN, required; the GLN is derived from it), name, street, city, zip, country (ISO 3166 alpha-2) required, state required for US, street2 optional.
  • product: gtin14, prefix_length (the GS1 company prefix length, 6 to 12), name and ndc required (written as the NDC-11; for an NDC-based GTIN it has to match the GTIN); dosage_form, strength, container_size, manufacturer and package_type optional.
  • package: gtin14, serial, lot and expiry_date required; sscc optional (the sealed case the unit ships in, as 18 digits or an SSCC URN). All units of one GTIN and lot need the same expiry date.

Optional keys for shipping on behalf of another party​

Four more keys are a documented extension point, for a sender that is not the seller (a 3PL shipping for the owner of the goods). Absent, None, False or empty, each gives exactly the default output.

KeyContent
sbdh_senderparty named as the SBDH sender (only its GLN is written); default the sender
extra_sources, extra_destinationslists of {"type": "owning_party" | "possessing_party" | "location", "sgln": <SGLN URN>} added to the shipping event's source and destination lists, after the standard entries, in the given order
commission_locationSGLN URN used as read point and business location of the restated commissioning and packing events; default the read point

Example​

Writing a file from your own module, with the GS1 documentation identifiers of the examples:

import datetime

from odoo.addons.aglow_rx_tracking.tools import gs1
from odoo.addons.aglow_rx_tracking.tools.epcis_writer import build_shipping_epcis

seller = {"sgln": gs1.sgln_urn("0614141000012", 7), "name": "Example Distributor Inc.", "street": "100 Main Street",
"city": "Springfield", "state": "IL", "zip": "62701", "country": "US"}
buyer = {"sgln": gs1.sgln_urn("0614141000029", 7), "name": "Example Pharmacy LLC", "street": "200 Oak Avenue",
"city": "Albany", "state": "NY", "zip": "12207", "country": "US"}
gtin = gs1.ndc_to_gtin14("99990-101-30")
xml = build_shipping_epcis({
"document_id": "EXAMPLE-0001",
"created_at": datetime.datetime(2026, 9, 29, 15, 0),
"sender": seller,
"receiver": buyer,
"products": [{"gtin14": gtin, "prefix_length": gs1.ndc_prefix_length("99990-101-30"),
"name": "Demoprazole 20 mg", "ndc": "99990-101-30"}],
"packages": [{"gtin14": gtin, "serial": "100000000001", "lot": "BPDPZ2509A", "expiry_date": "2028-06-30"}],
"shipment": {"event_time": datetime.datetime(2026, 9, 29, 15, 0), "timezone": "America/Chicago",
"biz_transactions": [{"type": "desadv", "gln": "0614141000012", "reference": "WH-OUT-00001"}]},
"transaction_statement": {"affirm": True, "legal_notice": "Seller has complied with each applicable subsection of "
"FDCA Sec. 581(27)(A)-(G)."},
})

xml is the file's bytes; the reader reads it back without a warning.