### Manage Subscriptions: Start, Invoice, and Cron Source: https://context7.com/oca/contract/llms.txt Activates a subscription, allowing manual invoice generation or automated processing via the daily cron job. The cron handles starting due subscriptions, invoicing, and closing expired ones. ```python # Activate the subscription subscription.action_start_subscription() print(f"Stage type: {subscription.stage_type}") # "in_progress" print(f"Next invoice: {subscription.recurring_next_date}") ``` ```python # Manually generate invoice subscription.generate_invoice() for inv in subscription.invoice_ids: print(f"{inv.name}: {inv.state} — {inv.amount_total}") # SINV/2024/00001: posted — 9.99 ``` ```python # Run the daily cron (starts due, invoices due, closes expired) env["sale.subscription"].cron_subscription_management() ``` -------------------------------- ### Configure Product for Invoice Dates Source: https://context7.com/oca/contract/llms.txt Ensure a product has 'must_have_dates' set to True to propagate billing period start and end dates from contract lines to invoice lines. ```python # Product must have "must_have_dates" set to True product = env["product.product"].browse(line.product_id.id) product.must_have_dates = True # Invoice line will then include start_date and end_date invoice_line_vals = line._prepare_invoice_line() print(invoice_line_vals.get("start_date")) # e.g. date(2024, 4, 1) print(invoice_line_vals.get("end_date")) # e.g. date(2024, 4, 30) # Useful for deferred revenue and period-based accounting ``` -------------------------------- ### Create a Monthly SaaS Contract Template Source: https://context7.com/oca/contract/llms.txt Example of creating a `contract.template` record for a monthly SaaS subscription. This includes defining recurrence, invoicing type, and line items with specific products and pricing. ```python # Create a monthly SaaS contract template template = env["contract.template"].create({ "name": "Monthly SaaS Subscription", "contract_type": "sale", "recurring_rule_type": "monthly", "recurring_interval": 1, "recurring_invoicing_type": "pre-paid", "line_recurrence": False, # recurrence controlled at header level "company_id": env.company.id, "contract_line_ids": [ (0, 0, { "product_id": env.ref("product.product_product_4").id, "name": "SaaS Platform License", "quantity": 1.0, "specific_price": 99.00, "discount": 0.0, "recurring_rule_type": "monthly", "recurring_interval": 1, "recurring_invoicing_type": "pre-paid", "date_start": "2024-01-01", }), (0, 0, { "display_type": "line_section", "name": "Add-ons", }), (0, 0, { "product_id": env.ref("product.product_product_5").id, "name": "Storage Add-on (100GB)", "quantity": 2.0, "specific_price": 9.50, }), ], }) ``` -------------------------------- ### Plan a Successor Contract Line Source: https://context7.com/oca/contract/llms.txt Plans a successor for a contract line, typically for upgrades or renewals. Specify the start and end dates, auto-renewal status, and the next recurring date. ```python successor = line.plan_successor( date_start=date(2024, 7, 1), date_end=date(2025, 6, 30), is_auto_renew=True, recurring_next_date=date(2024, 7, 1), post_message=True, ) print(f"Successor state: {successor.state}") # "upcoming" print(f"Predecessor: {successor.predecessor_contract_line_id.id}") # original line id ``` -------------------------------- ### Calculate Next Period End Date with contract.recurring.mixin Source: https://context7.com/oca/contract/llms.txt Illustrates the use of `get_next_period_date_end` from `contract.recurring.mixin` to calculate the end date of a billing period, factoring in start dates, recurrence rules, and invoicing types. ```python from datetime import date ContractLine = self.env["contract.line"] period_end = ContractLine.get_next_period_date_end( next_period_date_start=date(2024, 1, 1), recurring_rule_type="monthly", recurring_interval=1, max_date_end=False, next_invoice_date=date(2024, 1, 1), recurring_invoicing_type="pre-paid", ) # Returns: date(2024, 1, 31) ``` -------------------------------- ### Configure Product as Contract Product Source: https://context7.com/oca/contract/llms.txt Mark a product as a contract product and set its recurring and renewal properties. This allows contracts to be generated directly from sale orders. ```python # Mark a product as a contract product product = env["product.template"].browse(product_tmpl_id) product.write({ "is_contract": True, "type": "service", "property_contract_template_id": template.id, "recurring_rule_type": "monthly", "recurring_interval": 1, "recurring_invoicing_type": "pre-paid", "is_auto_renew": True, "auto_renew_rule_type": "yearly", "auto_renew_interval": 1, "termination_notice_interval": 30, "termination_notice_rule_type": "daily", "contract_start_date_method": "start_next", # start at beginning of next month "recurrence_number": 12, # 12-month minimum term "recurrence_interval": "monthly", }) # Create a sale order with a contract product order = env["sale.order"].create({ "partner_id": partner.id, "order_line": [(0, 0, { "product_id": product.product_variant_ids[:1].id, "product_uom_qty": 1, "price_unit": 99.00, })], }) order.action_confirm() # If company.create_contract_at_sale_order = True, contract is created immediately # Or create contract manually action = order.action_create_contract() print(f"Contracts created: {order.contract_count}") # Inspect the contract line from the sale order line sol = order.order_line[:1] print(f"Is contract: {sol.is_contract}") print(f"Contract: {sol.contract_id.name}") print(f"Date start: {sol.date_start}") print(f"Date end: {sol.date_end}") ``` -------------------------------- ### Use Line Lifecycle Wizard Source: https://context7.com/oca/contract/llms.txt Initiates contract line lifecycle actions through a UI wizard. Create a wizard record with the relevant contract line ID and desired parameters, then call the appropriate wizard method. ```python wizard = env["contract.line.wizard"].create({ "contract_line_id": line.id, "date_end": "2024-06-30", "is_auto_renew": True, "date_start": "2024-07-01", "recurring_next_date": "2024-07-01", }) # Depending on the button clicked in the view: wizard.stop() # stop with date_end wizard.plan_successor() # stop + create next line wizard.stop_plan_successor()# suspend + plan restart wizard.uncancel() # reverse a cancellation ``` -------------------------------- ### Configure and Generate Contract Forecast Periods Source: https://context7.com/oca/contract/llms.txt Enable `enable_contract_forecast` and configure the forecast horizon (type and interval) at the company level. Then, trigger the generation of forecast periods for contract lines. These periods can be queried or viewed in a pivot table. ```python # Configure forecast horizon on company env.company.enable_contract_forecast = True env.company.contract_forecast_rule_type = "monthly" env.company.contract_forecast_interval = 12 # 12-month rolling horizon # Generate forecasts for a contract (normally triggered via queue_job) for line in contract.contract_line_ids: line._generate_forecast_periods() # Query forecast periods forecasts = env["contract.line.forecast.period"].search([ ("contract_id", "=", contract.id), ]) for f in forecasts: print( f"{f.date_start} → {f.date_end}: " f"{f.quantity} × {f.price_unit} = {f.price_subtotal} {f.currency_id.name}" ) # 2024-04-01 → 2024-04-30: 10.0 × 150.0 = 1500.0 EUR # 2024-05-01 → 2024-05-31: 10.0 × 150.0 = 1500.0 EUR # ... # Show forecast pivot view from contract action = contract.action_show_contract_forecast() ``` -------------------------------- ### Automated Invoice Creation via Cron Source: https://context7.com/oca/contract/llms.txt Entry point for the scheduled daily invoice run. Processes all contracts due for invoicing. Can be called manually for testing or backfilling. ```python # Called by Odoo cron (ir.cron) — also callable manually for testing/backfill Contract = env["contract.contract"] # Run invoice creation for today Contract.cron_recurring_create_invoice() # Run invoice creation for a specific past date (backfill) from datetime import date Contract.cron_recurring_create_invoice(date_ref=date(2024, 3, 31)) # Underlying domain used to find eligible contracts domain = Contract._get_contracts_to_invoice_domain(date_ref=date(2024, 3, 31)) contracts_due = Contract.search(domain) print(f"Contracts to invoice: {len(contracts_due)}") # Full low-level flow (what the cron does internally) for contract in contracts_due: invoices_values = contract._prepare_recurring_invoices_values(date_ref=date(2024, 3, 31)) invoices = env["account.move"].create(invoices_values) contract._invoice_followers(invoices) contract._add_contract_origin(invoices) ``` -------------------------------- ### Generate Manual Invoice from Subscription Source: https://context7.com/oca/contract/llms.txt Shows how to trigger a manual invoice generation for a subscription. This action typically opens a draft account move form view in the UI. ```python action = subscription.manual_invoice() # Returns: window action opening a draft account.move form view ``` -------------------------------- ### Manual Invoice Wizard Simulation Source: https://context7.com/oca/contract/llms.txt Simulates the UI wizard for operators to generate invoices for a chosen date across all eligible contracts. Handles errors gracefully by continuing with other contracts. ```python # Simulate what the UI wizard does wizard = env["contract.manually.create.invoice"].create({ "invoice_date": "2024-04-01", "contract_type": "sale", }) # See which contracts will be invoiced print(f"Contracts to invoice: {wizard.contract_to_invoice_count}") # e.g. Contracts to invoice: 12 # Create the invoices result = wizard.create_invoice() # Returns: window action opening the created account.move records # On error in any single contract, continues and returns partial results # Access contracts directly for c in wizard.contract_to_invoice_ids: print(f" - {c.name} (next date: {c.recurring_next_date})") ``` -------------------------------- ### Create a New Contract from Template Source: https://context7.com/oca/contract/llms.txt Applies a contract template to create a new contract. The template population is triggered manually. ```python contract = env["contract.contract"].create({ "name": "ACME Corp — SaaS", "partner_id": env.ref("base.res_partner_2").id, "contract_template_id": template.id, }) # Trigger template population (normally fired by UI onchange) contract._onchange_contract_template_id() ``` -------------------------------- ### Create Subscription Template and Subscription Source: https://context7.com/oca/contract/llms.txt Defines a subscription template with recurring invoicing details and creates a subscription for a partner. Supports various invoicing modes and mail templates. ```python # Create a subscription template template = env["sale.subscription.template"].create({ "name": "Monthly Newsletter", "recurring_interval": 1, "recurring_rule_type": "months", "recurring_rule_boundary": "limited", "recurring_rule_count": 12, "invoicing_mode": "invoice", # draft | invoice | invoice_send | sale_and_invoice "invoice_mail_template_id": env.ref("account.email_template_ediinvoice").id, }) ``` ```python # Create a subscription subscription = env["sale.subscription"].create({ "template_id": template.id, "partner_id": partner.id, "date_start": "2024-01-01", "sale_subscription_line_ids": [(0, 0, { "product_id": product.id, "name": "Monthly Newsletter Access", "product_uom_qty": 1.0, "price_unit": 9.99, })], }) ``` -------------------------------- ### Create a Contract with Explicit Lines Source: https://context7.com/oca/contract/llms.txt Creates a contract with detailed line items, specifying recurrence, payment terms, and initial invoice details. Ensure partner and product IDs are valid. ```python # Create a contract with explicit lines contract = env["contract.contract"].create({ "name": "Enterprise Support Contract", "code": "SUP-2024-001", "partner_id": partner.id, "invoice_partner_id": partner.child_ids[:1].id, # different billing contact "contract_type": "sale", "recurring_rule_type": "monthly", "recurring_interval": 1, "recurring_invoicing_type": "pre-paid", "date_start": "2024-01-01", "payment_term_id": env.ref("account.account_payment_term_30days").id, "contract_line_ids": [ (0, 0, { "product_id": support_product.id, "name": "Support Hours", "quantity": 10.0, "specific_price": 150.00, "date_start": "2024-01-01", "recurring_next_date": "2024-01-01", }) ], }) # Manually trigger invoice creation for a specific date contract.recurring_create_invoice() # Check generated invoices print(f"Invoice count: {contract.invoice_count}") invoices = contract._get_related_invoices() for inv in invoices: print(f" {inv.name}: {inv.amount_total} {inv.currency_id.name} — {inv.state}") # Output: # Invoice count: 1 # INV/2024/00001: 1500.0 EUR — posted # Show invoices action (returns window action dict) action = contract.action_show_invoices() # Portal URL print(contract._compute_access_url()) # /my/contracts/42 ``` -------------------------------- ### Close Subscription with Reason Source: https://context7.com/oca/contract/llms.txt Demonstrates how to close a subscription by providing a specific reason. The `close_reason_id` parameter is used to link the reason to the subscription closure. ```python close_reason = env["sale.subscription.close.reason"].create({ "name": "Subscription Expired" }) subscription.close_subscription(close_reason_id=close_reason.id) print(f"Stage type: {subscription.stage_type}") # "post" ``` -------------------------------- ### Enable and Trigger Refund on Stop for Contract Lines Source: https://context7.com/oca/contract/llms.txt Configure the company setting `enable_contract_line_refund_on_stop` to automatically generate a credit note when a contract line is stopped before its already-invoiced period ends. The refund amount is prorated based on the remaining period. ```python from datetime import date # Suppose line was invoiced through 2024-03-31 but customer wants to stop on 2024-03-15 line = contract.contract_line_ids[:1] print(f"Last invoiced: {line.last_date_invoiced}") # 2024-03-31 # Enable refund on stop at company level env.company.enable_contract_line_refund_on_stop = True # Stop the line mid-period — refund is created automatically line.stop(date_end=date(2024, 3, 15)) # A credit note is automatically created for 2024-03-16 → 2024-03-31 refund_moves = env["account.move"].search([ ("move_type", "=", "out_refund"), ("invoice_line_ids.contract_line_id", "=", line.id), ]) print(f"Refund amount: {refund_moves[:1].amount_total}") # e.g. Refund amount: 750.0 (15 days prorated out of 1500) ``` -------------------------------- ### Prepare Invoice Line Dictionary Source: https://context7.com/oca/contract/llms.txt Prepares the dictionary of values for an invoice line based on the contract line's current billing state and recurrence. Used internally by the invoicing process. ```python # Prepare the invoice line dict (used internally by _prepare_recurring_invoices_values) period_start, period_end, invoice_date = line._get_period_to_invoice( last_date_invoiced=line.last_date_invoiced, recurring_next_date=line.recurring_next_date, ) invoice_line_vals = line._prepare_invoice_line() print(invoice_line_vals) # { # "product_id": 42, # "name": "Support Hours [01/01/2024 - 01/31/2024]", # "quantity": 10.0, # "price_unit": 150.0, # "discount": 0.0, # "product_uom_id": 1, # "analytic_distribution": {"100": 100.0}, # "contract_line_id": 7, # } ``` -------------------------------- ### Calculate Next Invoice Date with contract.recurring.mixin Source: https://context7.com/oca/contract/llms.txt Shows how to determine the next invoice date using `get_next_invoice_date` from `contract.recurring.mixin`, considering pre-paid and post-paid invoicing types, offsets, and maximum end dates. ```python from datetime import date ContractLine = self.env["contract.line"] # Pre-paid monthly: invoice date = period start next_invoice_date = ContractLine.get_next_invoice_date( next_period_date_start=date(2024, 2, 1), recurring_invoicing_type="pre-paid", recurring_invoicing_offset=0, recurring_rule_type="monthly", recurring_interval=1, max_date_end=False, ) # Returns: date(2024, 2, 1) # Post-paid monthly: invoice date = period start + 1 day offset next_invoice_date_post = ContractLine.get_next_invoice_date( next_period_date_start=date(2024, 2, 1), recurring_invoicing_type="post-paid", recurring_invoicing_offset=1, recurring_rule_type="monthly", recurring_interval=1, max_date_end=date(2024, 2, 29), ) # Returns: date(2024, 3, 1) ``` -------------------------------- ### Calculate Relative Date Deltas with contract.recurring.mixin Source: https://context7.com/oca/contract/llms.txt Demonstrates how to use the `get_relative_delta` method from `contract.recurring.mixin` to calculate date differences for various recurrence types like monthly, quarterly, and yearly. ```python from odoo.tests.common import TransactionCase class TestRecurringMixin(TransactionCase): def test_get_relative_delta(self): ContractLine = self.env["contract.line"] # Get relative delta for a monthly rule delta = ContractLine.get_relative_delta("monthly", 1) # Returns: relativedelta(months=+1) delta_quarterly = ContractLine.get_relative_delta("quarterly", 1) # Returns: relativedelta(months=+3) delta_yearly = ContractLine.get_relative_delta("yearly", 2) # Returns: relativedelta(years=+2) ``` -------------------------------- ### Enable Queue Job for Contract Invoice Generation Source: https://context7.com/oca/contract/llms.txt Set the system parameter `contract.queue.job` to `True` to enable queue-based invoice generation. This dispatches each contract's invoice creation as an independent queue job, allowing for parallel execution when processing multiple contracts. ```python # Enable queue-based invoice generation (system parameter) env["ir.config_parameter"].sudo().set_param("contract.queue.job", "True") # When processing multiple contracts, each is dispatched as a separate job contracts = env["contract.contract"].search([ ("recurring_next_date", "<=", fields.Date.today()), ]) # With queue_job enabled and len(contracts) > 1: # Each contract is queued independently via with_delay()._recurring_create_invoice() contracts._recurring_create_invoice() # Manual invoice wizard also gets a queued variant wizard = env["contract.manually.create.invoice"].create({ "invoice_date": fields.Date.today(), }) wizard.create_invoice_queued() # non-blocking, returns immediately ``` -------------------------------- ### Create and Apply Price Revision Wizard Source: https://context7.com/oca/contract/llms.txt Instantiate the `contract.price.revision.wizard` to automatically revise prices of contract lines. Specify the revision type (percentage or fixed) and the variation amount. The wizard can then be used to preview changes and apply them, creating successor lines. ```python # Create price revision wizard on a contract wizard = env["contract.price.revision.wizard"].with_context( active_model="contract.contract", active_ids=contract.ids, ).create({ "date_start": "2024-04-01", "date_end": "2025-03-31", "variation_type": "percentage", # or "fixed" "variation_percent": 5.0, # +5% price increase # "fixed_price": 110.00, # used when variation_type == "fixed" }) # Check which lines will be revised lines_to_revise = wizard._get_contract_lines_to_revise(contract) for l in lines_to_revise: new_price = wizard._get_new_price(l) print(f" {l.name}: {l.price_unit} → {new_price}") # Output: # Support Hours: 150.0 → 157.5 # Apply: stops each old line, creates new line with revised price wizard.action_apply() # Inspect result revised_line = contract.contract_line_ids.filtered( lambda l: l.predecessor_contract_line_id )[:1] print(f"New price: {revised_line.price_unit}") # 157.5 print(f"Variation %: {revised_line.variation_percent}") # 5.0 ``` -------------------------------- ### Propagate Banking Mandate to Invoices Source: https://context7.com/oca/contract/llms.txt Set a banking mandate on a contract and verify its propagation to generated invoices. The mandate is added to invoice values via `_prepare_invoice` override. ```python # Set a mandate on the contract contract.mandate_id = env["account.banking.mandate"].search([ ("partner_id", "=", contract.partner_id.id), ("state", "=", "valid"), ], limit=1) # The mandate is added to invoice vals via _prepare_invoice override invoice_vals = contract._prepare_invoice(date_invoice=date(2024, 4, 1)) print(invoice_vals["mandate_id"]) # e.g. 3 # Verify on generated invoice inv = contract._recurring_create_invoice()[0] print(inv.mandate_id.unique_mandate_reference) # e.g. "MANDATE-20240101-001" ``` -------------------------------- ### Generate Portal Access Link for Contract Source: https://context7.com/oca/contract/llms.txt Generates a customer-facing portal access link for a specific contract. The portal supports paginated lists and detail views, with optional access tokens for direct access. ```python # Example: generate a portal access link for a customer contract_url = contract._notify_get_action_link("view") print(contract_url) # https://yourodoo.com/my/contracts/42?access_token=abc123... ``` -------------------------------- ### Enable Sale Invoicing on Contract Source: https://context7.com/oca/contract/llms.txt Configures contracts to merge uninvoiced sale order lines into the contract's invoice run using the analytic account matching. When `invoicing_sales_into_contract` is false, separate invoices are generated for each sale order. ```python # Enable sale invoicing on contract contract.invoicing_sales = True contract.invoicing_sales_into_contract = True # merge into contract invoice # During the recurring invoice cron, uninvoiced SO lines matching # the contract's analytic group are collected and merged contract._recurring_create_invoice() ``` ```python # With invoicing_sales_into_contract = False, separate invoices are generated contract.invoicing_sales_into_contract = False contract._recurring_create_invoice() # Result: 1 contract invoice + N sale-order invoices ```