Merge PR #999 into 14.0

Signed-off-by simahawk
This commit is contained in:
OCA-git-bot
2023-10-12 10:29:18 +00:00
40 changed files with 4297 additions and 0 deletions

View File

@@ -0,0 +1 @@
../../../../subscription_oca

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

104
subscription_oca/README.rst Normal file
View File

@@ -0,0 +1,104 @@
=======================
Subscription management
=======================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:42fc353409c68ca6defc36ba2273b97ae36edb6f629c0c041db1ef7f1e01ba00
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/14.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-14-0/contract-14-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=14.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:%2014.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>
* `Ooops404 <https://www.ooops404.com>`__:
* Ilyas <irazor147@gmail.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/14.0/subscription_oca>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1,2 @@
from . import models
from . import wizard

View File

@@ -0,0 +1,29 @@
# 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": "14.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)",
"maintainers": ["ilyasProgrammer"],
"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,
}

View 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>

View 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>

View File

@@ -0,0 +1,985 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * subscription_oca
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: subscription_oca
#: model:sale.subscription.stage,description:subscription_oca.subscription_stage_in_progress
msgid ""
"\n"
" 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.\n"
" "
msgstr ""
#. module: subscription_oca
#: model:sale.subscription.stage,description:subscription_oca.subscription_stage_draft
msgid ""
"\n"
" 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.\n"
" "
msgstr ""
#. module: subscription_oca
#: model:sale.subscription.stage,description:subscription_oca.subscription_stage_closed
msgid ""
"\n"
" 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).\n"
" "
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form
msgid "<span class=\"every\">Every</span>"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form
msgid "<span class=\"mr-1\">For</span>"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.res_partner_view_form
msgid "<span class=\"o_stat_text\">Subscriptions</span>"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form
msgid ""
"A given subscription can be marked as closed when, for example, renewal is "
"not desired."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_needaction
msgid "Action Needed"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__active
msgid "Active"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_ids
msgid "Activities"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_exception_decoration
msgid "Activity Exception Decoration"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_state
msgid "Activity State"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_type_icon
msgid "Activity Type Icon"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.view_subscription_stage_form
msgid "Add new description..."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__amount_tax
msgid "Amount Tax"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__amount_total
msgid "Amount Total"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_attachment_count
msgid "Attachment Count"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__recurring_rule_boundary
msgid "Boundary"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.close_reason_wizard_view
msgid "Cancel"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.actions.act_window,help:subscription_oca.subscription_stage_action
msgid "Click to create a new subscription stage."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__close_reason_id
msgid "Close Reason"
msgstr ""
#. module: subscription_oca
#: model:ir.actions.act_window,name:subscription_oca.close_reason_wizard_act_window
#: model_terms:ir.ui.view,arch_db:subscription_oca.close_reason_wizard_view
msgid "Close reason"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_sale_subscription_close_reason
msgid "Close reason model"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_close_reason_wizard
msgid "Close reason wizard"
msgstr ""
#. module: subscription_oca
#: model:ir.actions.act_window,name:subscription_oca.subscription_close_reason_action
#: model:ir.ui.menu,name:subscription_oca.subscription_close_reason_menu
msgid "Close reasons"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form
msgid "Close subscription"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_stage__type__post
#: model:sale.subscription.stage,name:subscription_oca.subscription_stage_closed
msgid "Closed"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__code
msgid "Code"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__color
msgid "Color Index"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__user_id
msgid "Commercial agent"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__company_id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__company_id
msgid "Company"
msgstr ""
#. module: subscription_oca
#: model:ir.ui.menu,name:subscription_oca.sale_subscription_configuration_menu
msgid "Configuration"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.close_reason_wizard_view
msgid "Confirm"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_res_partner
msgid "Contact"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form
msgid "Create Invoice"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_close_reason_wizard__create_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__create_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_close_reason__create_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__create_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__create_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_tag__create_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__create_uid
msgid "Created by"
msgstr ""
#. module: subscription_oca
#: code:addons/subscription_oca/models/sale_subscription.py:0
#, python-format
msgid "Created invoice with reference"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_close_reason_wizard__create_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__create_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_close_reason__create_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__create_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__create_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_tag__create_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__create_date
msgid "Created on"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__currency_id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__currency_id
msgid "Currency"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_type__days
msgid "Day(s)"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_kanban
msgid "Delete"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__description
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__name
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__description
msgid "Description"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__discount
msgid "Discount (%)"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_close_reason_wizard__display_name
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__display_name
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_close_reason__display_name
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__display_name
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_tag__display_name
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__display_name
msgid "Display Name"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__display_name
msgid "Display name"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__invoicing_mode__draft
msgid "Draft"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__recurring_rule_boundary
msgid "Duration"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_kanban
msgid "Edit"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__stage_str
msgid "Etapa"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__date
msgid "Finish date"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__fiscal_position_id
msgid "Fiscal Position"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_boundary__limited
msgid "Fixed"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_follower_ids
msgid "Followers"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_partner_ids
msgid "Followers (Partners)"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__activity_type_icon
msgid "Font awesome icon e.g. fa-tasks"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_boundary__unlimited
msgid "Forever"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__has_message
msgid "Has Message"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_close_reason_wizard__id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_close_reason__id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_tag__id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__id
msgid "ID"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_exception_icon
msgid "Icon"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__activity_exception_icon
msgid "Icon to indicate an exception activity."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__message_needaction
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__message_unread
msgid "If checked, new messages require your attention."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__message_has_error
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__message_has_sms_error
msgid "If checked, some messages have a delivery error."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__image
msgid "Image"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__in_progress
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__in_progress
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_stage__type__in_progress
#: model:sale.subscription.stage,name:subscription_oca.subscription_stage_in_progress
msgid "In progress"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__invoicing_mode__invoice
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form
msgid "Invoice"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__invoicing_mode__invoice_send
msgid "Invoice & send"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__account_invoice_ids_count
msgid "Invoice Count"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__invoice_mail_template_id
msgid "Invoice Email"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__invoice_ids
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form
msgid "Invoices"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form
msgid "Invoicing"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__invoicing_mode
msgid "Invoicing mode"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_is_follower
msgid "Is Follower"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__journal_id
msgid "Journal"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_account_move
msgid "Journal Entry"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__fold
msgid "Kanban folded"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_close_reason_wizard____last_update
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription____last_update
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_close_reason____last_update
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line____last_update
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage____last_update
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_tag____last_update
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template____last_update
msgid "Last Modified on"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_close_reason_wizard__write_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__write_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_close_reason__write_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__write_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__write_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_tag__write_uid
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__write_uid
msgid "Last Updated by"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_close_reason_wizard__write_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__write_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_close_reason__write_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__write_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__write_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_tag__write_date
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__write_date
msgid "Last Updated on"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_main_attachment_id
msgid "Main Attachment"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_kanban
msgid "Menú desplegable"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_has_error
msgid "Message Delivery error"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_ids
msgid "Messages"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form
msgid "Misc"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_type__months
msgid "Month(s)"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__my_activity_date_deadline
msgid "My Activity Deadline"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__name
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_close_reason__name
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__name
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__name
msgid "Name"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form
msgid "New subscription"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_date_deadline
msgid "Next Activity Deadline"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_summary
msgid "Next Activity Summary"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_type_id
msgid "Next Activity Type"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__recurring_next_date
msgid "Next invoice date"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_needaction_counter
msgid "Number of Actions"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_has_error_counter
msgid "Number of errors"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__message_needaction_counter
msgid "Number of messages which requires an action"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__message_has_error_counter
msgid "Number of messages with delivery error"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__message_unread_counter
msgid "Number of unread messages"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__sale_order_ids
msgid "Orders"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__sale_order_id
msgid "Origin sale order"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form
msgid "Other info"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__partner_id
msgid "Partner"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.view_sale_order_pending_filter
msgid "Pending subscriptions"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__pricelist_id
msgid "Pricelist"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__product_id
msgid "Product"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_product_template
msgid "Product Template"
msgstr ""
#. module: subscription_oca
#: model:ir.actions.act_window,name:subscription_oca.subscription_product_template_action
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__product_ids
#: model:ir.ui.menu,name:subscription_oca.product_subscription_menu
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form
msgid "Products"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__product_uom_qty
msgid "Quantity"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_stage__type__pre
#: model:sale.subscription.stage,name:subscription_oca.subscription_stage_draft
msgid "Ready to start"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_close_reason_wizard__close_reason_id
msgid "Reason"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__recurring_rule_type
msgid "Recurrence"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__recurring_total
msgid "Recurring price"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__code
msgid "Reference"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__recurring_interval
msgid "Repeat every"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_user_id
msgid "Responsible User"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__recurring_rule_count
msgid "Rule count"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_has_sms_error
msgid "SMS Delivery error"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__sale_subscription_line_ids
msgid "Sale Subscription Line"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__invoicing_mode__sale_and_invoice
msgid "Sale order & Invoice"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__sale_order_ids_count
msgid "Sale orders"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__crm_team_id
msgid "Sale team"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form
msgid "Sales"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_sale_order
msgid "Sales Order"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_sale_order_line
msgid "Sales Order Line"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__sequence
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__sequence
msgid "Sequence"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__stage_id
msgid "Stage"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.view_subscription_stage_form
msgid ""
"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."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__date_start
msgid "Start date"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__activity_state
msgid ""
"Status based on activities\n"
"Overdue: Due date is already passed\n"
"Today: Activity date is today\n"
"Planned: Future activities."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_product_product__subscribable
#: model:ir.model.fields,field_description:subscription_oca.field_product_template__subscribable
msgid "Subscribable product"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_sale_subscription
#: model:ir.model.fields,field_description:subscription_oca.field_account_bank_statement_line__subscription_id
#: model:ir.model.fields,field_description:subscription_oca.field_account_move__subscription_id
#: model:ir.model.fields,field_description:subscription_oca.field_account_payment__subscription_id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_order__order_subscription_id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__sale_subscription_id
msgid "Subscription"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_res_partner__subscription_count
#: model:ir.model.fields,field_description:subscription_oca.field_res_users__subscription_count
msgid "Subscription Count"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form
msgid "Subscription lines"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_sale_subscription_line
msgid "Subscription lines added to a given subscription"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_sale_subscription_stage
#: model_terms:ir.ui.view,arch_db:subscription_oca.view_subscription_stage_form
msgid "Subscription stage"
msgstr ""
#. module: subscription_oca
#: model:ir.ui.menu,name:subscription_oca.subscription_stage_menu
msgid "Subscription stages"
msgstr ""
#. module: subscription_oca
#: model:ir.ui.menu,name:subscription_oca.subscription_tag_menu
msgid "Subscription tags"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_product_product__subscription_template_id
#: model:ir.model.fields,field_description:subscription_oca.field_product_template__subscription_template_id
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__template_id
msgid "Subscription template"
msgstr ""
#. module: subscription_oca
#: model:ir.actions.act_window,name:subscription_oca.sale_subscription_template_act_window
#: model:ir.model,name:subscription_oca.model_sale_subscription_template
#: model:ir.ui.menu,name:subscription_oca.sale_template_subscription_menu
msgid "Subscription templates"
msgstr ""
#. module: subscription_oca
#: model:ir.actions.act_window,name:subscription_oca.sale_subscription_action
#: model:ir.model.fields,field_description:subscription_oca.field_res_partner__subscription_ids
#: model:ir.model.fields,field_description:subscription_oca.field_res_users__subscription_ids
#: model:ir.model.fields,field_description:subscription_oca.field_sale_order__subscription_ids
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__subscription_ids
#: model:ir.ui.menu,name:subscription_oca.sale_subscription_menu
#: model:ir.ui.menu,name:subscription_oca.sale_subscription_root
#: model:ir.ui.menu,name:subscription_oca.subscription_menu
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form
#: model_terms:ir.ui.view,arch_db:subscription_oca.view_sale_order_form
msgid "Subscriptions"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_order__subscriptions_count
msgid "Subscriptions Count"
msgstr ""
#. module: subscription_oca
#: model:ir.actions.server,name:subscription_oca.ir_cron_subscription_management_ir_actions_server
#: model:ir.cron,cron_name:subscription_oca.ir_cron_subscription_management
#: model:ir.cron,name:subscription_oca.ir_cron_subscription_management
msgid "Subscriptions management"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__price_subtotal
msgid "Subtotal"
msgstr ""
#. module: subscription_oca
#: model:ir.actions.act_window,name:subscription_oca.subscription_stage_action
msgid "Susbcription stages"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.view_product_suscribable_filter
msgid "Suscribable products"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_tag__name
msgid "Tag name"
msgstr ""
#. module: subscription_oca
#: model:ir.actions.act_window,name:subscription_oca.subscription_tag_action
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__tag_ids
msgid "Tags"
msgstr ""
#. module: subscription_oca
#: model:ir.model,name:subscription_oca.model_sale_subscription_tag
msgid "Tags for sale subscription"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__tax_ids
msgid "Taxes"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__amount_tax_line_amount
msgid "Taxes Amount"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form
msgid "Terms and Conditions"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__terms
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__description
msgid "Terms and conditions"
msgstr ""
#. module: subscription_oca
#: code:addons/subscription_oca/models/sale_subscription_stage.py:0
#, python-format
msgid "There is already a Closed-type stage declared"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_kanban
msgid ""
"This bar allows to filter the opportunities based on scheduled activities."
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.view_subscription_stage_form
msgid ""
"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."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__to_renew
msgid "To renew"
msgstr ""
#. module: subscription_oca
#: code:addons/subscription_oca/models/sale_subscription.py:0
#, python-format
msgid "To validate"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__price_total
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_tree
msgid "Total"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_tree
msgid "Total Tax"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_tree
msgid "Total subtotal"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__type
msgid "Type"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__activity_exception_decoration
msgid "Type of the exception activity on record."
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__price_unit
msgid "Unit price"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_unread
msgid "Unread Messages"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_unread_counter
msgid "Unread Messages Counter"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__website_message_ids
msgid "Website Messages"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__website_message_ids
msgid "Website communication history"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_type__weeks
msgid "Week(s)"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_type__years
msgid "Year(s)"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form
msgid "e.g. Monthly Subscription"
msgstr ""
#. module: subscription_oca
#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form
msgid "month(s)"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__product_ids_count
msgid "product_ids"
msgstr ""
#. module: subscription_oca
#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__subscription_count
msgid "subscription_ids"
msgstr ""

View 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

View 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"
)

View 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"
)

View 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,
},
}

View 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

View 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,
}

View 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"].browse(
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", limit=1)
.id
)
return super(SaleSubscription, self).create(values)

View 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)

View 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,
}

View 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"))

View 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)

View 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)],
}

View File

@@ -0,0 +1,6 @@
* Carlos Martínez <carlos@domatix.com>
* `Ooops404 <https://www.ooops404.com>`__:
* Ilyas <irazor147@gmail.com>

View 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.

View 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.

View 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.

View 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
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_custom_sale_subscription_template sale.subscription.template model_sale_subscription_template sales_team.group_sale_salesman 1 1 1 1
3 access_custom_sale_subscription sale.subscription model_sale_subscription sales_team.group_sale_salesman 1 1 1 1
4 access_custom_sale_subscription_close_reason sale.subscription.close.reason model_sale_subscription_close_reason sales_team.group_sale_salesman 1 1 1 1
5 access_custom_sale_subscription_stage sale.subscription.stage model_sale_subscription_stage sales_team.group_sale_salesman 1 1 1 1
6 access_custom_sale_subscription_line sale.subscription.line model_sale_subscription_line sales_team.group_sale_salesman 1 1 1 1
7 access_custom_sale_subscription_tag sale.subscription.tag model_sale_subscription_tag sales_team.group_sale_salesman 1 1 1 1
8 access_close_subscription Close subscription access model_close_reason_wizard sales_team.group_sale_salesman 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,451 @@
<?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: https://docutils.sourceforge.io/" />
<title>Subscription management</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See https://docutils.sourceforge.io/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:42fc353409c68ca6defc36ba2273b97ae36edb6f629c0c041db1ef7f1e01ba00
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/contract/tree/14.0/subscription_oca"><img alt="OCA/contract" src="https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/contract-14-0/contract-14-0-subscription_oca"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/contract&amp;target_branch=14.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="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>To make a subscription:</p>
<ol class="arabic simple">
<li>Go to <em>Subscriptions &gt; Configuration &gt; 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 &gt; 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 &gt; Subscriptions &gt; 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="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-2">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Refactor all the onchanges that have business logic to computed write-able fields when possible. Keep onchanges only for UI purposes.</li>
<li>Add tests.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/contract/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/contract/issues/new?body=module:%20subscription_oca%0Aversion:%2014.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="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>Domatix</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul class="simple">
<li>Carlos Martínez &lt;<a class="reference external" href="mailto:carlos&#64;domatix.com">carlos&#64;domatix.com</a>&gt;</li>
<li><a class="reference external" href="https://www.ooops404.com">Ooops404</a>:<ul>
<li>Ilyas &lt;<a class="reference external" href="mailto:irazor147&#64;gmail.com">irazor147&#64;gmail.com</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-7">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/contract/tree/14.0/subscription_oca">OCA/contract</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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_subscription_oca

View File

@@ -0,0 +1,643 @@
# Copyright 2023 ooops404
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import uuid
from dateutil.relativedelta import relativedelta
from odoo import exceptions, fields
from odoo.tests import SavepointCase
class TestSubscriptionOCA(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.portal_user = cls.env.ref("base.demo_user0")
cls.cash_journal = cls.env["account.journal"].search(
[
("type", "=", "cash"),
("company_id", "=", cls.env.ref("base.main_company").id),
]
)[0]
cls.sale_journal = cls.env["account.journal"].search(
[
("type", "=", "sale"),
("company_id", "=", cls.env.ref("base.main_company").id),
]
)[0]
cls.pricelist1 = cls.env["product.pricelist"].create(
{
"name": "pricelist for contract test",
}
)
cls.pricelist2 = cls.env["product.pricelist"].create(
{
"name": "pricelist for contract test 2",
"discount_policy": "with_discount",
}
)
cls.partner = cls.env["res.partner"].create(
{
"name": "partner test subscription_oca",
"property_product_pricelist": cls.pricelist1.id,
"email": "demo1@demo.com",
}
)
cls.partner_2 = cls.env["res.partner"].create(
{
"name": "partner test subscription_oca 2",
"property_product_pricelist": cls.pricelist1.id,
"email": "demo2@demo.com",
}
)
cls.tax_10pc_incl = cls.env["account.tax"].create(
{
"name": "10% Tax incl",
"amount_type": "percent",
"amount": 10,
"price_include": True,
}
)
cls.product_1 = cls.env.ref("product.product_product_1")
cls.product_1.subscribable = True
cls.product_1.taxes_id = [(6, 0, cls.tax_10pc_incl.ids)]
cls.product_2 = cls.env.ref("product.product_product_2")
cls.product_2.subscribable = True
cls.country = cls.env["res.country"].search([], limit=1)
cls.fiscal = cls.env["account.fiscal.position"].create(
{
"name": "Regime National",
"auto_apply": True,
"country_id": cls.country.id,
"vat_required": True,
"sequence": 10,
}
)
cls.tmpl1 = cls.create_sub_template({})
cls.tmpl2 = cls.create_sub_template(
{
"recurring_rule_boundary": "limited",
"recurring_rule_type": "days",
}
)
cls.tmpl3 = cls.create_sub_template(
{
"recurring_rule_boundary": "unlimited",
"recurring_rule_type": "weeks",
}
)
cls.tmpl4 = cls.create_sub_template(
{
"recurring_rule_boundary": "limited",
"invoicing_mode": "invoice",
"recurring_rule_type": "years",
}
)
cls.tmpl5 = cls.create_sub_template(
{
"recurring_rule_boundary": "unlimited",
"invoicing_mode": "invoice",
"recurring_rule_type": "days",
}
)
cls.stage = cls.env["sale.subscription.stage"].create(
{
"name": "Test Sub Stage",
}
)
cls.stage_2 = cls.env["sale.subscription.stage"].create(
{
"name": "Test Sub Stage 2",
"type": "pre",
}
)
cls.tag = cls.env["sale.subscription.tag"].create(
{
"name": "Test Tag",
}
)
cls.sub1 = cls.create_sub({})
cls.sub2 = cls.create_sub(
{
"template_id": cls.tmpl3.id,
}
)
cls.sub3 = cls.create_sub(
{
"template_id": cls.tmpl2.id,
"pricelist_id": cls.pricelist2.id,
}
)
cls.sub4 = cls.create_sub(
{
"template_id": cls.tmpl3.id,
"recurring_rule_boundary": False,
"date_start": fields.Date.today(),
}
)
cls.sub5 = cls.create_sub(
{
"template_id": cls.tmpl4.id,
"pricelist_id": cls.pricelist2.id,
"date_start": fields.Date.today(),
"recurring_next_date": fields.Date.today() - relativedelta(days=1),
}
)
cls.sub6 = cls.create_sub(
{
"template_id": cls.tmpl5.id,
"recurring_rule_boundary": False,
"date_start": "2099-01-01",
}
)
cls.sub7 = cls.create_sub(
{
"template_id": cls.tmpl2.id,
"pricelist_id": cls.pricelist2.id,
"date_start": fields.Date.today() - relativedelta(days=100),
"in_progress": True,
}
)
cls.sub8 = cls.create_sub(
{
"template_id": cls.tmpl2.id,
"pricelist_id": cls.pricelist2.id,
"date_start": fields.Date.today() - relativedelta(days=100),
"in_progress": True,
"journal_id": cls.cash_journal.id,
}
)
cls.sub_line = cls.create_sub_line(cls.sub1)
cls.sub_line2 = cls.env["sale.subscription.line"].create(
{
"company_id": 1,
"sale_subscription_id": cls.sub1.id,
}
)
cls.sub_line21 = cls.create_sub_line(cls.sub2)
cls.sub_line22 = cls.create_sub_line(cls.sub2, cls.product_2.id)
cls.sub_line31 = cls.create_sub_line(cls.sub3)
cls.sub_line32 = cls.create_sub_line(cls.sub3, cls.product_2.id)
cls.sub_line41 = cls.create_sub_line(cls.sub4)
cls.sub_line42 = cls.create_sub_line(cls.sub4, cls.product_2.id)
cls.sub_line51 = cls.create_sub_line(cls.sub5)
cls.sub_line52 = cls.create_sub_line(cls.sub5, cls.product_2.id)
cls.sub_line71 = cls.create_sub_line(cls.sub7)
cls.sub_line72 = cls.create_sub_line(cls.sub7, cls.product_2.id)
cls.close_reason = cls.env["sale.subscription.close.reason"].create(
{
"name": "Test Close Reason",
}
)
cls.sub_line2.read(["name", "price_unit"])
cls.sub_line2.unlink()
# Pricelists.
cls.pricelist_default = cls.env.ref("product.list0")
cls.pricelist_l1 = cls._create_price_list("Level 1")
cls.pricelist_l2 = cls._create_price_list("Level 2")
cls.pricelist_l3 = cls._create_price_list("Level 3")
cls.env["product.pricelist.item"].create(
{
"pricelist_id": cls.pricelist_l3.id,
"applied_on": "0_product_variant",
"compute_price": "formula",
"base": "pricelist",
"base_pricelist_id": cls.pricelist_l1.id,
"product_id": cls.product_1.id,
}
)
cls.env["product.pricelist.item"].create(
{
"pricelist_id": cls.pricelist_l2.id,
"applied_on": "3_global",
"compute_price": "formula",
"base": "pricelist",
"base_pricelist_id": cls.pricelist_l1.id,
}
)
cls.env["product.pricelist.item"].create(
{
"pricelist_id": cls.pricelist_l1.id,
"applied_on": "3_global",
"compute_price": "formula",
"base": "standard_price",
"fixed_price": 1000,
}
)
@classmethod
def create_sub_template(cls, vals):
code = str(uuid.uuid4().hex)
default_vals = {
"name": "Test Template " + code,
"code": code,
"description": "Some sort of subscription terms",
"product_ids": [(6, 0, [cls.product_1.id, cls.product_2.id])],
}
default_vals.update(vals)
rec = cls.env["sale.subscription.template"].create(default_vals)
return rec
@classmethod
def create_sub(cls, vals):
default_vals = {
"company_id": 1,
"partner_id": cls.partner.id,
"template_id": cls.tmpl1.id,
"tag_ids": [(6, 0, [cls.tag.id])],
"stage_id": cls.stage.id,
"pricelist_id": cls.pricelist1.id,
"fiscal_position_id": cls.fiscal.id,
}
default_vals.update(vals)
rec = cls.env["sale.subscription"].create(default_vals)
return rec
@classmethod
def create_sub_line(cls, sub, prod=None):
ssl = cls.env["sale.subscription.line"].create(
{
"company_id": 1,
"sale_subscription_id": sub.id,
"product_id": prod or cls.product_1.id,
}
)
return ssl
@classmethod
def _create_price_list(cls, name):
return cls.env["product.pricelist"].create(
{
"name": name,
"active": True,
"currency_id": cls.env.ref("base.USD").id,
"company_id": cls.env.user.company_id.id,
}
)
def test_subscription_oca_sale_order(self):
# SO standard flow
so = self.env["sale.order"].create(
{
"partner_id": self.partner.id,
"partner_invoice_id": self.partner.id,
"partner_shipping_id": self.partner.id,
"order_line": [
(
0,
0,
{
"name": self.product_1.name,
"product_id": self.product_1.id,
"product_uom_qty": 2,
"product_uom": self.product_1.uom_id.id,
"price_unit": self.product_1.list_price,
},
)
],
}
)
so._compute_subscriptions_count()
self.assertEqual(so.subscriptions_count, 0)
action = so.action_view_subscriptions()
self.assertIsInstance(action, dict)
so.with_context(uid=1).action_confirm() # without subs.
def test_subscription_oca_sub_lines(self):
# sale.subscription.line
self.assertEqual(self.sub_line.name, self.sub_line.product_id.name)
self.assertIsNotNone(self.sub_line.tax_ids)
self.assertEqual(self.sub_line.price_unit, 30.75)
self.assertEqual(self.sub_line.discount, 0)
res = self.sub_line._get_display_price(self.product_2)
self.assertEqual(res, 38.25)
sol_res = self.sub_line._prepare_sale_order_line()
self.assertIsInstance(sol_res, dict)
move_res = self.sub_line._prepare_account_move_line()
self.assertIsInstance(move_res, dict)
def test_subscription_oca_sub_cron(self):
# sale.subscription
self.sub1.cron_subscription_management()
# invoice should be created by cron
inv_id = self.env["account.move"].search(
[("subscription_id", "=", self.sub1.id)]
)
self.assertEqual(len(inv_id), 1)
self.assertEqual(self.sub1.recurring_total, 27.95)
self.assertEqual(self.sub1.amount_total, 30.75)
self.assertEqual(self.sub2.recurring_total, 66.2)
self.assertEqual(self.sub2.amount_total, 69)
def test_subscription_oca_sub1_workflow(self):
res = self._collect_all_sub_test_results(self.sub1)
self.assertTrue(res[0])
self.assertTrue(res[1])
self.assertEqual(res[3], 2)
self.assertEqual(res[4], 2 * 30.75)
self.assertEqual(res[5], 2)
self.assertEqual(res[7], 1)
self.assertEqual(
res[9],
fields.Date.today() + relativedelta(months=1),
)
self.assertEqual(res[10], self.partner_2.property_product_pricelist.id)
self.assertFalse(res[11])
self.assertEqual(res[12], "ir.actions.act_window")
def test_subscription_oca_sub2_workflow(self):
res = self._collect_all_sub_test_results(self.sub2)
self.assertTrue(res[0])
self.assertTrue(res[1])
self.assertEqual(res[3], 2)
self.assertEqual(res[4], 138)
self.assertEqual(res[5], 2)
self.assertEqual(res[7], 1)
self.assertEqual(
res[9],
fields.Date.today() + relativedelta(weeks=1),
)
self.assertEqual(res[10], self.partner_2.property_product_pricelist.id)
self.assertFalse(res[11])
def test_subscription_oca_sub3_workflow(self):
res = self._collect_all_sub_test_results(self.sub3)
self.assertTrue(res[0])
self.assertTrue(res[1])
self.assertEqual(res[3], 2)
self.assertEqual(res[4], 138)
self.assertEqual(res[5], 2)
self.assertEqual(res[6], "ir.actions.act_window")
self.assertEqual(res[7], 1)
self.assertEqual(
res[9],
fields.Date.today() + relativedelta(days=1),
)
self.assertEqual(res[10], self.partner_2.property_product_pricelist.id)
self.assertFalse(res[11])
def test_subscription_oca_sub4_workflow(self):
res = self._collect_all_sub_test_results(self.sub4)
self.assertTrue(res[0])
self.assertTrue(res[1])
self.assertEqual(res[2], "ir.actions.act_window")
self.assertEqual(res[3], 2)
self.assertEqual(res[4], 138)
self.assertEqual(res[5], 2)
self.assertEqual(res[7], 1)
self.assertEqual(
res[9],
fields.Date.today() + relativedelta(weeks=1),
)
self.assertEqual(res[10], self.partner_2.property_product_pricelist.id)
self.assertFalse(res[11])
def test_subscription_oca_sub5_workflow(self):
res = self._collect_all_sub_test_results(self.sub5)
self.assertTrue(res[0])
self.assertTrue(res[1])
self.assertEqual(res[3], 2)
self.assertEqual(res[4], 138)
self.assertEqual(res[5], 2)
self.assertEqual(res[7], 1)
self.assertEqual(
res[9],
fields.Date.today() + relativedelta(years=1),
)
self.assertEqual(res[10], self.partner_2.property_product_pricelist.id)
self.assertFalse(res[11])
self.sub5.recurring_next_date = fields.Date.today()
self.sub5.template_id = self.tmpl5
self.sub5._onchange_template_id()
self.sub5.invoice_ids.unlink()
self.sub5._onchange_template_id()
def test_subscription_oca_sub7_workflow(self):
res = self._collect_all_sub_test_results(self.sub7.with_context(uom=2))
self.assertTrue(res[0])
self.assertTrue(res[1])
self.assertEqual(res[3], 2)
self.assertEqual(res[4], 138)
self.assertEqual(res[5], 2)
self.assertEqual(res[7], 1)
self.assertEqual(
res[9],
fields.Date.today() + relativedelta(days=1),
)
self.assertEqual(res[10], self.partner_2.property_product_pricelist.id)
self.assertFalse(res[11])
def test_subscription_oca_sub8_workflow(self):
subscription = self.sub8
subscription.create_sale_order()
with self.assertRaises(exceptions.UserError):
subscription.create_invoice()
self.sub8.journal_id = self.sale_journal
subscription.create_invoice()
self.sub8.template_id.invoicing_mode = "invoice"
with self.assertRaises(exceptions.UserError):
subscription.generate_invoice()
self.sub8.template_id.invoicing_mode = "invoice_send"
with self.assertRaises(exceptions.UserError):
subscription.generate_invoice()
self.sub8.template_id.invoicing_mode = "sale_and_invoice"
with self.assertRaises(exceptions.UserError):
subscription.generate_invoice()
# add lines and repeat
self.sub_line81 = self.env["sale.subscription.line"].create(
{
"company_id": 1,
"sale_subscription_id": self.sub8.id,
"product_id": self.product_1.id,
}
)
self.sub_line82 = self.env["sale.subscription.line"].create(
{
"company_id": 1,
"sale_subscription_id": self.sub8.id,
"product_id": self.product_2.id,
}
)
subscription.create_sale_order()
subscription.create_invoice()
subscription.journal_id = self.sale_journal
subscription.create_invoice()
subscription.template_id.invoicing_mode = "invoice"
subscription.generate_invoice()
subscription.template_id.invoicing_mode = "invoice_send"
subscription.generate_invoice()
subscription.template_id.invoicing_mode = "sale_and_invoice"
order = subscription.create_sale_order()
order.with_context(uid=1).action_confirm()
subscription.sale_subscription_line_ids.mapped("product_id").write(
{"invoice_policy": "order"}
)
subscription.generate_invoice()
subscription._check_dates("2099-01-01", "2099-01-01")
subscription._check_dates("2098-01-01", "2099-01-01")
subscription._check_dates("2098-01-01", "2097-01-01")
subscription._check_dates(fields.Date.today(), fields.Date.today())
subscription._check_dates(fields.Datetime.now(), fields.Datetime.now())
subscription.write({"stage_id": self.stage_2})
def test_subscription_oca_sub8_workflow_portal(self):
# portal user
subscription = self.sub8.with_user(self.portal_user)
sale_order = subscription.create_sale_order()
self.assertFalse(sale_order)
move_id = subscription.with_user(self.portal_user).create_invoice()
self.assertFalse(move_id)
with self.assertRaises(exceptions.AccessError):
subscription.manual_invoice()
with self.assertRaises(exceptions.AccessError):
subscription.calculate_recurring_next_date(fields.Datetime.now())
with self.assertRaises(exceptions.AccessError):
subscription.partner_id = self.partner_2
def test_subscription_oca_sub_stage(self):
# sale.subscription.stage
self.stage._check_lot_product() # should not raise
def test_x_subscription_oca_pricelist_related(self):
res = self.partner.read(["subscription_count", "subscription_ids"])
self.assertEqual(res[0]["subscription_count"], 8)
res = self.partner.action_view_subscription_ids()
self.assertIsInstance(res, dict)
sale_order = self.sub1.create_sale_order()
sale_order.with_context(uid=1).create_subscription(
sale_order.order_line, self.tmpl1
)
sale_order.get_next_interval(
self.tmpl1.recurring_rule_type, self.tmpl1.recurring_interval
)
self.sub_line.sale_subscription_id.pricelist_id.discount_policy = (
"without_discount"
)
self.sub_line.product_uom_qty = 100
self.env.user.groups_id = [
(4, self.env.ref("product.group_discount_per_so_line").id)
]
disc = self.sub_line.read(["discount"])
self.assertEqual(disc[0]["discount"], 0)
wiz = self.env["close.reason.wizard"].create({})
wiz.with_context(active_id=self.sub1.id).button_confirm()
self.assertEqual(self.sub1.stage_id.name, "Closed")
self.assertFalse(self.sub1.active)
self.tmpl1.action_view_subscription_ids()
self.tmpl1.action_view_product_ids()
self.tmpl1.read(["product_ids_count", "subscription_count"])
with self.assertRaises(exceptions.ValidationError):
self.env["sale.subscription.stage"].create(
{
"name": "Test Sub Stage",
"type": "post",
}
)
pricelist = self.sub_line.sale_subscription_id.pricelist_id.copy(
{"currency_id": self.env.ref("base.THB").id}
)
item1 = self.env["product.pricelist.item"].create(
{
"pricelist_id": pricelist.id,
"product_id": self.product_1.product_variant_id.id,
"name": "Test special rule 1",
"applied_on": "0_product_variant",
"price": 3,
}
)
self.sub_line.sale_subscription_id.pricelist_id = pricelist
self.sub_line.product_uom_qty = 200
res = self.sub_line.read(["discount"])
self.assertEqual(res[0]["discount"], 100)
item1.unlink()
self.env["product.pricelist.item"].create(
{
"pricelist_id": pricelist.id,
"product_id": self.product_1.product_variant_id.id,
"name": "Test special rule 2",
"base": "pricelist",
"base_pricelist_id": self.pricelist1.id,
"applied_on": "0_product_variant",
}
)
self.env["product.pricelist.item"].create(
{
"pricelist_id": self.pricelist1.id,
"product_id": self.product_1.product_variant_id.id,
"name": "Test special rule 3",
"applied_on": "0_product_variant",
"base": "standard_price",
}
)
self.sub_line.sale_subscription_id.pricelist_id = pricelist
self.sub_line.product_uom_qty = 300
res = self.sub_line.read(["discount"])
self.assertEqual(res[0]["discount"], 100)
def test_x_subscription_oca_pricelist_related_2(self):
self.pricelist_l3.discount_policy = "without_discount"
self.pricelist_l3.currency_id = self.env.ref("base.THB")
self.sub_line.sale_subscription_id.pricelist_id = self.pricelist_l3
res = self.sub_line._get_display_price(self.product_1)
self.assertAlmostEqual(int(res), 514)
self.sub_line.product_uom_qty = 300
res = self.sub_line.read(["discount"])
self.assertEqual(res[0]["discount"], 0)
def _collect_all_sub_test_results(self, subscription):
test_res = []
sale_order = subscription.create_sale_order()
test_res.append(sale_order)
move_id = subscription.create_invoice()
test_res.append(move_id)
res = subscription.manual_invoice()
test_res.append(res["type"])
inv_ids = self.env["account.move"].search(
[("subscription_id", "=", subscription.id)]
)
# self.assertEqual(len(inv_ids), 2)
# self.assertEqual(sum(inv_ids.mapped("amount_total")), 2 * 30.75)
# self.assertEqual(subscription.account_invoice_ids_count, 2)
test_res.append(len(inv_ids))
test_res.append(sum(inv_ids.mapped("amount_total")))
test_res.append(subscription.account_invoice_ids_count)
res = subscription.action_view_account_invoice_ids()
# self.assertEqual(res["type"], "ir.actions.act_window")
# self.assertEqual(subscription.sale_order_ids_count, 1)
test_res.append(res["type"])
test_res.append(subscription.sale_order_ids_count)
subscription.action_view_sale_order_ids()
# self.assertIn(str(subscription.sale_order_ids.id), str(res["domain"]))
test_res.append(subscription.sale_order_ids.id)
subscription.calculate_recurring_next_date(fields.Datetime.now())
# self.assertEqual(
# subscription.recurring_next_date,
# fields.Date.today() + relativedelta(months=1),
# )
test_res.append(subscription.recurring_next_date)
subscription.partner_id = self.partner_2
subscription.onchange_partner_id()
# self.assertEqual(
# subscription.pricelist_id.id, self.partner_2.property_product_pricelist.id
# )
test_res.append(subscription.pricelist_id.id)
subscription.onchange_partner_id_fpos()
# self.assertFalse(subscription.fiscal_position_id)
test_res.append(subscription.fiscal_position_id)
res = subscription.action_close_subscription()
self.assertEqual(res["type"], "ir.actions.act_window")
test_res.append(res["type"])
group_stage_ids = subscription._read_group_stage_ids(
stages=self.env["sale.subscription.stage"].search([]), domain=[], order="id"
)
test_res.append(group_stage_ids)
return test_res

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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="{&quot;planned&quot;: &quot;success&quot;, &quot;today&quot;: &quot;warning&quot;, &quot;overdue&quot;: &quot;danger&quot;}"
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>

View File

@@ -0,0 +1 @@
from . import close_subscription_wizard

View 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

View 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>