Feature Catalog
Everything in Quotery, organized by workflow stage. From client PDF to delivered order.
Capture — Turn client requests into structured quotes
AI reads client documents and matches every line to your product catalog. What used to take 20 minutes takes under 60 seconds.
AI Quote Import
3-call AI pipeline
SalesStructure extraction, product matching, and summary — three sequential AI calls in a single atomic transaction.
- Call A: Extracts groups, sections, and line items from unstructured text using AI structured outputs
- Call B: Batched parallel AI decisioning — up to 6 concurrent workers picking from 15-candidate shortlists
- Call C: Locale-aware summary banner rendered locally by default, AI only when opted in
Deterministic 4-code matching
SalesMatches supplier codes against SKU, import_code, internal_code, and export_code before any AI call. You only pay for the unknown portion.
- Case-sensitive exact match across all four product code columns
- Exact matches skip AI decisioning entirely — deterministic and free
- Match classification: exact_match (green), ai_decision (blue), not_found (rose)
- Hallucination guard: rejects model picks outside the presented shortlist
Multi-format ingestion
SalesAccepts PDF, XLSX, XLS, CSV, or pasted plain text — whatever your client sends.
- File upload up to 20 MB
- Pasted text from 10 to 100,000 characters
- Always lands as a draft quote for human review — never auto-sends
Product Catalog
Four parallel product codes
AdminSKU + import_code + internal_code + export_code — the AI import engine matches against all four.
- SKU unique within tenant, validated on creation
- All four code columns indexed for trigram search
- Codes feed the deterministic matching stage of AI import
Temporary product workflow
AdminSales reps create products on the fly. Quotes with unapproved products are blocked from sending until an admin approves.
- Temporary products forced to is_approved=false at the backend
- Quotes with unapproved products blocked from send, close, and PDF export
- Inline approval toggle for admins and managers
Deterministic product import
AdminImport product catalogs from Excel with row-by-row classification and resolution.
- Row classification: new, duplicate, duplicate SKU, invalid unit, invalid price, soft duplicate
- Bulk resolve-all controls with tabbed filtering
- Previews persist server-side for 24 hours
Quote — Polish, price, and send
A rich editor with drag-and-drop sections, back-solve pricing, and AI-drafted follow-ups.
Quote Editor & Lifecycle
Full lifecycle state machine
SalesDraft → Sent → Closed → Partially Delivered → Delivered, plus cancel and reopen branches.
- Draft and sent are the editable states — edits rejected after close with HTTP 409
- Cancel from closed releases all stock reservations
- Reopen from cancelled to draft or sent; reopen from closed releases reservations
- Partially delivered and delivered are terminal — must use return notes
Gapless Q-YYYY-NNNN numbering
SalesPer-tenant, per-year sequential numbering allocated under pessimistic lock.
- SELECT ... FOR UPDATE on QuoteNumberSequence table
- Guarantees no gaps and no duplicates within tenant/year
- Allocated inside the create transaction
Rich editor with back-solve pricing
SalesDrag-and-drop sections, keyboard shortcuts, and type-a-round-total discount calculation.
- Drag-and-drop reordering of lines and section headers
- Type a target subtotal or total — discount back-solves to 6 decimal places
- Per-line notes, description overrides, and section grouping
- Discount limits enforced per tenant — admins and managers exempt
Tenant-branded quote PDFs
SalesUS Letter PDF with logo, accent color, grouped line items, and localized labels in 3 languages.
- Tenant logo + accent color applied to hero band and totals card
- Lines grouped by section; unmatched import lines excluded
- Labels, dates, and money formatting localized per locale
- Blocked while any line references an unapproved temporary product
AI Follow-up Email Drafter
AI drafts follow-ups — you send
SalesLLM-drafted follow-up emails for sent quotes idle for 3+ days. You review, edit, and send. AI never sends without you.
- Available only on quotes in sent status idle for 3+ days
- PII minimized: prompt contains only customer display name, top 3 line items, total, validity date
- Confidence scoring: low for first-time customers, high otherwise
- Warning detection: screens for unauthorized discount promises, free delivery mentions
- Gated by tenant ai_followup_enabled flag
Close — Client accepts, stock reserves
Tokenized customer portal with e-signature, plus optional finance approval gate.
Customer Portal
Token-based anonymous access
Client reviews, accepts, or requests changes — no account, no password, no friction.
- Signed Django tokens with configurable expiry (default 30 days)
- Token revocation via soft-delete or expiration
- Cross-tenant lookups return 404, not 403
Electronic acceptance with SHA-256
Typed-name e-signature hashed with signer name, email, quote ID, and timestamp.
- SHA-256 hash binds the four canonical fields together
- Idempotent: duplicate accept returns existing row with already_accepted=true
- Acceptance transitions quote to closed, triggers stock reservation
- Audit event recorded with full metadata
Change requests & conversation thread
Client can request changes inline. Entire conversation — sales messages + client requests — shown chronologically.
- Free-text change request creates a QuoteChangeRequest row
- Sales rep notified via in-app + email
- Conversation panel renders WhatsApp-style above action buttons
Token-gated PDF download
Same branded PDF as the authenticated export, authorized solely by the portal token.
- Records audit event with actor=None, token_id, recipient_email, locale
- Unmatched import lines excluded; unapproved products block export
- Locale resolved from cookie → Accept-Language → en-US
Finance Stage Approval
Optional per-tenant approval gate
FinancePauses accepted quotes at ready_for_billing until finance confirms payment. Stock not reserved until paid.
- Gated by Tenant.requires_finance_approval flag (default off)
- Payment state machine: pending → paid / failed / cancelled
- Paid transition runs the standard close machinery (reserve stock + draft DN)
- Revert from paid allowed only when no DN has shipped
Client notifications & internal fan-out
FinanceFinance explicitly notifies the client; internal team notified on every transition.
- Client notification inserts append-only QuotePaymentNotification row
- Internal fan-out to sales owner, managers, and admins on every transition
- Customer portal banner driven by notifications, not raw payment status
Stock Reservation
Auto-reservation on close
Closing a quote atomically reserves stock. Over-reservation is allowed — shortages come back as a list, not an error.
- RESERVATION StockMovement rows written per tracked line
- StockItem rows locked with select_for_update in deterministic product order
- Over-reservation: close never returns 409 on insufficient stock
- Shortages array identifies product, location, and resulting available quantity
Deliver — Pick, pack, ship, and return
Delivery notes, mobile driver app, returns, receipts, and a full stock ledger.
Delivery Notes
Draft → Shipped → Delivered lifecycle
WarehouseAuto-generated on quote close. Manual creation from closed quotes. Rollover for partial deliveries.
- Auto-generated draft DN covers all trackable lines on quote close
- Per-line warehouse location — split one delivery across multiple warehouses
- Posting writes Delivery ledger entries and decrements on-hand
- Rollover DN auto-created for undelivered quantities after partial post
QR-coded delivery PDFs
WarehouseBranded PDFs with aisle locations, per-section subtotals, and scannable QR codes.
- QR code encodes delivery note number for mobile scanning
- Aisle resolved by (product, location, consigning supplier) triple
- Signature blocks for driver and receiving party
Mobile Driver App
Dedicated mobile surface at /delivery
DriverDrivers see only shipped and delivered notes — never drafts. QR scan, photo capture, one-tap confirmation.
- Access restricted to Driver group users only
- Hardened endpoint filters out all non-shipped delivery notes
- Up to 3 photos per delivery, 10 MB each, stored in application database
- Final confirmation transitions DN to Delivered, recomputes parent quote status
- Post-delivery notifications exclude the driver themselves
Returns & Stock Receipts
Return notes linked to deliveries
WarehouseTrack what comes back with reason and condition. Posting increments on-hand without touching reserved.
- Two creation modes: linked to delivery (pre-populated lines) or general (typeahead)
- Return reasons: Damaged, Excess, Refused by customer, Wrong item, Other
- Conditions: Resellable, Damaged, Inspect
- Posting writes Return ledger entries — parent quote status unaffected
Stock receipts with consigned flag
WarehouseReceive inventory from suppliers. Consigned flag tracks supplier-owned stock separately.
- Consigned stock toggle creates stock items keyed against the consigning supplier
- Posting writes Receipt ledger entries; cancellation writes Receipt reversal entries
- Unit cost captured on receipt — reserved for future costing features
- Inline quick-create supplier from the receipt form
Stock Movements Ledger
Append-only ledger
Warehouse7 movement kinds. Every change in on-hand or reserved stock recorded. No modify, no delete — even for admins.
- Movement kinds: Receipt, Receipt reversal, Reservation, Release, Delivery, Return, Adjustment
- Each row references its source document (quote, DN, RN, SR)
- Filterable by product, location, movement kind, and date range
- Exportable as branded PDF
Warehouse Hub
Aggregate operational dashboard at /warehouse
WarehouseSingle-screen overview of all in-flight fulfillment work — deliveries, returns, receipts — with KPI counters and merged activity feed.
- 3 KPI counter cards: drafts and posted counts for deliveries, returns, receipts
- Merged activity feed interleaving all three document types by updated_at
- Side links to stock overview, consigned stock, and movements ledger
- Client-side only — introduces no new endpoints or mutations
Know — Ask questions, get insights
AI-powered conversational Q&A, daily briefs, dashboards, reports, and notifications.
Conversational AI Ask
Natural language Q&A in 3 languages
SalesAsk about quotes, clients, stock, and operations in English, Portuguese, or Spanish. Gets structured answers with charts and tables.
- Metrics: count_quotes, sum_revenue, count_clients, avg_cycle_time_days
- Buckets: client, user (sales rep), month, status
- Deterministic stock branch handles inventory prompts without LLM call
- Refusal escape hatch: model declines out-of-scope questions rather than guessing
- Per-user scoping: commercial users see only their own data with a banner
AI Daily Brief
Personalized action cards every morning
SalesOnce-per-day AI-generated summary of what needs your attention. Role-aware — different cards for sales, managers, and admins.
- Generated once per user per day — unique constraint on (tenant, user, date)
- Role-aware prompt: commercial gets personal CTAs, managers get team CTAs
- Card types: follow_up, stale_draft, reorder_opportunity, recent_win, quote_risk, and more
- Structured cards with priority levels, icon kind, insight text, and CTA buttons
- Capped at 8 cards max with validation and sanitization
Reporting & Dashboards
5 dashboard panels
ManagerPipeline by status, aging distribution, top clients, top reps, and conversion rate — all in one API call.
- Single GET /api/reports/dashboard/ returns all 5 panels
- Window parameter: 30d, 90d, or ytd (default 90d)
- Redis-cached per tenant per window, auto-rotated at UTC day boundary
- Quotes-on-map widget with client-side ZIP centroid lookup
7 quote report modes
ManagerRegister, aging, expiring, follow-up, win-loss, discounts, and AI-imports — each with structured filters and PDF export.
- Structured filters: status tokens, owner UUIDs, date pickers, money formatting, discount percentages
- Free-text search debounced at 3-character minimum
- PDF exports the full filtered dataset, not just the current page
6 stock report modes
WarehouseCurrent, exceptions, consigned, movements, depletion velocity, and predicted demand — with analytical methodology sections.
- Depletion velocity: at-risk highlight when ≤30 days of supply remain
- Predicted demand: positive shortfall highlight, insufficient-data envelope below 30 quotes/year
- Entity filters validated as UUIDs before queryset evaluation
Streaming CSV exports
ManagerSingle quote, dashboard, and product usage CSVs — streamed row-by-row, never held in memory.
- Uses Django StreamingHttpResponse with generator yielding one row at a time
- Constant memory regardless of dataset size — safe for 50,000+ row exports
Notifications
In-app + email, audience-aware
Quote accepted, change requested, delivery completed, consigned stock moved, stock available again — each with the right audience and per-tenant toggles.
- In-app feed with unread badge, mark-read individually or in bulk
- Transactional email with HTML + auto-projected plain-text part
- Audience-aware fan-out: supervisors, warehouse, sales rep each get different CTAs
- Stock-available detector: re-checks all open quotes after any on-hand increase
- Deduplication key prevents repeat notifications for the same event
Foundation — Multi-tenancy, security, and platform infrastructure
The architectural bedrock that makes everything above possible.
Multi-Tenancy
Strict tenant isolation
Every database query carries a mandatory tenant filter. Cross-tenant lookups return 404, not 403.
- Tenant ID injected from authenticated session — never from request payload
- Cross-tenant lookups indistinguishable from not-found to prevent inference
- Per-tenant configuration: branding, seats, tax, discount limits, notification toggles, document text
Authentication
Google Workspace + Microsoft 365 OAuth
Domain-gated sign-in with automatic user provisioning. Consumer domains rejected explicitly.
- Only verified email addresses from authorized tenant domains permitted
- First sign-in: auto-provisions user with profile photo from provider
- Subsequent sign-ins: reuse existing record, refresh profile photo only
Email/password with same-domain joining
Password auth for non-SSO tenants. Same-domain signups join existing tenant automatically.
- Signup with unclaimed domain creates new tenant + first admin
- Signup with claimed non-SSO domain joins as sales rep (seat capacity enforced)
- SSO-enforced tenants reject password signup with clear error
Team invitations with seat enforcement
Admins and managers invite by email. Tokens expire in 7 days. Seat capacity checked at invite, accept, and reactivation.
- Invitee receives one-time link, sets password, assigned to inviter-chosen role
- Pending invitations show alongside active users with resend/revoke controls
- Concurrent session limit: 2 active sessions per user (configurable per subscriber)
Team & Roles
48 permission codes, 5 standard groups
Admin, Manager, Sales Rep, Warehouse Operator, and Driver — each pre-configured with the right permissions for their job.
- Standard groups are read-only and enforced at the backend
- Custom groups composed freely from the 48-code catalog via checkbox grid
- Permissions organized by area: users, clients, quotes, inventory, products, reporting, fulfillment, driver, suppliers, audit, AI imports
Audit Trail
Append-only audit of every material action
Audit rows written in the same transaction as the action. Deletion of audited entities nulls FKs but preserves the event payload.
- Free-text search across metadata with 300ms debounce
- Multi-select filtering by action, target type, and actor
- Per-row expandable metadata with Copy control
- No admin interface for modification or deletion of audit rows
Internationalization
3 languages, full coverage
en-US, pt-BR, and es-US across UI, transactional emails, and PDF documents. Consistent terms across all surfaces.
- Every visible string, toast, error, and label translated
- Transactional emails with localized CTA buttons
- PDF documents: labels, dates, and money formatting localized
- Language cascade: cookie → browser Accept-Language → en-US fallback
- Authenticated users: saved preference overrides cookie and browser
Billing
Stripe subscription management
Plans, seats, and billing intervals managed via Stripe. DB is source of truth — Stripe reconciled to match.
- One Stripe Product per Plan, one or two Stripe Prices per PlanPrice
- Immutable Stripe Prices: changing amount creates new Price, archives old one
- Append-only PlanPriceHistory ledger of archived Stripe Price IDs
- Cycle-end change application: downgrades, seat reductions, interval switches applied at period end
Security & Performance
Defense-in-depth controls
Cache discipline, throttling, database indexing, concurrency discipline, and query budget enforcement.
- Cache: every key tenant-namespaced, invalidation in same atomic transaction, Redis outage non-fatal
- Throttling: public endpoints rate-limited per IP, Ask endpoint 20 req/min per user
- Trigram GIN indexes on all searchable text fields; composite indexes on common filter combinations
- Pessimistic locks acquired in deterministic order for deadlock-free concurrency
- Regression test suite enforces per-route query budget to prevent N+1 regressions