Compliance - Signals Documentation

Location: api/nextango/apps/compliance/cannabis/signals.py Last Updated: 2026-07-06

Overview

All compliance signals are registered in compliance/cannabis/signals.py. They are post-save handlers that keep cannabis profile models in sync with their parent records without requiring callers to invoke compliance services explicitly.


sync_cannabis_profile

Trigger: products.ProductVariant, post_save (any save)

Keeps ProductVariantCannabisProfile synchronized with ProductVariant.cannabis_compliance. Calls sync_cannabis_profile_from_variant(), which is also used by the data migration backfill; both paths use identical logic.

Fields synced from cannabis_compliance (JSONField):

Source keyTarget fieldNotes
limitCategorylimit_categoryDefaults to 'other' if invalid or missing
unitWeightMgunit_weight_mgNone if not present
menuTypemenu_typeDefaults to 'both' if invalid or missing
vapeTaxApplicablevape_tax_applicableDefaults to False

Creates the profile if it does not exist. Idempotent, safe to call multiple times for the same variant. Does not trigger another ProductVariant save.


create_cannabis_snapshot_for_return_line

Trigger: transactions.ReturnLineItem, post_save (created=True only)

Creates a ReturnLineItemCannabisSnapshot when a ReturnLineItem is created for a cannabis sale line item. Copies the Metrc package tag from the original SaleLineItemCannabisSnapshot so the return has a complete audit trail back to the originating package.

If the original sale line item has no cannabis snapshot (a non-cannabis item), no snapshot is created; this is correct behaviour.

Bulk-create caveat: Django's bulk_create() bypasses post_save signals. Any future bulk-create path for ReturnLineItem must write snapshots explicitly.


sync_metrc_tags_on_awaiting_compliance

Trigger: procurement.PurchaseOrder, post_save (status transition to AWAITING_COMPLIANCE_VERIFICATION only)

Fires when a PurchaseOrder transitions to AWAITING_COMPLIANCE_VERIFICATION. For each package ID in compliance_verification['packageIds'], creates a MetrcTag row (if it does not already exist) and calls get_state_track_client().pull_package().

Client behaviour:

Clientpull_package() result
StubStateTrackClient (default)Logs a PACKAGE_PULL StateTrackSyncRecord with status=STUB
MetrcClient (production)Calls the live Metrc API; writes VERIFIED status on success

Known gap: MetrcTag rows are created with purchase_order set but inventory_level=None. The package-to-InventoryLevel mapping is not available at this transition point. The link is established later when an employee calls the metrc-verify endpoint or performs manual reconciliation. Closing this gap requires adding package→InventoryLevel mapping to the compliance_verification slot (Phase 7 scope).


create_destruction_event_on_destruction_disposition

Trigger: transactions.ReturnLineItem, post_save (created=True and disposition=DESTRUCTION)

Creates a CannabisDestructionEvent when a ReturnLineItem with disposition=DESTRUCTION is created and the original sale line item has a cannabis snapshot.

Signal registration order matters: create_cannabis_snapshot_for_return_line fires before this signal, so instance.cannabis_snapshot is available when this handler runs.

Inventory level resolution: Derived from store + product + variant_sku via a queryset lookup (the same path used by InventoryAdjustmentService). The reference_inventory_levels M2M cannot be used here because post_save(created=True) fires before any M2M assignment.

witnessed_by: Set to None, not available from signal context. Must be updated via admin or a future employee-session context for compliance reports.

If the return line item has no cannabis snapshot (non-cannabis return), no destruction event is created.

Was this page helpful?