Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 40 additions & 23 deletions subscription_oca/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Subscription management
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ea96c858a40ee527d0e32ee03859918c082521f205ccc1cb26e96cb3bef27800
!! source digest: sha256:1d7163cf2d7da35b3460a208800f960258c0e321470ab537d8aee3c7dfa6197e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
Expand All @@ -36,6 +36,9 @@ This module allows creating subscriptions that generate recurring
invoices or orders. It also enables the sale of products that generate
subscriptions.

Subscriptions can be renewed into a linked child subscription, keeping
the genealogy between the original contract and its renewals.

**Table of contents**

.. contents::
Expand Down Expand Up @@ -74,21 +77,35 @@ To create subscriptions with the sale of a product:

Invoice and delivery addresses:

- Each subscription has an *Invoice address* and a *Delivery address*.
They default to the customer's corresponding addresses and can be
overridden per subscription.
- These addresses are propagated to the recurring invoices (the invoice
is addressed to the invoice address and records the delivery one) and
to the sale orders generated by the subscription.
- The fields are shown when the *Customer Addresses* setting (group
*Display Delivery / Invoice addresses*) is enabled.
- Each subscription has an *Invoice address* and a *Delivery address*.
They default to the customer's corresponding addresses and can be
overridden per subscription.
- These addresses are propagated to the recurring invoices (the invoice
is addressed to the invoice address and records the delivery one) and
to the sale orders generated by the subscription.
- The fields are shown when the *Customer Addresses* setting (group
*Display Delivery / Invoice addresses*) is enabled.

To renew a subscription:

1. On an in-progress subscription, press *Renew*. A renewal quote is
created as a child subscription in the *pre* stage, copying the
template, lines, pricelist, fiscal position, partner and journal. Its
start date is the finish date of the current subscription (or its
next invoice date when there is no finish date).
2. The *Renew* button is hidden while a renewal quote is still open.
3. When the renewal is started, the original subscription is closed
automatically and a link to the renewal is posted on its chatter.
4. Use the *Parent contract* and *Renewals* smart buttons to navigate
the renewal chain; *origin_subscription_id* always points at the
first contract of the chain.

Known issues / Roadmap
======================

- Refactor all the onchanges that have business logic to computed
write-able fields when possible. Keep onchanges only for UI purposes.
- Add tests.
- Refactor all the onchanges that have business logic to computed
write-able fields when possible. Keep onchanges only for UI purposes.
- Add tests.

Bug Tracker
===========
Expand All @@ -112,22 +129,22 @@ Authors
Contributors
------------

- Carlos Martínez <carlos@domatix.com>
- Carolina Ferrer <carolina@domatix.com>
- `Ooops404 <https://www.ooops404.com>`__:
- Carlos Martínez <carlos@domatix.com>
- Carolina Ferrer <carolina@domatix.com>
- `Ooops404 <https://www.ooops404.com>`__:

- Ilyas <irazor147@gmail.com>
- Ilyas <irazor147@gmail.com>

- `Sygel <https://www.sygel.es>`__:
- `Sygel <https://www.sygel.es>`__:

- Harald Panten
- Valentin Vinagre
- Alberto Martínez
- Harald Panten
- Valentin Vinagre
- Alberto Martínez

- Dennis Sluijk <d.sluijk@onestein.nl>
- `IKU Solutions <https://www.iku.solutions>`__:
- Dennis Sluijk <d.sluijk@onestein.nl>
- `IKU Solutions <https://www.iku.solutions>`__:

- Yan Chirino <yan.chirino@iku.solutions>
- Yan Chirino <yan.chirino@iku.solutions>

Maintainers
-----------
Expand Down
180 changes: 179 additions & 1 deletion subscription_oca/models/sale_subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from markupsafe import Markup

from odoo import Command, api, fields, models
from odoo.exceptions import AccessError
from odoo.exceptions import AccessError, UserError

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -153,6 +153,36 @@ def _read_group_stage_ids(self, stages, domain):
)
crm_team_id = fields.Many2one(comodel_name="crm.team", string="Sale team")
to_renew = fields.Boolean(default=False, string="To renew")
parent_subscription_id = fields.Many2one(
comodel_name="sale.subscription",
string="Parent contract",
index=True,
ondelete="restrict",
copy=False,
)
origin_subscription_id = fields.Many2one(
comodel_name="sale.subscription",
string="Original contract",
compute="_compute_origin_subscription_id",
store=True,
recursive=True,
index=True,
)
child_subscription_ids = fields.One2many(
comodel_name="sale.subscription",
inverse_name="parent_subscription_id",
string="Renewals",
)
renewal_count = fields.Integer(
compute="_compute_renewal_count", string="Renewal count"
)
is_renewed = fields.Boolean(
compute="_compute_is_renewed",
store=True,
index="btree_not_null",
help="True when this subscription has at least one child that is "
"not closed yet — its renewal is active or pending.",
)

@api.model
def cron_subscription_management(self):
Expand Down Expand Up @@ -314,6 +344,131 @@ def action_start_subscription(self):
[("type", "=", "in_progress")], limit=1
)
self.stage_id = in_progress_stage
for subscription in self:
parent = subscription.parent_subscription_id
if parent:
parent.with_company(parent.company_id)._handle_renewal_activation(
child=subscription
)

def _handle_renewal_activation(self, child):
self.ensure_one()
if self.stage_id.type == "post":
return
close_reason_id = self.env.context.get("default_close_reason_id", False)
self.close_subscription(close_reason_id=close_reason_id)
link = Markup(
'<a href=# data-oe-model=sale.subscription data-oe-id="{cid}">{cname}</a>'
).format(cid=child.id, cname=child.display_name or "")
self.message_post(
body=self.env._("Renewal %(link)s has been activated.", link=link)
)

def _prepare_renewal_lines(self):
self.ensure_one()
commands = []
for line in self.sale_subscription_line_ids:
commands.append(
Command.create(
{
"product_id": line.product_id.id,
"name": line.name,
"product_uom_qty": line.product_uom_qty,
"price_unit": line.price_unit,
"discount": line.discount,
"tax_ids": [Command.set(line.tax_ids.ids)],
"analytic_distribution": line.analytic_distribution,
}
)
)
return commands

def _prepare_renewal_values(self):
self.ensure_one()
renewal_start = self.date or self.recurring_next_date or fields.Date.today()
pre_stage = self.env["sale.subscription.stage"].search(
[("type", "=", "pre")], order="sequence", limit=1
)
if not pre_stage:
raise UserError(
self.env._(
"Cannot prepare a renewal: there is no subscription "
"stage of type 'pre' configured."
)
)
return {
"parent_subscription_id": self.id,
"partner_id": self.partner_id.id,
"template_id": self.template_id.id,
"pricelist_id": self.pricelist_id.id,
"fiscal_position_id": self.fiscal_position_id.id,
"journal_id": self.journal_id.id,
"user_id": self.user_id.id,
"crm_team_id": self.crm_team_id.id,
"company_id": self.company_id.id,
"date_start": renewal_start,
"recurring_next_date": renewal_start,
"stage_id": pre_stage.id,
"tag_ids": [Command.set(self.tag_ids.ids)],
"sale_subscription_line_ids": self._prepare_renewal_lines(),
}

def action_prepare_renewal(self):
self.ensure_one()
if self.stage_id.type == "post":
raise UserError(self.env._("Cannot renew a closed subscription."))
active_child = self.child_subscription_ids.filtered(
lambda c: c.stage_id.type != "post"
)
if active_child:
raise UserError(
self.env._(
"This subscription already has an active renewal "
"(%(names)s). Close or cancel it before creating a new one.",
names=", ".join(active_child.mapped("display_name")),
)
)
values = self._prepare_renewal_values()
# `create()` forces the initial stage to 'draft' when both
# date_start and recurring_next_date are given. We need the
# renewal to start in a 'pre' stage so it can be picked up by
# the cron and so the renewal-activation hook fires.
target_stage_id = values.pop("stage_id")
renewal = self.create(values)
renewal.stage_id = target_stage_id
link = Markup(
'<a href=# data-oe-model=sale.subscription data-oe-id="{rid}">{rname}</a>'
).format(rid=renewal.id, rname=renewal.display_name or "")
self.message_post(
body=self.env._("Renewal quote created: %(link)s.", link=link)
)
return {
"type": "ir.actions.act_window",
"res_model": "sale.subscription",
"res_id": renewal.id,
"view_mode": "form",
"target": "current",
}

def action_view_parent_subscription(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"res_model": "sale.subscription",
"res_id": self.parent_subscription_id.id,
"view_mode": "form",
"target": "current",
}

def action_view_child_subscriptions(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": self.display_name,
"res_model": "sale.subscription",
"view_mode": "list,form",
"domain": [("id", "in", self.child_subscription_ids.ids)],
}

def action_close_subscription(self):
return {
Expand Down Expand Up @@ -467,6 +622,29 @@ def manual_invoice(self):
"context": context,
}

@api.depends(
"parent_subscription_id", "parent_subscription_id.origin_subscription_id"
)
def _compute_origin_subscription_id(self):
for record in self:
parent = record.parent_subscription_id
if not parent:
record.origin_subscription_id = False
else:
record.origin_subscription_id = parent.origin_subscription_id or parent

@api.depends("child_subscription_ids")
def _compute_renewal_count(self):
for record in self:
record.renewal_count = len(record.child_subscription_ids)

@api.depends("child_subscription_ids.stage_id.type")
def _compute_is_renewed(self):
for record in self:
record.is_renewed = any(
child.stage_id.type != "post" for child in record.child_subscription_ids
)

@api.depends("invoice_ids", "sale_order_ids.invoice_ids")
def _compute_account_invoice_ids_count(self):
for record in self:
Expand Down
3 changes: 3 additions & 0 deletions subscription_oca/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
This module allows creating subscriptions that generate recurring
invoices or orders. It also enables the sale of products that generate
subscriptions.

Subscriptions can be renewed into a linked child subscription, keeping
the genealogy between the original contract and its renewals.
14 changes: 14 additions & 0 deletions subscription_oca/readme/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,17 @@ Invoice and delivery addresses:
to the sale orders generated by the subscription.
- The fields are shown when the *Customer Addresses* setting
(group *Display Delivery / Invoice addresses*) is enabled.

To renew a subscription:

1. On an in-progress subscription, press *Renew*. A renewal quote is
created as a child subscription in the *pre* stage, copying the
template, lines, pricelist, fiscal position, partner and journal.
Its start date is the finish date of the current subscription (or
its next invoice date when there is no finish date).
2. The *Renew* button is hidden while a renewal quote is still open.
3. When the renewal is started, the original subscription is closed
automatically and a link to the renewal is posted on its chatter.
4. Use the *Parent contract* and *Renewals* smart buttons to navigate
the renewal chain; *origin_subscription_id* always points at the
first contract of the chain.
18 changes: 17 additions & 1 deletion subscription_oca/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -372,12 +372,14 @@ <h1>Subscription management</h1>
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ea96c858a40ee527d0e32ee03859918c082521f205ccc1cb26e96cb3bef27800
!! source digest: sha256:1d7163cf2d7da35b3460a208800f960258c0e321470ab537d8aee3c7dfa6197e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/contract/tree/19.0/subscription_oca"><img alt="OCA/contract" src="https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/contract-19-0/contract-19-0-subscription_oca"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/contract&amp;target_branch=19.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows creating subscriptions that generate recurring
invoices or orders. It also enables the sale of products that generate
subscriptions.</p>
<p>Subscriptions can be renewed into a linked child subscription, keeping
the genealogy between the original contract and its renewals.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
Expand Down Expand Up @@ -433,6 +435,20 @@ <h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<li>The fields are shown when the <em>Customer Addresses</em> setting (group
<em>Display Delivery / Invoice addresses</em>) is enabled.</li>
</ul>
<p>To renew a subscription:</p>
<ol class="arabic simple">
<li>On an in-progress subscription, press <em>Renew</em>. A renewal quote is
created as a child subscription in the <em>pre</em> stage, copying the
template, lines, pricelist, fiscal position, partner and journal. Its
start date is the finish date of the current subscription (or its
next invoice date when there is no finish date).</li>
<li>The <em>Renew</em> button is hidden while a renewal quote is still open.</li>
<li>When the renewal is started, the original subscription is closed
automatically and a link to the renewal is posted on its chatter.</li>
<li>Use the <em>Parent contract</em> and <em>Renewals</em> smart buttons to navigate
the renewal chain; <em>origin_subscription_id</em> always points at the
first contract of the chain.</li>
</ol>
</div>
<div class="section" id="known-issues-roadmap">
<h2><a class="toc-backref" href="#toc-entry-2">Known issues / Roadmap</a></h2>
Expand Down
1 change: 1 addition & 0 deletions subscription_oca/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from . import test_subscription_security
from . import test_subscription_recurrence_dates
from . import test_subscription_partner_addresses
from . import test_subscription_renewal
Loading
Loading