diff --git a/crm_event/README.rst b/crm_event/README.rst new file mode 100644 index 000000000..be7cdf39f --- /dev/null +++ b/crm_event/README.rst @@ -0,0 +1,119 @@ +================== +CRM Event Category +================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fevent-lightgray.png?logo=github + :target: https://github.com/OCA/event/tree/12.0/crm_event + :alt: OCA/event +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/event-12-0/event-12-0-crm_event + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/199/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of CRM opportunities (and leads, if +enabled) to support linking them to event categories and to allow you to keep +track of leads interested in an upcoming event of some category. + +This is useful if you organize your events based on the amount of people +interested in a certain category of event. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To make use of this module, a user needs these minimal permissions: + +- Sales / User: Own Documents Only +- Events / User + +Usage +===== + +To link a lead or opportunity to an event category: + +#. Go to *CRM > Pipeline* and pick one lead. +#. Go to the lead form. +#. Use the new field *Event category*. + +To know if there are events planned of a certain category: + +#. Go to *Events > Configuration > Event Categories* and pick one. +#. Use the new *Events* smart button. + + * It only counts those that are upcoming or running. + * The number between parenthesis is the available seats sum of all those events. + +To know if there is people interested in a certain category of event: + +#. Go to *Events > Configuration > Event Categories* and pick one. +#. Use the new *Opportunities* smart button. + + * It only counts opportunities that aren't won or lost. + * The number between parenthesis is the wanted seats sum of all those events. + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* Jairo Llopis (https://www.tecnativa.com/) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-Yajo| image:: https://github.com/Yajo.png?size=40px + :target: https://github.com/Yajo + :alt: Yajo + +Current `maintainer `__: + +|maintainer-Yajo| + +This module is part of the `OCA/event `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/crm_event/__init__.py b/crm_event/__init__.py new file mode 100644 index 000000000..55ec7fc9a --- /dev/null +++ b/crm_event/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import reports diff --git a/crm_event/__manifest__.py b/crm_event/__manifest__.py new file mode 100644 index 000000000..391d80980 --- /dev/null +++ b/crm_event/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "CRM Event Category", + "summary": "Link opportunities to event categories", + "version": "13.0.1.0.0", + "development_status": "Beta", + "category": "Event Management", + "website": "https://github.com/OCA/event", + "author": "Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["Yajo"], + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": ["crm", "event"], + "data": [ + "reports/event_type_report_view.xml", + "security/ir.model.access.csv", + "views/crm_lead_view.xml", + "views/event_type_view.xml", + ], +} diff --git a/crm_event/i18n/crm_event.pot b/crm_event/i18n/crm_event.pot new file mode 100644 index 000000000..59d557bd3 --- /dev/null +++ b/crm_event/i18n/crm_event.pot @@ -0,0 +1,219 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * crm_event +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.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: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type_report__events_available_count +msgid "Available events count" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type_report__seats_limited_available +msgid "Available seats in limited events" +msgstr "" + +#. module: crm_event +#: model:ir.ui.menu,name:crm_event.menu_event_type_report +msgid "Categories" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type_report__name +msgid "Category name" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type_report__display_name +msgid "Display Name" +msgstr "" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.crm_opportunity_report_view_search +msgid "Event" +msgstr "" + +#. module: crm_event +#: model:ir.model,name:crm_event.model_event_type +msgid "Event Category" +msgstr "" + +#. module: crm_event +#: model:ir.model,name:crm_event.model_event_type_report +msgid "Event categories analysis report" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_crm_lead__event_type_id +msgid "Event category" +msgstr "" + +#. module: crm_event +#: model:ir.actions.act_window,name:crm_event.action_event_type_report +#: model_terms:ir.ui.view,arch_db:crm_event.view_event_type_report_graph +#: model_terms:ir.ui.view,arch_db:crm_event.view_event_type_report_pivot +#: model_terms:ir.ui.view,arch_db:crm_event.view_event_type_report_search +msgid "Event category analysis" +msgstr "" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.view_event_type_report_search +msgid "Event seats availability" +msgstr "" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.crm_opportunity_report_view_search +#: model_terms:ir.ui.view,arch_db:crm_event.view_crm_case_leads_filter +#: model_terms:ir.ui.view,arch_db:crm_event.view_crm_case_opportunities_filter +msgid "Event type" +msgstr "" + +#. module: crm_event +#: code:addons/crm_event/models/event_type.py:120 +#, python-format +msgid "Events" +msgstr "" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.view_event_type_form +msgid "Events (seats)" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type__seats_available_total +msgid "Events available (and seats)" +msgstr "" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.view_event_type_report_search +msgid "Extended Filters" +msgstr "" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.view_event_type_report_search +msgid "Group By" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type_report__id +msgid "ID" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_crm_lead__seats_wanted +msgid "If this lead/opportunity is related to a specific event category, indicate how many seats would you sell if won." +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_crm_lead__event_type_id +msgid "If this lead/opportunity is related to a specific event category, indicate it here." +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type_report____last_update +msgid "Last Modified on" +msgstr "" + +#. module: crm_event +#: model:ir.model,name:crm_event.model_crm_lead +msgid "Lead/Opportunity" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type__crm_lead_ids +msgid "Leads/Opportunities" +msgstr "" + +#. module: crm_event +#: selection:event.type.report,event_seats_availability:0 +msgid "Limited" +msgstr "" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.view_event_type_report_search +msgid "Limited events" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type__open_opportunities_count +#: model:ir.model.fields,field_description:crm_event.field_event_type_report__open_opportunities_count +msgid "Open Opportunities Count" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_event_type__seats_wanted_total +msgid "Open opportunities for events of this category (and wanted seats)." +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_event_type__open_opportunities_count +msgid "Open opportunities for events of this category." +msgstr "" + +#. module: crm_event +#: code:addons/crm_event/models/event_type.py:135 +#, python-format +msgid "Opportunities" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type__seats_wanted_total +msgid "Opportunities (seats)" +msgstr "" + +#. module: crm_event +#: model_terms:ir.actions.act_window,help:crm_event.action_event_type_report +msgid "Report to analyze interest and status of event categories, taking into account upcoming or running events and potential sale information." +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_crm_lead__seats_wanted +msgid "Seats Wanted" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type_report__event_seats_availability +msgid "Seats availability" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_event_type__seats_wanted_sum +msgid "Sum of wanted seats in opportunities for events of this category." +msgstr "" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.crm_case_tree_view_oppor +msgid "Total seats wanted" +msgstr "" + +#. module: crm_event +#: selection:event.type.report,event_seats_availability:0 +msgid "Unlimited" +msgstr "" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.view_event_type_report_search +msgid "Unlimited events" +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_event_type__seats_available_total +msgid "Upcoming/running events of this category (and available seats)." +msgstr "" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type__seats_wanted_sum +#: model:ir.model.fields,field_description:crm_event.field_event_type_report__seats_wanted +msgid "Wanted seats" +msgstr "" + diff --git a/crm_event/i18n/es.po b/crm_event/i18n/es.po new file mode 100644 index 000000000..765dfb36d --- /dev/null +++ b/crm_event/i18n/es.po @@ -0,0 +1,102 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * crm_event_type +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-02-10 12:25+0000\n" +"PO-Revision-Date: 2021-02-10 12:26+0000\n" +"Last-Translator: Jairo Llopis \n" +"Language-Team: \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.4.2\n" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.crm_opportunity_report_view_search +msgid "Event" +msgstr "Evento" + +#. module: crm_event +#: model:ir.model,name:crm_event.model_event_type +msgid "Event Category" +msgstr "Categoría de eventos" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_crm_lead__event_type_id +#: model_terms:ir.ui.view,arch_db:crm_event.crm_opportunity_report_view_search +#: model_terms:ir.ui.view,arch_db:crm_event.view_crm_case_leads_filter +#: model_terms:ir.ui.view,arch_db:crm_event.view_crm_case_opportunities_filter +msgid "Event category" +msgstr "Categoría de evento" + +#. module: crm_event +#: code:addons/crm_event/models/event_type.py:94 +#, python-format +msgid "Events" +msgstr "Eventos" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type__seats_available_total +msgid "Events (seats)" +msgstr "Eventos (plazas)" + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_crm_lead__seats_wanted +msgid "" +"If this lead/opportunity is related to a specific event category, indicate how " +"many seats would you sell if won." +msgstr "" +"Si esta iniciativa u oportunidad está relacionada con una categoría " +"específica de evento, indique cuántas plazas vendería si se gana." + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_crm_lead__event_type_id +msgid "" +"If this lead/opportunity is related to a specific event category, indicate it " +"here." +msgstr "" +"Si esta iniciativa u oportunidad está relacionada con una categoría " +"específica de evento, indíquelo aquí." + +#. module: crm_event +#: model:ir.model,name:crm_event.model_crm_lead +msgid "Lead/Opportunity" +msgstr "Iniciativa/Oportunidad" + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_event_type__seats_wanted_total +msgid "Open opportunities for events of this category (and wanted seats)." +msgstr "" +"Oportunidades abiertas para eventos de esta categoría (y plazas deseadas)." + +#. module: crm_event +#: code:addons/crm_event/models/event_type.py:109 +#, python-format +msgid "Opportunities" +msgstr "Oportunidades" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_event_type__seats_wanted_total +msgid "Opportunities (seats)" +msgstr "Oportunidades (plazas)" + +#. module: crm_event +#: model:ir.model.fields,field_description:crm_event.field_crm_lead__seats_wanted +msgid "Seats Wanted" +msgstr "Plazas deseadas" + +#. module: crm_event +#: model_terms:ir.ui.view,arch_db:crm_event.crm_case_tree_view_oppor +msgid "Total seats wanted" +msgstr "Total de plazas deseadas" + +#. module: crm_event +#: model:ir.model.fields,help:crm_event.field_event_type__seats_available_total +msgid "Upcoming/running events of this category (and available seats)." +msgstr "Eventos próximos/en marcha de esta categoría (y plazas disponibles)." diff --git a/crm_event/models/__init__.py b/crm_event/models/__init__.py new file mode 100644 index 000000000..f2404c4a7 --- /dev/null +++ b/crm_event/models/__init__.py @@ -0,0 +1,2 @@ +from . import crm_lead +from . import event_type diff --git a/crm_event/models/crm_lead.py b/crm_event/models/crm_lead.py new file mode 100644 index 000000000..bba018498 --- /dev/null +++ b/crm_event/models/crm_lead.py @@ -0,0 +1,26 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class CRMLead(models.Model): + _inherit = "crm.lead" + + event_type_id = fields.Many2one( + comodel_name="event.type", + index=True, + ondelete="restrict", + string="Event category", + help=( + "If this lead/opportunity is related to a specific event category, " + "indicate it here." + ), + ) + seats_wanted = fields.Integer( + groups="event.group_event_user", + help=( + "If this lead/opportunity is related to a specific event category, " + "indicate how many seats would you sell if won." + ), + ) diff --git a/crm_event/models/event_type.py b/crm_event/models/event_type.py new file mode 100644 index 000000000..7fc104bd7 --- /dev/null +++ b/crm_event/models/event_type.py @@ -0,0 +1,136 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class EventType(models.Model): + _inherit = "event.type" + + seats_available_total = fields.Char( + string="Events available (and seats)", + compute="_compute_event_totals", + help="Upcoming/running events of this category (and available seats).", + ) + crm_lead_ids = fields.One2many( + string="Leads/Opportunities", + comodel_name="crm.lead", + inverse_name="event_type_id", + ) + open_opportunities_count = fields.Integer( + compute="_compute_opportunities_totals", + store=True, + help="Open opportunities for events of this category.", + ) + seats_wanted_sum = fields.Integer( + string="Wanted seats", + compute="_compute_opportunities_totals", + store=True, + help="Sum of wanted seats in opportunities for events of this category.", + ) + seats_wanted_total = fields.Char( + string="Opportunities (seats)", + compute="_compute_opportunities_totals", + store=True, + help="Open opportunities for events of this category (and wanted seats).", + ) + + def _events_domain(self): + """Basic domain to get related events.""" + return [ + ("event_type_id", "in", self.ids), + # The following domain is the same as upstream's "Upcoming/Running" + # filter, which is the default when opening events view. It'd be + # more correct to filter for `date_end >= fields.Datetime.now()`, + # to exclude events that finished earlier today. However, that + # would make the smart button display a different count than the + # events when clicking on it, so it seems more user-friendly to + # include these events, even if they finished earlier today. + ("date_end", ">=", fields.Date.today()), + ("state", "!=", "cancel"), + ] + + def _compute_event_totals(self): + """Get how many open events and available seats exist.""" + domain = self._events_domain() + types_with_unlimited_seats = ( + self.env["event.event"] + .search(domain + [("seats_availability", "=", "unlimited")],) + .mapped("event_type_id") + ) + results = self.env["event.event"].read_group( + domain=domain, fields=["seats_available"], groupby=["event_type_id"], + ) + translated_unlimited = dict( + self.env["event.event"].fields_get(["seats_availability"])[ + "seats_availability" + ]["selection"] + )["unlimited"] + totals = {group["event_type_id"][0]: group for group in results} + for one in self: + totals_item = totals.get(one.id, {}) + event_count = totals_item.get("event_type_id_count", 0) + seats_sum = ( + translated_unlimited + if one in types_with_unlimited_seats + else totals_item.get("seats_available", "0") + ) + one.seats_available_total = "%d (%s)" % (event_count, seats_sum) + + @api.depends( + "crm_lead_ids.active", + "crm_lead_ids.probability", + "crm_lead_ids.seats_wanted", + "crm_lead_ids.type", + ) + def _compute_opportunities_totals(self): + """Get how many open opportunities and wanted seats exist.""" + results = self.env["crm.lead"].read_group( + domain=[ + ("event_type_id", "in", self.ids), + ("type", "=", "opportunity"), + # Ignore lost and won opportunities + ("active", "=", True), + ("probability", "<", "100"), + ], + fields=["seats_wanted"], + groupby="event_type_id", + orderby="id", + ) + totals = {group["event_type_id"][0]: group for group in results} + for one in self: + totals_item = totals.get(one.id, {}) + oppt_count = totals_item.get("event_type_id_count", 0) + seats_sum = totals_item.get("seats_wanted", 0) + one.open_opportunities_count = oppt_count + one.seats_wanted_sum = seats_sum + one.seats_wanted_total = "%d (%d)" % (oppt_count, seats_sum) + + def action_open_events(self): + return { + "context": { + "default_event_type_id": self.id, + "search_default_upcoming": True, + }, + "domain": [("event_type_id", "=", self.id)], + "name": _("Events"), + "res_model": "event.event", + "type": "ir.actions.act_window", + "view_mode": "kanban,calendar,tree,form,pivot", + "view_type": "form", + } + + def action_open_opportunities(self): + return { + "context": { + "default_event_type_id": self.id, + "default_seats_wanted": True, + "search_default_open_opportunities": True, + }, + "domain": [("event_type_id", "=", self.id)], + "name": _("Opportunities"), + "res_model": "crm.lead", + "type": "ir.actions.act_window", + "view_mode": "kanban,tree,graph,pivot,form,calendar,activity", + "view_type": "form", + } diff --git a/crm_event/readme/CONFIGURE.rst b/crm_event/readme/CONFIGURE.rst new file mode 100644 index 000000000..3e1ff454b --- /dev/null +++ b/crm_event/readme/CONFIGURE.rst @@ -0,0 +1,4 @@ +To make use of this module, a user needs these minimal permissions: + +- Sales / User: Own Documents Only +- Events / User diff --git a/crm_event/readme/CONTRIBUTORS.rst b/crm_event/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..7ee45dc9b --- /dev/null +++ b/crm_event/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jairo Llopis (https://www.tecnativa.com/) diff --git a/crm_event/readme/DESCRIPTION.rst b/crm_event/readme/DESCRIPTION.rst new file mode 100644 index 000000000..0790b1630 --- /dev/null +++ b/crm_event/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module extends the functionality of CRM opportunities (and leads, if +enabled) to support linking them to event categories and to allow you to keep +track of leads interested in an upcoming event of some category. + +This is useful if you organize your events based on the amount of people +interested in a certain category of event. diff --git a/crm_event/readme/USAGE.rst b/crm_event/readme/USAGE.rst new file mode 100644 index 000000000..318647566 --- /dev/null +++ b/crm_event/readme/USAGE.rst @@ -0,0 +1,21 @@ +To link a lead or opportunity to an event category: + +#. Go to *CRM > Pipeline* and pick one lead. +#. Go to the lead form. +#. Use the new field *Event category*. + +To know if there are events planned of a certain category: + +#. Go to *Events > Configuration > Event Categories* and pick one. +#. Use the new *Events* smart button. + + * It only counts those that are upcoming or running. + * The number between parenthesis is the available seats sum of all those events. + +To know if there is people interested in a certain category of event: + +#. Go to *Events > Configuration > Event Categories* and pick one. +#. Use the new *Opportunities* smart button. + + * It only counts opportunities that aren't won or lost. + * The number between parenthesis is the wanted seats sum of all those events. diff --git a/crm_event/reports/__init__.py b/crm_event/reports/__init__.py new file mode 100644 index 000000000..1274103c2 --- /dev/null +++ b/crm_event/reports/__init__.py @@ -0,0 +1 @@ +from . import event_type_report diff --git a/crm_event/reports/event_type_report.py b/crm_event/reports/event_type_report.py new file mode 100644 index 000000000..b54426126 --- /dev/null +++ b/crm_event/reports/event_type_report.py @@ -0,0 +1,105 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from psycopg2 import sql + +from odoo import fields, models, tools + + +class EventTypeReport(models.Model): + _name = "event.type.report" + _description = "Event categories analysis report" + _auto = False + _order = "name" + + name = fields.Char("Category name", readonly=True) + events_available_count = fields.Integer( + string="Available events count", readonly=True + ) + event_seats_availability = fields.Selection( + string="Seats availability", + selection=[("limited", "Limited"), ("unlimited", "Unlimited")], + readonly=True, + ) + seats_limited_available = fields.Integer( + string="Available seats in limited events", readonly=True + ) + open_opportunities_count = fields.Integer(readonly=True) + seats_wanted = fields.Integer(string="Wanted seats", readonly=True) + + def _query(self): + """Composed view.""" + return sql.SQL("SELECT {} FROM {} WHERE {} GROUP BY {}").format( + self._select(), self._from(), self._where(), self._groupby(), + ) + + def _select(self, fields_=()): + """Combine fields to select. + + Arguments: + fields_: `(("field_alias", "SUM(field_definition)"), ...)` + """ + fields_ += ( + ("id", "et.id"), + ("name", "et.name"), + ("events_available_count", "COUNT(ee.*)"), + ( + "event_seats_availability", + """CASE WHEN 'unlimited' = ANY(ARRAY_AGG(seats_availability)) + THEN 'unlimited' ELSE 'limited' END""", + ), + ("seats_limited_available", "COALESCE(SUM(ee.seats_available), 0)"), + ("open_opportunities_count", "et.open_opportunities_count"), + ("seats_wanted", "et.seats_wanted_sum"), + ) + parts = [] + for alias, source in fields_: + parts.append(sql.SQL(source + " AS ") + sql.Identifier(alias)) + result = sql.Composed(parts).join(", ") + return result + + def _from(self, clauses=()): + """Combine clauses to form the complete FROM clause. + + Arguments: + clauses: `("LEFT JOIN my_table mt ON mt.other_id = other.id", ...)` + """ + clauses = ( + ("event_type et"), + ("LEFT JOIN event_event ee ON et.id = ee.event_type_id"), + ) + clauses + result = sql.Composed(map(sql.SQL, clauses)).join(" ") + return result + + def _where(self, clauses=()): + """Combine where clauses. + + Arguments: + clauses: `("mt.field >= 10 OR mt.field < 0", ...)` + """ + clauses += ( + "ee.state IS NULL OR ee.state != 'cancel'", + "ee.date_end IS NULL OR ee.date_end >= CURRENT_DATE", + ) + result = sql.Composed(map(sql.SQL, clauses)).join(" AND ") + return result + + def _groupby(self, clauses=()): + """Combine group by clauses. + + Arguments: + clauses: `("mt.field", ...)` + """ + clauses += ("et.id",) + result = sql.Composed(map(sql.SQL, clauses)).join(", ") + return result + + def init(self): + """(Re-)create report view.""" + tools.drop_view_if_exists(self.env.cr, self._table) + # pylint: disable=sql-injection + self.env.cr.execute( + sql.SQL("CREATE OR REPLACE VIEW {} AS ({})").format( + sql.Identifier(self._table), self._query(), + ), + ) diff --git a/crm_event/reports/event_type_report_view.xml b/crm_event/reports/event_type_report_view.xml new file mode 100644 index 000000000..37baadfbf --- /dev/null +++ b/crm_event/reports/event_type_report_view.xml @@ -0,0 +1,79 @@ + + + + + Event category report pivot + event.type.report + + + + + + + + + + + + + Event category report graph + event.type.report + + + + + + + + + + + + + Event category report search + event.type.report + + + + + + + + + + + + + + + + Event category analysis + event.type.report + pivot,graph + + + {} + Report to analyze interest and status of event categories, taking into account upcoming or running events and potential sale information. + + + diff --git a/crm_event/security/ir.model.access.csv b/crm_event/security/ir.model.access.csv new file mode 100644 index 000000000..66022a0b8 --- /dev/null +++ b/crm_event/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_event_type_report_manager,Access to event.type.report for event managers,crm_event.model_event_type_report,event.group_event_manager,1,0,0,0 diff --git a/crm_event/static/description/icon.png b/crm_event/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/crm_event/static/description/icon.png differ diff --git a/crm_event/static/description/index.html b/crm_event/static/description/index.html new file mode 100644 index 000000000..a477b4a01 --- /dev/null +++ b/crm_event/static/description/index.html @@ -0,0 +1,462 @@ + + + + + + +CRM Event Category + + + +
+

CRM Event Category

+ + +

Beta License: AGPL-3 OCA/event Translate me on Weblate Try me on Runbot

+

This module extends the functionality of CRM opportunities (and leads, if +enabled) to support linking them to event categories and to allow you to keep +track of leads interested in an upcoming event of some category.

+

This is useful if you organize your events based on the amount of people +interested in a certain category of event.

+

Table of contents

+ +
+

Configuration

+

To make use of this module, a user needs these minimal permissions:

+
    +
  • Sales / User: Own Documents Only
  • +
  • Events / User
  • +
+
+
+

Usage

+

To link a lead or opportunity to an event category:

+
    +
  1. Go to CRM > Pipeline and pick one lead.
  2. +
  3. Go to the lead form.
  4. +
  5. Use the new field Event category.
  6. +
+

To know if there are events planned of a certain category:

+
    +
  1. Go to Events > Configuration > Event Categories and pick one.
  2. +
  3. Use the new Events smart button.
      +
    • It only counts those that are upcoming or running.
    • +
    • The number between parenthesis is the available seats sum of all those events.
    • +
    +
  4. +
+

To know if there is people interested in a certain category of event:

+
    +
  1. Go to Events > Configuration > Event Categories and pick one.
  2. +
  3. Use the new Opportunities smart button.
      +
    • It only counts opportunities that aren’t won or lost.
    • +
    • The number between parenthesis is the wanted seats sum of all those events.
    • +
    +
  4. +
+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+ +
+

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.

+

Current maintainer:

+

Yajo

+

This module is part of the OCA/event project on GitHub.

+

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

+
+
+
+ + diff --git a/crm_event/tests/__init__.py b/crm_event/tests/__init__.py new file mode 100644 index 000000000..4c918ffcc --- /dev/null +++ b/crm_event/tests/__init__.py @@ -0,0 +1 @@ +from . import test_event_type diff --git a/crm_event/tests/test_event_type.py b/crm_event/tests/test_event_type.py new file mode 100644 index 000000000..81f2ed82a --- /dev/null +++ b/crm_event/tests/test_event_type.py @@ -0,0 +1,103 @@ +# Copyright 2021 Tecnativa - Jairo Llopis +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo.tests.common import SavepointCase + + +class EventTypeCase(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Event type test data + cls.type_a = cls.env["event.type"].create({"name": "Event type A"}) + cls.type_b = cls.env["event.type"].create({"name": "Event type B"}) + # Event test data + cls.a_events = cls.env["event.event"].create( + [ + { + "event_type_id": cls.type_a.id, + "date_begin": datetime.now() - timedelta(days=1), + "date_end": datetime.now() - timedelta(minutes=1), + "name": "Today's past event", + "seats_availability": "limited", + "seats_max": 1, + }, + { + "event_type_id": cls.type_a.id, + "date_begin": datetime.now() - timedelta(days=2), + "date_end": datetime.now() - timedelta(days=1), + "name": "Yesterday's past event", + "seats_availability": "limited", + "seats_max": 10, + }, + { + "event_type_id": cls.type_a.id, + "date_begin": datetime.now() - timedelta(days=1), + "date_end": datetime.now() + timedelta(days=1), + "name": "Present event", + "seats_availability": "limited", + "seats_max": 100, + }, + { + "event_type_id": cls.type_a.id, + "date_begin": datetime.now() + timedelta(days=1), + "date_end": datetime.now() + timedelta(days=2), + "name": "Future event", + "seats_availability": "limited", + "seats_max": 1000, + }, + ] + ) + for event in cls.a_events: + cls.a_events |= event.copy({"active": False}) + cls.b_events = cls.env["event.event"] + for event in cls.a_events: + cls.b_events |= event.copy({"event_type_id": cls.type_b.id}) + cls.b_events[0].seats_availability = "unlimited" + # Leads and opportunities test data + cls.opportunities = cls.env["crm.lead"].create( + [ + { + "event_type_id": cls.type_a.id, + "name": "new", + "probability": 0, + "seats_wanted": 1, + "type": "opportunity", + }, + { + "event_type_id": cls.type_a.id, + "name": "running", + "probability": 50, + "seats_wanted": 10, + "type": "opportunity", + }, + { + "event_type_id": cls.type_a.id, + "name": "won", + "probability": 100, + "seats_wanted": 100, + "type": "opportunity", + }, + { + "active": False, + "event_type_id": cls.type_a.id, + "name": "lost", + "probability": 0, + "seats_wanted": 1000, + "type": "opportunity", + }, + ] + ) + cls.leads = cls.env["crm.lead"] + for opp in cls.opportunities: + cls.leads |= opp.copy({"type": "lead"}) + + def test_event_totals(self): + self.assertEqual(self.type_a.seats_available_total, "3 (1101)") + self.assertEqual(self.type_b.seats_available_total, "3 (Unlimited)") + + def test_opportunity_totals(self): + self.assertEqual(self.type_a.seats_wanted_total, "2 (11)") + self.assertEqual(self.type_b.seats_wanted_total, "0 (0)") diff --git a/crm_event/views/crm_lead_view.xml b/crm_event/views/crm_lead_view.xml new file mode 100644 index 000000000..7cda3ee0a --- /dev/null +++ b/crm_event/views/crm_lead_view.xml @@ -0,0 +1,127 @@ + + + + + + Search by event category in CRM leads + crm.lead + + + + + + + + + + + + + + Event category in CRM opportunities kanban + crm.lead + + + + + +
+ + + +
+
+
+
+
+ + Event category in CRM opportunities quick form + crm.lead + + + + + + + + + + + Event category in CRM opportunities tree + crm.lead + + + + + + + + + + + Search by event category in CRM opportunities + crm.lead + + + + + + + + + + + + + + Event category in CRM opportunities form + crm.lead + + + + + + + + + + + + + Search by event category in CRM opportunities report + crm.lead + + + + + + + + + + + +
diff --git a/crm_event/views/event_type_view.xml b/crm_event/views/event_type_view.xml new file mode 100644 index 000000000..d656282ab --- /dev/null +++ b/crm_event/views/event_type_view.xml @@ -0,0 +1,56 @@ + + + + + Link to events and opportunities from event category form + event.type + + + +
+ +
+ + + +
+
+
+
+ + Opportunities info from event category tree + event.type + + + + + + + + + + +
diff --git a/setup/crm_event/odoo/addons/crm_event b/setup/crm_event/odoo/addons/crm_event new file mode 120000 index 000000000..3b5de88dd --- /dev/null +++ b/setup/crm_event/odoo/addons/crm_event @@ -0,0 +1 @@ +../../../../crm_event \ No newline at end of file diff --git a/setup/crm_event/setup.py b/setup/crm_event/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/crm_event/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)