From 1ba6fe33a218b67f25e3ad6fbbf8b5f6a24a5471 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 20 Sep 2023 21:49:04 +0200 Subject: [PATCH 1/9] [15.0][ADD] subscription_oca --- subscription_oca/README.rst | 99 ++++ subscription_oca/__init__.py | 2 + subscription_oca/__manifest__.py | 28 ++ subscription_oca/data/ir_cron.xml | 15 + .../data/sale_subscription_data.xml | 76 +++ subscription_oca/models/__init__.py | 11 + subscription_oca/models/account_move.py | 12 + subscription_oca/models/product_template.py | 12 + subscription_oca/models/res_partner.py | 33 ++ subscription_oca/models/sale_order.py | 83 +++ subscription_oca/models/sale_order_line.py | 17 + subscription_oca/models/sale_subscription.py | 470 +++++++++++++++++ .../models/sale_subscription_close_reason.py | 10 + .../models/sale_subscription_line.py | 322 ++++++++++++ .../models/sale_subscription_stage.py | 29 ++ .../models/sale_subscription_tag.py | 10 + .../models/sale_subscription_template.py | 102 ++++ subscription_oca/readme/CONTRIBUTORS.rst | 1 + subscription_oca/readme/DESCRIPTION.rst | 1 + subscription_oca/readme/ROADMAP.rst | 2 + subscription_oca/readme/USAGE.rst | 13 + subscription_oca/security/ir.model.access.csv | 8 + .../static/description/index.html | 439 ++++++++++++++++ subscription_oca/static/img/icon.png | Bin 0 -> 16096 bytes .../views/product_template_views.xml | 20 + subscription_oca/views/res_partner_views.xml | 28 ++ subscription_oca/views/sale_order_views.xml | 29 ++ .../views/sale_subscription_stage_views.xml | 73 +++ .../views/sale_subscription_tag_views.xml | 27 + .../sale_subscription_template_views.xml | 135 +++++ .../views/sale_subscription_views.xml | 473 ++++++++++++++++++ subscription_oca/wizard/__init__.py | 1 + .../wizard/close_subscription_wizard.py | 25 + .../wizard/close_subscription_wizard.xml | 30 ++ 34 files changed, 2636 insertions(+) create mode 100644 subscription_oca/README.rst create mode 100644 subscription_oca/__init__.py create mode 100644 subscription_oca/__manifest__.py create mode 100644 subscription_oca/data/ir_cron.xml create mode 100644 subscription_oca/data/sale_subscription_data.xml create mode 100644 subscription_oca/models/__init__.py create mode 100644 subscription_oca/models/account_move.py create mode 100644 subscription_oca/models/product_template.py create mode 100644 subscription_oca/models/res_partner.py create mode 100644 subscription_oca/models/sale_order.py create mode 100644 subscription_oca/models/sale_order_line.py create mode 100644 subscription_oca/models/sale_subscription.py create mode 100644 subscription_oca/models/sale_subscription_close_reason.py create mode 100644 subscription_oca/models/sale_subscription_line.py create mode 100644 subscription_oca/models/sale_subscription_stage.py create mode 100644 subscription_oca/models/sale_subscription_tag.py create mode 100644 subscription_oca/models/sale_subscription_template.py create mode 100644 subscription_oca/readme/CONTRIBUTORS.rst create mode 100644 subscription_oca/readme/DESCRIPTION.rst create mode 100644 subscription_oca/readme/ROADMAP.rst create mode 100644 subscription_oca/readme/USAGE.rst create mode 100644 subscription_oca/security/ir.model.access.csv create mode 100644 subscription_oca/static/description/index.html create mode 100644 subscription_oca/static/img/icon.png create mode 100644 subscription_oca/views/product_template_views.xml create mode 100644 subscription_oca/views/res_partner_views.xml create mode 100644 subscription_oca/views/sale_order_views.xml create mode 100644 subscription_oca/views/sale_subscription_stage_views.xml create mode 100644 subscription_oca/views/sale_subscription_tag_views.xml create mode 100644 subscription_oca/views/sale_subscription_template_views.xml create mode 100644 subscription_oca/views/sale_subscription_views.xml create mode 100644 subscription_oca/wizard/__init__.py create mode 100644 subscription_oca/wizard/close_subscription_wizard.py create mode 100644 subscription_oca/wizard/close_subscription_wizard.xml diff --git a/subscription_oca/README.rst b/subscription_oca/README.rst new file mode 100644 index 000000000..0b03b4bf1 --- /dev/null +++ b/subscription_oca/README.rst @@ -0,0 +1,99 @@ +======================= +Subscription management +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3772d65a58c07d0348bd13d3c882810c94bfb87389c62fec6d16fe8ef130252c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github + :target: https://github.com/OCA/contract/tree/15.0/subscription_oca + :alt: OCA/contract +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/contract-15-0/contract-15-0-subscription_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/contract&target_branch=15.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows creating subscriptions that generate recurring invoices or orders. It also enables the sale of products that generate subscriptions. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To make a subscription: + +#. Go to *Subscriptions > Configuration > Subscription templates*. +#. Create the templates you consider, choosing the billing frequency: daily, monthly... and the method of creating the invoice and/or order. +#. Go to *Subscription > Subscriptions*. +#. Create a subscription and indicate the start date. When the *Subscriptions Management* cron job is executed, the subscription will begin and the first invoice will be created if the execution date matches the start date. The invoice will also be created when the execution date matches the next invoice date. Additionally, you can manually change the subscription status and create an invoice. +#. The cron job will also end the subscription if its end date has been reached. + +To create subscriptions with the sale of a product: + +#. Go to *Subscriptions > Subscriptions > Products*. +#. Create the product and in the sales tab, complete the fields *Subscribable product* and *Subscription template* +#. Create a sales order with the product and confirm it. + +Known issues / Roadmap +====================== + +* Refactor all the onchanges that have business logic to computed write-able fields when possible. Keep onchanges only for UI purposes. +* Add tests. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Domatix + +Contributors +~~~~~~~~~~~~ + +* Carlos Martínez + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/subscription_oca/__init__.py b/subscription_oca/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/subscription_oca/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/subscription_oca/__manifest__.py b/subscription_oca/__manifest__.py new file mode 100644 index 000000000..8e9cb33b8 --- /dev/null +++ b/subscription_oca/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2023 Domatix - Carlos Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Subscription management", + "summary": "Generate recurring invoices.", + "version": "15.0.1.0.0", + "development_status": "Beta", + "category": "Subscription Management", + "website": "https://github.com/OCA/contract", + "license": "AGPL-3", + "author": "Domatix, Odoo Community Association (OCA)", + "depends": ["sale_management", "account"], + "data": [ + "views/product_template_views.xml", + "views/sale_subscription_views.xml", + "views/sale_subscription_stage_views.xml", + "views/sale_subscription_tag_views.xml", + "views/sale_subscription_template_views.xml", + "views/sale_order_views.xml", + "views/res_partner_views.xml", + "data/ir_cron.xml", + "data/sale_subscription_data.xml", + "wizard/close_subscription_wizard.xml", + "security/ir.model.access.csv", + ], + "installable": True, + "application": True, +} diff --git a/subscription_oca/data/ir_cron.xml b/subscription_oca/data/ir_cron.xml new file mode 100644 index 000000000..fbc7f7c88 --- /dev/null +++ b/subscription_oca/data/ir_cron.xml @@ -0,0 +1,15 @@ + + + + Subscriptions management + + + 24 + hours + -1 + + + code + model.cron_subscription_management() + + diff --git a/subscription_oca/data/sale_subscription_data.xml b/subscription_oca/data/sale_subscription_data.xml new file mode 100644 index 000000000..6ee56a620 --- /dev/null +++ b/subscription_oca/data/sale_subscription_data.xml @@ -0,0 +1,76 @@ + + + + + + sale_subscription_sequencer + sale.subscription + SUB + 5 + + + + + + + Ready to start + 0 + pre + + 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. + + + + + + + In progress + 1 + in_progress + + + 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. + + + + + + Closed + 2 + post + + + 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). + + + + + + + + + The subscription is too expensive + + + + + Subscription does not meet my requirements + + + + + The subscription ended + + + + + I don't really use it + + + + + Other + + + + diff --git a/subscription_oca/models/__init__.py b/subscription_oca/models/__init__.py new file mode 100644 index 000000000..6fa448188 --- /dev/null +++ b/subscription_oca/models/__init__.py @@ -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 diff --git a/subscription_oca/models/account_move.py b/subscription_oca/models/account_move.py new file mode 100644 index 000000000..570a02f29 --- /dev/null +++ b/subscription_oca/models/account_move.py @@ -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" + ) diff --git a/subscription_oca/models/product_template.py b/subscription_oca/models/product_template.py new file mode 100644 index 000000000..c866cfa3e --- /dev/null +++ b/subscription_oca/models/product_template.py @@ -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" + ) diff --git a/subscription_oca/models/res_partner.py b/subscription_oca/models/res_partner.py new file mode 100644 index 000000000..e4107108e --- /dev/null +++ b/subscription_oca/models/res_partner.py @@ -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, + }, + } diff --git a/subscription_oca/models/sale_order.py b/subscription_oca/models/sale_order.py new file mode 100644 index 000000000..0aabac0cb --- /dev/null +++ b/subscription_oca/models/sale_order.py @@ -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 diff --git a/subscription_oca/models/sale_order_line.py b/subscription_oca/models/sale_order_line.py new file mode 100644 index 000000000..b843e48f7 --- /dev/null +++ b/subscription_oca/models/sale_order_line.py @@ -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, + } diff --git a/subscription_oca/models/sale_subscription.py b/subscription_oca/models/sale_subscription.py new file mode 100644 index 000000000..4cc562ee4 --- /dev/null +++ b/subscription_oca/models/sale_subscription.py @@ -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 = ( + "%s %s" + % (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 = ( + "%s %s" + % (msg_static, new_invoice.id, invoice_number) + ) + if not invoice_number: + invoice_number = _("To validate") + message_body = "%s %s" % (msg_static, invoice_number) + self.calculate_recurring_next_date(self.recurring_next_date) + self.message_post(body=message_body) + + def manual_invoice(self): + invoice_id = self.create_invoice() + self.calculate_recurring_next_date(self.recurring_next_date) + context = dict(self.env.context) + context["form_view_initial_mode"] = "edit" + return { + "name": self.name, + "views": [ + (self.env.ref("account.view_move_form").id, "form"), + (self.env.ref("account.view_move_tree").id, "tree"), + ], + "view_type": "form", + "view_mode": "form", + "res_model": "account.move", + "res_id": invoice_id.id, + "type": "ir.actions.act_window", + "context": context, + } + + @api.depends("invoice_ids", "sale_order_ids.invoice_ids") + def _compute_account_invoice_ids_count(self): + for record in self: + record.account_invoice_ids_count = len(self.invoice_ids) + len( + self.sale_order_ids.invoice_ids + ) + + def action_view_account_invoice_ids(self): + return { + "name": self.name, + "views": [ + (self.env.ref("account.view_move_tree").id, "tree"), + (self.env.ref("account.view_move_form").id, "form"), + ], + "view_type": "form", + "view_mode": "tree,form", + "res_model": "account.move", + "type": "ir.actions.act_window", + "domain": [ + ("id", "in", self.invoice_ids.ids + self.sale_order_ids.invoice_ids.ids) + ], + "context": self.env.context, + } + + def _compute_sale_order_ids_count(self): + data = self.env["sale.order"].read_group( + domain=[("order_subscription_id", "in", self.ids)], + fields=["order_subscription_id"], + groupby=["order_subscription_id"], + ) + count_dict = { + item["order_subscription_id"][0]: item["order_subscription_id_count"] + for item in data + } + for record in self: + record.sale_order_ids_count = count_dict.get(record.id, 0) + + def action_view_sale_order_ids(self): + active_ids = self.sale_order_ids.ids + return { + "name": self.name, + "view_type": "form", + "view_mode": "tree,form", + "res_model": "sale.order", + "type": "ir.actions.act_window", + "domain": [("id", "in", active_ids)], + "context": self.env.context, + } + + def _check_dates(self, start, next_invoice): + if start and next_invoice: + date_start = start + date_next_invoice = next_invoice + if not isinstance(date_start, date) and not isinstance( + date_next_invoice, date + ): + date_start = fields.Date.to_date(start) + date_next_invoice = fields.Date.to_date(next_invoice) + if date_start > date_next_invoice: + return True + return False + + def write(self, values): + res = super().write(values) + if "stage_id" in values: + for record in self: + if record.stage_id: + if record.stage_id.type == "in_progress": + record.in_progress = True + record.date_start = date.today() + elif record.stage_id.type == "post": + record.close_reason_id = False + record.in_progress = False + else: + record.in_progress = False + + return res + + @api.model + def create(self, values): + if "recurring_rule_boundary" in values: + if not values["recurring_rule_boundary"]: + template_id = self.env["sale.subscription.template"].search( + [("id", "=", values["template_id"])] + ) + date_start = values["date_start"] + if not isinstance(values["date_start"], date): + date_start = fields.Date.to_date(values["date_start"]) + values["date"] = template_id._get_date(date_start) + if "date_start" in values and "recurring_next_date" in values: + res = self._check_dates(values["date_start"], values["recurring_next_date"]) + if res: + values["date_start"] = values["recurring_next_date"] + values["stage_id"] = ( + self.env["sale.subscription.stage"] + .search([("type", "=", "pre")], order="sequence desc")[-1] + .id + ) + return super(SaleSubscription, self).create(values) diff --git a/subscription_oca/models/sale_subscription_close_reason.py b/subscription_oca/models/sale_subscription_close_reason.py new file mode 100644 index 000000000..36107a029 --- /dev/null +++ b/subscription_oca/models/sale_subscription_close_reason.py @@ -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) diff --git a/subscription_oca/models/sale_subscription_line.py b/subscription_oca/models/sale_subscription_line.py new file mode 100644 index 000000000..b90792d1b --- /dev/null +++ b/subscription_oca/models/sale_subscription_line.py @@ -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, + } diff --git a/subscription_oca/models/sale_subscription_stage.py b/subscription_oca/models/sale_subscription_stage.py new file mode 100644 index 000000000..ee6b6bb46 --- /dev/null +++ b/subscription_oca/models/sale_subscription_stage.py @@ -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")) diff --git a/subscription_oca/models/sale_subscription_tag.py b/subscription_oca/models/sale_subscription_tag.py new file mode 100644 index 000000000..19101a160 --- /dev/null +++ b/subscription_oca/models/sale_subscription_tag.py @@ -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) diff --git a/subscription_oca/models/sale_subscription_template.py b/subscription_oca/models/sale_subscription_template.py new file mode 100644 index 000000000..df89f401b --- /dev/null +++ b/subscription_oca/models/sale_subscription_template.py @@ -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)], + } diff --git a/subscription_oca/readme/CONTRIBUTORS.rst b/subscription_oca/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..e4139ac7d --- /dev/null +++ b/subscription_oca/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Carlos Martínez diff --git a/subscription_oca/readme/DESCRIPTION.rst b/subscription_oca/readme/DESCRIPTION.rst new file mode 100644 index 000000000..648d69835 --- /dev/null +++ b/subscription_oca/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows creating subscriptions that generate recurring invoices or orders. It also enables the sale of products that generate subscriptions. diff --git a/subscription_oca/readme/ROADMAP.rst b/subscription_oca/readme/ROADMAP.rst new file mode 100644 index 000000000..c6355d44b --- /dev/null +++ b/subscription_oca/readme/ROADMAP.rst @@ -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. diff --git a/subscription_oca/readme/USAGE.rst b/subscription_oca/readme/USAGE.rst new file mode 100644 index 000000000..4c5a5ed85 --- /dev/null +++ b/subscription_oca/readme/USAGE.rst @@ -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. diff --git a/subscription_oca/security/ir.model.access.csv b/subscription_oca/security/ir.model.access.csv new file mode 100644 index 000000000..cd0f7dba9 --- /dev/null +++ b/subscription_oca/security/ir.model.access.csv @@ -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 diff --git a/subscription_oca/static/description/index.html b/subscription_oca/static/description/index.html new file mode 100644 index 000000000..5e4490677 --- /dev/null +++ b/subscription_oca/static/description/index.html @@ -0,0 +1,439 @@ + + + + + + +Subscription management + + + +
+

Subscription management

+ + +

Beta License: AGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runboat

+

This module allows creating subscriptions that generate recurring invoices or orders. It also enables the sale of products that generate subscriptions.

+

Table of contents

+ +
+

Usage

+

To make a subscription:

+
    +
  1. Go to Subscriptions > Configuration > Subscription templates.
  2. +
  3. Create the templates you consider, choosing the billing frequency: daily, monthly… and the method of creating the invoice and/or order.
  4. +
  5. Go to Subscription > Subscriptions.
  6. +
  7. 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.
  8. +
  9. The cron job will also end the subscription if its end date has been reached.
  10. +
+

To create subscriptions with the sale of a product:

+
    +
  1. Go to Subscriptions > Subscriptions > Products.
  2. +
  3. Create the product and in the sales tab, complete the fields Subscribable product and Subscription template
  4. +
  5. Create a sales order with the product and confirm it.
  6. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Domatix
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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/sale-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/subscription_oca/static/img/icon.png b/subscription_oca/static/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bab981dd92b92c32d5d27f100be3ff90bfa3b561 GIT binary patch literal 16096 zcmcIr1ydYNw8hmWR}cAIb8`#!clxHO2MIK+u13;gvaNHg%!QWgFOMv|kXrG;x=TT`G6`X&e> zjB>1QmQAi?s~oMW|9#iVJ%viO^UHv{jr+H^7)95`j-R>YuHx zv}vR(5h!wU^1oZblYQLCfBxS-vG%;6gvTY~@$#{8wk%KDZNOB}aVki)sqd9JXcypp z${u69@5)G`esFK%Z~^}sUVt&&mIE5lW@r0&2_?F*9BjD_gz3?*Fw$k=fHMw5f;~1Ta;k4gEPnc@*()pzF2Ot3kVD@GxM>kswSp_ssKx} z69_eu5(|llNP!CiEoR$Dj&|c#P+g5{KsIr3W-Y;SP*UXAq8Ri`jg@)q2jldZ)nNjI z2z~H0CfVUonU>7&kN8D;EV}yAt5LW#OlWBCGc&x9H)u>#TKE9*u%Aa|scK?`n)9>& zp4Xr!GfS2hKr8p}5p@MbWT&6`=UX6F&bW`)Zi#0M;&L z;Yi;XXN>ROIT|#6=d^Wi!`|XynQxef*O^uku6jlcRCRHQ{iAWr0TX}}p}@@PGYD2p z6D0hh{_PX<-kah_o~78oxSp2S~@Zsgx%WmmFmc1o%CLh zhq&im_?ZKPVl7+C2)!;+#mGXoymI19Y6n@UFan1~*Olons}6EyBju^1GEb|Zvgu|v zD}Rms2VrsxEuUd?)44?=l`m%gvcss^81zBY@L*1Y>KFKa9P{3fv_yTge$R9<&SA26z?F`*z7;^KOM6DN1NtI zp;m^aUIJ1zvf1sp8XO72YAv%QFP?0nGjFrxiRgOYgU+-RO#Q_wfu++~NJ=+wRA<

Ui*J$|ByCo)v~>D%$XS<*DoC_r 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 MPD)9#3TzDdAGpB)Gynhq literal 0 HcmV?d00001 diff --git a/subscription_oca/views/product_template_views.xml b/subscription_oca/views/product_template_views.xml new file mode 100644 index 000000000..0ef00a86b --- /dev/null +++ b/subscription_oca/views/product_template_views.xml @@ -0,0 +1,20 @@ + + + + product.template.sub.form + product.template + + + + + + + + + + + + diff --git a/subscription_oca/views/res_partner_views.xml b/subscription_oca/views/res_partner_views.xml new file mode 100644 index 000000000..3be7a6690 --- /dev/null +++ b/subscription_oca/views/res_partner_views.xml @@ -0,0 +1,28 @@ + + + + + res.partner.form + res.partner + + + + + + + diff --git a/subscription_oca/views/sale_order_views.xml b/subscription_oca/views/sale_order_views.xml new file mode 100644 index 000000000..674776bf1 --- /dev/null +++ b/subscription_oca/views/sale_order_views.xml @@ -0,0 +1,29 @@ + + + + view.sale_order.form + sale.order + + + + +

+ +
+ + + + + diff --git a/subscription_oca/views/sale_subscription_stage_views.xml b/subscription_oca/views/sale_subscription_stage_views.xml new file mode 100644 index 000000000..8e6b0bd72 --- /dev/null +++ b/subscription_oca/views/sale_subscription_stage_views.xml @@ -0,0 +1,73 @@ + + + + + view.subscription.stage.form + sale.subscription.stage + +
+ + + + + + + + + + + + + + + +
+
+
+ + + view.subscription.stage.tree + sale.subscription.stage + + + + + + + + + + Susbcription stages + sale.subscription.stage + tree,form + +

+ Click to create a new subscription stage. +

+
+
+ + + +
diff --git a/subscription_oca/views/sale_subscription_tag_views.xml b/subscription_oca/views/sale_subscription_tag_views.xml new file mode 100644 index 000000000..d5997c98c --- /dev/null +++ b/subscription_oca/views/sale_subscription_tag_views.xml @@ -0,0 +1,27 @@ + + + + view.sale.subscription.tag.tree + sale.subscription.tag + + + + + + + + + Tags + sale.subscription.tag + tree + + + + + diff --git a/subscription_oca/views/sale_subscription_template_views.xml b/subscription_oca/views/sale_subscription_template_views.xml new file mode 100644 index 000000000..d683c55b7 --- /dev/null +++ b/subscription_oca/views/sale_subscription_template_views.xml @@ -0,0 +1,135 @@ + + + + + sale.subscription.template.form + sale.subscription.template + + +
+ + +
+ + +
+
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + sale.subscription.template.tree + sale.subscription.template + + + + + + + + + + + Subscription templates + sale.subscription.template + tree,form + + + +
diff --git a/subscription_oca/views/sale_subscription_views.xml b/subscription_oca/views/sale_subscription_views.xml new file mode 100644 index 000000000..427914348 --- /dev/null +++ b/subscription_oca/views/sale_subscription_views.xml @@ -0,0 +1,473 @@ + + + + + sale.subscription.form + sale.subscription + +
+
+
+ + +
+ + + + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+
+
+
+ + + sale.subscription.tree + sale.subscription + + + + + + + + + + + + + + + + + + + sale.subscription.kanban + sale.subscription + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+
+ +
+ +
+ +
+ + + , + + + + +
+ +
+
+ +
+
+ + + + + +
+
+ +
+
+
+
+
+
+
+ + + sale.order.pending.filter + sale.subscription + + + + + + + + + + product.suscribable.filter + product.template + + + + + + + + + + view.subscription.close.reason.tree + sale.subscription.close.reason + + + + + + + + + Close reasons + sale.subscription.close.reason + tree + + + + Subscriptions + sale.subscription + tree,kanban,form + + + + Products + product.template + tree,form + + {'search_default_subsproducts': True, "default_type": "service", "default_subscribable": True} + + + + + + + + + + + + + + + +
diff --git a/subscription_oca/wizard/__init__.py b/subscription_oca/wizard/__init__.py new file mode 100644 index 000000000..a1aca59de --- /dev/null +++ b/subscription_oca/wizard/__init__.py @@ -0,0 +1 @@ +from . import close_subscription_wizard diff --git a/subscription_oca/wizard/close_subscription_wizard.py b/subscription_oca/wizard/close_subscription_wizard.py new file mode 100644 index 000000000..1f38879e4 --- /dev/null +++ b/subscription_oca/wizard/close_subscription_wizard.py @@ -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 diff --git a/subscription_oca/wizard/close_subscription_wizard.xml b/subscription_oca/wizard/close_subscription_wizard.xml new file mode 100644 index 000000000..132a89722 --- /dev/null +++ b/subscription_oca/wizard/close_subscription_wizard.xml @@ -0,0 +1,30 @@ + + + + close.reason.wizard.view + close.reason.wizard + +
+ + + +
+
+
+
+
+ + + Close reason + close.reason.wizard + form + new + +
From 2a5c22df66b86092bd10c61ecfb3eaa8e7c462d8 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 22 Sep 2023 09:07:47 +0000 Subject: [PATCH 2/9] [UPD] Update subscription_oca.pot --- subscription_oca/i18n/subscription_oca.pot | 985 +++++++++++++++++++++ 1 file changed, 985 insertions(+) create mode 100644 subscription_oca/i18n/subscription_oca.pot diff --git a/subscription_oca/i18n/subscription_oca.pot b/subscription_oca/i18n/subscription_oca.pot new file mode 100644 index 000000000..60a026204 --- /dev/null +++ b/subscription_oca/i18n/subscription_oca.pot @@ -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 15.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 "Every" +msgstr "" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form +msgid "For" +msgstr "" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.res_partner_view_form +msgid "Subscriptions" +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 "" From 66def8225a2a24d2fa3447004184a63209b50c23 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 22 Sep 2023 09:14:02 +0000 Subject: [PATCH 3/9] [BOT] post-merge updates --- subscription_oca/static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 subscription_oca/static/description/icon.png diff --git a/subscription_oca/static/description/icon.png b/subscription_oca/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From 5651ffd635ffdb52b80552501f3e01ef6e4f43c7 Mon Sep 17 00:00:00 2001 From: Ilyas Date: Tue, 26 Sep 2023 11:58:12 +0200 Subject: [PATCH 4/9] [IMP] subscription_oca: tests --- subscription_oca/readme/CONTRIBUTORS.rst | 5 + subscription_oca/tests/__init__.py | 3 + .../tests/test_subscription_oca.py | 182 ++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 subscription_oca/tests/__init__.py create mode 100644 subscription_oca/tests/test_subscription_oca.py diff --git a/subscription_oca/readme/CONTRIBUTORS.rst b/subscription_oca/readme/CONTRIBUTORS.rst index e4139ac7d..3324fc474 100644 --- a/subscription_oca/readme/CONTRIBUTORS.rst +++ b/subscription_oca/readme/CONTRIBUTORS.rst @@ -1 +1,6 @@ * Carlos Martínez + + +* `Ooops404 `__: + + * Ilyas diff --git a/subscription_oca/tests/__init__.py b/subscription_oca/tests/__init__.py new file mode 100644 index 000000000..f445239d7 --- /dev/null +++ b/subscription_oca/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_subscription_oca diff --git a/subscription_oca/tests/test_subscription_oca.py b/subscription_oca/tests/test_subscription_oca.py new file mode 100644 index 000000000..65f3b75d3 --- /dev/null +++ b/subscription_oca/tests/test_subscription_oca.py @@ -0,0 +1,182 @@ +# Copyright 2023 ooops404 +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from dateutil.relativedelta import relativedelta + +from odoo import fields +from odoo.tests import TransactionCase + + +class TestSubscriptionOCA(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.pricelist = cls.env["product.pricelist"].create( + {"name": "pricelist for contract test"} + ) + cls.partner = cls.env["res.partner"].create( + { + "name": "partner test subscription_oca", + "property_product_pricelist": cls.pricelist.id, + "email": "demo1@demo.com", + } + ) + cls.partner_2 = cls.env["res.partner"].create( + { + "name": "partner test subscription_oca 2", + "property_product_pricelist": cls.pricelist.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.taxes_id = [(6, 0, cls.tax_10pc_incl.ids)] + cls.product_2 = cls.env.ref("product.product_product_2") + 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.tmpl = cls.env["sale.subscription.template"].create( + { + "name": "Test Template", + "code": "OMG", + "description": "Some sort of subscription terms", + "product_ids": [(6, 0, [cls.product_1.id, cls.product_2.id])], + } + ) + cls.stage = cls.env["sale.subscription.stage"].create( + { + "name": "Test Sub Stage", + } + ) + cls.tag = cls.env["sale.subscription.tag"].create( + { + "name": "Test Tag", + } + ) + cls.sub = cls.env["sale.subscription"].create( + { + "company_id": 1, + "partner_id": cls.partner.id, + "template_id": cls.tmpl.id, + "tag_ids": [(6, 0, [cls.tag.id])], + "stage_id": cls.stage.id, + "pricelist_id": cls.pricelist.id, + "fiscal_position_id": cls.fiscal.id, + } + ) + cls.sub_line = cls.env["sale.subscription.line"].create( + { + "company_id": 1, + "sale_subscription_id": cls.sub.id, + "product_id": cls.product_1.id, + } + ) + cls.close_reason = cls.env["sale.subscription.close.reason"].create( + { + "name": "Test Close Reason", + } + ) + + 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.action_confirm() # without subs. + + def test_subscription_oca_sub_line(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.sub.cron_subscription_management() + # invoice should be created by cron + inv_id = self.env["account.move"].search( + [("subscription_id", "=", self.sub.id)] + ) + self.assertEqual(len(inv_id), 1) + self.assertEqual(self.sub.recurring_total, 27.95) + self.assertEqual(self.sub.amount_total, 30.75) + + def test_subscription_oca_sub_workflow(self): + sale_order = self.sub.create_sale_order() + self.assertTrue(sale_order) + move_id = self.sub.create_invoice() + self.assertTrue(move_id) + res = self.sub.manual_invoice() + self.assertEqual(res["type"], "ir.actions.act_window") + inv_ids = self.env["account.move"].search( + [("subscription_id", "=", self.sub.id)] + ) + self.assertEqual(len(inv_ids), 2) + self.assertEqual(sum(inv_ids.mapped("amount_total")), 2 * 30.75) + self.assertEqual(self.sub.account_invoice_ids_count, 2) + res = self.sub.action_view_account_invoice_ids() + self.assertEqual(res["type"], "ir.actions.act_window") + self.assertEqual(self.sub.sale_order_ids_count, 1) + res = self.sub.action_view_sale_order_ids() + self.assertIn(str(self.sub.sale_order_ids.id), str(res["domain"])) + self.sub.calculate_recurring_next_date(fields.Datetime.now()) + self.assertEqual( + self.sub.recurring_next_date, + fields.Date.today() + relativedelta(months=1), + ) + self.sub.partner_id = self.partner_2 + self.sub.onchange_partner_id() + self.assertEqual( + self.sub.pricelist_id.id, self.partner_2.property_product_pricelist.id + ) + self.sub.onchange_partner_id_fpos() + self.assertFalse(self.sub.fiscal_position_id) + res = self.sub.action_close_subscription() + self.assertEqual(res["type"], "ir.actions.act_window") + + def test_subscription_oca_sub_stage(self): + # sale.subscription.stage + self.stage._check_lot_product() # should not raise From e247ac0ce833c9fa8b6c4a4e16790d6a8bd90065 Mon Sep 17 00:00:00 2001 From: Ilyas Date: Tue, 26 Sep 2023 15:10:04 +0200 Subject: [PATCH 5/9] [IMP] subscription_oca: refactoring --- subscription_oca/models/sale_subscription.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/subscription_oca/models/sale_subscription.py b/subscription_oca/models/sale_subscription.py index 4cc562ee4..250f97a00 100644 --- a/subscription_oca/models/sale_subscription.py +++ b/subscription_oca/models/sale_subscription.py @@ -451,8 +451,8 @@ class SaleSubscription(models.Model): def create(self, values): if "recurring_rule_boundary" in values: if not values["recurring_rule_boundary"]: - template_id = self.env["sale.subscription.template"].search( - [("id", "=", values["template_id"])] + template_id = self.env["sale.subscription.template"].browse( + values["template_id"] ) date_start = values["date_start"] if not isinstance(values["date_start"], date): @@ -464,7 +464,7 @@ class SaleSubscription(models.Model): values["date_start"] = values["recurring_next_date"] values["stage_id"] = ( self.env["sale.subscription.stage"] - .search([("type", "=", "pre")], order="sequence desc")[-1] + .search([("type", "=", "pre")], order="sequence desc", limit=1) .id ) return super(SaleSubscription, self).create(values) From bb7ccf12c39c3289a380fa6bedd30d553e3a0efe Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 29 Sep 2023 17:19:33 +0000 Subject: [PATCH 6/9] [BOT] post-merge updates --- subscription_oca/README.rst | 7 ++- subscription_oca/__manifest__.py | 2 +- .../static/description/index.html | 54 +++++++++++-------- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/subscription_oca/README.rst b/subscription_oca/README.rst index 0b03b4bf1..f8a70688f 100644 --- a/subscription_oca/README.rst +++ b/subscription_oca/README.rst @@ -7,7 +7,7 @@ Subscription management !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:3772d65a58c07d0348bd13d3c882810c94bfb87389c62fec6d16fe8ef130252c + !! source digest: sha256:42fc353409c68ca6defc36ba2273b97ae36edb6f629c0c041db1ef7f1e01ba00 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -81,6 +81,11 @@ Contributors * Carlos Martínez + +* `Ooops404 `__: + + * Ilyas + Maintainers ~~~~~~~~~~~ diff --git a/subscription_oca/__manifest__.py b/subscription_oca/__manifest__.py index 8e9cb33b8..fe221adda 100644 --- a/subscription_oca/__manifest__.py +++ b/subscription_oca/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Subscription management", "summary": "Generate recurring invoices.", - "version": "15.0.1.0.0", + "version": "15.0.1.0.1", "development_status": "Beta", "category": "Subscription Management", "website": "https://github.com/OCA/contract", diff --git a/subscription_oca/static/description/index.html b/subscription_oca/static/description/index.html index 5e4490677..da81d249f 100644 --- a/subscription_oca/static/description/index.html +++ b/subscription_oca/static/description/index.html @@ -1,20 +1,20 @@ - + - + Subscription management