Skip to content

Invoices, Estimates & Recurring Invoices

BottleCRM ships a lightweight billing layer covering one-off invoices, customer-facing estimates (quotes), and scheduled recurring invoices. All three share the same line-item model and PDF renderer.

Documents

Type Purpose Public URL
Estimate Pre-sale quote. Customer can review and accept yes
Invoice Bill for delivered work or goods yes
Recurring invoice A schedule that generates invoices automatically no

Estimates and invoices each get a tokenised public URL so a customer can view (and accept / pay) without a login.

Line items

Each document is a list of line items:

  • Item name and description
  • Quantity, unit, unit price
  • Tax rate (per line: useful for mixed tax jurisdictions)
  • Discount (% or flat)

On top of the lines, the document itself carries a discount (a percentage or a flat amount), a tax rate and, on an invoice, shipping. Every tax rate, on a line or on the document, is held to 0 to 100, and shipping cannot be negative; the API refuses anything else with a 400. From django-crm 1.11.0 the phone app's invoice, estimate and recurring forms set the document discount and tax rate (and shipping, on an invoice), as the web forms do, and the phone's invoice page shows the subtotal, discount, tax, shipping and total.

Totals (subtotal, tax, discount, grand total) are recomputed server-side on every save, never trust client-computed totals.

Statuses

  • Invoices: Draft, Sent, Viewed, Partially Paid, Paid, Overdue, Pending, Cancelled.
  • Estimates: Draft, Sent, Viewed, Accepted, Declined, Expired.

Sending a Draft moves it to Sent. A daily task marks unpaid invoices Overdue once the due date passes, and expires estimates past their expiry date.

Issued documents are locked

From django-crm 1.11.0, once an invoice or estimate leaves Draft its figures are fixed, because the customer has seen them. Its lines, discount, tax, shipping and currency, and so its totals, can no longer change, on the web, the phone or the API: an edit that tries is refused with a 400 saying the lines and amounts can only be changed while it is a Draft. Status, payments, notes, dates and the other fields stay editable. Sending the stored value back unchanged is not an edit, so a form that resends the whole document to change its notes still works.

Recurring invoices

A recurring invoice is a template plus a cadence:

  • Frequency: weekly, bi-weekly, monthly, quarterly, semi-annually, yearly, or a custom interval in days
  • Start date, next generation date and optional end date
  • Auto-send toggle

A Celery beat task runs daily at midnight and generates the invoices that are due. Each generated invoice inherits the schedule's line items, account and contact. With auto-send on it is created as Sent and emailed; otherwise it is created as a Draft.

Lines are optional on a schedule, but from django-crm 1.11.0 auto-send needs at least one line: turning it on for a schedule with no lines is a 400, so a customer is never emailed a blank invoice. A schedule saved before that rule with auto-send on and no lines raises a draft instead of sending one.

Creating an estimate

From django-crm 1.10.0 an estimate can be created on its own, with New estimate on the estimates list in the web app and the phone app, or POST /api/invoices/estimates/. Linking a deal is optional.

It can also be started from a deal: Create estimate on the deal's page, on the web or the phone, opens a new estimate with the deal's account, currency, contacts and line items filled in, all of it still editable before you save.

Won deal to invoice

A deal in a won stage (any stage whose kind is won, whatever it is called) has a Create invoice action on its page in the web app and the phone app. One click creates a draft invoice with the deal's line items, account, first contact, currency and assignees, dated today and due in 30 days. Tax is left at 0 for you to set. The endpoint is POST /api/invoices/from-opportunity/<deal-id>/, and it refuses a deal that is not won, has no line items, no account, or no contact you can see.

Converting an estimate to an invoice

POST /api/invoices/estimates/<id>/convert/ clones the estimate's line items into a new invoice, links the two for traceability, and refuses a second attempt once an estimate has already been converted. Note the invoices/ prefix: estimates are mounted under the invoices app rather than at their own top-level path.

Public actions

Customers can:

  • View an estimate or invoice at its tokenised public URL.
  • Accept an estimate.

There is no online payment step. BottleCRM has no payment-gateway integration and the public invoice page deliberately does not draw a "Pay now" button, so a customer pays you however they already do and you record the payment against the invoice yourself, in full or in parts.

See also