Architecture
The ideas a developer needs before reading or extending the modules' code: the package ledger, the frozen DSCSA documents and their hash chain, where the checks run, and how the 3PL add-on switches itself on and keeps owners apart. Each part names the models involved; their fields are in the technical reference. What the same things look like on the screens is in the key concepts.
The package ledger
DSCSA products stay lot-tracked in Odoo (tracking = "lot"), so stock, reservation, removal strategies and expiry dates work by lot
as in standard Odoo. The serial number of each unit lives beside Odoo's stock, in the modules' own ledger: one dscsa.package record
per serialized unit, with its GTIN, serial, SGTIN, lot, expiry date, the SSCC of the case it came in, and a state
(expected, received, in_stock, shipped, returned, quarantined, missing, destroyed; the labels are on
Statuses). Stock moves link the packages they carry, which is how the ledger knows where each
unit came from and where it went.
- Only the ledger writes the ledger. Packages are created and changed by the ledger's own operations (supplier file import, serial scans, validation of a receipt, delivery, return or scrap, inventory counts, the opening balance, quarantine), which write as superuser. Any other write to a package, by any user including an Rx Tracking Manager and including over the external API, is refused with an access error. Access rights give both Rx Tracking groups read access only.
- One serial per unit. A transfer of DSCSA products is done only when every unit on it has exactly one scanned serial; a scrap or an inventory count of DSCSA products names the serials concerned.
- The states, the operations that change them and the searches are described for users in Packages and the serial ledger.
Frozen documents and the hash chain
A dscsa.document holds the transaction information, statement and history of one change of ownership: outbound for a delivery
or a return to a supplier, inbound for a receipt with a supplier's EPCIS file. It is created and posted in the same database
transaction as the validation of its transfer, so a transfer is never done without its document. Posting freezes it in three steps:
- Snapshot. Every element the document states (parties, products, lots, serials, references, the Transaction Statement text, the
history) is written once, from live data, as canonical JSON (sorted keys, ASCII, no whitespace) in
snapshot. - Files. The files are rendered from the snapshot, not from live data: for an outbound document the T3 report (PDF) and, when
the data allows, the EPCIS 1.2 file; for an inbound one the supplier's own file. They are stored as attachments (
file_ids) with the SHA-256 of each infile_checksums. Printing or downloading later serves the stored file; it is never rendered again. - Chain. The document takes the next position (
chain_index) in its company's chain, and its hash is computed assha256(previous_hash + canonical JSON of {name, direction, company_id, chain_index, snapshot, files}), stored as$1$<hex>(the1is the hash version). Posting takes a row lock on the company, so two documents of one company are never posted in parallel.
After posting, only the chatter and a few fields that the modules manage themselves (the portal access token, the legal hold, the off-site archive state) can change; they are outside the hash. Any other write is refused with "DSCSA document … is posted and can't be changed (fields: …).", and no document is ever deleted. Verify Integrity (Rx Tracking Manager) recomputes the whole chain of the current company and every stored file's checksum, and reports the first break (Verify the integrity of the DSCSA documents).
The same pattern (files stored with their SHA-256, never changed or deleted) holds for the monthly retention exports and the trace responses; with the off-site archive switched on, every posted document and export is also copied, with a JSON manifest, to an S3 bucket with Object Lock. What is kept and for how long: Records kept.
Where the checks run
Every check runs on the server, in the Odoo methods that every path goes through, so the screens, an import, the external API, the portal, Point of Sale and other apps meet the same check with the same message:
| Check | Where it runs |
|---|---|
| Trading partner authorized (valid licenses for its DSCSA role) | sale order confirmation, purchase order confirmation and approval, and validation of a transfer to or from a partner |
| One scanned serial per unit | validation of a receipt, delivery or return; scraps and inventory counts of DSCSA products |
| Frozen documents, ledger, logs and exports | the write and unlink of the records concerned |
| Moves outside the ledger's flows | every stock move of a DSCSA product when it is done (stock.move._action_done): a move from or to a partner has to be on a transfer, and manufacturing or repair moves of DSCSA products are refused |
Onchange warnings in the forms are only a convenience; the rule is the server-side check. Only products with is_dscsa_product set are
checked: a line of another product passes every check unchanged. The full list of checks and the paths each covers is
What is checked where.
Companies
The modules' records belong to one company (a license may also be shared by all companies), and record rules limit each user to the companies selected in the company switcher. Each company has its own document numbering and its own hash chain. The rules are listed on Security.
The 3PL add-on
The 3PL add-on (aglow_rx_tracking_3pl) lets one warehouse hold DSCSA stock for other companies, the owners, next to its own.
The switch
res.company.dscsa_3pl_enabled (the Third-party logistics (3PL) setting) is the add-on's switch, off by default. While it is off,
the add-on changes nothing and no owner profile can be created or activated.
- Turning it on is refused while DSCSA stock is already held for another owner through Odoo's consignment (such stock has no owner per serial). It then creates the company's title-transfer location and each warehouse's title-transfer operation type, and turns on Odoo's Consignment setting (a database-wide group).
- Turning it off is refused while an owner profile is active or DSCSA stock is held for an owner.
res.company.dscsa_3pl_usedbecomes true when the company's first owner profile is created and is never reset: from then on, the company has owner data to keep apart, even if the switch is turned off again.
Three kinds of override
Every override of the add-on belongs to one of three kinds, and a module that extends the add-on follows the same split (With the 3PL add-on):
| Kind | Gate | Examples |
|---|---|---|
| Operational: changes what happens when stock moves or a document is made | company._dscsa_3pl_on() | reservation and scans by owner, the owner checks when a transfer is done, documents in the owner's name, owner orders, holds and notices |
| Keeps owner data apart or shows it | company._dscsa_3pl_used() | trace requests limited to one owner, the owner column of the ledger CSV, the owner fields a portal user may read |
| Only reads the add-on's own data | none | numbering with an owner prefix, the add-on's record rules and field definitions |
With the switch on and no owner profile yet, the first two kinds return the core's result unchanged, so the database behaves exactly as without the add-on. Stock the company owns behaves as in the core with the switch on too.
Owners and owner keys
"Own" stock is stock with no owner or with the company's own partner as owner. So that the two never compare as different, the
add-on compares owners by their owner key: owner_key(partner, company) returns 0 for own stock and the owner partner's id
otherwise. Where an owner is recorded:
| Record | Field | Meaning |
|---|---|---|
stock.picking | owner_id | the owner of every DSCSA move on the transfer (one owner per transfer) |
stock.move | restrict_partner_id | the move's owner: reservation takes only that owner's stock |
stock.move.line, stock.quant | owner_id | Odoo's own owner fields; a line's owner has to match its move's owner when the transfer is done |
dscsa.package | dscsa_owner_id | the owner of the unit while it is in the warehouse (empty = own); dscsa_seller_id, the seller of record, once it ships |
dscsa.owner.profile | partner_id | the owner itself, with its settings in this company: its code, its warehouses, how documents are issued in its name, its contacts, the title transfers it allows |
Owner stock never appears on the company's own sale or purchase orders: an owner's goods leave on the owner's instructions. How owner stock looks to users: Owner stock and own stock and Title transfer.
The pure-Python libraries
Three Python modules of the core work without the Odoo ORM: they take and return plain Python data, so your own modules and tests
can call them directly (imported as odoo.addons.aglow_rx_tracking.tools.<name>):
| Library | Entry point | Contract |
|---|---|---|
aglow_rx_tracking.tools.gs1 | functions for GS1 identifiers: check digits, GTIN from NDC, SGTIN, SGLN and SSCC URNs, GS1 element strings | the only place where the modules compute GS1 identifiers; reuse it rather than writing your own |
aglow_rx_tracking.tools.epcis_writer | build_shipping_epcis(data) | Outbound EPCIS |
aglow_rx_tracking.tools.epcis_reader | parse_epcis(data) | Inbound EPCIS |
The module docstrings of the three libraries describe each function. The optional input keys of the writer and the possessing-party keys of the reader are documented extension points.