diff --git a/setup/subscription_oca/odoo/addons/subscription_oca b/setup/subscription_oca/odoo/addons/subscription_oca new file mode 120000 index 000000000..55d96c3d7 --- /dev/null +++ b/setup/subscription_oca/odoo/addons/subscription_oca @@ -0,0 +1 @@ +../../../../subscription_oca \ No newline at end of file diff --git a/setup/subscription_oca/setup.py b/setup/subscription_oca/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/subscription_oca/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/subscription_oca/README.rst b/subscription_oca/README.rst new file mode 100644 index 000000000..9ba4a3b68 --- /dev/null +++ b/subscription_oca/README.rst @@ -0,0 +1,104 @@ +======================= +Subscription management +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:42fc353409c68ca6defc36ba2273b97ae36edb6f629c0c041db1ef7f1e01ba00 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github + :target: https://github.com/OCA/contract/tree/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 + + +* `Ooops404 `__: + + * Ilyas + +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..fa4400f48 --- /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": "16.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/i18n/es.po b/subscription_oca/i18n/es.po new file mode 100644 index 000000000..73e16b048 --- /dev/null +++ b/subscription_oca/i18n/es.po @@ -0,0 +1,1018 @@ +# 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" +"PO-Revision-Date: 2023-10-29 17:39+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\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 "" +"\n" +" Como etapa de tipo \"En curso\", activará el proceso de facturación " +"periódica si procede. Si esta etapa es la primera -en orden secuencial- de " +"los tipos \"En curso\" disponibles y se produce un cambio de etapa de " +"cualquier otro tipo que no sea \"En curso\" a éste, se creará " +"automáticamente una factura si la fecha de inicio es el día actual.\n" +" " + +#. 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 "" +"\n" +" Equivalente a borrador, una suscripción está lista para comenzar " +"cuando no está marcada como en curso pero puede estarlo en cualquier " +"momento. Si no hay definida ninguna etapa de tipo 'Cerrada', cuando una " +"suscripción llegue a su fin por medios automáticos, se marcará con esta " +"etapa.\n" +" " + +#. 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 "" +"\n" +" La etapa final de una suscripción. Hay dos formas de marcar una " +"suscripción como cerrada. La más sencilla es utilizar las funciones de " +"movimiento de tarjetas Kanban, pulsando el botón \"Cerrar suscripción\" (" +"sólo disponible si una suscripción está en curso).\n" +" " + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form +msgid "Every" +msgstr "Cada" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form +msgid "For" +msgstr "Para" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.res_partner_view_form +msgid "Subscriptions" +msgstr "Suscripciones" + +#. 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 "" +"Una suscripción determinada puede marcarse como cerrada cuando, por ejemplo, " +"no se desea renovarla." + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_needaction +msgid "Action Needed" +msgstr "Necesita Acción" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__active +msgid "Active" +msgstr "Activo" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_ids +msgid "Activities" +msgstr "Actividades" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decoración de Excepción de la Actividad" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_state +msgid "Activity State" +msgstr "Estado de la Actividad" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_type_icon +msgid "Activity Type Icon" +msgstr "Tipo de Icono de Actividad" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.view_subscription_stage_form +msgid "Add new description..." +msgstr "Añadir nueva descripción..." + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__amount_tax +msgid "Amount Tax" +msgstr "Importe del Impuesto" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__amount_total +msgid "Amount Total" +msgstr "Importe Total" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_attachment_count +msgid "Attachment Count" +msgstr "Recuento de Archivos Adjuntos" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__recurring_rule_boundary +msgid "Boundary" +msgstr "Límite" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.close_reason_wizard_view +msgid "Cancel" +msgstr "Cancelar" + +#. module: subscription_oca +#: model_terms:ir.actions.act_window,help:subscription_oca.subscription_stage_action +msgid "Click to create a new subscription stage." +msgstr "Haga clic para crear una nueva etapa de suscripción." + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__close_reason_id +msgid "Close Reason" +msgstr "Razón de Cierre" + +#. 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 "Motivo de cierre" + +#. module: subscription_oca +#: model:ir.model,name:subscription_oca.model_sale_subscription_close_reason +msgid "Close reason model" +msgstr "Cerrar modelo de razón" + +#. module: subscription_oca +#: model:ir.model,name:subscription_oca.model_close_reason_wizard +msgid "Close reason wizard" +msgstr "Cerrar el asistente de razones" + +#. 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 "Razón de Cierre" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form +msgid "Close subscription" +msgstr "Cerrar suscripción" + +#. 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 "Cerrado" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__code +msgid "Code" +msgstr "Código" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__color +msgid "Color Index" +msgstr "Índice de Color" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__user_id +msgid "Commercial agent" +msgstr "Agente comercial" + +#. 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 "Compañía" + +#. module: subscription_oca +#: model:ir.ui.menu,name:subscription_oca.sale_subscription_configuration_menu +msgid "Configuration" +msgstr "Configuración" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.close_reason_wizard_view +msgid "Confirm" +msgstr "Confirmar" + +#. module: subscription_oca +#: model:ir.model,name:subscription_oca.model_res_partner +msgid "Contact" +msgstr "Contacto" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form +msgid "Create Invoice" +msgstr "Crear Factura" + +#. 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 "Creado por" + +#. module: subscription_oca +#: code:addons/subscription_oca/models/sale_subscription.py:0 +#, python-format +msgid "Created invoice with reference" +msgstr "Factura creada con referencia" + +#. 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 "Creado el" + +#. 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 "Divisa" + +#. module: subscription_oca +#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_type__days +msgid "Day(s)" +msgstr "Día(s)" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_kanban +msgid "Delete" +msgstr "Borrar" + +#. 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 "Descripción" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__discount +msgid "Discount (%)" +msgstr "(%) Descuento" + +#. 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 "Mostrar Nombre" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__display_name +msgid "Display name" +msgstr "Mostrar nombre" + +#. module: subscription_oca +#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__invoicing_mode__draft +msgid "Draft" +msgstr "Borrador" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__recurring_rule_boundary +msgid "Duration" +msgstr "Duración" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_kanban +msgid "Edit" +msgstr "Editar" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__stage_str +msgid "Etapa" +msgstr "Etapa" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__date +msgid "Finish date" +msgstr "Fecha de fin" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__fiscal_position_id +msgid "Fiscal Position" +msgstr "Posición Fiscal" + +#. module: subscription_oca +#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_boundary__limited +msgid "Fixed" +msgstr "Fijado" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_follower_ids +msgid "Followers" +msgstr "Seguidores" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguidores/as (Empresas)" + +#. 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 "Icono de fuente impresionante, por ejemplo fa-tasks" + +#. module: subscription_oca +#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_boundary__unlimited +msgid "Forever" +msgstr "Para Siempre" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__has_message +msgid "Has Message" +msgstr "Tiene Mensaje" + +#. 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 "ID (identificación)" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_exception_icon +msgid "Icon" +msgstr "Icono" + +#. module: subscription_oca +#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icono para indicar la excepción de la actividad." + +#. 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 "Si está marcada, nuevos mensajes requieren tu atención." + +#. module: subscription_oca +#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Si está marcada, algunos mensajes tienen error de entrega." + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__image +msgid "Image" +msgstr "Imagen" + +#. 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 "En progreso" + +#. 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 "Factura" + +#. module: subscription_oca +#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__invoicing_mode__invoice_send +msgid "Invoice & send" +msgstr "Facturar y enviar" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__account_invoice_ids_count +msgid "Invoice Count" +msgstr "Conteo de Facturas" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__invoice_mail_template_id +msgid "Invoice Email" +msgstr "Correo Electrónico de Facturación" + +#. 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 "Facturas" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form +msgid "Invoicing" +msgstr "Facturación" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__invoicing_mode +msgid "Invoicing mode" +msgstr "Modo de facturación" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_is_follower +msgid "Is Follower" +msgstr "Es Seguidor/a" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__journal_id +msgid "Journal" +msgstr "Dario" + +#. module: subscription_oca +#: model:ir.model,name:subscription_oca.model_account_move +msgid "Journal Entry" +msgstr "Entrada Diaria" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__fold +msgid "Kanban folded" +msgstr "Kanban plegado" + +#. 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 "Última Modificación el" + +#. 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 "Última Actualización por" + +#. 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 "Ultima Actualización el" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_main_attachment_id +msgid "Main Attachment" +msgstr "Adjunto Principal" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_kanban +msgid "Menú desplegable" +msgstr "Menú desplegable" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_has_error +msgid "Message Delivery error" +msgstr "Error en Entrega del Mensaje" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_ids +msgid "Messages" +msgstr "Mensajes" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form +msgid "Misc" +msgstr "Misc" + +#. module: subscription_oca +#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_type__months +msgid "Month(s)" +msgstr "Mes(es)" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Fecha Límite de Mi Actividad" + +#. 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 "Nombre" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form +msgid "New subscription" +msgstr "Nueva suscripción" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Fecha Límite para la siguiente Actividad" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_summary +msgid "Next Activity Summary" +msgstr "Resumen de la Siguiente Actividad" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo Siguiente Actividad" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__recurring_next_date +msgid "Next invoice date" +msgstr "Fecha siguiente factura" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_needaction_counter +msgid "Number of Actions" +msgstr "Número de acciones" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_has_error_counter +msgid "Number of errors" +msgstr "Número de errores" + +#. 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 "Número de mensajes que requieren una acción" + +#. 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 "Número de mensajes con error de entrega" + +#. module: subscription_oca +#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__message_unread_counter +msgid "Number of unread messages" +msgstr "Número de mensajes no leídos" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__sale_order_ids +msgid "Orders" +msgstr "Órdenes" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__sale_order_id +msgid "Origin sale order" +msgstr "Orden de venta en origen" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form +msgid "Other info" +msgstr "Otra información" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__partner_id +msgid "Partner" +msgstr "Socio" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.view_sale_order_pending_filter +msgid "Pending subscriptions" +msgstr "Subscripciones pendientes" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__pricelist_id +msgid "Pricelist" +msgstr "Lista de Precios" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__product_id +msgid "Product" +msgstr "Producto" + +#. module: subscription_oca +#: model:ir.model,name:subscription_oca.model_product_template +msgid "Product Template" +msgstr "Plantilla del Producto" + +#. 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 "Productos" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__product_uom_qty +msgid "Quantity" +msgstr "Cantidad" + +#. 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 "Listo para empezar" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_close_reason_wizard__close_reason_id +msgid "Reason" +msgstr "Razón" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__recurring_rule_type +msgid "Recurrence" +msgstr "Recurrencia" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__recurring_total +msgid "Recurring price" +msgstr "Precio recurrente" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__code +msgid "Reference" +msgstr "Referencia" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__recurring_interval +msgid "Repeat every" +msgstr "Repetir cada" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__activity_user_id +msgid "Responsible User" +msgstr "Usuario Responsable" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__recurring_rule_count +msgid "Rule count" +msgstr "Conteo de reglas" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__sale_subscription_line_ids +msgid "Sale Subscription Line" +msgstr "Línea de Suscripciones de Venta" + +#. 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 "Orden de venta y factura" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__sale_order_ids_count +msgid "Sale orders" +msgstr "Pedidos de venta" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__crm_team_id +msgid "Sale team" +msgstr "Equipo de venta" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form +msgid "Sales" +msgstr "Ventas" + +#. module: subscription_oca +#: model:ir.model,name:subscription_oca.model_sale_order +msgid "Sales Order" +msgstr "Orden de Venta" + +#. module: subscription_oca +#: model:ir.model,name:subscription_oca.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de Orden de Venta" + +#. 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 "Secuencia" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__stage_id +msgid "Stage" +msgstr "Etapa" + +#. 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 "" +"Las etapas definen el ciclo de vida de una suscripción determinada; es " +"decir, una suscripción puede ser de tipo " +"\"Lista para empezar\", \"En curso\" o \"Cerrada\". Tenga en " +"cuenta que sólo puede haber una etapa de tipo \"Cerrada\"." + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__date_start +msgid "Start date" +msgstr "Fecha inicio" + +#. 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 "" +"Estados basados en actividades\n" +"Retrasada: La fecha límite ya ha pasado\n" +"Hoy: La fecha límite de la actividad es hoy\n" +"Futuras: Actividades futuras." + +#. 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 "Producto suscribible" + +#. 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 "Suscripción" + +#. 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 "Conteo de Suscripciones" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_form +msgid "Subscription lines" +msgstr "Líneas de suscripción" + +#. module: subscription_oca +#: model:ir.model,name:subscription_oca.model_sale_subscription_line +msgid "Subscription lines added to a given subscription" +msgstr "Líneas de suscripción agregadas a una suscripción determinada" + +#. 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 "Nombre de suscripción" + +#. module: subscription_oca +#: model:ir.ui.menu,name:subscription_oca.subscription_stage_menu +msgid "Subscription stages" +msgstr "Etapas de suscripción" + +#. module: subscription_oca +#: model:ir.ui.menu,name:subscription_oca.subscription_tag_menu +msgid "Subscription tags" +msgstr "Etiquetas de suscripción" + +#. 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 "Plantilla de suscripción" + +#. 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 "Plantillas de suscripción" + +#. 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 "Suscripciones" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_order__subscriptions_count +msgid "Subscriptions Count" +msgstr "Conteo de Suscripciones" + +#. 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 "Gestión de suscripciones" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__price_subtotal +msgid "Subtotal" +msgstr "Subtotal" + +#. module: subscription_oca +#: model:ir.actions.act_window,name:subscription_oca.subscription_stage_action +msgid "Susbcription stages" +msgstr "Etapas de suscripción" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.view_product_suscribable_filter +msgid "Suscribable products" +msgstr "Producto suscribible" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_tag__name +msgid "Tag name" +msgstr "Nombre de Etiqueta" + +#. 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 "Categorías" + +#. module: subscription_oca +#: model:ir.model,name:subscription_oca.model_sale_subscription_tag +msgid "Tags for sale subscription" +msgstr "Etiquetas para la venta de suscripción" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__tax_ids +msgid "Taxes" +msgstr "Impuestos" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__amount_tax_line_amount +msgid "Taxes Amount" +msgstr "Importe de impuestos" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form +msgid "Terms and Conditions" +msgstr "Términos y Condiciones" + +#. 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 "Términos y condiciones" + +#. 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 "Ya se ha declarado una etapa de tipo Cerrado" + +#. 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 "" +"Esta barra permite filtrar las oportunidades en función de las actividades " +"programadas." + +#. 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 "" +"Este campo dicta el orden visual de las etapas en la vista Kanban y " +"formulario. Aunque es puramente visual, tenga en cuenta que si el orden no " +"es consecuente con sus necesidades, podría " +"tener una etapa de tipo 'Cerrada' antes de una 'Lista para empezar'." + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__to_renew +msgid "To renew" +msgstr "Para renovar" + +#. module: subscription_oca +#: code:addons/subscription_oca/models/sale_subscription.py:0 +#, python-format +msgid "To validate" +msgstr "Para Validar" + +#. 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 "Total" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_tree +msgid "Total Tax" +msgstr "Impuesto Total" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_tree +msgid "Total subtotal" +msgstr "Subtotal total" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_stage__type +msgid "Type" +msgstr "Tipo" + +#. 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 "Tipo (o clase) de actividad excepcional registrada." + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_line__price_unit +msgid "Unit price" +msgstr "Precio unitario" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_unread +msgid "Unread Messages" +msgstr "Mensajes no Leídos" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__message_unread_counter +msgid "Unread Messages Counter" +msgstr "Contador de Mensajes no Leídos" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription__website_message_ids +msgid "Website Messages" +msgstr "Mensajes del sitio Web" + +#. module: subscription_oca +#: model:ir.model.fields,help:subscription_oca.field_sale_subscription__website_message_ids +msgid "Website communication history" +msgstr "Historial de la comunicación en el sitio web" + +#. module: subscription_oca +#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_type__weeks +msgid "Week(s)" +msgstr "Semana(s)" + +#. module: subscription_oca +#: model:ir.model.fields.selection,name:subscription_oca.selection__sale_subscription_template__recurring_rule_type__years +msgid "Year(s)" +msgstr "Año(s)" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form +msgid "e.g. Monthly Subscription" +msgstr "p. ej. Suscripción Mensual" + +#. module: subscription_oca +#: model_terms:ir.ui.view,arch_db:subscription_oca.sale_subscription_template_form +msgid "month(s)" +msgstr "Mes(es)" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__product_ids_count +msgid "product_ids" +msgstr "product_ids" + +#. module: subscription_oca +#: model:ir.model.fields,field_description:subscription_oca.field_sale_subscription_template__subscription_count +msgid "subscription_ids" +msgstr "subscription_ids" diff --git a/subscription_oca/i18n/subscription_oca.pot b/subscription_oca/i18n/subscription_oca.pot new file mode 100644 index 000000000..0502817f3 --- /dev/null +++ b/subscription_oca/i18n/subscription_oca.pot @@ -0,0 +1,979 @@ +# 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 +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__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 "" 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..684570684 --- /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) + ) + + 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"].browse( + values["template_id"] + ) + date_start = values["date_start"] + if not isinstance(values["date_start"], date): + date_start = fields.Date.to_date(values["date_start"]) + values["date"] = template_id._get_date(date_start) + if "date_start" in values and "recurring_next_date" in values: + res = self._check_dates(values["date_start"], values["recurring_next_date"]) + if res: + values["date_start"] = values["recurring_next_date"] + values["stage_id"] = ( + self.env["sale.subscription.stage"] + .search([("type", "=", "pre")], order="sequence desc", limit=1) + .id + ) + return super(SaleSubscription, self).create(values) 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..a780e4075 --- /dev/null +++ b/subscription_oca/models/sale_subscription_line.py @@ -0,0 +1,319 @@ +# 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 + ) + ) + # 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, + ) + 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"] + product_price = product.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) + pricelist_item = PricelistItem.browse(rule_id) + + if pricelist_item.base == "standard_price": + product_price = product.standard_price + product_currency = product.cost_currency_id + elif ( + pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id + ): + product_price = pricelist_item.base_pricelist_id._get_product_price( + product, self.product_uom_qty or 1.0, uom=self.product_id.uom_id + ) + 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_price * uom_factor * cur_factor, currency_id + + def _get_display_price(self, product): + if self.sale_subscription_id.pricelist_id.discount_policy == "with_discount": + return self.sale_subscription_id.pricelist_id._get_product_price( + product, self.product_uom_qty or 1.0, uom=self.product_id.uom_id + ) + + 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, + ) + 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..8bc396178 --- /dev/null +++ b/subscription_oca/models/sale_subscription_stage.py @@ -0,0 +1,34 @@ +# 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", compute="_compute_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")) + + @api.depends("name") + def _compute_display_name(self): + for stage in self: + stage.display_name = stage.name 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..8ddfb0521 --- /dev/null +++ b/subscription_oca/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* Carlos Martínez +* Carolina Ferrer + + +* `Ooops404 `__: + + * Ilyas 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..f3ef9cf85 --- /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 by using the *Create Invoice* button. This action creates just an invoice even if the subscription template has the *Sale Order & Invoice* option selected, because the *Invoicing mode* option is triggered through the cron job. +#. 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/icon.png b/subscription_oca/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/subscription_oca/static/description/icon.png differ diff --git a/subscription_oca/static/description/index.html b/subscription_oca/static/description/index.html new file mode 100644 index 000000000..da81d249f --- /dev/null +++ b/subscription_oca/static/description/index.html @@ -0,0 +1,451 @@ + + + + + + +Subscription management + + + +
+

Subscription management

+ + +

Beta License: AGPL-3 OCA/contract 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. +
+
+
+

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

+ +
+
+

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