Skip to main content

First-class Contract, Invoice, and ServiceProvider models for Nautobot.

Project description

nautobot-contract-models

A Nautobot content plugin that adds first-class models for vendor contracts, invoices, renewal tracking, and PDF attachments, with the relationships needed to answer questions like:

  • Which contracts expire in the next 60 days?
  • Which devices are covered by an active support contract, and which aren't?
  • What did we pay last quarter for circuit X, and is it trending up?
  • Show me the signed PDF of the master services agreement we have with Acme.

Inspired by netbox-contract, but re-architected for Nautobot 3.x conventions: PrimaryModel, the Status framework, Tenant, ChangeLog, the Job framework, and the modern NautobotUIViewSet / ObjectDetailContent UI Component Framework.

Status

Tested against Nautobot 3.1.1. CalVer versioning (YYYY.M.D) — see pyproject.toml for the version that was current when you cloned.

Latest: v2026.5.17 — adds iCal calendar export, a Device-detail Active Contracts panel, a Vendor Concentration Risk dashboard panel, and a Coverage Drift report.

Coexistence with nautobot-app-device-lifecycle (DLM)

Both plugins ship a "Contracts" surface; DLM's ContractLCM is structurally simpler than our Contract. Since v2026.5.11 the two plugins coexist without colliding on Django's Status reverse accessor. v2026.5.12 adds two opt-in features so operators can make our Contract the canonical contracts surface:

  • Migration: A one-way idempotent Migrate ContractLCM → Contract Job copies every ContractLCM row (including its device M2M, converted to our polymorphic ContractAssignment) into our model.
  • Nav hide: The hide_dlm_contracts_nav PLUGINS_CONFIG flag (default False) removes DLM's Contracts sidebar group when both apps are installed.

What this does NOT do. Neither feature disables DLM. With the flag set and the migration complete, DLM's ContractLCM table still exists, its URLs (/plugins/nautobot-device-lifecycle-mgmt/contract/…) still resolve, its REST API still serves, and its Hardware Notices / Software Lifecycle / Reports nav groups are untouched. We only hide one nav group and copy the contract rows — never delete, never block, never overwrite. Scripts that hit DLM's contract endpoints keep working.

If you don't want either feature, simply don't enable them. Both plugins coexist fine with two parallel "Contracts" surfaces — that's the v2026.5.11 baseline.

Full step-by-step walkthrough with screenshots: Coexistence with nautobot-app-device-lifecycle.

Models

Model Role Lifecycle
ServiceProvider The vendor / counterparty Independent — referenced by Contracts
Contract The master agreement: dates, costs, currency, status Owned by a ServiceProvider (PROTECT); optional Tenant (SET_NULL)
Invoice One billing line on a Contract CASCADE on Contract delete
ContractAssignment Generic-FK link between a Contract and any Nautobot object (Device, Circuit, VirtualMachine, …) CASCADE on Contract delete; PROTECT on target ContentType
InvoiceAttachment A file uploaded against an Invoice (typically the vendor PDF) CASCADE on Invoice delete
ContractAttachment A file uploaded against a Contract (signed PDF, SOW, renewal letter) CASCADE on Contract delete

All six are PrimaryModel subclasses, so they get for free: ChangeLog, custom fields, tags, dynamic groups, REST API, GraphQL, webhooks, notes, contacts, computed fields.

The ContractAssignment model uses Django's ContentType + GenericForeignKey so one model handles all target types — no separate ContractDevice, ContractCircuit, ContractVM tables. Operators can attach a Contract to anything in the Nautobot ORM with a UUID PK.

Install

pip install nautobot-contract-models

Add to nautobot_config.py:

PLUGINS = ["nautobot_contract_models"]

PLUGINS_CONFIG = {
    "nautobot_contract_models": {
        # Days-out window for the renewal-alert Job + home-dashboard panel.
        # Default: 60. Override via env var, file, etc. as you would any
        # PLUGINS_CONFIG entry.
        "renewal_window_days": 60,
    },
}

Then run migrations:

nautobot-server migrate nautobot_contract_models

The 0002_register_statuses data migration creates four Status records (Active, Expired, Cancelled, Pending) and binds them to the Contract and Invoice content types. Idempotent — safe to re-run.

After install

The renewal-check Job ships disabled (Nautobot 3.x default for newly-discovered Jobs). To enable it:

  1. Apps → Jobs, find "Check upcoming renewals" under the Contracts group
  2. Edit the Job, check Enabled, save
  3. (Optional) Configure a recurring schedule: Apps → Jobs → Scheduled Jobs → Add

Configuration via PLUGINS_CONFIG

Key Type Default Effect
renewal_window_days int 60 Window in days for the renewal-alert Job's default + the homepage "Upcoming Renewals" panel
hide_dlm_contracts_nav bool False When True AND nautobot-app-device-lifecycle-mgmt is installed, removes DLM's Contracts sidebar group. DLM's URLs, REST API, and other nav groups (Hardware Notices, Software Lifecycle, Reports) are untouched. See the Coexistence section above for full context.
vendor_concentration_threshold_pct int 50 Top-vendor percentage that trips the concentration-risk flag on the home dashboard's Vendor Concentration panel. Range 0–100. (Added in v2026.5.17.)

REST API

The plugin exposes a full REST API under /api/plugins/contracts/. Authentication is the standard Nautobot token; pass via Authorization: Token <key> header.

TOKEN=...
BASE=https://nautobot.example.com/api/plugins/contracts

# List contracts, with count fields populated
curl -H "Authorization: Token $TOKEN" "$BASE/contracts/"

# Same query but with FKs expanded inline (provider, status, tenant)
curl -H "Authorization: Token $TOKEN" "$BASE/contracts/?depth=1"

# Filter by name + currency
curl -H "Authorization: Token $TOKEN" "$BASE/contracts/?currency=USD&name__ic=acme"

# Find contracts expiring before a date
curl -H "Authorization: Token $TOKEN" "$BASE/contracts/?end_date__lte=2026-12-31"

# Create a Contract
curl -H "Authorization: Token $TOKEN" -X POST "$BASE/contracts/" \
  -d '{"name":"Acme MSA","provider":"<provider-uuid>","status":"<active-status-uuid>",
       "start_date":"2026-01-01","end_date":"2027-01-01","recurring_cost":"1200.00"}' \
  -H "Content-Type: application/json"

Six endpoints, all with the standard list/detail/create/edit/delete + filter + bulk actions:

Path Model
/api/plugins/contracts/service-providers/ ServiceProvider
/api/plugins/contracts/contracts/ Contract
/api/plugins/contracts/invoices/ Invoice
/api/plugins/contracts/contract-assignments/ ContractAssignment
/api/plugins/contracts/contract-attachments/ ContractAttachment
/api/plugins/contracts/invoice-attachments/ InvoiceAttachment

Count annotations included in responses: contract_count (on ServiceProvider); invoice_count, assignment_count, attachment_count (on Contract); attachment_count (on Invoice).

GraphQL

All six models register in Nautobot's GraphQL schema automatically. Single-call cross-table queries:

{
  contracts {
    name
    end_date
    recurring_cost
    currency
    provider { name account_number }
    status { name }
  }
  service_providers {
    name
    contracts { name end_date }
  }
}

POST to /api/graphql/ with Authorization: Token <key> and Content-Type: application/json. The interactive GraphiQL explorer is at /graphql/.

The renewal-check Job

Check upcoming renewals (under the Contracts group):

  • Walks active contracts, finds rows whose end_date falls within window_days (default from PLUGINS_CONFIG.renewal_window_days)
  • Logs a per-contract entry — WARNING level for contracts expiring within 7 days, INFO otherwise
  • Returns the count, surfaced in the JobResult UI's "Result" field
  • Read-only: doesn't modify contracts
# Run via CLI
nautobot-server runjob "Contracts.RenewalCheckJob"

# Or via the API
curl -H "Authorization: Token $TOKEN" -X POST \
  "https://nautobot.example.com/api/extras/jobs/<job-uuid>/run/" \
  -d '{"data": {"window_days": 30, "include_expired": false}}' \
  -H "Content-Type: application/json"

Each per-contract log entry has the Contract attached as the JobLogEntry's object, so it shows up as a clickable link in the result UI. To route warnings into Slack/email/PagerDuty, configure a webhook on JobLogEntry creation in Apps → Webhooks.

Home dashboard panel

A "Contracts" panel appears on Nautobot's home page showing the next 10 contracts within renewal_window_days, ordered soonest first. Each row links to the contract detail page. The panel respects the user's view_contract permission and renders an empty-state message when there are no upcoming renewals.

Cost analytics

Contracts have a billing_period field (monthly, quarterly, semiannual, annual, one_time) so cost helpers can normalize across mixed billing cadences. Without it, a $1,200 annual contract and a $1,200 monthly contract are indistinguishable at the schema level — aggregating gives wrong answers.

The nautobot_contract_models.cost module exposes:

Helper Returns Purpose
monthly_cost(contract) Decimal recurring_cost normalized to a per-month figure
annual_cost(contract) Decimal monthly_cost × 12
total_contract_value(contract) Decimal monthly × term_months + one_time_cost
burn_rate_by_currency() dict[str, Decimal] sum of monthly_cost across active contracts, grouped by currency
renewal_cost_in_window(days) dict[str, Decimal] total contract value for end-dates falling in the window
spend_by_vendor(limit=10) list[(provider, monthly, currency)] top vendors by current monthly spend

Aggregations always group by Contract.currency — we do not do FX conversion in v1.

Two home dashboard panels surface the data: Cost Summary (current monthly burn per currency, annualized, top 5 vendors) and Renewal Forecast (renewal cost in 30/90/365-day windows).

The Monthly cost report Job (under the Contracts group) logs the same numbers to JobLogEntry. Schedule it weekly to get a cost trend in JobResult history without standing up a separate time-series store.

⚠️ Migration note for upgrading installs: migration 0007_contract_billing_period defaults every existing contract to billing_period='monthly'. If you have annual / quarterly contracts already in the database, edit them after upgrade — otherwise the burn-rate panels will over-count by 12x (annual) or 3x (quarterly).

Bulk CSV import

Migrating from a spreadsheet of existing contracts? Use the standard Nautobot import flow at Contracts → Contracts → Import (or visit /plugins/contracts/contracts/import/). Two tabs: paste CSV body, or upload a file. The page auto-generates a field-reference table — required vs optional, format hints (date format, FK-by-name lookup syntax, boolean literals).

FK lookups by natural key: provider=Acme Networks resolves the ServiceProvider by name; status=Active resolves the Status the same way. UUIDs also work.

A working sample lives at development/sample-data/contracts.csv — six representative rows covering hardware support, SaaS, a Microsoft EA, a multi-year warranty, mixed currencies, and every billing-period choice. See development/sample-data/README.md for format quirks.

Renewal Calendar

A dedicated /plugins/contracts/reports/renewal-calendar/ page renders a forward-looking, month-by-month grid of contract renewals (default 12 months, configurable up to 36). Cells encode total renewal value (recurring × term + one-time fees) with an amber saturation scale — pale wash for small months, saturated for the renewal cliff. Click any cell to drill into the contract list filtered to that month + currency.

Design notes:

  • Per-currency rows. No FX conversion. USD and EUR contracts appear on separate rows.
  • Single-hue scale. Amber lightness ramp; works in light and dark mode (the CSS swaps the lightness curve for dark backgrounds).
  • Accessibility. Real <table> semantics, screen-reader labels per cell, prefers-reduced-motion honored, focus-visible outlines.
  • Print-friendly. @media print strips colors and adds borders so procurement teams can take it to budget meetings.
  • Anchored to month boundaries. The window starts at the first of the current month, so partial-month renewals at the left edge aren't dropped.

Linked from the Contracts → Reports → Renewal Calendar nav menu.

Cost History

/plugins/contracts/reports/cost-history/ renders three time-series line charts (monthly burn, 90-day renewal forecast, active contract count), one line per currency, over a configurable window (4/12/26/52 weeks). Inline SVG — no JS chart library, prints natively.

Data comes from the CostSnapshot model. Schedule the Capture cost history snapshot Job weekly to feed the trend; on a fresh install the page renders an empty state pointing operators at the Job. The Detect cost anomalies Job (also under Contracts) compares this week's snapshots to a configurable baseline (default 4 weeks ago) and emits a WARNING-level JobLogEntry whenever burn rate or 90-day renewal forecast moves by more than threshold_pct (default 20%) per currency — wire a webhook to JobLogEntry creation to route into Slack/email/a ticket.

Snapshots are exposed via a read-only REST API at /api/plugins/contracts/cost-snapshots/ for external tooling (Grafana, BI dashboards). Filterable by snapshot_date__gte, snapshot_date__lte, and currency. Writes (POST/PATCH/DELETE) return 405 Method Not Allowed — snapshots are write-once historical facts, captured exclusively by the Job.

Notes

Every Contract, Invoice, ServiceProvider, ContractAssignment, and Attachment detail page exposes a Notes tab that supports Markdown — useful for renewal reminders, vendor escalation contacts, internal context that doesn't fit in the structured fields. Notes are framework-provided by Nautobot (no plugin code added); they persist across changelog/object updates and are attributed to the user who created them.

File attachments

Both Contract and Invoice support multiple file attachments (the upload field accepts any file type — typically PDF for invoices and signed contracts).

Files are stored under Nautobot's MEDIA_ROOT:

  • invoice_attachments/YYYY/MM/<filename>
  • contract_attachments/YYYY/MM/<filename>

Served at /media/invoice_attachments/... and /media/contract_attachments/.... The nautobot-media Docker volume persists files across container restarts.

⚠️ Production-deploy note: files are NOT included in DB dumps. Production deployments need a separate backup strategy for MEDIA_ROOT (or configure DEFAULT_FILE_STORAGE for S3/cloud storage and back that up via cloud-provider tooling).

UI walkthrough

After install, the Nautobot left sidebar gains a "Contracts" tab with four list views: Contracts, Invoices, Service Providers, Assignments. Each list view supports the standard Nautobot conventions:

  • Filtering, sorting, column toggling
  • Bulk edit / bulk delete
  • CSV import / export
  • Saved views (per-user filter sets)

Each detail page renders the model's fields, plus per-parent panels for child collections:

  • Contract detail → Invoices, Coverage (assignments), Attachments
  • Invoice detail → Attachments
  • ServiceProvider detail → Contracts

Each child panel has an "Add <child>" button that pre-populates the parent FK, so creating an invoice from a contract's detail page lands on the create form with the contract already selected.

Limitations

Honest about what v1 doesn't do:

  • Single currency per contract / invoice. Costs are stored as Decimal(12, 2) plus a currency ISO 4217 CharField. No FX conversion. Reports across mixed-currency contracts are the operator's problem.
  • No approval workflows for contract changes. Nautobot's general ApprovalQueue covers this case if you need it.
  • No external-system sync. Contract data lives in Nautobot's own DB. If you need to read contracts from Hudu / ConnectWise / Lansweeper, build a separate SSoT plugin that syncs into these models.
  • No per-line-item invoice breakdown. One Invoice row = one billing period. Use Nautobot custom fields or notes if you need finer granularity.
  • No multi-currency rate-tracking. Reporting contract value in a base currency means doing the math at query time.
  • File attachments are model-specificInvoiceAttachment and ContractAttachment are sibling models, not a generic GFK. Adding a third attachment type means duplicating the pattern (or refactoring to a GFK model). v1 follows netbox-contract's convention of separate models per parent.
  • Production media-volume backups are out of scope. See note above.

Development

cd development/
cp .env.example .env
$EDITOR .env
make build && make up

See development/README.md for the full bringup walkthrough, including the four known gotchas (COMPOSE_PROJECT_NAME collision, volume-permission first-boot fix, worker restarts after editing jobs.py, etc.).

License

TBD with the operator.

Acknowledgements

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nautobot_contract_models-2026.5.17.tar.gz (1.9 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nautobot_contract_models-2026.5.17-py3-none-any.whl (2.0 MB view details)

Uploaded Python 3

File details

Details for the file nautobot_contract_models-2026.5.17.tar.gz.

File metadata

  • Download URL: nautobot_contract_models-2026.5.17.tar.gz
  • Upload date:
  • Size: 1.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for nautobot_contract_models-2026.5.17.tar.gz
Algorithm Hash digest
SHA256 01028a3c4691100117ee16ac417efabd5286e4ddae016938e1dca183fdb2632e
MD5 11389640f0ad84db08cd3f9c6f65c013
BLAKE2b-256 acd04df69a771627516c9248dcab0265e16134cf471b17a496a1199da6dd8288

See more details on using hashes here.

File details

Details for the file nautobot_contract_models-2026.5.17-py3-none-any.whl.

File metadata

  • Download URL: nautobot_contract_models-2026.5.17-py3-none-any.whl
  • Upload date:
  • Size: 2.0 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for nautobot_contract_models-2026.5.17-py3-none-any.whl
Algorithm Hash digest
SHA256 c7f2f2d4ce24d6026604f900dd230073bd0f9db181c39736717f617e375c7d35
MD5 5596b0c475b7ea76147d76c43c48b897
BLAKE2b-256 d2364be6ac6b5d63b65f40d069f9f0255843e9cf241290fa4cb358e82fcdc8d8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page