mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
[15.0][ADD] subscription_oca
This commit is contained in:
99
subscription_oca/README.rst
Normal file
99
subscription_oca/README.rst
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
=======================
|
||||||
|
Subscription management
|
||||||
|
=======================
|
||||||
|
|
||||||
|
..
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!! This file is generated by oca-gen-addon-readme !!
|
||||||
|
!! changes will be overwritten. !!
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!! source digest: sha256:3772d65a58c07d0348bd13d3c882810c94bfb87389c62fec6d16fe8ef130252c
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
|
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||||
|
:target: https://odoo-community.org/page/development-status
|
||||||
|
:alt: Beta
|
||||||
|
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||||
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||||
|
:alt: License: AGPL-3
|
||||||
|
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github
|
||||||
|
:target: https://github.com/OCA/contract/tree/15.0/subscription_oca
|
||||||
|
:alt: OCA/contract
|
||||||
|
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||||
|
:target: https://translation.odoo-community.org/projects/contract-15-0/contract-15-0-subscription_oca
|
||||||
|
:alt: Translate me on Weblate
|
||||||
|
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||||
|
:target: https://runboat.odoo-community.org/builds?repo=OCA/contract&target_branch=15.0
|
||||||
|
:alt: Try me on Runboat
|
||||||
|
|
||||||
|
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||||
|
|
||||||
|
This module allows creating subscriptions that generate recurring invoices or orders. It also enables the sale of products that generate subscriptions.
|
||||||
|
|
||||||
|
**Table of contents**
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
To make a subscription:
|
||||||
|
|
||||||
|
#. Go to *Subscriptions > Configuration > Subscription templates*.
|
||||||
|
#. Create the templates you consider, choosing the billing frequency: daily, monthly... and the method of creating the invoice and/or order.
|
||||||
|
#. Go to *Subscription > Subscriptions*.
|
||||||
|
#. Create a subscription and indicate the start date. When the *Subscriptions Management* cron job is executed, the subscription will begin and the first invoice will be created if the execution date matches the start date. The invoice will also be created when the execution date matches the next invoice date. Additionally, you can manually change the subscription status and create an invoice.
|
||||||
|
#. The cron job will also end the subscription if its end date has been reached.
|
||||||
|
|
||||||
|
To create subscriptions with the sale of a product:
|
||||||
|
|
||||||
|
#. Go to *Subscriptions > Subscriptions > Products*.
|
||||||
|
#. Create the product and in the sales tab, complete the fields *Subscribable product* and *Subscription template*
|
||||||
|
#. Create a sales order with the product and confirm it.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Bug Tracker
|
||||||
|
===========
|
||||||
|
|
||||||
|
Bugs are tracked on `GitHub Issues <https://github.com/OCA/contract/issues>`_.
|
||||||
|
In case of trouble, please check there if your issue has already been reported.
|
||||||
|
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||||
|
`feedback <https://github.com/OCA/contract/issues/new?body=module:%20subscription_oca%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||||
|
|
||||||
|
Do not contact contributors directly about support or help with technical issues.
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Authors
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
* Domatix
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* Carlos Martínez <carlos@domatix.com>
|
||||||
|
|
||||||
|
Maintainers
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is maintained by the OCA.
|
||||||
|
|
||||||
|
.. image:: https://odoo-community.org/logo.png
|
||||||
|
:alt: Odoo Community Association
|
||||||
|
:target: https://odoo-community.org
|
||||||
|
|
||||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
|
mission is to support the collaborative development of Odoo features and
|
||||||
|
promote its widespread use.
|
||||||
|
|
||||||
|
This module is part of the `OCA/contract <https://github.com/OCA/contract/tree/15.0/subscription_oca>`_ project on GitHub.
|
||||||
|
|
||||||
|
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||||
2
subscription_oca/__init__.py
Normal file
2
subscription_oca/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
28
subscription_oca/__manifest__.py
Normal file
28
subscription_oca/__manifest__.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
{
|
||||||
|
"name": "Subscription management",
|
||||||
|
"summary": "Generate recurring invoices.",
|
||||||
|
"version": "15.0.1.0.0",
|
||||||
|
"development_status": "Beta",
|
||||||
|
"category": "Subscription Management",
|
||||||
|
"website": "https://github.com/OCA/contract",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"author": "Domatix, Odoo Community Association (OCA)",
|
||||||
|
"depends": ["sale_management", "account"],
|
||||||
|
"data": [
|
||||||
|
"views/product_template_views.xml",
|
||||||
|
"views/sale_subscription_views.xml",
|
||||||
|
"views/sale_subscription_stage_views.xml",
|
||||||
|
"views/sale_subscription_tag_views.xml",
|
||||||
|
"views/sale_subscription_template_views.xml",
|
||||||
|
"views/sale_order_views.xml",
|
||||||
|
"views/res_partner_views.xml",
|
||||||
|
"data/ir_cron.xml",
|
||||||
|
"data/sale_subscription_data.xml",
|
||||||
|
"wizard/close_subscription_wizard.xml",
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
],
|
||||||
|
"installable": True,
|
||||||
|
"application": True,
|
||||||
|
}
|
||||||
15
subscription_oca/data/ir_cron.xml
Normal file
15
subscription_oca/data/ir_cron.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="ir_cron_subscription_management" model="ir.cron">
|
||||||
|
<field name="name">Subscriptions management</field>
|
||||||
|
<field eval="True" name="active" />
|
||||||
|
<field name="user_id" ref="base.user_root" />
|
||||||
|
<field name="interval_number">24</field>
|
||||||
|
<field name="interval_type">hours</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field eval="False" name="doall" />
|
||||||
|
<field ref="model_sale_subscription" name="model_id" />
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.cron_subscription_management()</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
76
subscription_oca/data/sale_subscription_data.xml
Normal file
76
subscription_oca/data/sale_subscription_data.xml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<data>
|
||||||
|
<record id="seq_id" model="ir.sequence">
|
||||||
|
<field name="name">sale_subscription_sequencer</field>
|
||||||
|
<field name="code">sale.subscription</field>
|
||||||
|
<field name="prefix">SUB</field>
|
||||||
|
<field name="padding">5</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="subscription_stage_draft" model="sale.subscription.stage">
|
||||||
|
<!-- <field eval="True" name="active"/> -->
|
||||||
|
<field name="name">Ready to start</field>
|
||||||
|
<field name="sequence">0</field>
|
||||||
|
<field name="type">pre</field>
|
||||||
|
<field name="description">
|
||||||
|
Draft equivalent, a subscription is ready to start when is not marked as in progress but it can be at any moment. If there's no 'Closed'-type stage defined, when a subscription comes to an end by automatic means, it will be marked with this stage.
|
||||||
|
</field>
|
||||||
|
<field eval="False" name="fold" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="subscription_stage_in_progress" model="sale.subscription.stage">
|
||||||
|
<!-- <field eval="True" name="active"/> -->
|
||||||
|
<field name="name">In progress</field>
|
||||||
|
<field name="sequence">1</field>
|
||||||
|
<field name="type">in_progress</field>
|
||||||
|
<field eval="False" name="fold" />
|
||||||
|
<field name="description">
|
||||||
|
As an 'In progress'-type of stage, it will trigger the recurring invoicing process if applicable. If this stage is the first - sequence order - of the available 'In progress' types and there's a stage change from any other non-'In progress' types to this one, an invoice will be created automatically if the start date is the present day.
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="subscription_stage_closed" model="sale.subscription.stage">
|
||||||
|
<!-- <field eval="True" name="active"/> -->
|
||||||
|
<field name="name">Closed</field>
|
||||||
|
<field name="sequence">2</field>
|
||||||
|
<field name="type">post</field>
|
||||||
|
<field eval="False" name="fold" />
|
||||||
|
<field name="description">
|
||||||
|
The final stage of a subscription. There are two ways to mark a subscription as closed. The easiest one is using the kanban card-moving capabilities, pressing the 'Close subscription' button (only available if a subscription is in progress).
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<record id="close_reason_expensive" model="sale.subscription.close.reason">
|
||||||
|
<!-- <field eval="True" name="active"/> -->
|
||||||
|
<field name="name">The subscription is too expensive</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="close_reason_requirement" model="sale.subscription.close.reason">
|
||||||
|
<!-- <field eval="True" name="active"/> -->
|
||||||
|
<field name="name">Subscription does not meet my requirements</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="close_reason_ended" model="sale.subscription.close.reason">
|
||||||
|
<!-- <field eval="True" name="active"/> -->
|
||||||
|
<field name="name">The subscription ended</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="close_reason_use" model="sale.subscription.close.reason">
|
||||||
|
<!-- <field eval="True" name="active"/> -->
|
||||||
|
<field name="name">I don't really use it</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="close_reason_other" model="sale.subscription.close.reason">
|
||||||
|
<!-- <field eval="True" name="active"/> -->
|
||||||
|
<field name="name">Other</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
</odoo>
|
||||||
11
subscription_oca/models/__init__.py
Normal file
11
subscription_oca/models/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from . import account_move
|
||||||
|
from . import product_template
|
||||||
|
from . import res_partner
|
||||||
|
from . import sale_order
|
||||||
|
from . import sale_order_line
|
||||||
|
from . import sale_subscription
|
||||||
|
from . import sale_subscription_close_reason
|
||||||
|
from . import sale_subscription_line
|
||||||
|
from . import sale_subscription_stage
|
||||||
|
from . import sale_subscription_tag
|
||||||
|
from . import sale_subscription_template
|
||||||
12
subscription_oca/models/account_move.py
Normal file
12
subscription_oca/models/account_move.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_inherit = "account.move"
|
||||||
|
|
||||||
|
subscription_id = fields.Many2one(
|
||||||
|
comodel_name="sale.subscription", string="Subscription"
|
||||||
|
)
|
||||||
12
subscription_oca/models/product_template.py
Normal file
12
subscription_oca/models/product_template.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class Product(models.Model):
|
||||||
|
_inherit = "product.template"
|
||||||
|
|
||||||
|
subscribable = fields.Boolean(string="Subscribable product")
|
||||||
|
subscription_template_id = fields.Many2one(
|
||||||
|
comodel_name="sale.subscription.template", string="Subscription template"
|
||||||
|
)
|
||||||
33
subscription_oca/models/res_partner.py
Normal file
33
subscription_oca/models/res_partner.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class Partner(models.Model):
|
||||||
|
_inherit = "res.partner"
|
||||||
|
|
||||||
|
subscription_ids = fields.One2many(
|
||||||
|
comodel_name="sale.subscription",
|
||||||
|
inverse_name="partner_id",
|
||||||
|
string="Subscriptions",
|
||||||
|
)
|
||||||
|
subscription_count = fields.Integer(
|
||||||
|
required=False,
|
||||||
|
compute="_compute_subscription_count",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_subscription_count(self):
|
||||||
|
for record in self:
|
||||||
|
record.subscription_count = len(record.subscription_ids)
|
||||||
|
|
||||||
|
def action_view_subscription_ids(self):
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "sale.subscription",
|
||||||
|
"domain": [("id", "in", self.subscription_ids.ids)],
|
||||||
|
"name": self.name,
|
||||||
|
"view_mode": "tree,form",
|
||||||
|
"context": {
|
||||||
|
"default_partner_id": self.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
83
subscription_oca/models/sale_order.py
Normal file
83
subscription_oca/models/sale_order.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SaleOrder(models.Model):
|
||||||
|
_inherit = "sale.order"
|
||||||
|
|
||||||
|
subscription_ids = fields.One2many(
|
||||||
|
comodel_name="sale.subscription",
|
||||||
|
inverse_name="sale_order_id",
|
||||||
|
string="Subscriptions",
|
||||||
|
)
|
||||||
|
subscriptions_count = fields.Integer(compute="_compute_subscriptions_count")
|
||||||
|
order_subscription_id = fields.Many2one(
|
||||||
|
comodel_name="sale.subscription", string="Subscription"
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("subscription_ids")
|
||||||
|
def _compute_subscriptions_count(self):
|
||||||
|
for record in self:
|
||||||
|
record.subscriptions_count = len(record.subscription_ids)
|
||||||
|
|
||||||
|
def action_view_subscriptions(self):
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "sale.subscription",
|
||||||
|
"domain": [("id", "in", self.subscription_ids.ids)],
|
||||||
|
"name": self.name,
|
||||||
|
"view_mode": "tree,form",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_next_interval(self, type_interval, interval):
|
||||||
|
date_start = date.today()
|
||||||
|
date_start += relativedelta(**{type_interval: interval})
|
||||||
|
return date_start
|
||||||
|
|
||||||
|
def create_subscription(self, lines, subscription_tmpl):
|
||||||
|
subscription_lines = []
|
||||||
|
for line in lines:
|
||||||
|
subscription_lines.append((0, 0, line.get_subscription_line_values()))
|
||||||
|
|
||||||
|
if subscription_tmpl:
|
||||||
|
rec = self.env["sale.subscription"].create(
|
||||||
|
{
|
||||||
|
"partner_id": self.partner_id.id,
|
||||||
|
"user_id": self._context["uid"],
|
||||||
|
"template_id": subscription_tmpl.id,
|
||||||
|
"pricelist_id": self.partner_id.property_product_pricelist.id,
|
||||||
|
"date_start": date.today(),
|
||||||
|
"sale_order_id": self.id,
|
||||||
|
"sale_subscription_line_ids": subscription_lines,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rec.action_start_subscription()
|
||||||
|
self.subscription_ids = [(4, rec.id)]
|
||||||
|
rec.recurring_next_date = self.get_next_interval(
|
||||||
|
subscription_tmpl.recurring_rule_type,
|
||||||
|
subscription_tmpl.recurring_interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
def group_subscription_lines(self):
|
||||||
|
grouped = defaultdict(list)
|
||||||
|
for order_line in self.order_line.filtered(
|
||||||
|
lambda line: line.product_id.subscribable
|
||||||
|
):
|
||||||
|
grouped[
|
||||||
|
order_line.product_id.product_tmpl_id.subscription_template_id
|
||||||
|
].append(order_line)
|
||||||
|
return grouped
|
||||||
|
|
||||||
|
def action_confirm(self):
|
||||||
|
res = super(SaleOrder, self).action_confirm()
|
||||||
|
for record in self:
|
||||||
|
grouped = self.group_subscription_lines()
|
||||||
|
for tmpl, lines in grouped.items():
|
||||||
|
record.create_subscription(lines, tmpl)
|
||||||
|
return res
|
||||||
17
subscription_oca/models/sale_order_line.py
Normal file
17
subscription_oca/models/sale_order_line.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class SaleOrderLine(models.Model):
|
||||||
|
_inherit = "sale.order.line"
|
||||||
|
|
||||||
|
def get_subscription_line_values(self):
|
||||||
|
return {
|
||||||
|
"product_id": self.product_id.id,
|
||||||
|
"name": self.product_id.name,
|
||||||
|
"product_uom_qty": self.product_uom_qty,
|
||||||
|
"price_unit": self.price_unit,
|
||||||
|
"discount": self.discount,
|
||||||
|
"price_subtotal": self.price_subtotal,
|
||||||
|
}
|
||||||
470
subscription_oca/models/sale_subscription.py
Normal file
470
subscription_oca/models/sale_subscription.py
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
import logging
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import AccessError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SaleSubscription(models.Model):
|
||||||
|
_name = "sale.subscription"
|
||||||
|
_description = "Subscription"
|
||||||
|
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||||
|
_order = "id desc"
|
||||||
|
|
||||||
|
color = fields.Integer("Color Index")
|
||||||
|
name = fields.Char(
|
||||||
|
compute="_compute_name",
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
sequence = fields.Integer()
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company",
|
||||||
|
"Company",
|
||||||
|
required=True,
|
||||||
|
index=True,
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
)
|
||||||
|
partner_id = fields.Many2one(
|
||||||
|
comodel_name="res.partner", required=True, string="Partner", index=True
|
||||||
|
)
|
||||||
|
fiscal_position_id = fields.Many2one(
|
||||||
|
"account.fiscal.position",
|
||||||
|
string="Fiscal Position",
|
||||||
|
domain="[('company_id', '=', company_id)]",
|
||||||
|
check_company=True,
|
||||||
|
)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
template_id = fields.Many2one(
|
||||||
|
comodel_name="sale.subscription.template",
|
||||||
|
required=True,
|
||||||
|
string="Subscription template",
|
||||||
|
)
|
||||||
|
code = fields.Char(
|
||||||
|
string="Reference",
|
||||||
|
default=lambda self: self.env["ir.sequence"].next_by_code("sale.subscription"),
|
||||||
|
)
|
||||||
|
in_progress = fields.Boolean(string="In progress", default=False)
|
||||||
|
recurring_rule_boundary = fields.Boolean(
|
||||||
|
string="Boundary", compute="_compute_rule_boundary", store=True
|
||||||
|
)
|
||||||
|
pricelist_id = fields.Many2one(
|
||||||
|
comodel_name="product.pricelist", required=True, string="Pricelist"
|
||||||
|
)
|
||||||
|
recurring_next_date = fields.Date(string="Next invoice date", default=date.today())
|
||||||
|
user_id = fields.Many2one(
|
||||||
|
comodel_name="res.users",
|
||||||
|
string="Commercial agent",
|
||||||
|
default=lambda self: self.env.user.id,
|
||||||
|
)
|
||||||
|
date_start = fields.Date(string="Start date", default=date.today())
|
||||||
|
date = fields.Date(
|
||||||
|
string="Finish date",
|
||||||
|
compute="_compute_rule_boundary",
|
||||||
|
store=True,
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
description = fields.Text()
|
||||||
|
sale_order_id = fields.Many2one(
|
||||||
|
comodel_name="sale.order", string="Origin sale order"
|
||||||
|
)
|
||||||
|
terms = fields.Text(
|
||||||
|
string="Terms and conditions",
|
||||||
|
compute="_compute_terms",
|
||||||
|
store=True,
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
invoice_ids = fields.One2many(
|
||||||
|
comodel_name="account.move",
|
||||||
|
inverse_name="subscription_id",
|
||||||
|
string="Invoices",
|
||||||
|
)
|
||||||
|
sale_order_ids = fields.One2many(
|
||||||
|
comodel_name="sale.order",
|
||||||
|
inverse_name="order_subscription_id",
|
||||||
|
string="Orders",
|
||||||
|
)
|
||||||
|
recurring_total = fields.Monetary(
|
||||||
|
compute="_compute_total", string="Recurring price", store=True
|
||||||
|
)
|
||||||
|
amount_tax = fields.Monetary(compute="_compute_total", store=True)
|
||||||
|
amount_total = fields.Monetary(compute="_compute_total", store=True)
|
||||||
|
tag_ids = fields.Many2many(comodel_name="sale.subscription.tag", string="Tags")
|
||||||
|
image = fields.Binary("Image", related="user_id.image_512", store=True)
|
||||||
|
journal_id = fields.Many2one(comodel_name="account.journal", string="Journal")
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
related="pricelist_id.currency_id",
|
||||||
|
depends=["pricelist_id"],
|
||||||
|
store=True,
|
||||||
|
ondelete="restrict",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _read_group_stage_ids(self, stages, domain, order):
|
||||||
|
stage_ids = stages.search([], order=order)
|
||||||
|
return stage_ids
|
||||||
|
|
||||||
|
stage_id = fields.Many2one(
|
||||||
|
comodel_name="sale.subscription.stage",
|
||||||
|
string="Stage",
|
||||||
|
tracking=True,
|
||||||
|
group_expand="_read_group_stage_ids",
|
||||||
|
store="true",
|
||||||
|
)
|
||||||
|
stage_str = fields.Char(
|
||||||
|
related="stage_id.name",
|
||||||
|
string="Etapa",
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
sale_subscription_line_ids = fields.One2many(
|
||||||
|
comodel_name="sale.subscription.line",
|
||||||
|
inverse_name="sale_subscription_id",
|
||||||
|
)
|
||||||
|
sale_order_ids_count = fields.Integer(
|
||||||
|
compute="_compute_sale_order_ids_count", string="Sale orders"
|
||||||
|
)
|
||||||
|
account_invoice_ids_count = fields.Integer(
|
||||||
|
compute="_compute_account_invoice_ids_count", string="Invoice Count"
|
||||||
|
)
|
||||||
|
close_reason_id = fields.Many2one(
|
||||||
|
comodel_name="sale.subscription.close.reason", string="Close Reason"
|
||||||
|
)
|
||||||
|
crm_team_id = fields.Many2one(comodel_name="crm.team", string="Sale team")
|
||||||
|
to_renew = fields.Boolean(default=False, string="To renew")
|
||||||
|
|
||||||
|
def cron_subscription_management(self):
|
||||||
|
today = date.today()
|
||||||
|
for subscription in self.search([]):
|
||||||
|
if subscription.in_progress:
|
||||||
|
if (
|
||||||
|
subscription.recurring_next_date == today
|
||||||
|
and subscription.sale_subscription_line_ids
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
subscription.generate_invoice()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error on subscription invoice generate")
|
||||||
|
if not subscription.recurring_rule_boundary:
|
||||||
|
if subscription.date == today:
|
||||||
|
subscription.action_close_subscription()
|
||||||
|
|
||||||
|
else:
|
||||||
|
if subscription.date_start == today:
|
||||||
|
subscription.action_start_subscription()
|
||||||
|
subscription.generate_invoice()
|
||||||
|
|
||||||
|
@api.depends("sale_subscription_line_ids")
|
||||||
|
def _compute_total(self):
|
||||||
|
for record in self:
|
||||||
|
recurring_total = amount_tax = 0.0
|
||||||
|
for order_line in record.sale_subscription_line_ids:
|
||||||
|
recurring_total += order_line.price_subtotal
|
||||||
|
amount_tax += order_line.amount_tax_line_amount
|
||||||
|
record.update(
|
||||||
|
{
|
||||||
|
"recurring_total": recurring_total,
|
||||||
|
"amount_tax": amount_tax,
|
||||||
|
"amount_total": recurring_total + amount_tax,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("template_id", "code")
|
||||||
|
def _compute_name(self):
|
||||||
|
for record in self:
|
||||||
|
template_code = record.template_id.code if record.template_id.code else ""
|
||||||
|
code = record.code if record.code else ""
|
||||||
|
slash = "/" if template_code and code else ""
|
||||||
|
record.name = "{}{}{}".format(template_code, slash, code)
|
||||||
|
|
||||||
|
@api.depends("template_id", "date_start")
|
||||||
|
def _compute_rule_boundary(self):
|
||||||
|
for record in self:
|
||||||
|
if record.template_id.recurring_rule_boundary == "unlimited":
|
||||||
|
record.date = False
|
||||||
|
record.recurring_rule_boundary = True
|
||||||
|
else:
|
||||||
|
record.date = (
|
||||||
|
relativedelta(months=+record.template_id.recurring_rule_count)
|
||||||
|
+ record.date_start
|
||||||
|
)
|
||||||
|
record.recurring_rule_boundary = False
|
||||||
|
|
||||||
|
@api.depends("template_id")
|
||||||
|
def _compute_terms(self):
|
||||||
|
for record in self:
|
||||||
|
record.terms = record.template_id.description
|
||||||
|
|
||||||
|
@api.onchange("template_id", "date_start")
|
||||||
|
def _onchange_template_id(self):
|
||||||
|
today = date.today()
|
||||||
|
if self.date_start:
|
||||||
|
today = self.date_start
|
||||||
|
if self.template_id and self.account_invoice_ids_count > 0:
|
||||||
|
self.calculate_recurring_next_date(self.recurring_next_date)
|
||||||
|
else:
|
||||||
|
self.calculate_recurring_next_date(today)
|
||||||
|
|
||||||
|
def calculate_recurring_next_date(self, start_date):
|
||||||
|
if self.account_invoice_ids_count == 0:
|
||||||
|
self.recurring_next_date = date.today()
|
||||||
|
else:
|
||||||
|
type_interval = self.template_id.recurring_rule_type
|
||||||
|
interval = int(self.template_id.recurring_interval)
|
||||||
|
self.recurring_next_date = start_date + relativedelta(
|
||||||
|
**{type_interval: interval}
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.onchange("partner_id")
|
||||||
|
def onchange_partner_id(self):
|
||||||
|
self.pricelist_id = self.partner_id.property_product_pricelist
|
||||||
|
|
||||||
|
@api.onchange("partner_id", "company_id")
|
||||||
|
def onchange_partner_id_fpos(self):
|
||||||
|
self.fiscal_position_id = (
|
||||||
|
self.env["account.fiscal.position"]
|
||||||
|
.with_company(self.company_id)
|
||||||
|
.get_fiscal_position(self.partner_id.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_start_subscription(self):
|
||||||
|
self.close_reason_id = False
|
||||||
|
in_progress_stage = self.env["sale.subscription.stage"].search(
|
||||||
|
[("type", "=", "in_progress")], limit=1
|
||||||
|
)
|
||||||
|
self.stage_id = in_progress_stage
|
||||||
|
|
||||||
|
def action_close_subscription(self):
|
||||||
|
self.recurring_next_date = False
|
||||||
|
return {
|
||||||
|
"view_type": "form",
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_model": "close.reason.wizard",
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"target": "new",
|
||||||
|
"res_id": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _prepare_sale_order(self, line_ids=False):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"partner_id": self.partner_id.id,
|
||||||
|
"fiscal_position_id": self.fiscal_position_id.id,
|
||||||
|
"date_order": datetime.now(),
|
||||||
|
"payment_term_id": self.partner_id.property_payment_term_id.id,
|
||||||
|
"user_id": self.user_id.id,
|
||||||
|
"origin": self.name,
|
||||||
|
"order_line": line_ids,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _prepare_account_move(self, line_ids):
|
||||||
|
self.ensure_one()
|
||||||
|
values = {
|
||||||
|
"partner_id": self.partner_id.id,
|
||||||
|
"invoice_date": self.recurring_next_date,
|
||||||
|
"invoice_payment_term_id": self.partner_id.property_payment_term_id.id,
|
||||||
|
"invoice_origin": self.name,
|
||||||
|
"invoice_user_id": self.user_id.id,
|
||||||
|
"partner_bank_id": self.company_id.partner_id.bank_ids[:1].id,
|
||||||
|
"invoice_line_ids": line_ids,
|
||||||
|
}
|
||||||
|
if self.journal_id:
|
||||||
|
values["journal_id"] = self.journal_id.id
|
||||||
|
return values
|
||||||
|
|
||||||
|
def create_invoice(self):
|
||||||
|
if not self.env["account.move"].check_access_rights("create", False):
|
||||||
|
try:
|
||||||
|
self.check_access_rights("write")
|
||||||
|
self.check_access_rule("write")
|
||||||
|
except AccessError:
|
||||||
|
return self.env["account.move"]
|
||||||
|
line_ids = []
|
||||||
|
for line in self.sale_subscription_line_ids:
|
||||||
|
line_values = line._prepare_account_move_line()
|
||||||
|
line_ids.append((0, 0, line_values))
|
||||||
|
invoice_values = self._prepare_account_move(line_ids)
|
||||||
|
invoice_id = (
|
||||||
|
self.env["account.move"]
|
||||||
|
.sudo()
|
||||||
|
.with_context(default_move_type="out_invoice", journal_type="sale")
|
||||||
|
.create(invoice_values)
|
||||||
|
)
|
||||||
|
self.write({"invoice_ids": [(4, invoice_id.id)]})
|
||||||
|
return invoice_id
|
||||||
|
|
||||||
|
def create_sale_order(self):
|
||||||
|
if not self.env["sale.order"].check_access_rights("create", False):
|
||||||
|
try:
|
||||||
|
self.check_access_rights("write")
|
||||||
|
self.check_access_rule("write")
|
||||||
|
except AccessError:
|
||||||
|
return self.env["sale.order"]
|
||||||
|
line_ids = []
|
||||||
|
for line in self.sale_subscription_line_ids:
|
||||||
|
line_values = line._prepare_sale_order_line()
|
||||||
|
line_ids.append((0, 0, line_values))
|
||||||
|
values = self._prepare_sale_order(line_ids)
|
||||||
|
order_id = self.env["sale.order"].sudo().create(values)
|
||||||
|
self.write({"sale_order_ids": [(4, order_id.id)]})
|
||||||
|
return order_id
|
||||||
|
|
||||||
|
def generate_invoice(self):
|
||||||
|
invoice_number = ""
|
||||||
|
msg_static = _("Created invoice with reference")
|
||||||
|
if self.template_id.invoicing_mode in ["draft", "invoice", "invoice_send"]:
|
||||||
|
invoice = self.create_invoice()
|
||||||
|
if self.template_id.invoicing_mode != "draft":
|
||||||
|
invoice.action_post()
|
||||||
|
if self.template_id.invoicing_mode == "invoice_send":
|
||||||
|
mail_template = self.template_id.invoice_mail_template_id
|
||||||
|
invoice.with_context(force_send=True).message_post_with_template(
|
||||||
|
mail_template.id,
|
||||||
|
composition_mode="comment",
|
||||||
|
email_layout_xmlid="mail.mail_notification_paynow",
|
||||||
|
)
|
||||||
|
invoice_number = invoice.name
|
||||||
|
message_body = (
|
||||||
|
"<b>%s</b> <a href=# data-oe-model=account.move data-oe-id=%d>%s</a>"
|
||||||
|
% (msg_static, invoice.id, invoice_number)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.template_id.invoicing_mode == "sale_and_invoice":
|
||||||
|
order_id = self.create_sale_order()
|
||||||
|
order_id.action_done()
|
||||||
|
new_invoice = order_id._create_invoices()
|
||||||
|
new_invoice.action_post()
|
||||||
|
new_invoice.invoice_origin = order_id.name + ", " + self.name
|
||||||
|
invoice_number = new_invoice.name
|
||||||
|
message_body = (
|
||||||
|
"<b>%s</b> <a href=# data-oe-model=account.move data-oe-id=%d>%s</a>"
|
||||||
|
% (msg_static, new_invoice.id, invoice_number)
|
||||||
|
)
|
||||||
|
if not invoice_number:
|
||||||
|
invoice_number = _("To validate")
|
||||||
|
message_body = "<b>%s</b> %s" % (msg_static, invoice_number)
|
||||||
|
self.calculate_recurring_next_date(self.recurring_next_date)
|
||||||
|
self.message_post(body=message_body)
|
||||||
|
|
||||||
|
def manual_invoice(self):
|
||||||
|
invoice_id = self.create_invoice()
|
||||||
|
self.calculate_recurring_next_date(self.recurring_next_date)
|
||||||
|
context = dict(self.env.context)
|
||||||
|
context["form_view_initial_mode"] = "edit"
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"views": [
|
||||||
|
(self.env.ref("account.view_move_form").id, "form"),
|
||||||
|
(self.env.ref("account.view_move_tree").id, "tree"),
|
||||||
|
],
|
||||||
|
"view_type": "form",
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_model": "account.move",
|
||||||
|
"res_id": invoice_id.id,
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"context": context,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.depends("invoice_ids", "sale_order_ids.invoice_ids")
|
||||||
|
def _compute_account_invoice_ids_count(self):
|
||||||
|
for record in self:
|
||||||
|
record.account_invoice_ids_count = len(self.invoice_ids) + len(
|
||||||
|
self.sale_order_ids.invoice_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_view_account_invoice_ids(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"views": [
|
||||||
|
(self.env.ref("account.view_move_tree").id, "tree"),
|
||||||
|
(self.env.ref("account.view_move_form").id, "form"),
|
||||||
|
],
|
||||||
|
"view_type": "form",
|
||||||
|
"view_mode": "tree,form",
|
||||||
|
"res_model": "account.move",
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"domain": [
|
||||||
|
("id", "in", self.invoice_ids.ids + self.sale_order_ids.invoice_ids.ids)
|
||||||
|
],
|
||||||
|
"context": self.env.context,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _compute_sale_order_ids_count(self):
|
||||||
|
data = self.env["sale.order"].read_group(
|
||||||
|
domain=[("order_subscription_id", "in", self.ids)],
|
||||||
|
fields=["order_subscription_id"],
|
||||||
|
groupby=["order_subscription_id"],
|
||||||
|
)
|
||||||
|
count_dict = {
|
||||||
|
item["order_subscription_id"][0]: item["order_subscription_id_count"]
|
||||||
|
for item in data
|
||||||
|
}
|
||||||
|
for record in self:
|
||||||
|
record.sale_order_ids_count = count_dict.get(record.id, 0)
|
||||||
|
|
||||||
|
def action_view_sale_order_ids(self):
|
||||||
|
active_ids = self.sale_order_ids.ids
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"view_type": "form",
|
||||||
|
"view_mode": "tree,form",
|
||||||
|
"res_model": "sale.order",
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"domain": [("id", "in", active_ids)],
|
||||||
|
"context": self.env.context,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _check_dates(self, start, next_invoice):
|
||||||
|
if start and next_invoice:
|
||||||
|
date_start = start
|
||||||
|
date_next_invoice = next_invoice
|
||||||
|
if not isinstance(date_start, date) and not isinstance(
|
||||||
|
date_next_invoice, date
|
||||||
|
):
|
||||||
|
date_start = fields.Date.to_date(start)
|
||||||
|
date_next_invoice = fields.Date.to_date(next_invoice)
|
||||||
|
if date_start > date_next_invoice:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def write(self, values):
|
||||||
|
res = super().write(values)
|
||||||
|
if "stage_id" in values:
|
||||||
|
for record in self:
|
||||||
|
if record.stage_id:
|
||||||
|
if record.stage_id.type == "in_progress":
|
||||||
|
record.in_progress = True
|
||||||
|
record.date_start = date.today()
|
||||||
|
elif record.stage_id.type == "post":
|
||||||
|
record.close_reason_id = False
|
||||||
|
record.in_progress = False
|
||||||
|
else:
|
||||||
|
record.in_progress = False
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, values):
|
||||||
|
if "recurring_rule_boundary" in values:
|
||||||
|
if not values["recurring_rule_boundary"]:
|
||||||
|
template_id = self.env["sale.subscription.template"].search(
|
||||||
|
[("id", "=", values["template_id"])]
|
||||||
|
)
|
||||||
|
date_start = values["date_start"]
|
||||||
|
if not isinstance(values["date_start"], date):
|
||||||
|
date_start = fields.Date.to_date(values["date_start"])
|
||||||
|
values["date"] = template_id._get_date(date_start)
|
||||||
|
if "date_start" in values and "recurring_next_date" in values:
|
||||||
|
res = self._check_dates(values["date_start"], values["recurring_next_date"])
|
||||||
|
if res:
|
||||||
|
values["date_start"] = values["recurring_next_date"]
|
||||||
|
values["stage_id"] = (
|
||||||
|
self.env["sale.subscription.stage"]
|
||||||
|
.search([("type", "=", "pre")], order="sequence desc")[-1]
|
||||||
|
.id
|
||||||
|
)
|
||||||
|
return super(SaleSubscription, self).create(values)
|
||||||
10
subscription_oca/models/sale_subscription_close_reason.py
Normal file
10
subscription_oca/models/sale_subscription_close_reason.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SaleSubscriptionCloseReason(models.Model):
|
||||||
|
_name = "sale.subscription.close.reason"
|
||||||
|
_description = "Close reason model"
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
322
subscription_oca/models/sale_subscription_line.py
Normal file
322
subscription_oca/models/sale_subscription_line.py
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.tools.misc import get_lang
|
||||||
|
|
||||||
|
|
||||||
|
class SaleSubscriptionLine(models.Model):
|
||||||
|
_name = "sale.subscription.line"
|
||||||
|
_description = "Subscription lines added to a given subscription"
|
||||||
|
|
||||||
|
product_id = fields.Many2one(
|
||||||
|
comodel_name="product.product",
|
||||||
|
domain=[("sale_ok", "=", True)],
|
||||||
|
string="Product",
|
||||||
|
)
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
"res.currency",
|
||||||
|
related="sale_subscription_id.currency_id",
|
||||||
|
store=True,
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
name = fields.Char(
|
||||||
|
string="Description", compute="_compute_name", store=True, readonly=False
|
||||||
|
)
|
||||||
|
product_uom_qty = fields.Float(default=1.0, string="Quantity")
|
||||||
|
price_unit = fields.Float(
|
||||||
|
string="Unit price", compute="_compute_price_unit", store=True, readonly=False
|
||||||
|
)
|
||||||
|
discount = fields.Float(
|
||||||
|
string="Discount (%)", compute="_compute_discount", store=True, readonly=False
|
||||||
|
)
|
||||||
|
tax_ids = fields.Many2many(
|
||||||
|
comodel_name="account.tax",
|
||||||
|
relation="subscription_line_tax",
|
||||||
|
column1="subscription_line_id",
|
||||||
|
column2="tax_id",
|
||||||
|
string="Taxes",
|
||||||
|
compute="_compute_tax_ids",
|
||||||
|
store=True,
|
||||||
|
readonly=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("product_id", "price_unit", "product_uom_qty", "discount", "tax_ids")
|
||||||
|
def _compute_subtotal(self):
|
||||||
|
for record in self:
|
||||||
|
price = record.price_unit * (1 - (record.discount or 0.0) / 100.0)
|
||||||
|
taxes = record.tax_ids.compute_all(
|
||||||
|
price,
|
||||||
|
record.currency_id,
|
||||||
|
record.product_uom_qty,
|
||||||
|
product=record.product_id,
|
||||||
|
partner=record.sale_subscription_id.partner_id,
|
||||||
|
)
|
||||||
|
record.update(
|
||||||
|
{
|
||||||
|
"amount_tax_line_amount": sum(
|
||||||
|
t.get("amount", 0.0) for t in taxes.get("taxes", [])
|
||||||
|
),
|
||||||
|
"price_total": taxes["total_included"],
|
||||||
|
"price_subtotal": taxes["total_excluded"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
price_subtotal = fields.Monetary(
|
||||||
|
string="Subtotal", readonly="True", compute=_compute_subtotal, store=True
|
||||||
|
)
|
||||||
|
price_total = fields.Monetary(
|
||||||
|
string="Total", readonly="True", compute=_compute_subtotal, store=True
|
||||||
|
)
|
||||||
|
amount_tax_line_amount = fields.Float(
|
||||||
|
string="Taxes Amount", compute="_compute_subtotal", store=True
|
||||||
|
)
|
||||||
|
sale_subscription_id = fields.Many2one(
|
||||||
|
comodel_name="sale.subscription", string="Subscription"
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
related="sale_subscription_id.company_id",
|
||||||
|
string="Company",
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("product_id")
|
||||||
|
def _compute_name(self):
|
||||||
|
for record in self:
|
||||||
|
if not record.product_id:
|
||||||
|
record.name = False
|
||||||
|
lang = get_lang(self.env, record.sale_subscription_id.partner_id.lang).code
|
||||||
|
product = record.product_id.with_context(lang=lang)
|
||||||
|
record.name = product.with_context(
|
||||||
|
lang=lang
|
||||||
|
).get_product_multiline_description_sale()
|
||||||
|
|
||||||
|
@api.depends("product_id", "sale_subscription_id.fiscal_position_id")
|
||||||
|
def _compute_tax_ids(self):
|
||||||
|
for line in self:
|
||||||
|
fpos = (
|
||||||
|
line.sale_subscription_id.fiscal_position_id
|
||||||
|
or line.sale_subscription_id.fiscal_position_id.get_fiscal_position(
|
||||||
|
line.sale_subscription_id.partner_id.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# If company_id is set, always filter taxes by the company
|
||||||
|
taxes = line.product_id.taxes_id.filtered(
|
||||||
|
lambda t: t.company_id == line.env.company
|
||||||
|
)
|
||||||
|
line.tax_ids = fpos.map_tax(taxes)
|
||||||
|
|
||||||
|
@api.depends(
|
||||||
|
"product_id",
|
||||||
|
"sale_subscription_id.partner_id",
|
||||||
|
"sale_subscription_id.pricelist_id",
|
||||||
|
)
|
||||||
|
def _compute_price_unit(self):
|
||||||
|
for record in self:
|
||||||
|
if not record.product_id:
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
record.sale_subscription_id.pricelist_id
|
||||||
|
and record.sale_subscription_id.partner_id
|
||||||
|
):
|
||||||
|
product = record.product_id.with_context(
|
||||||
|
partner=record.sale_subscription_id.partner_id,
|
||||||
|
quantity=record.product_uom_qty,
|
||||||
|
date=fields.datetime.now(),
|
||||||
|
pricelist=record.sale_subscription_id.pricelist_id.id,
|
||||||
|
uom=record.product_id.uom_id.id,
|
||||||
|
)
|
||||||
|
record.price_unit = product._get_tax_included_unit_price(
|
||||||
|
record.company_id,
|
||||||
|
record.sale_subscription_id.currency_id,
|
||||||
|
fields.datetime.now(),
|
||||||
|
"sale",
|
||||||
|
fiscal_position=record.sale_subscription_id.fiscal_position_id,
|
||||||
|
product_price_unit=record._get_display_price(product),
|
||||||
|
product_currency=record.sale_subscription_id.currency_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends(
|
||||||
|
"product_id",
|
||||||
|
"price_unit",
|
||||||
|
"product_uom_qty",
|
||||||
|
"tax_ids",
|
||||||
|
"sale_subscription_id.partner_id",
|
||||||
|
"sale_subscription_id.pricelist_id",
|
||||||
|
)
|
||||||
|
def _compute_discount(self):
|
||||||
|
for record in self:
|
||||||
|
if not (
|
||||||
|
record.product_id
|
||||||
|
and record.product_id.uom_id
|
||||||
|
and record.sale_subscription_id.partner_id
|
||||||
|
and record.sale_subscription_id.pricelist_id
|
||||||
|
and record.sale_subscription_id.pricelist_id.discount_policy
|
||||||
|
== "without_discount"
|
||||||
|
and self.env.user.has_group("product.group_discount_per_so_line")
|
||||||
|
):
|
||||||
|
record.discount = 0.0
|
||||||
|
continue
|
||||||
|
|
||||||
|
record.discount = 0.0
|
||||||
|
product = record.product_id.with_context(
|
||||||
|
lang=record.sale_subscription_id.partner_id.lang,
|
||||||
|
partner=record.sale_subscription_id.partner_id,
|
||||||
|
quantity=record.product_uom_qty,
|
||||||
|
date=fields.Datetime.now(),
|
||||||
|
pricelist=record.sale_subscription_id.pricelist_id.id,
|
||||||
|
uom=record.product_id.uom_id.id,
|
||||||
|
fiscal_position=record.sale_subscription_id.fiscal_position_id
|
||||||
|
or self.env.context.get("fiscal_position"),
|
||||||
|
)
|
||||||
|
|
||||||
|
price, rule_id = record.sale_subscription_id.pricelist_id.with_context(
|
||||||
|
partner_id=record.sale_subscription_id.partner_id.id,
|
||||||
|
date=fields.Datetime.now(),
|
||||||
|
uom=record.product_id.uom_id.id,
|
||||||
|
).get_product_price_rule(
|
||||||
|
record.product_id,
|
||||||
|
record.product_uom_qty or 1.0,
|
||||||
|
record.sale_subscription_id.partner_id,
|
||||||
|
)
|
||||||
|
new_list_price, currency = record.with_context(
|
||||||
|
partner_id=record.sale_subscription_id.partner_id.id,
|
||||||
|
date=fields.Datetime.now(),
|
||||||
|
uom=record.product_id.uom_id.id,
|
||||||
|
)._get_real_price_currency(
|
||||||
|
product, rule_id, record.product_uom_qty, record.product_id.uom_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if new_list_price != 0:
|
||||||
|
if record.sale_subscription_id.pricelist_id.currency_id != currency:
|
||||||
|
new_list_price = currency._convert(
|
||||||
|
new_list_price,
|
||||||
|
record.sale_subscription_id.pricelist_id.currency_id,
|
||||||
|
record.sale_subscription_id.company_id or self.env.company,
|
||||||
|
fields.Date.today(),
|
||||||
|
)
|
||||||
|
discount = (new_list_price - price) / new_list_price * 100
|
||||||
|
if (discount > 0 and new_list_price > 0) or (
|
||||||
|
discount < 0 and new_list_price < 0
|
||||||
|
):
|
||||||
|
record.discount = discount
|
||||||
|
|
||||||
|
def _get_real_price_currency(self, product, rule_id, qty, uom):
|
||||||
|
PricelistItem = self.env["product.pricelist.item"]
|
||||||
|
field_name = "lst_price"
|
||||||
|
currency_id = None
|
||||||
|
product_currency = product.currency_id
|
||||||
|
if rule_id:
|
||||||
|
pricelist_item = PricelistItem.browse(rule_id)
|
||||||
|
if pricelist_item.pricelist_id.discount_policy == "without_discount":
|
||||||
|
while (
|
||||||
|
pricelist_item.base == "pricelist"
|
||||||
|
and pricelist_item.base_pricelist_id
|
||||||
|
and pricelist_item.base_pricelist_id.discount_policy
|
||||||
|
== "without_discount"
|
||||||
|
):
|
||||||
|
_price, rule_id = pricelist_item.base_pricelist_id.with_context(
|
||||||
|
uom=uom.id
|
||||||
|
).get_product_price_rule(
|
||||||
|
product, qty, self.sale_subscription_id.partner_id
|
||||||
|
)
|
||||||
|
pricelist_item = PricelistItem.browse(rule_id)
|
||||||
|
|
||||||
|
if pricelist_item.base == "standard_price":
|
||||||
|
field_name = "standard_price"
|
||||||
|
product_currency = product.cost_currency_id
|
||||||
|
elif (
|
||||||
|
pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id
|
||||||
|
):
|
||||||
|
field_name = "price"
|
||||||
|
product = product.with_context(
|
||||||
|
pricelist=pricelist_item.base_pricelist_id.id
|
||||||
|
)
|
||||||
|
product_currency = pricelist_item.base_pricelist_id.currency_id
|
||||||
|
currency_id = pricelist_item.pricelist_id.currency_id
|
||||||
|
|
||||||
|
if not currency_id:
|
||||||
|
currency_id = product_currency
|
||||||
|
cur_factor = 1.0
|
||||||
|
else:
|
||||||
|
if currency_id.id == product_currency.id:
|
||||||
|
cur_factor = 1.0
|
||||||
|
else:
|
||||||
|
cur_factor = currency_id._get_conversion_rate(
|
||||||
|
product_currency,
|
||||||
|
currency_id,
|
||||||
|
self.company_id or self.env.company,
|
||||||
|
fields.Date.today(),
|
||||||
|
)
|
||||||
|
|
||||||
|
product_uom = self.env.context.get("uom") or product.uom_id.id
|
||||||
|
if uom and uom.id != product_uom:
|
||||||
|
# the unit price is in a different uom
|
||||||
|
uom_factor = uom._compute_price(1.0, product.uom_id)
|
||||||
|
else:
|
||||||
|
uom_factor = 1.0
|
||||||
|
|
||||||
|
return product[field_name] * uom_factor * cur_factor, currency_id
|
||||||
|
|
||||||
|
def _get_display_price(self, product):
|
||||||
|
if self.sale_subscription_id.pricelist_id.discount_policy == "with_discount":
|
||||||
|
return product.with_context(
|
||||||
|
pricelist=self.sale_subscription_id.pricelist_id.id,
|
||||||
|
uom=self.product_id.uom_id.id,
|
||||||
|
).price
|
||||||
|
|
||||||
|
final_price, rule_id = self.sale_subscription_id.pricelist_id.with_context(
|
||||||
|
partner_id=self.sale_subscription_id.partner_id.id,
|
||||||
|
date=fields.Datetime.now(),
|
||||||
|
uom=self.product_id.uom_id.id,
|
||||||
|
).get_product_price_rule(
|
||||||
|
product or self.product_id,
|
||||||
|
self.product_uom_qty or 1.0,
|
||||||
|
self.sale_subscription_id.partner_id,
|
||||||
|
)
|
||||||
|
base_price, currency = self.with_context(
|
||||||
|
partner_id=self.sale_subscription_id.partner_id.id,
|
||||||
|
date=fields.Datetime.now(),
|
||||||
|
uom=self.product_id.uom_id.id,
|
||||||
|
)._get_real_price_currency(
|
||||||
|
product, rule_id, self.product_uom_qty, self.product_id.uom_id
|
||||||
|
)
|
||||||
|
if currency != self.sale_subscription_id.pricelist_id.currency_id:
|
||||||
|
base_price = currency._convert(
|
||||||
|
base_price,
|
||||||
|
self.sale_subscription_id.pricelist_id.currency_id,
|
||||||
|
self.sale_subscription_id.company_id or self.env.company,
|
||||||
|
fields.Date.today(),
|
||||||
|
)
|
||||||
|
return max(base_price, final_price)
|
||||||
|
|
||||||
|
def _prepare_sale_order_line(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"product_id": self.product_id.id,
|
||||||
|
"name": self.name,
|
||||||
|
"product_uom_qty": self.product_uom_qty,
|
||||||
|
"price_unit": self.price_unit,
|
||||||
|
"discount": self.discount,
|
||||||
|
"price_subtotal": self.price_subtotal,
|
||||||
|
"tax_id": self.tax_ids,
|
||||||
|
"product_uom": self.product_id.uom_id.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _prepare_account_move_line(self):
|
||||||
|
self.ensure_one()
|
||||||
|
account = (
|
||||||
|
self.product_id.property_account_income_id
|
||||||
|
or self.product_id.categ_id.property_account_income_categ_id
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"product_id": self.product_id.id,
|
||||||
|
"name": self.name,
|
||||||
|
"quantity": self.product_uom_qty,
|
||||||
|
"price_unit": self.price_unit,
|
||||||
|
"discount": self.discount,
|
||||||
|
"price_subtotal": self.price_subtotal,
|
||||||
|
"tax_ids": [(6, 0, self.tax_ids.ids)],
|
||||||
|
"product_uom_id": self.product_id.uom_id.id,
|
||||||
|
"account_id": account.id,
|
||||||
|
}
|
||||||
29
subscription_oca/models/sale_subscription_stage.py
Normal file
29
subscription_oca/models/sale_subscription_stage.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class SaleSubscriptionStage(models.Model):
|
||||||
|
_name = "sale.subscription.stage"
|
||||||
|
_description = "Subscription stage"
|
||||||
|
_order = "sequence, name, id"
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True)
|
||||||
|
sequence = fields.Integer()
|
||||||
|
display_name = fields.Char(string="Display name")
|
||||||
|
in_progress = fields.Boolean(string="In progress", default=False)
|
||||||
|
fold = fields.Boolean(string="Kanban folded")
|
||||||
|
description = fields.Text(translate=True)
|
||||||
|
type = fields.Selection(
|
||||||
|
[("pre", "Ready to start"), ("in_progress", "In progress"), ("post", "Closed")],
|
||||||
|
default="pre",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.constrains("type")
|
||||||
|
def _check_lot_product(self):
|
||||||
|
post_stages = self.env["sale.subscription.stage"].search(
|
||||||
|
[("type", "=", "post")]
|
||||||
|
)
|
||||||
|
if len(post_stages) > 1:
|
||||||
|
raise ValidationError(_("There is already a Closed-type stage declared"))
|
||||||
10
subscription_oca/models/sale_subscription_tag.py
Normal file
10
subscription_oca/models/sale_subscription_tag.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SaleSubscriptionTag(models.Model):
|
||||||
|
_name = "sale.subscription.tag"
|
||||||
|
_description = "Tags for sale subscription"
|
||||||
|
|
||||||
|
name = fields.Char("Tag name", required=True)
|
||||||
102
subscription_oca/models/sale_subscription_template.py
Normal file
102
subscription_oca/models/sale_subscription_template.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SaleSubscriptionTemplate(models.Model):
|
||||||
|
_name = "sale.subscription.template"
|
||||||
|
_description = "Subscription templates"
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
description = fields.Text(string="Terms and conditions")
|
||||||
|
recurring_interval = fields.Integer(string="Repeat every", default=1)
|
||||||
|
recurring_rule_type = fields.Selection(
|
||||||
|
[
|
||||||
|
("days", "Day(s)"),
|
||||||
|
("weeks", "Week(s)"),
|
||||||
|
("months", "Month(s)"),
|
||||||
|
("years", "Year(s)"),
|
||||||
|
],
|
||||||
|
string="Recurrence",
|
||||||
|
default="months",
|
||||||
|
)
|
||||||
|
recurring_rule_boundary = fields.Selection(
|
||||||
|
[("unlimited", "Forever"), ("limited", "Fixed")],
|
||||||
|
string="Duration",
|
||||||
|
default="unlimited",
|
||||||
|
)
|
||||||
|
invoicing_mode = fields.Selection(
|
||||||
|
default="draft",
|
||||||
|
string="Invoicing mode",
|
||||||
|
selection=[
|
||||||
|
("draft", "Draft"),
|
||||||
|
("invoice", "Invoice"),
|
||||||
|
("invoice_send", "Invoice & send"),
|
||||||
|
("sale_and_invoice", "Sale order & Invoice"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
code = fields.Char()
|
||||||
|
recurring_rule_count = fields.Integer(default=1, string="Rule count")
|
||||||
|
invoice_mail_template_id = fields.Many2one(
|
||||||
|
comodel_name="mail.template",
|
||||||
|
string="Invoice Email",
|
||||||
|
domain="[('model', '=', 'account.move')]",
|
||||||
|
)
|
||||||
|
product_ids = fields.One2many(
|
||||||
|
comodel_name="product.template",
|
||||||
|
inverse_name="subscription_template_id",
|
||||||
|
string="Products",
|
||||||
|
)
|
||||||
|
product_ids_count = fields.Integer(
|
||||||
|
compute="_compute_product_ids_count", string="product_ids"
|
||||||
|
)
|
||||||
|
subscription_ids = fields.One2many(
|
||||||
|
comodel_name="sale.subscription",
|
||||||
|
inverse_name="template_id",
|
||||||
|
string="Subscriptions",
|
||||||
|
)
|
||||||
|
subscription_count = fields.Integer(
|
||||||
|
compute="_compute_subscription_count", string="subscription_ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_subscription_count(self):
|
||||||
|
data = self.env["sale.subscription"].read_group(
|
||||||
|
domain=[("template_id", "in", self.ids)],
|
||||||
|
fields=["template_id"],
|
||||||
|
groupby=["template_id"],
|
||||||
|
)
|
||||||
|
count_dict = {
|
||||||
|
item["template_id"][0]: item["template_id_count"] for item in data
|
||||||
|
}
|
||||||
|
for record in self:
|
||||||
|
record.subscription_count = count_dict.get(record.id, 0)
|
||||||
|
|
||||||
|
def action_view_subscription_ids(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"view_mode": "tree,form",
|
||||||
|
"res_model": "sale.subscription",
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"domain": [("id", "in", self.subscription_ids.ids)],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_date(self, date_start):
|
||||||
|
self.ensure_one()
|
||||||
|
return relativedelta(months=+self.recurring_rule_count) + date_start
|
||||||
|
|
||||||
|
@api.depends("product_ids")
|
||||||
|
def _compute_product_ids_count(self):
|
||||||
|
for record in self:
|
||||||
|
record.product_ids_count = len(self.product_ids)
|
||||||
|
|
||||||
|
def action_view_product_ids(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"view_type": "form",
|
||||||
|
"view_mode": "tree,form",
|
||||||
|
"res_model": "product.template",
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"domain": [("id", "in", self.product_ids.ids)],
|
||||||
|
}
|
||||||
1
subscription_oca/readme/CONTRIBUTORS.rst
Normal file
1
subscription_oca/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* Carlos Martínez <carlos@domatix.com>
|
||||||
1
subscription_oca/readme/DESCRIPTION.rst
Normal file
1
subscription_oca/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This module allows creating subscriptions that generate recurring invoices or orders. It also enables the sale of products that generate subscriptions.
|
||||||
2
subscription_oca/readme/ROADMAP.rst
Normal file
2
subscription_oca/readme/ROADMAP.rst
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
* Refactor all the onchanges that have business logic to computed write-able fields when possible. Keep onchanges only for UI purposes.
|
||||||
|
* Add tests.
|
||||||
13
subscription_oca/readme/USAGE.rst
Normal file
13
subscription_oca/readme/USAGE.rst
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
To make a subscription:
|
||||||
|
|
||||||
|
#. Go to *Subscriptions > Configuration > Subscription templates*.
|
||||||
|
#. Create the templates you consider, choosing the billing frequency: daily, monthly... and the method of creating the invoice and/or order.
|
||||||
|
#. Go to *Subscription > Subscriptions*.
|
||||||
|
#. Create a subscription and indicate the start date. When the *Subscriptions Management* cron job is executed, the subscription will begin and the first invoice will be created if the execution date matches the start date. The invoice will also be created when the execution date matches the next invoice date. Additionally, you can manually change the subscription status and create an invoice.
|
||||||
|
#. The cron job will also end the subscription if its end date has been reached.
|
||||||
|
|
||||||
|
To create subscriptions with the sale of a product:
|
||||||
|
|
||||||
|
#. Go to *Subscriptions > Subscriptions > Products*.
|
||||||
|
#. Create the product and in the sales tab, complete the fields *Subscribable product* and *Subscription template*
|
||||||
|
#. Create a sales order with the product and confirm it.
|
||||||
8
subscription_oca/security/ir.model.access.csv
Normal file
8
subscription_oca/security/ir.model.access.csv
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_custom_sale_subscription_template,sale.subscription.template,model_sale_subscription_template,sales_team.group_sale_salesman,1,1,1,1
|
||||||
|
access_custom_sale_subscription,sale.subscription,model_sale_subscription,sales_team.group_sale_salesman,1,1,1,1
|
||||||
|
access_custom_sale_subscription_close_reason,sale.subscription.close.reason,model_sale_subscription_close_reason,sales_team.group_sale_salesman,1,1,1,1
|
||||||
|
access_custom_sale_subscription_stage,sale.subscription.stage,model_sale_subscription_stage,sales_team.group_sale_salesman,1,1,1,1
|
||||||
|
access_custom_sale_subscription_line,sale.subscription.line,model_sale_subscription_line,sales_team.group_sale_salesman,1,1,1,1
|
||||||
|
access_custom_sale_subscription_tag,sale.subscription.tag,model_sale_subscription_tag,sales_team.group_sale_salesman,1,1,1,1
|
||||||
|
access_close_subscription,Close subscription access,model_close_reason_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||||
|
439
subscription_oca/static/description/index.html
Normal file
439
subscription_oca/static/description/index.html
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
|
||||||
|
<title>Subscription management</title>
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
/*
|
||||||
|
:Author: David Goodger (goodger@python.org)
|
||||||
|
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||||
|
:Copyright: This stylesheet has been placed in the public domain.
|
||||||
|
|
||||||
|
Default cascading style sheet for the HTML output of Docutils.
|
||||||
|
|
||||||
|
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||||
|
customize this style sheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* used to remove borders from tables and images */
|
||||||
|
.borderless, table.borderless td, table.borderless th {
|
||||||
|
border: 0 }
|
||||||
|
|
||||||
|
table.borderless td, table.borderless th {
|
||||||
|
/* Override padding for "table.docutils td" with "! important".
|
||||||
|
The right padding separates the table cells. */
|
||||||
|
padding: 0 0.5em 0 0 ! important }
|
||||||
|
|
||||||
|
.first {
|
||||||
|
/* Override more specific margin styles with "! important". */
|
||||||
|
margin-top: 0 ! important }
|
||||||
|
|
||||||
|
.last, .with-subtitle {
|
||||||
|
margin-bottom: 0 ! important }
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none }
|
||||||
|
|
||||||
|
.subscript {
|
||||||
|
vertical-align: sub;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
.superscript {
|
||||||
|
vertical-align: super;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
a.toc-backref {
|
||||||
|
text-decoration: none ;
|
||||||
|
color: black }
|
||||||
|
|
||||||
|
blockquote.epigraph {
|
||||||
|
margin: 2em 5em ; }
|
||||||
|
|
||||||
|
dl.docutils dd {
|
||||||
|
margin-bottom: 0.5em }
|
||||||
|
|
||||||
|
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||||
|
dl.docutils dt {
|
||||||
|
font-weight: bold }
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.abstract {
|
||||||
|
margin: 2em 5em }
|
||||||
|
|
||||||
|
div.abstract p.topic-title {
|
||||||
|
font-weight: bold ;
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||||
|
div.hint, div.important, div.note, div.tip, div.warning {
|
||||||
|
margin: 2em ;
|
||||||
|
border: medium outset ;
|
||||||
|
padding: 1em }
|
||||||
|
|
||||||
|
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||||
|
div.important p.admonition-title, div.note p.admonition-title,
|
||||||
|
div.tip p.admonition-title {
|
||||||
|
font-weight: bold ;
|
||||||
|
font-family: sans-serif }
|
||||||
|
|
||||||
|
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||||
|
div.danger p.admonition-title, div.error p.admonition-title,
|
||||||
|
div.warning p.admonition-title, .code .error {
|
||||||
|
color: red ;
|
||||||
|
font-weight: bold ;
|
||||||
|
font-family: sans-serif }
|
||||||
|
|
||||||
|
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||||
|
compound paragraphs.
|
||||||
|
div.compound .compound-first, div.compound .compound-middle {
|
||||||
|
margin-bottom: 0.5em }
|
||||||
|
|
||||||
|
div.compound .compound-last, div.compound .compound-middle {
|
||||||
|
margin-top: 0.5em }
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.dedication {
|
||||||
|
margin: 2em 5em ;
|
||||||
|
text-align: center ;
|
||||||
|
font-style: italic }
|
||||||
|
|
||||||
|
div.dedication p.topic-title {
|
||||||
|
font-weight: bold ;
|
||||||
|
font-style: normal }
|
||||||
|
|
||||||
|
div.figure {
|
||||||
|
margin-left: 2em ;
|
||||||
|
margin-right: 2em }
|
||||||
|
|
||||||
|
div.footer, div.header {
|
||||||
|
clear: both;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
div.line-block {
|
||||||
|
display: block ;
|
||||||
|
margin-top: 1em ;
|
||||||
|
margin-bottom: 1em }
|
||||||
|
|
||||||
|
div.line-block div.line-block {
|
||||||
|
margin-top: 0 ;
|
||||||
|
margin-bottom: 0 ;
|
||||||
|
margin-left: 1.5em }
|
||||||
|
|
||||||
|
div.sidebar {
|
||||||
|
margin: 0 0 0.5em 1em ;
|
||||||
|
border: medium outset ;
|
||||||
|
padding: 1em ;
|
||||||
|
background-color: #ffffee ;
|
||||||
|
width: 40% ;
|
||||||
|
float: right ;
|
||||||
|
clear: right }
|
||||||
|
|
||||||
|
div.sidebar p.rubric {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-size: medium }
|
||||||
|
|
||||||
|
div.system-messages {
|
||||||
|
margin: 5em }
|
||||||
|
|
||||||
|
div.system-messages h1 {
|
||||||
|
color: red }
|
||||||
|
|
||||||
|
div.system-message {
|
||||||
|
border: medium outset ;
|
||||||
|
padding: 1em }
|
||||||
|
|
||||||
|
div.system-message p.system-message-title {
|
||||||
|
color: red ;
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
div.topic {
|
||||||
|
margin: 2em }
|
||||||
|
|
||||||
|
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||||
|
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||||
|
margin-top: 0.4em }
|
||||||
|
|
||||||
|
h1.title {
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
h2.subtitle {
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
hr.docutils {
|
||||||
|
width: 75% }
|
||||||
|
|
||||||
|
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||||
|
clear: left ;
|
||||||
|
float: left ;
|
||||||
|
margin-right: 1em }
|
||||||
|
|
||||||
|
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||||
|
clear: right ;
|
||||||
|
float: right ;
|
||||||
|
margin-left: 1em }
|
||||||
|
|
||||||
|
img.align-center, .figure.align-center, object.align-center {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.align-center {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left }
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
clear: both ;
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right }
|
||||||
|
|
||||||
|
/* reset inner alignment in figures */
|
||||||
|
div.align-right {
|
||||||
|
text-align: inherit }
|
||||||
|
|
||||||
|
/* div.align-center * { */
|
||||||
|
/* text-align: left } */
|
||||||
|
|
||||||
|
.align-top {
|
||||||
|
vertical-align: top }
|
||||||
|
|
||||||
|
.align-middle {
|
||||||
|
vertical-align: middle }
|
||||||
|
|
||||||
|
.align-bottom {
|
||||||
|
vertical-align: bottom }
|
||||||
|
|
||||||
|
ol.simple, ul.simple {
|
||||||
|
margin-bottom: 1em }
|
||||||
|
|
||||||
|
ol.arabic {
|
||||||
|
list-style: decimal }
|
||||||
|
|
||||||
|
ol.loweralpha {
|
||||||
|
list-style: lower-alpha }
|
||||||
|
|
||||||
|
ol.upperalpha {
|
||||||
|
list-style: upper-alpha }
|
||||||
|
|
||||||
|
ol.lowerroman {
|
||||||
|
list-style: lower-roman }
|
||||||
|
|
||||||
|
ol.upperroman {
|
||||||
|
list-style: upper-roman }
|
||||||
|
|
||||||
|
p.attribution {
|
||||||
|
text-align: right ;
|
||||||
|
margin-left: 50% }
|
||||||
|
|
||||||
|
p.caption {
|
||||||
|
font-style: italic }
|
||||||
|
|
||||||
|
p.credits {
|
||||||
|
font-style: italic ;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
p.label {
|
||||||
|
white-space: nowrap }
|
||||||
|
|
||||||
|
p.rubric {
|
||||||
|
font-weight: bold ;
|
||||||
|
font-size: larger ;
|
||||||
|
color: maroon ;
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
p.sidebar-title {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-weight: bold ;
|
||||||
|
font-size: larger }
|
||||||
|
|
||||||
|
p.sidebar-subtitle {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
p.topic-title {
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
pre.address {
|
||||||
|
margin-bottom: 0 ;
|
||||||
|
margin-top: 0 ;
|
||||||
|
font: inherit }
|
||||||
|
|
||||||
|
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||||
|
margin-left: 2em ;
|
||||||
|
margin-right: 2em }
|
||||||
|
|
||||||
|
pre.code .ln { color: grey; } /* line numbers */
|
||||||
|
pre.code, code { background-color: #eeeeee }
|
||||||
|
pre.code .comment, code .comment { color: #5C6576 }
|
||||||
|
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||||
|
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||||
|
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||||
|
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||||
|
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||||
|
|
||||||
|
span.classifier {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-style: oblique }
|
||||||
|
|
||||||
|
span.classifier-delimiter {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
span.interpreted {
|
||||||
|
font-family: sans-serif }
|
||||||
|
|
||||||
|
span.option {
|
||||||
|
white-space: nowrap }
|
||||||
|
|
||||||
|
span.pre {
|
||||||
|
white-space: pre }
|
||||||
|
|
||||||
|
span.problematic {
|
||||||
|
color: red }
|
||||||
|
|
||||||
|
span.section-subtitle {
|
||||||
|
/* font-size relative to parent (h1..h6 element) */
|
||||||
|
font-size: 80% }
|
||||||
|
|
||||||
|
table.citation {
|
||||||
|
border-left: solid 1px gray;
|
||||||
|
margin-left: 1px }
|
||||||
|
|
||||||
|
table.docinfo {
|
||||||
|
margin: 2em 4em }
|
||||||
|
|
||||||
|
table.docutils {
|
||||||
|
margin-top: 0.5em ;
|
||||||
|
margin-bottom: 0.5em }
|
||||||
|
|
||||||
|
table.footnote {
|
||||||
|
border-left: solid 1px black;
|
||||||
|
margin-left: 1px }
|
||||||
|
|
||||||
|
table.docutils td, table.docutils th,
|
||||||
|
table.docinfo td, table.docinfo th {
|
||||||
|
padding-left: 0.5em ;
|
||||||
|
padding-right: 0.5em ;
|
||||||
|
vertical-align: top }
|
||||||
|
|
||||||
|
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||||
|
font-weight: bold ;
|
||||||
|
text-align: left ;
|
||||||
|
white-space: nowrap ;
|
||||||
|
padding-left: 0 }
|
||||||
|
|
||||||
|
/* "booktabs" style (no vertical lines) */
|
||||||
|
table.docutils.booktabs {
|
||||||
|
border: 0px;
|
||||||
|
border-top: 2px solid;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
table.docutils.booktabs * {
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
table.docutils.booktabs th {
|
||||||
|
border-bottom: thin solid;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||||
|
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||||
|
font-size: 100% }
|
||||||
|
|
||||||
|
ul.auto-toc {
|
||||||
|
list-style-type: none }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="document" id="subscription-management">
|
||||||
|
<h1 class="title">Subscription management</h1>
|
||||||
|
|
||||||
|
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!! This file is generated by oca-gen-addon-readme !!
|
||||||
|
!! changes will be overwritten. !!
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!! source digest: sha256:7118b690b12a3d475a89b42c8caf9802168a5c9de8d668e84db674bf9fe382fa
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||||
|
<p><a class="reference external" 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" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/sale-workflow/tree/15.0/subscription_oca"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/sale-workflow-15-0/sale-workflow-15-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" href="https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&target_branch=15.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><strong>Table of contents</strong></p>
|
||||||
|
<div class="contents local topic" id="contents">
|
||||||
|
<ul class="simple">
|
||||||
|
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
|
||||||
|
<li><a class="reference internal" href="#bug-tracker" id="id2">Bug Tracker</a></li>
|
||||||
|
<li><a class="reference internal" href="#credits" id="id3">Credits</a><ul>
|
||||||
|
<li><a class="reference internal" href="#authors" id="id4">Authors</a></li>
|
||||||
|
<li><a class="reference internal" href="#contributors" id="id5">Contributors</a></li>
|
||||||
|
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="usage">
|
||||||
|
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
|
||||||
|
<p>To make a subscription:</p>
|
||||||
|
<ol class="arabic simple">
|
||||||
|
<li>Go to <em>Subscriptions > Configuration > Subscription templates</em>.</li>
|
||||||
|
<li>Create the templates you consider, choosing the billing frequency: daily, monthly… and the method of creating the invoice and/or order.</li>
|
||||||
|
<li>Go to <em>Subscription > Subscriptions</em>.</li>
|
||||||
|
<li>Create a subscription and indicate the start date. When the <em>Subscriptions Management</em> cron job is executed, the subscription will begin and the first invoice will be created if the execution date matches the start date. The invoice will also be created when the execution date matches the next invoice date. Additionally, you can manually change the subscription status and create an invoice.</li>
|
||||||
|
<li>The cron job will also end the subscription if its end date has been reached.</li>
|
||||||
|
</ol>
|
||||||
|
<p>To create subscriptions with the sale of a product:</p>
|
||||||
|
<ol class="arabic simple">
|
||||||
|
<li>Go to <em>Subscriptions > Subscriptions > Products</em>.</li>
|
||||||
|
<li>Create the product and in the sales tab, complete the fields <em>Subscribable product</em> and <em>Subscription template</em></li>
|
||||||
|
<li>Create a sales order with the product and confirm it.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="bug-tracker">
|
||||||
|
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
|
||||||
|
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/sale-workflow/issues">GitHub Issues</a>.
|
||||||
|
In case of trouble, please check there if your issue has already been reported.
|
||||||
|
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||||
|
<a class="reference external" href="https://github.com/OCA/sale-workflow/issues/new?body=module:%20subscription_oca%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||||
|
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="credits">
|
||||||
|
<h1><a class="toc-backref" href="#id3">Credits</a></h1>
|
||||||
|
<div class="section" id="authors">
|
||||||
|
<h2><a class="toc-backref" href="#id4">Authors</a></h2>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>Domatix</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="contributors">
|
||||||
|
<h2><a class="toc-backref" href="#id5">Contributors</a></h2>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>Carlos Martínez <<a class="reference external" href="mailto:carlos@domatix.com">carlos@domatix.com</a>></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="maintainers">
|
||||||
|
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
|
||||||
|
<p>This module is maintained by the OCA.</p>
|
||||||
|
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||||
|
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
|
mission is to support the collaborative development of Odoo features and
|
||||||
|
promote its widespread use.</p>
|
||||||
|
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/sale-workflow/tree/15.0/subscription_oca">OCA/sale-workflow</a> project on GitHub.</p>
|
||||||
|
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
subscription_oca/static/img/icon.png
Normal file
BIN
subscription_oca/static/img/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
20
subscription_oca/views/product_template_views.xml
Normal file
20
subscription_oca/views/product_template_views.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="product_template_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">product.template.sub.form</field>
|
||||||
|
<field name="model">product.template</field>
|
||||||
|
<field name="priority" eval="8" />
|
||||||
|
<field name="inherit_id" ref="product.product_template_form_view" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//page[@name='sales']" position="inside">
|
||||||
|
<group>
|
||||||
|
<field name="subscribable" />
|
||||||
|
<field
|
||||||
|
name="subscription_template_id"
|
||||||
|
attrs="{'invisible': [('subscribable', '=', False)]}"
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
28
subscription_oca/views/res_partner_views.xml
Normal file
28
subscription_oca/views/res_partner_views.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Inherit Form View to Modify it -->
|
||||||
|
<record id="res_partner_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.form</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<button name="action_view_partner_invoices" position="after">
|
||||||
|
<field name="subscription_ids" invisible="1" />
|
||||||
|
<button
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-recycle"
|
||||||
|
name="action_view_subscription_ids"
|
||||||
|
attrs="{'invisible': [('subscription_ids', '=', False)]}"
|
||||||
|
>
|
||||||
|
<div class="o_form_field o_stat_info">
|
||||||
|
<span class="o_stat_value">
|
||||||
|
<field name="subscription_count" />
|
||||||
|
</span>
|
||||||
|
<span class="o_stat_text">Subscriptions</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</button>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
29
subscription_oca/views/sale_order_views.xml
Normal file
29
subscription_oca/views/sale_order_views.xml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding='UTF-8' ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_sale_order_form" model="ir.ui.view">
|
||||||
|
<field name="name">view.sale_order.form</field>
|
||||||
|
<field name="model">sale.order</field>
|
||||||
|
<field name="inherit_id" ref="sale.view_order_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button
|
||||||
|
name="action_view_subscriptions"
|
||||||
|
attrs="{'invisible': [('subscriptions_count', '=', 0)]}"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-pencil-square-o"
|
||||||
|
>
|
||||||
|
<field
|
||||||
|
name="subscriptions_count"
|
||||||
|
widget="statinfo"
|
||||||
|
string="Subscriptions"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
73
subscription_oca/views/sale_subscription_stage_views.xml
Normal file
73
subscription_oca/views/sale_subscription_stage_views.xml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_subscription_stage_form" model="ir.ui.view">
|
||||||
|
<field name="name">view.subscription.stage.form</field>
|
||||||
|
<field name="model">sale.subscription.stage</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Subscription stage">
|
||||||
|
<sheet>
|
||||||
|
<group name="main">
|
||||||
|
<group>
|
||||||
|
<field name="name" />
|
||||||
|
<field name="in_progress" invisible="True" />
|
||||||
|
<field
|
||||||
|
name="sequence"
|
||||||
|
help="
|
||||||
|
This field dictates the stages' visual order on the Kanban and form view. Although is purely visual, mind that if the order isn't
|
||||||
|
consequent with your needs, you could have a 'Closed'-type stage before a 'Ready to start' one.
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="type"
|
||||||
|
help="Stages define the life-cycle of a given subscription; this is,
|
||||||
|
a subscription can be a 'Ready to start', 'In progress' or 'Closed' type of stage.
|
||||||
|
Bear in mind that there can only be one 'Closed'-type stage."
|
||||||
|
/>
|
||||||
|
<field name="fold" />
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field
|
||||||
|
name="description"
|
||||||
|
placeholder="Add new description..."
|
||||||
|
nolabel="1"
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_subscription_stage_tree" model="ir.ui.view">
|
||||||
|
<field name="name">view.subscription.stage.tree</field>
|
||||||
|
<field name="model">sale.subscription.stage</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree>
|
||||||
|
<field name="name" />
|
||||||
|
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="subscription_stage_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Susbcription stages</field>
|
||||||
|
<field name="res_model">sale.subscription.stage</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="oe_view_nocontent_create">
|
||||||
|
Click to create a new subscription stage.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="subscription_stage_menu"
|
||||||
|
parent="sale_subscription_configuration_menu"
|
||||||
|
action="subscription_stage_action"
|
||||||
|
sequence="20"
|
||||||
|
name="Subscription stages"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
27
subscription_oca/views/sale_subscription_tag_views.xml
Normal file
27
subscription_oca/views/sale_subscription_tag_views.xml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_sale_subscription_tag_tree" model="ir.ui.view">
|
||||||
|
<field name="name">view.sale.subscription.tag.tree</field>
|
||||||
|
<field name="model">sale.subscription.tag</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree editable="top">
|
||||||
|
<field name="name" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="subscription_tag_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Tags</field>
|
||||||
|
<field name="res_model">sale.subscription.tag</field>
|
||||||
|
<field name="view_mode">tree</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="subscription_tag_menu"
|
||||||
|
parent="sale_subscription_configuration_menu"
|
||||||
|
action="subscription_tag_action"
|
||||||
|
sequence="40"
|
||||||
|
name="Subscription tags"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
135
subscription_oca/views/sale_subscription_template_views.xml
Normal file
135
subscription_oca/views/sale_subscription_template_views.xml
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="sale_subscription_template_form" model="ir.ui.view">
|
||||||
|
<field name="name">sale.subscription.template.form</field>
|
||||||
|
<field name="model">sale.subscription.template</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button
|
||||||
|
name="action_view_product_ids"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-pencil-square-o"
|
||||||
|
>
|
||||||
|
<field
|
||||||
|
name="product_ids_count"
|
||||||
|
widget="statinfo"
|
||||||
|
string="Products"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
name="action_view_subscription_ids"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-pencil-square-o"
|
||||||
|
>
|
||||||
|
<field
|
||||||
|
name="subscription_count"
|
||||||
|
widget="statinfo"
|
||||||
|
string="Subscriptions"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field
|
||||||
|
name="name"
|
||||||
|
placeholder="e.g. Monthly Subscription"
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<page string="Invoicing">
|
||||||
|
<group>
|
||||||
|
<group name="left_group">
|
||||||
|
<label for="recurring_interval" string="Invoice" />
|
||||||
|
<div class="o_row oe_inline">
|
||||||
|
<span class="every">Every</span>
|
||||||
|
<field name="recurring_interval" colspan="2" />
|
||||||
|
<field
|
||||||
|
name="recurring_rule_type"
|
||||||
|
colspan="2"
|
||||||
|
required="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<field
|
||||||
|
name="recurring_rule_boundary"
|
||||||
|
widget="radio"
|
||||||
|
options="{'horizontal':true}"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="recurring_rule_count"
|
||||||
|
attrs="{'invisible': [('recurring_rule_boundary','=','unlimited')]}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="o_row "
|
||||||
|
attrs="{'invisible': [('recurring_rule_boundary','=','unlimited')]}"
|
||||||
|
>
|
||||||
|
<span class="mr-1">For</span>
|
||||||
|
<field
|
||||||
|
name="recurring_rule_count"
|
||||||
|
class="oe_inline"
|
||||||
|
/>
|
||||||
|
month(s)
|
||||||
|
</div>
|
||||||
|
<field name="invoicing_mode" widget="radio" />
|
||||||
|
<field
|
||||||
|
name="invoice_mail_template_id"
|
||||||
|
attrs="{'invisible': [('invoicing_mode','!=','invoice_send')], 'required': [('invoicing_mode', '=', 'invoice_send')]}"
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
<group name="right_group">
|
||||||
|
<field name="code" readonly="0" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Terms and Conditions">
|
||||||
|
<group>
|
||||||
|
<field
|
||||||
|
nolabel="1"
|
||||||
|
name="description"
|
||||||
|
placeholder="Terms and Conditions"
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
|
||||||
|
</sheet>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sale_subscription_template_tree" model="ir.ui.view">
|
||||||
|
<field name="name">sale.subscription.template.tree</field>
|
||||||
|
<field name="model">sale.subscription.template</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree>
|
||||||
|
<field name="name" />
|
||||||
|
<field name="recurring_interval" />
|
||||||
|
<field name="recurring_rule_type" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sale_subscription_template_act_window" model="ir.actions.act_window">
|
||||||
|
<field name="name">Subscription templates</field>
|
||||||
|
<field name="res_model">sale.subscription.template</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="sale_template_subscription_menu"
|
||||||
|
parent="sale_subscription_configuration_menu"
|
||||||
|
action="sale_subscription_template_act_window"
|
||||||
|
sequence="2"
|
||||||
|
/>
|
||||||
|
</odoo>
|
||||||
473
subscription_oca/views/sale_subscription_views.xml
Normal file
473
subscription_oca/views/sale_subscription_views.xml
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="sale_subscription_form" model="ir.ui.view">
|
||||||
|
<field name="name">sale.subscription.form</field>
|
||||||
|
<field name="model">sale.subscription</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="New subscription">
|
||||||
|
<header>
|
||||||
|
<button
|
||||||
|
string="Create Invoice"
|
||||||
|
name="manual_invoice"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
string="Close subscription"
|
||||||
|
class="btn-danger"
|
||||||
|
name="action_close_subscription"
|
||||||
|
type="object"
|
||||||
|
help="A given subscription can be marked as closed when, for example, renewal is not desired."
|
||||||
|
attrs="{'invisible': [('in_progress', '=', False)]}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<field
|
||||||
|
name="stage_id"
|
||||||
|
widget="statusbar"
|
||||||
|
clickable="1"
|
||||||
|
options="{'fold_field': 'fold'}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button
|
||||||
|
name="action_view_sale_order_ids"
|
||||||
|
type="object"
|
||||||
|
attrs="{'invisible': [('sale_order_ids_count', '=', 0)]}"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-pencil-square-o"
|
||||||
|
>
|
||||||
|
<field
|
||||||
|
name="sale_order_ids_count"
|
||||||
|
widget="statinfo"
|
||||||
|
string="Sales"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
name="action_view_account_invoice_ids"
|
||||||
|
attrs="{'invisible': [('account_invoice_ids_count', '=', 0)]}"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-pencil-square-o"
|
||||||
|
>
|
||||||
|
<field
|
||||||
|
name="account_invoice_ids_count"
|
||||||
|
widget="statinfo"
|
||||||
|
string="Invoices"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<widget
|
||||||
|
name="web_ribbon"
|
||||||
|
text="Archived"
|
||||||
|
bg_color="bg-danger"
|
||||||
|
attrs="{'invisible': [('active', '=', True)]}"
|
||||||
|
/>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1 class="flex-fill d-flex flex-row">
|
||||||
|
<field name="name" force_save="1" />
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group name="left_group">
|
||||||
|
<field name="active" invisible="1" />
|
||||||
|
<field name="in_progress" invisible="True" />
|
||||||
|
<field name="partner_id" />
|
||||||
|
<field name="currency_id" invisible="1" />
|
||||||
|
<field name="pricelist_id" />
|
||||||
|
<field
|
||||||
|
name="date_start"
|
||||||
|
attrs="{'readonly':['|',('active','=',False), ('in_progress', '=', True)]}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="date"
|
||||||
|
attrs="{'invisible':[('recurring_rule_boundary', '=', True)]}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="close_reason_id"
|
||||||
|
attrs="{'invisible': [('active', '=', True)]}"
|
||||||
|
/>
|
||||||
|
<field name="recurring_rule_boundary" invisible="1" />
|
||||||
|
|
||||||
|
</group>
|
||||||
|
<group name="right_group">
|
||||||
|
|
||||||
|
<field name="template_id" />
|
||||||
|
<field name="crm_team_id" />
|
||||||
|
<field
|
||||||
|
name="recurring_next_date"
|
||||||
|
attrs="{'invisible': ['|', ('recurring_next_date', '=', False), ('in_progress', '=', False)]}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="company_id"
|
||||||
|
options="{'no_create': True}"
|
||||||
|
groups="base.group_multi_company"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page
|
||||||
|
string="Subscription lines"
|
||||||
|
name="subscription_lines_page"
|
||||||
|
>
|
||||||
|
<field name="sale_subscription_line_ids">
|
||||||
|
<tree editable="bottom">
|
||||||
|
<field name="product_id" required="True" />
|
||||||
|
<field
|
||||||
|
name="name"
|
||||||
|
required="True"
|
||||||
|
widget="section_and_note_text"
|
||||||
|
/>
|
||||||
|
<field name="currency_id" invisible="1" />
|
||||||
|
<field name="product_uom_qty" required="True" />
|
||||||
|
<field name="price_unit" required="True" />
|
||||||
|
<field name="discount" required="True" />
|
||||||
|
<field name="tax_ids" widget="many2many_tags" />
|
||||||
|
<field
|
||||||
|
name="price_subtotal"
|
||||||
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="price_total"
|
||||||
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
<group
|
||||||
|
class="oe_subtotal_footer oe_right"
|
||||||
|
colspan="2"
|
||||||
|
name="subscription_total"
|
||||||
|
>
|
||||||
|
<field
|
||||||
|
name="recurring_total"
|
||||||
|
widget="monetary"
|
||||||
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="amount_tax"
|
||||||
|
widget="monetary"
|
||||||
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="oe_subtotal_footer_separator oe_inline o_td_label"
|
||||||
|
>
|
||||||
|
<label for="amount_total" />
|
||||||
|
</div>
|
||||||
|
<field
|
||||||
|
name="amount_total"
|
||||||
|
nolabel="1"
|
||||||
|
class="oe_subtotal_footer_separator"
|
||||||
|
widget="monetary"
|
||||||
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Misc" name="misc_page">
|
||||||
|
<group>
|
||||||
|
<field name="description" />
|
||||||
|
<field name="terms" />
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Other info" name="other_info_page">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="code" />
|
||||||
|
<field name="tag_ids" widget="many2many_tags" />
|
||||||
|
<field name="sale_order_id" />
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field
|
||||||
|
name="journal_id"
|
||||||
|
domain="[('type', '=', 'sale')]"
|
||||||
|
/>
|
||||||
|
<field name="fiscal_position_id" />
|
||||||
|
<field name="user_id" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
|
||||||
|
</sheet>
|
||||||
|
|
||||||
|
<div class="oe_chatter">
|
||||||
|
<field name="message_follower_ids" widget="mail_followers" />
|
||||||
|
<field name="activity_ids" widget="mail_activity" />
|
||||||
|
<field name="message_ids" widget="mail_thread" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sale_subscription_tree" model="ir.ui.view">
|
||||||
|
<field name="name">sale.subscription.tree</field>
|
||||||
|
<field name="model">sale.subscription</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree>
|
||||||
|
<field name="name" optional="show" />
|
||||||
|
<field name="partner_id" optional="show" />
|
||||||
|
<field name="recurring_next_date" optional="show" />
|
||||||
|
<field name="code" optional="show" />
|
||||||
|
<field name="user_id" optional="show" />
|
||||||
|
<field name="recurring_total" sum="Total subtotal" optional="show" />
|
||||||
|
<field name="amount_tax" sum="Total Tax" optional="show" />
|
||||||
|
<field name="amount_total" sum="Total" optional="show" />
|
||||||
|
<field name="template_id" optional="show" />
|
||||||
|
<field name="stage_str" optional="show" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="sale_subscription_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">sale.subscription.kanban</field>
|
||||||
|
<field name="model">sale.subscription</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban
|
||||||
|
default_group_by="stage_id"
|
||||||
|
default_order="write_date,sequence"
|
||||||
|
class="o_kanban_small_column o_opportunity_kanban"
|
||||||
|
>
|
||||||
|
<!-- <field name="stage_id" options='{"group_by_tooltip": {"requirements": "Description"}}'/> -->
|
||||||
|
<field name="stage_id" />
|
||||||
|
<field name="code" />
|
||||||
|
<field name="write_date" />
|
||||||
|
<field name="sequence" invisible="1" />
|
||||||
|
<field name="partner_id" />
|
||||||
|
<field name="user_id" />
|
||||||
|
<field name="recurring_total" />
|
||||||
|
<field name="currency_id" />
|
||||||
|
<field name="activity_state" />
|
||||||
|
<field name="tag_ids" />
|
||||||
|
<field name="amount_total" />
|
||||||
|
<field name="activity_ids" />
|
||||||
|
<field name="image" />
|
||||||
|
<field name="color" />
|
||||||
|
<progressbar
|
||||||
|
field="activity_state"
|
||||||
|
colors="{"planned": "success", "today": "warning", "overdue": "danger"}"
|
||||||
|
sum_field="recurring_total"
|
||||||
|
help="This bar allows to filter the opportunities based on scheduled activities."
|
||||||
|
/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="kanban-box">
|
||||||
|
<div
|
||||||
|
t-attf-class="
|
||||||
|
#{!selection_mode ? kanban_color(record.color.raw_value) : ''}
|
||||||
|
oe_kanban_global_click_edit
|
||||||
|
oe_semantic_html_override
|
||||||
|
oe_kanban_card"
|
||||||
|
>
|
||||||
|
<div class="o_dropdown_kanban dropdown" modifiers="{}">
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
class="dropdown-toggle o-no-caret btn"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
data-display="static"
|
||||||
|
href="#"
|
||||||
|
aria-label="Menú desplegable"
|
||||||
|
title="Menú desplegable"
|
||||||
|
modifiers="{}"
|
||||||
|
>
|
||||||
|
<span class="fa fa-ellipsis-v" modifiers="{}" />
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu" role="menu" modifiers="{}">
|
||||||
|
<a
|
||||||
|
role="menuitem"
|
||||||
|
class="dropdown-item oe_kanban_action oe_kanban_action_a"
|
||||||
|
modifiers="{}"
|
||||||
|
data-type="edit"
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
role="menuitem"
|
||||||
|
class="dropdown-item oe_kanban_action oe_kanban_action_a"
|
||||||
|
modifiers="{}"
|
||||||
|
data-type="delete"
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="oe_kanban_colorpicker"
|
||||||
|
data-field="color"
|
||||||
|
modifiers="{}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_kanban_content">
|
||||||
|
<div>
|
||||||
|
<field name="name" />
|
||||||
|
<strong>
|
||||||
|
<div>
|
||||||
|
<field
|
||||||
|
name="recurring_total"
|
||||||
|
widget="monetary"
|
||||||
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<field name="tag_ids" widget="many2many_tags" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-muted o_kanban_record_subtitle">
|
||||||
|
<t t-if="record.amount_total.raw_value">
|
||||||
|
<field
|
||||||
|
name="amount_total"
|
||||||
|
widget="monetary"
|
||||||
|
options="{'currency_field': 'company_currency'}"
|
||||||
|
/>
|
||||||
|
<span t-if="record.partner_id.value">,</span>
|
||||||
|
</t>
|
||||||
|
<span t-if="record.partner_id.value">
|
||||||
|
<t t-esc="record.partner_id.value" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_kanban_record_bottom">
|
||||||
|
<div class="oe_kanban_bottom_left">
|
||||||
|
<field
|
||||||
|
name="activity_ids"
|
||||||
|
widget="kanban_activity"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="oe_kanban_bottom_right">
|
||||||
|
<t t-set="unassigned">
|
||||||
|
<t t-esc="_t('Unassigned')" />
|
||||||
|
</t>
|
||||||
|
<img
|
||||||
|
t-att-src="kanban_image('res.users', 'image', record.user_id.raw_value)"
|
||||||
|
t-att-title="record.user_id.value || unassigned"
|
||||||
|
t-att-alt="record.user_id.value"
|
||||||
|
class="oe_kanban_avatar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_sale_order_pending_filter" model="ir.ui.view">
|
||||||
|
<field name="name">sale.order.pending.filter</field>
|
||||||
|
<field name="model">sale.subscription</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="to_renew" />
|
||||||
|
<filter
|
||||||
|
string="Pending subscriptions"
|
||||||
|
name="pendingsubs"
|
||||||
|
domain="[('to_renew','=', True)]"
|
||||||
|
/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_product_suscribable_filter" model="ir.ui.view">
|
||||||
|
<field name="name">product.suscribable.filter</field>
|
||||||
|
<field name="model">product.template</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="subscribable" />
|
||||||
|
<filter
|
||||||
|
string="Suscribable products"
|
||||||
|
name="subsproducts"
|
||||||
|
domain="[('subscribable','=', True)]"
|
||||||
|
/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_subscription_close_reason_tree" model="ir.ui.view">
|
||||||
|
<field name="name">view.subscription.close.reason.tree</field>
|
||||||
|
<field name="model">sale.subscription.close.reason</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree editable="bottom">
|
||||||
|
<field name="name" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="subscription_close_reason_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Close reasons</field>
|
||||||
|
<field name="res_model">sale.subscription.close.reason</field>
|
||||||
|
<field name="view_mode">tree</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sale_subscription_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Subscriptions</field>
|
||||||
|
<field name="res_model">sale.subscription</field>
|
||||||
|
<field name="view_mode">tree,kanban,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="subscription_product_template_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Products</field>
|
||||||
|
<field name="res_model">product.template</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="context">
|
||||||
|
{'search_default_subsproducts': True, "default_type": "service", "default_subscribable": True}
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="sale_subscription_root"
|
||||||
|
groups="sales_team.group_sale_salesman_all_leads"
|
||||||
|
name="Subscriptions"
|
||||||
|
web_icon="subscription_oca,static/img/icon.png"
|
||||||
|
sequence="7"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="sale_subscription_configuration_menu"
|
||||||
|
parent="sale_subscription_root"
|
||||||
|
sequence="20"
|
||||||
|
name="Configuration"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="subscription_menu"
|
||||||
|
parent="sale_subscription_root"
|
||||||
|
name="Subscriptions"
|
||||||
|
sequence="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="sale_subscription_menu"
|
||||||
|
parent="subscription_menu"
|
||||||
|
action="sale_subscription_action"
|
||||||
|
sequence="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="product_subscription_menu"
|
||||||
|
parent="subscription_menu"
|
||||||
|
action="subscription_product_template_action"
|
||||||
|
sequence="3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="subscription_close_reason_menu"
|
||||||
|
parent="sale_subscription_configuration_menu"
|
||||||
|
action="subscription_close_reason_action"
|
||||||
|
sequence="30"
|
||||||
|
name="Close reasons"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
1
subscription_oca/wizard/__init__.py
Normal file
1
subscription_oca/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import close_subscription_wizard
|
||||||
25
subscription_oca/wizard/close_subscription_wizard.py
Normal file
25
subscription_oca/wizard/close_subscription_wizard.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Copyright 2023 Domatix - Carlos Martínez
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class CloseSubscriptionWizard(models.TransientModel):
|
||||||
|
_name = "close.reason.wizard"
|
||||||
|
_description = "Close reason wizard"
|
||||||
|
|
||||||
|
close_reason_id = fields.Many2one(
|
||||||
|
comodel_name="sale.subscription.close.reason", string="Reason"
|
||||||
|
)
|
||||||
|
|
||||||
|
def button_confirm(self):
|
||||||
|
sale_subscription = self.env["sale.subscription"].browse(
|
||||||
|
self.env.context["active_id"]
|
||||||
|
)
|
||||||
|
sale_subscription.close_reason_id = self.close_reason_id.id
|
||||||
|
stage = sale_subscription.stage_id
|
||||||
|
closed_stage = self.env["sale.subscription.stage"].search(
|
||||||
|
[("type", "=", "post")], limit=1
|
||||||
|
)
|
||||||
|
if stage != closed_stage:
|
||||||
|
sale_subscription.stage_id = closed_stage
|
||||||
|
sale_subscription.active = False
|
||||||
30
subscription_oca/wizard/close_subscription_wizard.xml
Normal file
30
subscription_oca/wizard/close_subscription_wizard.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="close_reason_wizard_view" model="ir.ui.view">
|
||||||
|
<field name="name">close.reason.wizard.view</field>
|
||||||
|
<field name="model">close.reason.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Close reason">
|
||||||
|
<group>
|
||||||
|
<field name="close_reason_id" />
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button
|
||||||
|
name="button_confirm"
|
||||||
|
type="object"
|
||||||
|
class="oe_highlight"
|
||||||
|
string="Confirm"
|
||||||
|
/>
|
||||||
|
<button special="cancel" string="Cancel" class="oe_link" />
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="close_reason_wizard_act_window" model="ir.actions.act_window">
|
||||||
|
<field name="name">Close reason</field>
|
||||||
|
<field name="res_model">close.reason.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user