Skip to main content

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:

  1. 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.
  2. 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 in file_checksums. Printing or downloading later serves the stored file; it is never rendered again.
  3. Chain. The document takes the next position (chain_index) in its company's chain, and its hash is computed as sha256(previous_hash + canonical JSON of {name, direction, company_id, chain_index, snapshot, files}), stored as $1$<hex> (the 1 is 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:

CheckWhere 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 unitvalidation of a receipt, delivery or return; scraps and inventory counts of DSCSA products
Frozen documents, ledger, logs and exportsthe write and unlink of the records concerned
Moves outside the ledger's flowsevery 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_used becomes 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):

KindGateExamples
Operational: changes what happens when stock moves or a document is madecompany._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 itcompany._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 datanonenumbering 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:

RecordFieldMeaning
stock.pickingowner_idthe owner of every DSCSA move on the transfer (one owner per transfer)
stock.moverestrict_partner_idthe move's owner: reservation takes only that owner's stock
stock.move.line, stock.quantowner_idOdoo's own owner fields; a line's owner has to match its move's owner when the transfer is done
dscsa.packagedscsa_owner_idthe owner of the unit while it is in the warehouse (empty = own); dscsa_seller_id, the seller of record, once it ships
dscsa.owner.profilepartner_idthe 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>):

LibraryEntry pointContract
aglow_rx_tracking.tools.gs1functions for GS1 identifiers: check digits, GTIN from NDC, SGTIN, SGLN and SSCC URNs, GS1 element stringsthe only place where the modules compute GS1 identifiers; reuse it rather than writing your own
aglow_rx_tracking.tools.epcis_writerbuild_shipping_epcis(data)Outbound EPCIS
aglow_rx_tracking.tools.epcis_readerparse_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.