Extending the modules
You change what the Rx Tracking modules do with your own Odoo module that depends on them, never by editing their files: an update of the modules replaces every file. This page gives the rules such a module follows, the thirteen documented extension points, two worked examples and notes for the Barcode app. The records and ideas behind them are on Architecture.
Rules for your module
- Depend on the module, then inherit. Add the module you extend to your manifest's
dependsand extend its models with_inherit. For the 3PL points, depend onaglow_rx_tracking_3pl. - Call
super()and build on its result. Every override returns whatsuper()returns, extended or narrowed only where your case applies. Another module (the 3PL add-on, or a second module of yours) may override the same method:super()is how both keep working. - Only DSCSA products. A check or change you add applies to lines whose product has
is_dscsa_productset. A line of any other product has to pass exactly as in standard Odoo; add a test that proves it. - Enforce on the server. Put a rule in the model method that every path goes through (the button's method,
createorwrite, the validation of the transfer), not only in an onchange or a view. An import, an external API call or the portal skips the onchange; see Where the checks run. - A default is today's behaviour. A hook you add for others to override returns exactly the current result when nobody overrides it, as the modules' own extension points do.
- A context key is never a security boundary. Any caller of the external API can pass any context key with any value, so a context value may never switch a check off, widen access or decide who may do something. Decide that from the user's groups, the records themselves, or a value that only your server-side code can set. The context keys the modules pass between their own methods are internal: don't set them, and don't read them in your code.
- Leave the modules' records to the modules. Packages, posted documents, logs and exports are written only by the modules' own operations, as superuser; a direct write is refused even for an Rx Tracking Manager. Call the public method that the button calls instead (the External API recipes show several).
- Use the modules' GS1 library. Compute check digits, GTINs from NDCs and SGTIN, SGLN or SSCC URNs with
aglow_rx_tracking.tools.gs1, so every identifier the database holds is computed the same way. - Test with Odoo's test framework.
odoo.tests.TransactionCase, tagged@tagged("post_install", "-at_install"), and run the modules' own suites with your module installed (Testing).
With the 3PL add-on
The 3PL add-on splits its overrides into three kinds (Three kinds of override). An override of yours that touches owner stock follows the same split:
- An override that changes what happens when stock moves or a document is made starts with
if not company._dscsa_3pl_on(): return super()...for the company of the record concerned. - An override that keeps owner data apart or shows it is gated with
company._dscsa_3pl_used()instead, so owner data stays apart after the switch is turned off. - Compare owners with
owner_key(partner, company), never as records: own stock has either no owner or the company's own partner. - A check that runs when a transfer is done appends its messages to the list
super()returns (see the second example below).
The extension points
These methods and data keys are meant to be overridden or used by other modules. Each has a default that is the modules' own behaviour, and contract tests in the modules' test suites that prove the default and that an override takes effect: run them after an update to see that a point still behaves as documented. The same list, generated from the modules, is on the technical reference.
| Point | Where |
|---|---|
| The party a document names as us | dscsa.document._dscsa_own_party() |
| The owner of a returned unit | dscsa.package._dscsa_unit_owner() |
| The parties a supplier EPCIS file is checked against | dscsa.epcis.import._dscsa_expected_parties(picking) |
| The document fields a portal user may read | dscsa.document._dscsa_portal_readable_fields() |
| Optional input keys of the EPCIS writer | build_shipping_epcis(data) |
| What the opening balance counts as room for new packages | dscsa.package._dscsa_room_quant_domain(product, lot), _dscsa_room_package_domain(product, lot) |
| Possessing parties read from a supplier EPCIS file | parse_epcis(data) |
| Columns and values of the package-ledger CSV | dscsa.retention.export._dscsa_ledger_columns(), _dscsa_ledger_row_extra(line, package) |
| Is the 3PL switch on | res.company._dscsa_3pl_on() |
| Has the company ever had an owner | res.company._dscsa_3pl_used() |
| The active owner profile of a partner | res.company._dscsa_3pl_profile(partner) |
| The owner key of stock | owner_key(partner, company) |
| Owner checks before a transfer is validated | stock.picking._dscsa_3pl_done_errors(final) |
The first eight are in the core (aglow_rx_tracking) since version 18.0.0.2.0, with their contract tests in its
tests/test_extension_seams.py; the last five are in the 3PL add-on (aglow_rx_tracking_3pl) since 18.0.1.0.0.
The party a document names as us
dscsa.document._dscsa_own_party() returns the partner a DSCSA document names as "us": the seller of an outbound document, the buyer
of an inbound one. The document's snapshot, its own history entry and the seller location of its EPCIS file read it. Default: the
company's partner. The 3PL add-on returns the owner a document is issued for. Return super() for every document your case doesn't
cover. Contract tests: TestExtensionSeams.test_c1_own_party_is_company_partner, TestExtensionSeams.test_c1_seam_names_us_everywhere.
The owner of a returned unit
dscsa.package._dscsa_unit_owner() returns the owner of a unit that a customer return verifies or rejects. The return moves are
grouped by it and carry it as their owner, so the unit goes back into that owner's stock. Default: no owner (an empty recordset), which
gives ownerless moves. Contract tests: TestExtensionSeamReturns.test_c4_unit_owner_empty,
TestExtensionSeamReturns.test_c4_seam_groups_and_owns_the_moves.
The parties a supplier EPCIS file is checked against
dscsa.epcis.import._dscsa_expected_parties(picking) returns the seller and buyer a supplier's EPCIS file has to name when it is
imported on the receipt picking, as {"seller": (set_of_glns, name), "buyer": (set_of_glns, name)}. Default: the GLNs of the
receipt's vendor (its company, contacts and addresses) and of the company (its partner, contacts and warehouses). A file that names
a party with another GLN is refused; a party without a GLN, in the file or in Odoo, only gives a warning. Only the check changes: the
import's supplier stays the receipt's vendor. Contract tests:
TestExtensionSeams.test_c5_expected_parties_default, TestExtensionSeams.test_c5_seam_sets_the_checked_parties.
The document fields a portal user may read
dscsa.document._dscsa_portal_readable_fields() (a model method) returns the set of dscsa.document fields that portal and public
users may read or search; every other field is refused to them. Default: the core's fixed list (number, direction, state, dates, the
trading partner, references, the Transaction Statement text, the lot-level flag, the portal URL and token fields, and the lot and NDC
summaries). Add a field only if every customer may see it on every document it can open. Contract tests:
TestExtensionSeams.test_c7b_portal_fields_default, TestExtensionSeams.test_c7b_seam_sets_the_portal_fields.
Optional input keys of the EPCIS writer
aglow_rx_tracking.tools.epcis_writer.build_shipping_epcis(data) accepts four optional input keys for code that writes a file on
behalf of another party: sbdh_sender (another sender in the header), extra_sources and extra_destinations (extra parties of the
shipping event) and commission_location (the read point and location of the restated events). Without them, the output is
byte-identical to the writer's default. Their format:
Outbound EPCIS. Contract tests:
TestExtensionSeamsEpcis.test_c8_absent_keys_byte_identical, TestExtensionSeamsEpcis.test_c8_keys_written,
TestExtensionSeamsEpcis.test_c8_keys_validated.
What the opening balance counts as room for new packages
When the opening balance registers packages for units already on the shelf, the modules check that the ledger never holds more
packages of a lot than there are units in stock. dscsa.package._dscsa_room_quant_domain(product, lot) returns the domain of the
stock counted, and _dscsa_room_package_domain(product, lot) the domain of the packages counted (both model methods; the modules add
the location themselves). Default: all internal and transit stock of the product and lot, and all packages of that lot still in the
company's custody. The 3PL add-on narrows both to one owner. Contract tests: TestExtensionSeams.test_c9_room_domains_default,
TestExtensionSeams.test_c9_seam_sets_what_is_counted.
Possessing parties read from a supplier EPCIS file
aglow_rx_tracking.tools.epcis_reader.parse_epcis(data) gives each shipment two keys a module can use: ship_from_possessor_gln and
ship_to_possessor_gln, the first possessing party of each side of the shipping event (a GLN, or None). They never add a warning.
The rest of the result: Inbound EPCIS. Contract test:
TestExtensionSeamsEpcis.test_c10_possessing_party_keys.
Columns and values of the package-ledger CSV
dscsa.retention.export._dscsa_ledger_columns() returns the column list of the package-ledger CSV, used by the monthly retention
export and the packages.csv of a trace response; _dscsa_ledger_row_extra(line, package) returns extra values merged over each
row (line is the done stock move line of the row, empty for a package that never moved). Both are model methods. Default: the core's
columns (a new list at each call) and no extra values. The 3PL add-on adds an owner column once the company has owner data. See the
first example. Contract tests: TestExtensionSeams.test_c11_default_identical_csv,
TestExtensionSeams.test_c11_seam_sets_columns_and_values.
Is the 3PL switch on
res.company._dscsa_3pl_on() returns whether the company's Third-party logistics (3PL) setting is on (False for an empty
recordset). It reads as superuser, so it works for any user. Gate every override that changes what happens to stock with it, and return
super() when it is off. Contract tests: TestThreePLInstall.test_defaults, TestThreePLCommon.test_owner_receipt_helper.
Has the company ever had an owner
res.company._dscsa_3pl_used() returns whether the company has ever had an owner profile (False for an empty recordset). Gate the
overrides that keep owner data apart or show it with it. Contract tests: TestThreePLInstall.test_defaults,
TestThreePLCommon.test_owner_receipt_helper.
The active owner profile of a partner
res.company._dscsa_3pl_profile(partner) returns the active owner profile (dscsa.owner.profile) of the partner's commercial partner
in this company, or an empty recordset when there is no partner, the partner is the company's own, or it has no active profile.
partner may be a record or an id. The profile is returned as superuser, for reading. Contract test:
TestThreePLInstall.test_owner_needs_profile_not_company.
The owner key of stock
aglow_rx_tracking_3pl.models.dscsa_3pl_common.owner_key(partner, company) returns 0 for own stock (no owner, or the company's
own partner) and the owner partner's id otherwise. partner may be a record, an id or False. Compare owners by their keys, never as
records. Contract test: TestThreePLCommon.test_company_partner_is_own.
Owner checks before a transfer is validated
stock.picking._dscsa_3pl_done_errors(final) returns every owner check's message for the transfers in self, as a list of strings
(empty = pass). It runs for every caller of the validation, before Odoo's own validation: with final=False from the Validate
button's checks, and with final=True again right before the stock moves are done. If the list isn't empty, the validation is
refused with one error that lists every message under "DSCSA owner isolation (3PL):". An extension appends its messages to the list
super() returns. See the second example. Contract test:
TestThreePLHooks.test_done_errors_extension.
Example: add a column to the package-ledger CSV
A module that adds the transfer's Source Document as a source_document column to the monthly retention export and to the
packages.csv of trace responses:
from odoo import api, models
class DscsaRetentionExport(models.Model):
_inherit = "dscsa.retention.export"
@api.model
def _dscsa_ledger_columns(self):
return super()._dscsa_ledger_columns() + ["source_document"]
@api.model
def _dscsa_ledger_row_extra(self, line, package):
extra = super()._dscsa_ledger_row_extra(line, package)
extra["source_document"] = line.picking_id.origin or "" if line else ""
return extra
Its manifest depends on aglow_rx_tracking. The new column comes last, after the core's columns and any the 3PL add-on adds. A test
that proves it, built on the core's test helpers:
from odoo.tests import tagged
from odoo.addons.aglow_rx_tracking.tests.test_package_ledger import PackageLedgerCase
@tagged("post_install", "-at_install")
class TestLedgerColumn(PackageLedgerCase):
def test_source_document_column(self):
receipt = self.receipt(2)
receipt.origin = "PO-EXAMPLE-1"
receipt._dscsa_register_scans([self.scan("X1"), self.scan("X2")])
self.validate(receipt)
action = self.env["dscsa.retention.export"].action_dscsa_export_now()
export = self.env["dscsa.retention.export"].browse(action["res_id"])
header, *rows = export.ledger_file_id.raw.decode().splitlines()
self.assertTrue(header.endswith(",source_document"))
self.assertEqual(len(rows), 2)
self.assertTrue(all(row.endswith(",PO-EXAMPLE-1") for row in rows))
The test helpers (PackageLedgerCase and its receipt, scan and validate) come from the modules' test suites. They make tests
short, but they are not an extension point: a new version may change them.
Example: an owner check when a transfer is done
A module for a 3PL that refuses to ship an owner's goods unless the transfer's Source Document holds the owner's order number:
from odoo import models
from odoo.addons.aglow_rx_tracking_3pl.models.dscsa_3pl_common import owner_key
class StockPicking(models.Model):
_inherit = "stock.picking"
def _dscsa_3pl_done_errors(self, final):
errors = super()._dscsa_3pl_done_errors(final)
for picking in self.filtered(lambda p: p.picking_type_code == "outgoing" and p.company_id._dscsa_3pl_on()):
if owner_key(picking.owner_id, picking.company_id) and not picking.origin:
errors.append(self.env._("%(picking)s: enter the owner's order number in Source Document.",
picking=picking.name))
return errors
Its manifest depends on aglow_rx_tracking_3pl. Validating an owner's delivery without a Source Document is then refused,
whether from the screen, the Barcode app or the external API:
DSCSA owner isolation (3PL):
- WH/OUT/00005: enter the owner's order number in Source Document.
The company's own deliveries (owner key 0) are not affected. Both examples were tested on a new database, with the modules'
own tests, before this page was published.
The Barcode app
The Barcode add-on changes the Enterprise Barcode app with JavaScript patch() calls on three of its models, and the 3PL Barcode
bridge adds one more:
| Module | Patched | What changes |
|---|---|---|
| Barcode add-on | BarcodeModel.prototype._parseBarcode | the GS1 serial element (AI 21) is kept as the unit's serial; for DSCSA products the lot comes from AI 10 only |
| Barcode add-on | BarcodePickingModel.prototype._processBarcode | on a transfer, a DataMatrix with a serial of a DSCSA product is sent to the server (stock.picking.dscsa_barcode_register_scan), which registers the unit in the ledger; the app then reloads the transfer |
| Barcode add-on | BarcodeQuantModel.prototype._processBarcode | in an inventory count, the same unit can't be counted twice |
| 3PL Barcode bridge | BarcodePickingModel.prototype.setData | the lot pre-fill offers only stock of the transfer's owner |
If you patch the same Barcode app methods:
- Depend on the module whose patch you build on (
aglow_rx_tracking_barcodeoraglow_rx_tracking_3pl_barcode), so your assets load after it and your patch wraps its patch. - Call
superfor every scan your change doesn't handle; scans without a serial and scans of other products go through the standard flow unchanged. - Treat the JavaScript as convenience only: the server repeats every check when the scan is registered and when the transfer is done, whichever client sent it.
Stability
The extension points, their signatures and their defaults may change from one version to the next. The changes are listed in the changelog, and the known issues of each version in the release notes. Everything that isn't on this page or in the technical reference is internal: an update can change it without a note.