diff --git a/README.md b/README.md index 186a69b451e..d8998c97ebe 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ addon | version | maintainers | summary --- | --- | --- | --- [attachment_delete_restrict](attachment_delete_restrict/) | 18.0.1.0.0 | yostashiro Kev-Roche | Restrict Deletion of Attachments [attachment_queue](attachment_queue/) | 18.0.1.0.1 | florian-dacosta sebastienbeau | Base module adding the concept of queue for processing files +[attachment_queue_email](attachment_queue_email/) | 18.0.1.0.0 | florian-dacosta sebastienbeau bealdav | Create attachment from emails to be processed depending on their type [attachment_synchronize](attachment_synchronize/) | 18.0.1.0.0 | florian-dacosta sebastienbeau GSLabIt bealdav | Attachment Synchronize [attachment_unindex_content](attachment_unindex_content/) | 18.0.1.0.0 | moylop260 ebirbe luisg123v | Disable indexing of attachments [auditlog](auditlog/) | 18.0.2.0.9 | | Audit Log @@ -47,6 +48,7 @@ addon | version | maintainers | summary [base_temporary_action](base_temporary_action/) | 18.0.1.0.0 | | This addon allows to create temporary actions [base_time_parameter](base_time_parameter/) | 18.0.1.0.0 | appstogrow nimarosa | Time dependent parameters Adds the feature to define parameters with time based versions. [base_time_window](base_time_window/) | 18.0.1.1.1 | | Base model to handle time windows +[base_url](base_url/) | 18.0.1.0.0 | sebastienbeau | Abstract module/mixin to manage clean url on odoo record [base_view_inheritance_extension](base_view_inheritance_extension/) | 18.0.1.0.2 | | Adds more operators for view inheritance [bus_alt_connection](bus_alt_connection/) | 18.0.1.0.0 | | Needed when using PgBouncer as a connection pooler [database_autovacuum_tuning](database_autovacuum_tuning/) | 18.0.1.0.1 | | Scheduled checks for Odoo autovacuum thresholds and scale factors @@ -76,7 +78,7 @@ addon | version | maintainers | summary [test_base_time_window](test_base_time_window/) | 18.0.1.0.0 | | Test Base model to handle time windows [tracking_manager](tracking_manager/) | 18.0.1.1.0 | Kev-Roche sebastienbeau | This module tracks all fields of a model, including one2many and many2many ones. [tracking_manager_domain](tracking_manager_domain/) | 18.0.1.0.1 | CRogos | This module extends the tracking manager to allow to define a domain on fields to track changes only when certain conditions apply. -[upgrade_analysis](upgrade_analysis/) | 18.0.1.4.6 | StefanRijnhart legalsylvain | Performs a difference analysis between modules installed on two different Odoo instances +[upgrade_analysis](upgrade_analysis/) | 18.0.1.4.7 | StefanRijnhart legalsylvain | Performs a difference analysis between modules installed on two different Odoo instances [//]: # (end addons) diff --git a/attachment_queue_email/README.rst b/attachment_queue_email/README.rst new file mode 100644 index 00000000000..1ccf92f3672 --- /dev/null +++ b/attachment_queue_email/README.rst @@ -0,0 +1,123 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +====================== +Attachment Queue Email +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:27f9d13a667b05afb86742b7074e5ac32070df09ff3cc42906202f5a1d2a950d + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/18.0/attachment_queue_email + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-attachment_queue_email + :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/server-tools&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Abstract module for importing emails attachments. + +Each email's attachment matching a given **"Attachment Condition"** will +be imported creating a new ``attachment.queue`` object. These +``attachment.queue`` objects are files wrapped with additional fields +(mainly a **Filed Type** and a **State**) making them ready to be +processed by a custom module as you can read in the +`attachment_queue `__ +documentation. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Adding an **"Attachment Condition"** to your *Incoming Mail Server* +configuration will lead to the creation of ``attachment.queue`` objects +from emails attachments coming from this server and matching the given +*Attachment Condition*. + + 🔎 Recalling that, as the *Incoming Mail Servers* has only one + *"Create a New Record"* field, the emails coming from that server + **cannot be used to create other type of objects** than + ``attachment.queue``. + +Filling the condition's **File Type** field will spread this value to +the newly created ``attchment.queue`` objects so they can be processed +by a custom module following this **File Type** field value. + +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 +------- + +* Akretion + +Contributors +------------ + +- Valentin CHEMIERE +- Florian DA COSTA +- Sebastien BEAU +- Clément Mombereau +- Benoit GUILLOT + +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-florian-dacosta| image:: https://github.com/florian-dacosta.png?size=40px + :target: https://github.com/florian-dacosta + :alt: florian-dacosta +.. |maintainer-sebastienbeau| image:: https://github.com/sebastienbeau.png?size=40px + :target: https://github.com/sebastienbeau + :alt: sebastienbeau +.. |maintainer-bealdav| image:: https://github.com/bealdav.png?size=40px + :target: https://github.com/bealdav + :alt: bealdav + +Current `maintainers `__: + +|maintainer-florian-dacosta| |maintainer-sebastienbeau| |maintainer-bealdav| + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/attachment_queue_email/__init__.py b/attachment_queue_email/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/attachment_queue_email/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/attachment_queue_email/__manifest__.py b/attachment_queue_email/__manifest__.py new file mode 100644 index 00000000000..051130f2237 --- /dev/null +++ b/attachment_queue_email/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2012 Akretion (https://www.akretion.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Attachment Queue Email", + "version": "18.0.1.0.0", + "author": "Akretion,Odoo Community Association (OCA)", + "category": "Generic Modules", + "summary": "Create attachment from emails to be processed depending on their type", + "license": "AGPL-3", + "website": "https://github.com/OCA/server-tools", + "maintainers": ["florian-dacosta", "sebastienbeau", "bealdav"], + "depends": ["attachment_queue", "mail"], + "demo": [], + "data": [ + "security/ir.model.access.csv", + "views/fetchmail_attachment_condition.xml", + "views/fetchmail_server.xml", + ], + "installable": True, + "images": [], +} diff --git a/attachment_queue_email/i18n/attachment_queue_email.pot b/attachment_queue_email/i18n/attachment_queue_email.pot new file mode 100644 index 00000000000..dc0d13420d2 --- /dev/null +++ b/attachment_queue_email/i18n/attachment_queue_email.pot @@ -0,0 +1,200 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * attachment_queue_email +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_attachment_queue__assigned_attachment_ids +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__assigned_attachment_ids +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_server__assigned_attachment_ids +msgid "Assigned Attachments" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_server__attachment_condition_ids +msgid "Attachment Condition" +msgstr "" + +#. module: attachment_queue_email +#: model_terms:ir.ui.view,arch_db:attachment_queue_email.view_fetchmail_attachment_condition_form +msgid "Attachment Condition requirements" +msgstr "" + +#. module: attachment_queue_email +#: model_terms:ir.ui.view,arch_db:attachment_queue_email.view_email_server_form +msgid "Attachment Conditions" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model,name:attachment_queue_email.model_attachment_queue +msgid "Attachment Queue" +msgstr "" + +#. module: attachment_queue_email +#: model_terms:ir.ui.view,arch_db:attachment_queue_email.view_fetchmail_attachment_condition_form +msgid "Attachment process" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__company_id +msgid "Company" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__name +msgid "Condition Name" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__create_uid +msgid "Created by" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__create_date +msgid "Created on" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__display_name +msgid "Display Name" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__email_from +msgid "Email From" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_attachment_queue__fetchmail_server_id +msgid "Email Server" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__email_subject +msgid "Email Subject" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__email_to +msgid "Email To" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_attachment_queue__fetchmail_attachment_condition_id +msgid "FetchMail condition" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model,name:attachment_queue_email.model_fetchmail_attachment_condition +msgid "Fetchmail Attachment Conditions" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__file_extension +msgid "File Extension" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__file_type +msgid "File Type" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_server__attachment_condition_ids +msgid "" +"Files attached to the emails matching these conditions will be imported in " +"Odoo as 'Attachment Queue' objects" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__id +msgid "ID" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__email_to +msgid "" +"If empty, catches the email no matter the recipient.\n" +"Otherwise catches the emails where the recipients emails contains the given characters." +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__email_from +msgid "" +"If empty, catches the emails from every senders.\n" +"Otherwise catches the emails where the sender's email contains the given characters" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__email_subject +msgid "" +"If empty, catches the emails with every kind of Subjects.\n" +"Otherwise catches the emails where the Subject contains the given characters" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model,name:attachment_queue_email.model_fetchmail_server +msgid "Incoming Mail Server" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__write_date +msgid "Last Updated on" +msgstr "" + +#. module: attachment_queue_email +#: model_terms:ir.ui.view,arch_db:attachment_queue_email.view_fetchmail_attachment_condition_form +msgid "Requirements" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__server_id +msgid "Server Mail" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_attachment_queue__smart_search +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__smart_search +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_server__smart_search +msgid "Smart Search" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__file_type +msgid "" +"The 'file type' is transmited to the 'Attachment Queue' objects created from the selected emails attachments.\n" +"It will allow Odoo to recognize what do do with them once created." +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_attachment_queue__fetchmail_attachment_condition_id +msgid "The Fetchmail attachment condition used to create this attachment" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_attachment_queue__fetchmail_server_id +msgid "The email server used to create this attachment" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__file_extension +msgid "" +"The extension (or part of the name) of the sought files. If empty, all the " +"email's attachments will be imported." +msgstr "" diff --git a/attachment_queue_email/i18n/it.po b/attachment_queue_email/i18n/it.po new file mode 100644 index 00000000000..e0886734fc2 --- /dev/null +++ b/attachment_queue_email/i18n/it.po @@ -0,0 +1,201 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * attachment_queue_email +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\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" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_attachment_queue__assigned_attachment_ids +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__assigned_attachment_ids +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_server__assigned_attachment_ids +msgid "Assigned Attachments" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_server__attachment_condition_ids +msgid "Attachment Condition" +msgstr "" + +#. module: attachment_queue_email +#: model_terms:ir.ui.view,arch_db:attachment_queue_email.view_fetchmail_attachment_condition_form +msgid "Attachment Condition requirements" +msgstr "" + +#. module: attachment_queue_email +#: model_terms:ir.ui.view,arch_db:attachment_queue_email.view_email_server_form +msgid "Attachment Conditions" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model,name:attachment_queue_email.model_attachment_queue +msgid "Attachment Queue" +msgstr "" + +#. module: attachment_queue_email +#: model_terms:ir.ui.view,arch_db:attachment_queue_email.view_fetchmail_attachment_condition_form +msgid "Attachment process" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__company_id +msgid "Company" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__name +msgid "Condition Name" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__create_uid +msgid "Created by" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__create_date +msgid "Created on" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__display_name +msgid "Display Name" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__email_from +msgid "Email From" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_attachment_queue__fetchmail_server_id +msgid "Email Server" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__email_subject +msgid "Email Subject" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__email_to +msgid "Email To" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_attachment_queue__fetchmail_attachment_condition_id +msgid "FetchMail condition" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model,name:attachment_queue_email.model_fetchmail_attachment_condition +msgid "Fetchmail Attachment Conditions" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__file_extension +msgid "File Extension" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__file_type +msgid "File Type" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_server__attachment_condition_ids +msgid "" +"Files attached to the emails matching these conditions will be imported in " +"Odoo as 'Attachment Queue' objects" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__id +msgid "ID" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__email_to +msgid "" +"If empty, catches the email no matter the recipient.\n" +"Otherwise catches the emails where the recipients emails contains the given characters." +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__email_from +msgid "" +"If empty, catches the emails from every senders.\n" +"Otherwise catches the emails where the sender's email contains the given characters" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__email_subject +msgid "" +"If empty, catches the emails with every kind of Subjects.\n" +"Otherwise catches the emails where the Subject contains the given characters" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model,name:attachment_queue_email.model_fetchmail_server +msgid "Incoming Mail Server" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__write_date +msgid "Last Updated on" +msgstr "" + +#. module: attachment_queue_email +#: model_terms:ir.ui.view,arch_db:attachment_queue_email.view_fetchmail_attachment_condition_form +msgid "Requirements" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__server_id +msgid "Server Mail" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,field_description:attachment_queue_email.field_attachment_queue__smart_search +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_attachment_condition__smart_search +#: model:ir.model.fields,field_description:attachment_queue_email.field_fetchmail_server__smart_search +msgid "Smart Search" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__file_type +msgid "" +"The 'file type' is transmited to the 'Attachment Queue' objects created from the selected emails attachments.\n" +"It will allow Odoo to recognize what do do with them once created." +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_attachment_queue__fetchmail_attachment_condition_id +msgid "The Fetchmail attachment condition used to create this attachment" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_attachment_queue__fetchmail_server_id +msgid "The email server used to create this attachment" +msgstr "" + +#. module: attachment_queue_email +#: model:ir.model.fields,help:attachment_queue_email.field_fetchmail_attachment_condition__file_extension +msgid "" +"The extension (or part of the name) of the sought files. If empty, all the " +"email's attachments will be imported." +msgstr "" diff --git a/attachment_queue_email/models/__init__.py b/attachment_queue_email/models/__init__.py new file mode 100644 index 00000000000..b95e9fad4c1 --- /dev/null +++ b/attachment_queue_email/models/__init__.py @@ -0,0 +1,3 @@ +from . import attachment_queue +from . import fetchmail_server +from . import fetchmail_attachment_condition diff --git a/attachment_queue_email/models/attachment_queue.py b/attachment_queue_email/models/attachment_queue.py new file mode 100644 index 00000000000..62446c1ca14 --- /dev/null +++ b/attachment_queue_email/models/attachment_queue.py @@ -0,0 +1,105 @@ +# Copyright 2012 Akretion (https://www.akretion.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import base64 + +from odoo import api, fields, models + + +class AttachmentQueue(models.Model): + _inherit = "attachment.queue" + + fetchmail_attachment_condition_id = fields.Many2one( + "fetchmail.attachment.condition", + string="FetchMail condition", + help="The Fetchmail attachment condition used to create this attachment", + ) + fetchmail_server_id = fields.Many2one( + "fetchmail.server", + string="Email Server", + related="fetchmail_attachment_condition_id.server_id", + store=True, + readonly=True, + help="The email server used to create this attachment", + ) + + @api.model + def _get_attachment_queue_data(self, condition, msg, att): + data = att[1] + # seems files are byte when it comes from zip file so we manage + # both case + if isinstance(data, str): + data = data.encode() + values = { + "fetchmail_attachment_condition_id": condition.id, + "file_type": condition.file_type, + "name": att[0], + "datas": base64.b64encode(data), + "state": "pending", + "company_id": condition.company_id.id, + } + return values + + @api.model + def prepare_data_from_basic_condition(self, cond, msg_dict): + vals_list = [] + # match_from and match_subj are True if empty or if matching with + # msg_dict's values + match_from = ( + cond.email_from + and cond.email_from in msg_dict.get("from", "") + or not cond.email_from + ) + match_subj = ( + cond.email_subject + and cond.email_subject in msg_dict.get("subject", "") + or not cond.email_subject + ) + match_to = ( + cond.email_to + and cond.email_to in msg_dict.get("to", "") + or not cond.email_to + ) + + if match_from and match_subj and match_to: + for att in msg_dict["attachments"]: + if cond.file_extension in att[0] or not cond.file_extension: + vals_list.append( + self._get_attachment_queue_data(cond, msg_dict, att) + ) + return vals_list + + @api.model + def _prepare_data_for_attachment_queue(self, msg_dict): + """Prepare the datas for the creation of one or many attachment.queue depending + on the number of the email's attachments files and if the email matches the + fetchmail.attachment.condition's conditions. + + :param msg_dict: a dictionnary with the email data + :type: dict + + :return: a list of dictionnary that contains the attachment.queue data + :rtype: list + """ + res = [] + server_id = self.env.context.get("default_fetchmail_server_id", False) + file_condition_obj = self.env["fetchmail.attachment.condition"] + conds = file_condition_obj.search([("server_id", "=", server_id)]) + for cond in conds: + vals_list = self.prepare_data_from_basic_condition(cond, msg_dict) + if vals_list: + res += vals_list + return res + + @api.model + def message_new(self, msg_dict, custom_values=None): + """Create Attachments Queues objects from the received email's attachments.""" + # Rewriting completely ``message_new`` instead of overiding it in order to + # allows the creation of many new objects instead of only one. + created_recs = [] + res = self._prepare_data_for_attachment_queue(msg_dict) + if res: + for vals in res: + created_recs.append(self.create(vals)) + return created_recs[0] + return None diff --git a/attachment_queue_email/models/fetchmail_attachment_condition.py b/attachment_queue_email/models/fetchmail_attachment_condition.py new file mode 100644 index 00000000000..583b2acbd9e --- /dev/null +++ b/attachment_queue_email/models/fetchmail_attachment_condition.py @@ -0,0 +1,45 @@ +# Copyright 2012 Akretion (https://www.akretion.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class FetchmailAttachmentCondition(models.Model): + _name = "fetchmail.attachment.condition" + _description = "Fetchmail Attachment Conditions" + + name = fields.Char( + string="Condition Name", + required=True, + ) + email_from = fields.Char( + help="If empty, catches the emails from every senders.\n" + "Otherwise catches the emails where the sender's email contains the given " + "characters", + ) + email_to = fields.Char( + help="If empty, catches the email no matter the recipient.\n" + "Otherwise catches the emails where the recipients emails contains the given " + "characters.", + ) + email_subject = fields.Char( + help="If empty, catches the emails with every kind of Subjects.\n" + "Otherwise catches the emails where the Subject contains the given characters", + ) + file_extension = fields.Char( + help="The extension (or part of the name) of the sought files. " + "If empty, all the email's attachments will be imported.", + ) + server_id = fields.Many2one("fetchmail.server", string="Server Mail") + file_type = fields.Selection( + selection=[], + help="The 'file type' is transmited to the 'Attachment Queue' objects created " + "from the selected emails attachments.\nIt will allow Odoo to recognize " + "what do do with them once created.", + ) + company_id = fields.Many2one( + "res.company", + string="Company", + required=True, + default=lambda self: self.env.company, + ) diff --git a/attachment_queue_email/models/fetchmail_server.py b/attachment_queue_email/models/fetchmail_server.py new file mode 100644 index 00000000000..ad04a971ebb --- /dev/null +++ b/attachment_queue_email/models/fetchmail_server.py @@ -0,0 +1,25 @@ +# Copyright 2012 Akretion (https://www.akretion.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class FetchmailServer(models.Model): + _inherit = "fetchmail.server" + + attachment_condition_ids = fields.One2many( + "fetchmail.attachment.condition", + "server_id", + string="Attachment Condition", + help="Files attached to the emails matching these conditions will be imported " + "in Odoo as 'Attachment Queue' objects", + ) + + @api.onchange("attachment_condition_ids") + def onchange_attachment_condition(self): + for server in self: + if server.attachment_condition_ids: + server.object_id = self.env["ir.model"].search( + [("model", "=", "attachment.queue")] + ) + server.attach = True diff --git a/attachment_queue_email/pyproject.toml b/attachment_queue_email/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/attachment_queue_email/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/attachment_queue_email/readme/CONTRIBUTORS.md b/attachment_queue_email/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..ceb876de84d --- /dev/null +++ b/attachment_queue_email/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- Valentin CHEMIERE \ +- Florian DA COSTA \ +- Sebastien BEAU \ +- Clément Mombereau \ +- Benoit GUILLOT \ diff --git a/attachment_queue_email/readme/DESCRIPTION.md b/attachment_queue_email/readme/DESCRIPTION.md new file mode 100644 index 00000000000..2d2af96face --- /dev/null +++ b/attachment_queue_email/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +Abstract module for importing emails attachments. + +Each email's attachment matching a given **"Attachment Condition"** will +be imported creating a new `attachment.queue` object. These +`attachment.queue` objects are files wrapped with additional fields +(mainly a **Filed Type** and a **State**) making them ready to be +processed by a custom module as you can read in the +[attachment_queue](https://github.com/OCA/server-tools/tree/18.0/attachment_queue) +documentation. diff --git a/attachment_queue_email/readme/USAGE.md b/attachment_queue_email/readme/USAGE.md new file mode 100644 index 00000000000..8d3ca619408 --- /dev/null +++ b/attachment_queue_email/readme/USAGE.md @@ -0,0 +1,13 @@ +Adding an **"Attachment Condition"** to your *Incoming Mail Server* +configuration will lead to the creation of `attachment.queue` objects +from emails attachments coming from this server and matching the given +*Attachment Condition*. + +> 🔎 Recalling that, as the *Incoming Mail Servers* has only one +> *"Create a New Record"* field, the emails coming from that server +> **cannot be used to create other type of objects** than +> `attachment.queue`. + +Filling the condition's **File Type** field will spread this value to +the newly created `attchment.queue` objects so they can be processed by +a custom module following this **File Type** field value. diff --git a/attachment_queue_email/security/ir.model.access.csv b/attachment_queue_email/security/ir.model.access.csv new file mode 100644 index 00000000000..68aeab002a2 --- /dev/null +++ b/attachment_queue_email/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fetchmail_attachment_condition_manager,fetchmail.attachment.condition.manager,model_fetchmail_attachment_condition,base.group_system,1,1,1,1 +access_fetchmail_attachment_condition_user,fetchmail.attachment.condition.user,model_fetchmail_attachment_condition,base.group_user,1,0,0,0 diff --git a/attachment_queue_email/static/description/icon.png b/attachment_queue_email/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/attachment_queue_email/static/description/icon.png differ diff --git a/attachment_queue_email/static/description/index.html b/attachment_queue_email/static/description/index.html new file mode 100644 index 00000000000..c2b061fa61a --- /dev/null +++ b/attachment_queue_email/static/description/index.html @@ -0,0 +1,458 @@ + + + + + +Attachment Queue Email + + + +
+ + + +Odoo Community Association + +
+

Attachment Queue Email

+ +

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

Abstract module for importing emails attachments.

+

Each email’s attachment matching a given “Attachment Condition” will +be imported creating a new attachment.queue object. These +attachment.queue objects are files wrapped with additional fields +(mainly a Filed Type and a State) making them ready to be +processed by a custom module as you can read in the +attachment_queue +documentation.

+

Table of contents

+ +
+

Usage

+

Adding an “Attachment Condition” to your Incoming Mail Server +configuration will lead to the creation of attachment.queue objects +from emails attachments coming from this server and matching the given +Attachment Condition.

+
+🔎 Recalling that, as the Incoming Mail Servers has only one +“Create a New Record” field, the emails coming from that server +cannot be used to create other type of objects than +attachment.queue.
+

Filling the condition’s File Type field will spread this value to +the newly created attchment.queue objects so they can be processed +by a custom module following this File Type field value.

+
+
+

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

+
    +
  • Akretion
  • +
+
+
+

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.

+

Current maintainers:

+

florian-dacosta sebastienbeau bealdav

+

This module is part of the OCA/server-tools project on GitHub.

+

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

+
+
+
+
+ + diff --git a/attachment_queue_email/tests/__init__.py b/attachment_queue_email/tests/__init__.py new file mode 100644 index 00000000000..c866d7f469f --- /dev/null +++ b/attachment_queue_email/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_attachment_queue_email diff --git a/attachment_queue_email/tests/test_attachment_queue_email.py b/attachment_queue_email/tests/test_attachment_queue_email.py new file mode 100644 index 00000000000..26630c5e9e9 --- /dev/null +++ b/attachment_queue_email/tests/test_attachment_queue_email.py @@ -0,0 +1,74 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.mail.tests.common import MailCommon +from odoo.addons.test_mail.data.test_mail_data import ( + MAIL_MULTIPART_IMAGE, + MAIL_SINGLE_BINARY, +) + + +class TestAttachmentQueueEmail(MailCommon): + def setUp(self): + super().setUp() + self.attach_condition_1 = self.env["fetchmail.attachment.condition"].create( + { + "name": "Attchment Condition 1", + "email_from": "Bruce Wayne", + "file_extension": ".pdf", + } + ) + self.attach_condition_2 = self.env["fetchmail.attachment.condition"].create( + { + "name": "Attchment Condition 2", + "email_subject": "Wonderful pictures", + "file_extension": ".gif", + } + ) + + self.attach_queue_model = self.env["ir.model"].search( + [("model", "=", "attachment.queue")] + ) + self.fetchmail_server = self.env["fetchmail.server"].create( + { + "name": "Test Fetchmail Server 1", + "server_type": "imap", + "attach": True, + "object_id": self.attach_queue_model.id, + "attachment_condition_ids": [ + (6, 0, [self.attach_condition_1.id, self.attach_condition_2.id]) + ], + } + ) + self.context_server = { + "default_fetchmail_server_id": self.fetchmail_server.id, + } + + def test_message_single_binary(self): + """Run message_process on emails with attachment check if an attachment_queue + is created + """ + self.assertFalse( + self.env["attachment.queue"].search([("name", "=", "thetruth.pdf")]) + ) + + self.env["mail.thread"].with_context(**self.context_server).message_process( + self.fetchmail_server.object_id.model, + MAIL_SINGLE_BINARY, + ) + + attach_queue = self.env["attachment.queue"].search( + [("name", "=", "thetruth.pdf")] + ) + self.assertEqual(len(attach_queue), 1) + self.assertEqual(attach_queue.datas, b"SSBhbSB0aGUgQmF0TWFuCg==") + + def test_message_multi_image(self): + mail = MAIL_MULTIPART_IMAGE.format(subject="Wonderful pictures", to="") + + self.env["mail.thread"].with_context(**self.context_server).message_process( + self.fetchmail_server.object_id.model, + mail, + ) + attach_queues = self.env["attachment.queue"].search([("name", "like", ".gif")]) + self.assertEqual(len(attach_queues), 3) + self.assertEqual(attach_queues[0].name, "orangée.gif") diff --git a/attachment_queue_email/views/fetchmail_attachment_condition.xml b/attachment_queue_email/views/fetchmail_attachment_condition.xml new file mode 100644 index 00000000000..d833e748c3b --- /dev/null +++ b/attachment_queue_email/views/fetchmail_attachment_condition.xml @@ -0,0 +1,47 @@ + + + + + view_fetchmail_attachment_condition_tree + fetchmail.attachment.condition + + + + + + + + + + + + + + + view_fetchmail_attachment_condition_form + fetchmail.attachment.condition + +
+ +
+
+ + + + + + + + + + + +
+
+
+
+
diff --git a/attachment_queue_email/views/fetchmail_server.xml b/attachment_queue_email/views/fetchmail_server.xml new file mode 100644 index 00000000000..0f7c855d670 --- /dev/null +++ b/attachment_queue_email/views/fetchmail_server.xml @@ -0,0 +1,14 @@ + + + + fetchmail.server + + + + + + + + + + diff --git a/autovacuum_message_attachment/i18n/autovacuum_message_attachment.pot b/autovacuum_message_attachment/i18n/autovacuum_message_attachment.pot index 6a58409b6b8..d980e74462f 100644 --- a/autovacuum_message_attachment/i18n/autovacuum_message_attachment.pot +++ b/autovacuum_message_attachment/i18n/autovacuum_message_attachment.pot @@ -25,6 +25,7 @@ msgstr "" #. module: autovacuum_message_attachment #: model:ir.model.fields,field_description:autovacuum_message_attachment.field__unknown__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_abstract_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account_tag__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_accrued_orders_wizard__assigned_attachment_ids @@ -405,6 +406,7 @@ msgstr "" #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_generate_record_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_install_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_record__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_url_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_vacuum_rule__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_validate_account_move__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_web_editor_assets__assigned_attachment_ids diff --git a/autovacuum_message_attachment/i18n/ca.po b/autovacuum_message_attachment/i18n/ca.po index d80842eaf90..f90e4448bb5 100644 --- a/autovacuum_message_attachment/i18n/ca.po +++ b/autovacuum_message_attachment/i18n/ca.po @@ -26,6 +26,7 @@ msgstr "" #. module: autovacuum_message_attachment #: model:ir.model.fields,field_description:autovacuum_message_attachment.field__unknown__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_abstract_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account_tag__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_accrued_orders_wizard__assigned_attachment_ids @@ -406,6 +407,7 @@ msgstr "" #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_generate_record_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_install_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_record__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_url_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_vacuum_rule__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_validate_account_move__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_web_editor_assets__assigned_attachment_ids diff --git a/autovacuum_message_attachment/i18n/es.po b/autovacuum_message_attachment/i18n/es.po index ce5446f9caa..6645008625b 100644 --- a/autovacuum_message_attachment/i18n/es.po +++ b/autovacuum_message_attachment/i18n/es.po @@ -28,6 +28,7 @@ msgstr "Todos" #. module: autovacuum_message_attachment #: model:ir.model.fields,field_description:autovacuum_message_attachment.field__unknown__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_abstract_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account_tag__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_accrued_orders_wizard__assigned_attachment_ids @@ -408,6 +409,7 @@ msgstr "Todos" #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_generate_record_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_install_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_record__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_url_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_vacuum_rule__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_validate_account_move__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_web_editor_assets__assigned_attachment_ids diff --git a/autovacuum_message_attachment/i18n/es_AR.po b/autovacuum_message_attachment/i18n/es_AR.po index 5e8bf774183..78bc8a5bbe2 100644 --- a/autovacuum_message_attachment/i18n/es_AR.po +++ b/autovacuum_message_attachment/i18n/es_AR.po @@ -28,6 +28,7 @@ msgstr "Todos" #. module: autovacuum_message_attachment #: model:ir.model.fields,field_description:autovacuum_message_attachment.field__unknown__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_abstract_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account_tag__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_accrued_orders_wizard__assigned_attachment_ids @@ -408,6 +409,7 @@ msgstr "Todos" #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_generate_record_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_install_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_record__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_url_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_vacuum_rule__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_validate_account_move__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_web_editor_assets__assigned_attachment_ids diff --git a/autovacuum_message_attachment/i18n/fr.po b/autovacuum_message_attachment/i18n/fr.po index d6fb771c614..0977f5e65da 100644 --- a/autovacuum_message_attachment/i18n/fr.po +++ b/autovacuum_message_attachment/i18n/fr.po @@ -28,6 +28,7 @@ msgstr "Tous" #. module: autovacuum_message_attachment #: model:ir.model.fields,field_description:autovacuum_message_attachment.field__unknown__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_abstract_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account_tag__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_accrued_orders_wizard__assigned_attachment_ids @@ -408,6 +409,7 @@ msgstr "Tous" #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_generate_record_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_install_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_record__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_url_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_vacuum_rule__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_validate_account_move__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_web_editor_assets__assigned_attachment_ids diff --git a/autovacuum_message_attachment/i18n/it.po b/autovacuum_message_attachment/i18n/it.po index e903ab50b16..1b4dc29de2c 100644 --- a/autovacuum_message_attachment/i18n/it.po +++ b/autovacuum_message_attachment/i18n/it.po @@ -28,6 +28,7 @@ msgstr "Tutti" #. module: autovacuum_message_attachment #: model:ir.model.fields,field_description:autovacuum_message_attachment.field__unknown__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_abstract_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_account_tag__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_account_accrued_orders_wizard__assigned_attachment_ids @@ -408,6 +409,7 @@ msgstr "Tutti" #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_generate_record_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_install_wizard__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_upgrade_record__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_url_url__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_vacuum_rule__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_validate_account_move__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_web_editor_assets__assigned_attachment_ids diff --git a/base_url/README.rst b/base_url/README.rst new file mode 100644 index 00000000000..bc136c9dfa1 --- /dev/null +++ b/base_url/README.rst @@ -0,0 +1,127 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======== +Base Url +======== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f9dfa26eb2d20e77e5a3d90c4818ecf98fb27d59c44dd438348e523c361ccbbd + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/18.0/base_url + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-base_url + :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/server-tools&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +The aim of this module is to provide a generic mixin for building urls +on records. + +The native odoo implementation of a record url is based on having an +"id" in the url, this module provides an alternative way of doing it. +Urls are generated and stored in an unique table (to ensure unicity). +Redirections are also managed in case of url changes. + +This project was initial build for shopinvader as we need to have unique +url for the connected webshop. But this concept can be reused in other +cases and even replace the odoo url implementation. + +**Table of contents** + +.. contents:: + :local: + +Development +=========== + +To use this module, you need to inherit the ``abstract.url`` model and +override the ``_get_keyword_fields`` method to include your custom url +fields. By default, the ``_get_keyword_fields`` method returns the +record ``name``. + +Here is an example of how to override the ``_get_keyword_fields`` method +to include the ``default_code`` field for ``product.template`` records: + +.. code:: python + + + class ProductTemplate(models.Model): + _inherit = ["product.template", "abstract.url"] + _name = "product.template" + + def _get_keyword_fields(self): + return super()._get_keyword_fields() + ["default_code"] + +Your product template records will now have an ``url_key`` field that is +generated from the ``name`` and ``default_code`` fields and a +``redirect_url_key`` field that will contain a list of redirect URLs +(old ``url_key`` field values). + +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 +------- + +* Akretion +* ACSONE SA/NV + +Contributors +------------ + +- Sebastien Beau sebastien.beau@akretion.com +- Laurent Mignon laurent.mignon@acsone.eu +- Florian Mounier florian.mounier@akretion.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-sebastienbeau| image:: https://github.com/sebastienbeau.png?size=40px + :target: https://github.com/sebastienbeau + :alt: sebastienbeau + +Current `maintainer `__: + +|maintainer-sebastienbeau| + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_url/__init__.py b/base_url/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/base_url/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py new file mode 100644 index 00000000000..a1cb1dfc5e3 --- /dev/null +++ b/base_url/__manifest__.py @@ -0,0 +1,22 @@ +# Rewrite url Base module for OpenERP +# Copyright (C) 2013 Akretion (http://www.akretion.com) +# @author EBII MonsieurB +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Base Url", + "version": "18.0.1.0.0", + "category": "tools", + "license": "AGPL-3", + "summary": "Abstract module/mixin to manage clean url on odoo record", + "author": "Akretion, ACSONE SA/NV, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-tools", + "depends": ["base"], + "maintainers": ["sebastienbeau"], + "data": [ + "views/url_view.xml", + "security/res_groups.xml", + "security/ir.model.access.csv", + ], + "installable": True, +} diff --git a/base_url/i18n/base_url.pot b/base_url/i18n/base_url.pot new file mode 100644 index 00000000000..33a799a5c66 --- /dev/null +++ b/base_url/i18n/base_url.pot @@ -0,0 +1,172 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_url +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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: base_url +#: model:ir.model,name:base_url.model_abstract_url +msgid "Abstract Url" +msgstr "" + +#. module: base_url +#: model:ir.model.constraint,message:base_url.constraint_url_url_unique_key_per_referential_per_lang +msgid "Already exists in database" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__count_url +msgid "Count Url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__create_uid +msgid "Created by" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__create_date +msgid "Created on" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__display_name +msgid "Display Name" +msgstr "" + +#. module: base_url +#: model:res.groups,name:base_url.group_edit_url +msgid "Edit url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__id +msgid "ID" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,help:base_url.field_url_url__res_id +msgid "ID of the target record in the database" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,help:base_url.field_url_url__redirect +msgid "If tick this url is a redirection to the new url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__key +msgid "Key" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__lang_id +msgid "Lang" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__write_date +msgid "Last Updated on" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__manual +msgid "Manual" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__res_model_field +msgid "Model Name" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__need_refresh +msgid "Need Refresh" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__res_id +msgid "Record ID" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__redirect +msgid "Redirect" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__redirect_url_key +msgid "Redirect Url Keys" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__referential +msgid "Referential" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__res_model +msgid "Res Model" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__smart_search +#: model:ir.model.fields,field_description:base_url.field_url_url__smart_search +msgid "Smart Search" +msgstr "" + +#. module: base_url +#: model:ir.model,name:base_url.model_url_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_ids +msgid "Url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_key +msgid "Url Key" +msgstr "" + +#. module: base_url +#. odoo-python +#: code:addons/base_url/models/abstract_url.py:0 +msgid "" +"Url Key already exists in other model\n" +"- Model name: %(model_name)s\n" +"- Record ID: %(record_id)s\n" +"- Url Key: %(url_key)s\n" +"- Url ID: %(url_id)s" +msgstr "" + +#. module: base_url +#: model:ir.actions.act_window,name:base_url.base_url_action_view +msgid "Url List" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_need_refresh +msgid "Url Need Refresh" +msgstr "" + +#. module: base_url +#: model:ir.ui.menu,name:base_url.url_menu_view +msgid "Url Rewrite" +msgstr "" + +#. module: base_url +#: model_terms:ir.ui.view,arch_db:base_url.url_view_form +msgid "Url rewriting edit form" +msgstr "" diff --git a/base_url/i18n/it.po b/base_url/i18n/it.po new file mode 100644 index 00000000000..011471d0e89 --- /dev/null +++ b/base_url/i18n/it.po @@ -0,0 +1,173 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_url +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\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" + +#. module: base_url +#: model:ir.model,name:base_url.model_abstract_url +msgid "Abstract Url" +msgstr "" + +#. module: base_url +#: model:ir.model.constraint,message:base_url.constraint_url_url_unique_key_per_referential_per_lang +msgid "Already exists in database" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__count_url +msgid "Count Url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__create_uid +msgid "Created by" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__create_date +msgid "Created on" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__display_name +msgid "Display Name" +msgstr "" + +#. module: base_url +#: model:res.groups,name:base_url.group_edit_url +msgid "Edit url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__id +msgid "ID" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,help:base_url.field_url_url__res_id +msgid "ID of the target record in the database" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,help:base_url.field_url_url__redirect +msgid "If tick this url is a redirection to the new url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__key +msgid "Key" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__lang_id +msgid "Lang" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__write_date +msgid "Last Updated on" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__manual +msgid "Manual" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__res_model_field +msgid "Model Name" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__need_refresh +msgid "Need Refresh" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__res_id +msgid "Record ID" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__redirect +msgid "Redirect" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__redirect_url_key +msgid "Redirect Url Keys" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__referential +msgid "Referential" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__res_model +msgid "Res Model" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__smart_search +#: model:ir.model.fields,field_description:base_url.field_url_url__smart_search +msgid "Smart Search" +msgstr "" + +#. module: base_url +#: model:ir.model,name:base_url.model_url_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_ids +msgid "Url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_key +msgid "Url Key" +msgstr "" + +#. module: base_url +#. odoo-python +#: code:addons/base_url/models/abstract_url.py:0 +msgid "" +"Url Key already exists in other model\n" +"- Model name: %(model_name)s\n" +"- Record ID: %(record_id)s\n" +"- Url Key: %(url_key)s\n" +"- Url ID: %(url_id)s" +msgstr "" + +#. module: base_url +#: model:ir.actions.act_window,name:base_url.base_url_action_view +msgid "Url List" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_need_refresh +msgid "Url Need Refresh" +msgstr "" + +#. module: base_url +#: model:ir.ui.menu,name:base_url.url_menu_view +msgid "Url Rewrite" +msgstr "" + +#. module: base_url +#: model_terms:ir.ui.view,arch_db:base_url.url_view_form +msgid "Url rewriting edit form" +msgstr "" diff --git a/base_url/models/__init__.py b/base_url/models/__init__.py new file mode 100644 index 00000000000..bc2f6141e74 --- /dev/null +++ b/base_url/models/__init__.py @@ -0,0 +1,2 @@ +from . import abstract_url +from . import url_url diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py new file mode 100644 index 00000000000..79a4b12775f --- /dev/null +++ b/base_url/models/abstract_url.py @@ -0,0 +1,246 @@ +# Copyright (C) 2016 Akretion (http://www.akretion.com) +# @author EBII MonsieurB +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from lxml import etree + +from odoo import api, fields, models +from odoo.exceptions import UserError + +DEFAULT_LANG = "en_US" + +_logger = logging.getLogger(__name__) + + +SMART_BUTTON = """ +""" + + +class AbstractUrl(models.AbstractModel): + _name = "abstract.url" + _description = "Abstract Url" + _have_url = True + + url_ids = fields.One2many("url.url", "res_id") + url_need_refresh = fields.Boolean( + compute="_compute_url_need_refresh", store=True, readonly=False + ) + count_url = fields.Integer(compute="_compute_count_url") + url_key = fields.Char(compute="_compute_url_key") + redirect_url_key = fields.Json( + compute="_compute_url_key", + string="Redirect Url Keys", + ) + + def _compute_count_url(self): + res = self.env["url.url"]._read_group( + domain=[ + ("res_id", "in", self.ids), + ("res_model", "=", self._name), + ], + groupby=["res_id"], + aggregates=["__count"], + ) + mapped_data = {item: count for item, count in res} + for record in self: + record.count_url = mapped_data.get(record.id, 0) + + def _compute_url_need_refresh_depends(self): + return self._get_keyword_fields() + + @api.depends(lambda self: self._compute_url_need_refresh_depends()) + def _compute_url_need_refresh(self): + for record in self: + record.url_need_refresh = True + + @api.depends("url_ids") + @api.depends_context("lang", "referential") + def _compute_url_key(self): + referential = self._context.get("referential", "global") + lang = self._context.get("lang", "en_US") + for record in self: + record.url_key = record._get_main_url(referential, lang).key + record.redirect_url_key = record._get_redirect_urls( + referential, lang + ).mapped("key") + + def _get_keyword_fields(self): + """This method return a list of field that will be concatenated + with '-' to generate the url + Ex: if you return ['name', 'code'] the url will be + f"{record.name}-{record.code}" + + Note: the self already include in the context the lang of the record + Note: you can return key like in depends ex: ["categ_id.name", "code"] + """ + # TODO: IMO we should add the ID here by default + # to make sure the URL is always unique + # seb.beau: not sure, most of site do not have id in url + # the url have an seo impact so it's better to only put meaning information + # moreover for unicity you can put the default code of the product or ean13 + return ["name"] + + def _generate_url_key(self, referential, lang): + def get(self, key_path): + value = self + for key_field in key_path.split("."): + value = value[key_field] + return value + + self.ensure_one() + return self.env["ir.http"]._slugify( + "-".join([get(self, k) for k in self._get_keyword_fields() if get(self, k)]) + ) + + def _get_redirect_urls(self, referential, lang): + self.ensure_one() + return self.url_ids.filtered( + lambda s: ( + s.lang_id.code == lang and s.referential == referential and s.redirect + ) + ) + + def _get_main_url(self, referential, lang): + self.ensure_one() + return self.url_ids.filtered( + lambda s: ( + s.lang_id.code == lang + and s.referential == referential + and not s.redirect + ) + ) + + @api.model + def _prepare_url(self, referential, lang, url_key): + return { + "key": url_key, + "redirect": False, + "res_model": self._name, + "res_id": self.id, + "referential": referential, + "lang_id": self.env["res.lang"]._lang_get(lang).id, + "manual": False, + } + + def _reuse_url(self, existing_url): + # TODO add user notification in the futur SEO dashboard + existing_url.write( + { + "res_model": self._name, + "res_id": self.id, + "redirect": False, + } + ) + + def _update_url_key(self, referential="global", lang=None): + lang = lang or self.env.context.get("lang", DEFAULT_LANG) + for record in self.with_context(lang=lang): + # TODO maybe we should have a computed field that flag the + # current url if the key used for building the url have changed + # so we can skip this check if nothing have changed + current_url = record._get_main_url(referential, lang) + if not current_url.manual: + # Updating an url is done for a specific context + # a lang and a referential + # if something have changed on the record the url_need_refresh + # is flagged. + # Before updating one specific url (referential + lang) + # the flag is propagated on all valid url + if record.url_need_refresh: + record.url_ids.filtered( + lambda s: not s.redirect and not s.manual + ).write({"need_refresh": True}) + if not current_url or current_url.need_refresh: + current_url.need_refresh = False + url_key = record._generate_url_key(referential, lang) + # maybe some change have been done but the url is the same + # so check it + if current_url.key != url_key: + current_url.redirect = True + record._add_url(referential, lang, url_key) + if record.url_need_refresh: + record.url_need_refresh = False + + def _add_url(self, referential, lang, url_key): + self.ensure_one() + existing_url = self.env["url.url"].search( + [ + ("referential", "=", referential), + ("lang_id.code", "=", lang), + ("key", "=", url_key), + ] + ) + if existing_url: + if existing_url.redirect: + self._reuse_url(existing_url) + else: + raise UserError( + self.env._( + "Url Key already exists in other model\n" + "- Model name: %(model_name)s\n" + "- Record ID: %(record_id)s\n" + "- Url Key: %(url_key)s\n" + "- Url ID: %(url_id)s" + ) + % dict( + model_name=existing_url.res_model, + record_id=existing_url.res_id, + url_key=existing_url.key, + url_id=existing_url.id, + ) + ) + + else: + vals = self._prepare_url(referential, lang, url_key) + self.env["url.url"].create(vals) + + def _redirect_existing_url(self, action): + """ + This method is called when the record is deactivated to give a chance + to the concrete model to implement a redirect strategy + action can be "archived" or "unlink" + """ + return True + + def unlink(self): + for record in self: + record._redirect_existing_url("unlink") + # Remove dead url that have been not redirected + record.url_ids.unlink() + return super().unlink() + + def write(self, vals): + res = super().write(vals) + if "active" in vals and not vals["active"]: + self._redirect_existing_url("archived") + return res + + @api.model + def _get_view(self, view_id=None, view_type="form", **options): + arch, view = super()._get_view(view_id=view_id, view_type=view_type, **options) + button_box = arch.xpath("//div[@name='button_box']") + if button_box: + button_box[0].append(etree.fromstring(SMART_BUTTON)) + return arch, view + + def open_url(self): + self.ensure_one() + action = self.env.ref("base_url.base_url_action_view").read()[0] + action["domain"] = [("res_model", "=", self._name), ("res_id", "in", self.ids)] + action["context"] = { + "hide_res_model": True, + "hide_res_id": True, + "default_res_model": self._name, + "default_res_id": self.id, + } + return action diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py new file mode 100644 index 00000000000..0a2618ad233 --- /dev/null +++ b/base_url/models/url_url.py @@ -0,0 +1,84 @@ +# Copyright (C) 2016 Akretion (http://www.akretion.com) +# @author EBII MonsieurB +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, fields, models, tools + +_logger = logging.getLogger(__name__) + + +class UrlUrl(models.Model): + _name = "url.url" + _description = "Url" + _order = "res_model,res_id,redirect desc" + + manual = fields.Boolean(default=True, readonly=True) + key = fields.Char(required=True, index=True) + res_id = fields.Many2oneReference( + string="Record ID", + help="ID of the target record in the database", + model_field="res_model_field", + readonly=True, + index=True, + ) + res_model = fields.Selection( + selection=lambda s: s._get_model_with_url_selection(), readonly=True, index=True + ) + res_model_field = fields.Char( + string="Model Name", + compute="_compute_res_model_field", + readonly=True, + store=True, + ) + redirect = fields.Boolean(help="If tick this url is a redirection to the new url") + referential = fields.Selection( + selection=lambda s: s._get_all_referential(), + index=True, + default="global", + required=True, + ) + lang_id = fields.Many2one("res.lang", string="Lang", index=True, required=True) + need_refresh = fields.Boolean() + + _sql_constraints = [ + ( + "unique_key_per_referential_per_lang", + "unique(key, referential, lang_id)", + "Already exists in database", + ) + ] + + def init(self): + self.env.cr.execute( + f"""CREATE UNIQUE INDEX IF NOT EXISTS main_url_uniq + ON {self._table} (referential, lang_id, res_id, res_model) + WHERE redirect = False""" + ) + return super().init() + + @tools.ormcache() + @api.model + def _get_model_with_url_selection(self): + return [ + (model, self.env[model]._description) + for model in self.env + if ( + hasattr(self.env[model], "_have_url") + and not self.env[model]._abstract + and not self.env[model]._transient + ) + ] + + @api.depends("res_model") + def _compute_res_model_field(self): + for record in self: + record.res_model_field = record.res_model + + def _get_all_referential(self): + """Return the list of referential for your url, by default it's global + but you can do your own implementation to have url per search engine + index for example + """ + return [("global", "Global")] diff --git a/base_url/pyproject.toml b/base_url/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/base_url/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_url/readme/CONTRIBUTORS.md b/base_url/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..794e60ad6db --- /dev/null +++ b/base_url/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Sebastien Beau +- Laurent Mignon +- Florian Mounier diff --git a/base_url/readme/DESCRIPTION.md b/base_url/readme/DESCRIPTION.md new file mode 100644 index 00000000000..fbea4eae37d --- /dev/null +++ b/base_url/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +The aim of this module is to provide a generic mixin for building urls on records. + +The native odoo implementation of a record url is based on having an "id" in the url, +this module provides an alternative way of doing it. +Urls are generated and stored in an unique table (to ensure unicity). +Redirections are also managed in case of url changes. + +This project was initial build for shopinvader as we need to have unique url for the connected webshop. +But this concept can be reused in other cases and even replace the odoo url implementation. diff --git a/base_url/readme/DEVELOP.md b/base_url/readme/DEVELOP.md new file mode 100644 index 00000000000..24c954b1aaf --- /dev/null +++ b/base_url/readme/DEVELOP.md @@ -0,0 +1,20 @@ +To use this module, you need to inherit the `abstract.url` model and override +the `_get_keyword_fields` method to include your custom url fields. By default, +the `_get_keyword_fields` method returns the record `name`. + + +Here is an example of how to override the `_get_keyword_fields` method to include the `default_code` field for `product.template` records: + + +```python + +class ProductTemplate(models.Model): + _inherit = ["product.template", "abstract.url"] + _name = "product.template" + + def _get_keyword_fields(self): + return super()._get_keyword_fields() + ["default_code"] + +``` + +Your product template records will now have an `url_key` field that is generated from the `name` and `default_code` fields and a `redirect_url_key` field that will contain a list of redirect URLs (old `url_key` field values). diff --git a/base_url/security/ir.model.access.csv b/base_url/security/ir.model.access.csv new file mode 100644 index 00000000000..cd039fa1c30 --- /dev/null +++ b/base_url/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_read_url_url,access_url_url,model_url_url,base.group_user,1,0,0,0 +access_edit_url_url,access_url_url,model_url_url,group_edit_url,1,1,1,1 diff --git a/base_url/security/res_groups.xml b/base_url/security/res_groups.xml new file mode 100644 index 00000000000..921e2663656 --- /dev/null +++ b/base_url/security/res_groups.xml @@ -0,0 +1,10 @@ + + + + Edit url + + + diff --git a/base_url/static/description/icon.png b/base_url/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/base_url/static/description/icon.png differ diff --git a/base_url/static/description/index.html b/base_url/static/description/index.html new file mode 100644 index 00000000000..7be7254221e --- /dev/null +++ b/base_url/static/description/index.html @@ -0,0 +1,464 @@ + + + + + +Base Url + + + +
+ + + +Odoo Community Association + +
+

Base Url

+ +

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

The aim of this module is to provide a generic mixin for building urls +on records.

+

The native odoo implementation of a record url is based on having an +“id” in the url, this module provides an alternative way of doing it. +Urls are generated and stored in an unique table (to ensure unicity). +Redirections are also managed in case of url changes.

+

This project was initial build for shopinvader as we need to have unique +url for the connected webshop. But this concept can be reused in other +cases and even replace the odoo url implementation.

+

Table of contents

+ +
+

Development

+

To use this module, you need to inherit the abstract.url model and +override the _get_keyword_fields method to include your custom url +fields. By default, the _get_keyword_fields method returns the +record name.

+

Here is an example of how to override the _get_keyword_fields method +to include the default_code field for product.template records:

+
+class ProductTemplate(models.Model):
+    _inherit = ["product.template", "abstract.url"]
+    _name = "product.template"
+
+    def _get_keyword_fields(self):
+        return super()._get_keyword_fields() + ["default_code"]
+
+

Your product template records will now have an url_key field that is +generated from the name and default_code fields and a +redirect_url_key field that will contain a list of redirect URLs +(old url_key field values).

+
+
+

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

+
    +
  • Akretion
  • +
  • ACSONE SA/NV
  • +
+
+ +
+

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:

+

sebastienbeau

+

This module is part of the OCA/server-tools project on GitHub.

+

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

+
+
+
+
+ + diff --git a/base_url/tests/__init__.py b/base_url/tests/__init__.py new file mode 100644 index 00000000000..68bd2af57f7 --- /dev/null +++ b/base_url/tests/__init__.py @@ -0,0 +1 @@ +from . import test_abstract_url diff --git a/base_url/tests/models.py b/base_url/tests/models.py new file mode 100644 index 00000000000..9bd69522662 --- /dev/null +++ b/base_url/tests/models.py @@ -0,0 +1,25 @@ +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class FakeProduct(models.Model): + _inherit = ["abstract.url"] + _name = "fake.product" + _description = "Fake Product" + + code = fields.Char() + name = fields.Char(translate=True) + active = fields.Boolean(default=True) + categ_id = fields.Many2one("fake.categ") + + def _get_keyword_fields(self): + return ["categ_id.name"] + super()._get_keyword_fields() + ["code"] + + +class FakeCateg(models.Model): + _name = "fake.categ" + _description = "Fake Category" + + name = fields.Char() diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py new file mode 100644 index 00000000000..411893d1289 --- /dev/null +++ b/base_url/tests/test_abstract_url.py @@ -0,0 +1,171 @@ +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from unittest import mock + +from odoo_test_helper import FakeModelLoader + +from odoo.exceptions import UserError +from odoo.tests import TransactionCase + + +class TestAbstractUrl(TransactionCase, FakeModelLoader): + def setUp(self): + super().setUp() + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() + from .models import FakeCateg, FakeProduct + + self.loader.update_registry([FakeProduct, FakeCateg]) + + self.lang_en = self.env.ref("base.lang_en") + self.lang_fr = self.env.ref("base.lang_fr") + self.lang_fr.active = True + self.product = ( + self.env["fake.product"] + .with_context(lang="en_US") + .create({"name": "My Product"}) + ) + self.product.with_context(lang="fr_FR").name = "Mon Produit" + + def _expect_url_for_lang(self, lang, url_key): + self.assertEqual(self.product._get_main_url("global", lang).key, url_key) + self.assertEqual(self.product.with_context(lang=lang).url_key, url_key) + + def tearDown(self): + self.loader.restore_registry() + super().tearDown() + + def test_update_url_key(self): + self.product._update_url_key("global", "en_US") + self._expect_url_for_lang("en_US", "my-product") + url = self.product.url_ids.filtered(lambda s: s.lang_id == self.lang_en) + self.assertEqual(len(url), 1) + self.assertFalse(url.manual) + self.assertFalse(url.redirect) + + def test_update_url_2_lang(self): + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self._expect_url_for_lang("en_US", "my-product") + self._expect_url_for_lang("fr_FR", "mon-produit") + self.assertEqual(len(self.product.url_ids), 2) + + url = self.product.url_ids.filtered(lambda s: s.lang_id == self.lang_fr) + self.assertEqual(len(url), 1) + self.assertFalse(url.manual) + self.assertFalse(url.redirect) + + def test_update_no_translatable_field(self): + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self.product.write({"code": "1234"}) + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self.assertEqual(len(self.product.url_ids), 4) + redirects = self.product._get_redirect_urls("global", "en_US") + self._expect_url_for_lang("en_US", "my-product-1234") + self.assertEqual(len(redirects), 1) + redirects = self.product._get_redirect_urls("global", "fr_FR") + self.assertEqual(len(redirects), 1) + self._expect_url_for_lang("fr_FR", "mon-produit-1234") + self.assertEqual( + self.product.with_context(lang="en_US").redirect_url_key, + [ + "my-product", + ], + ) + self.assertEqual( + self.product.with_context(lang="fr_FR").redirect_url_key, + [ + "mon-produit", + ], + ) + + def test_update_translatable_field(self): + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self.product.name = "My Product With Redirect" + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self.assertEqual(len(self.product.url_ids), 3) + redirects = self.product._get_redirect_urls("global", "en_US") + self._expect_url_for_lang("en_US", "my-product-with-redirect") + self.assertEqual(len(redirects), 1) + + def test_update_product_never_generated(self): + self.product.name = "My product never had url generated" + self.product._update_url_key("global", "en_US") + self._expect_url_for_lang("en_US", "my-product-never-had-url-generated") + self.assertEqual(len(self.product.url_ids), 1) + + def test_update_with_relation(self): + self.product._update_url_key("global", "en_US") + categ = self.env["fake.categ"].create({"name": "Foo"}) + self.product.write({"categ_id": categ.id}) + self.product._update_url_key("global", "en_US") + self.assertEqual(len(self.product.url_ids), 2) + redirects = self.product._get_redirect_urls("global", "en_US") + self.assertEqual(len(redirects), 1) + self._expect_url_for_lang("en_US", "foo-my-product") + + def test_create_manual_url(self): + self.env["url.url"].create( + { + "manual": True, + "key": "my-custom-key", + "lang_id": self.lang_en.id, + "res_id": self.product.id, + "res_model": "fake.product", + "referential": "global", + } + ) + self._expect_url_for_lang("en_US", "my-custom-key") + self.assertEqual(len(self.product.url_ids), 1) + + self.product.name = "My name have change but my url is the same" + self.product._update_url_key("global", "en_US") + self._expect_url_for_lang("en_US", "my-custom-key") + + def test_write_inactive(self): + # when we deactivate a record, the redirect method should be called + with mock.patch.object( + self.product.__class__, "_redirect_existing_url" + ) as mocked_redirect: + self.product.active = False + mocked_redirect.assert_called_once() + + def test_update_twice_write_once(self): + """ + When we update twice the same record, the write method should be called + only once. This is important because for example, by default, in + shopinvader_search_engine_update, when the method write is called, + it mark the record as to_recompute. The recompute will then call the + serializer method on each binding one by one and set the state to done. + Unfortunately, if a record has 2 bindings, the serializer will call 2 + times the _update_url_key method. The first time, if every time the + method make a write on the record, the record will end up with the + state to_recompute. + """ + + # we mock the write method to check the number of call but we want the + # method to be executed + original_write = self.product.write + with mock.patch.object(type(self.product), "write") as mocked_write: + mocked_write.side_effect = original_write + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "en_US") + mocked_write.assert_called_once() + + def test_update_url_key_not_existing(self): + self.env["url.url"].create( + { + "manual": True, + "key": "my-product", + "lang_id": self.lang_en.id, + "res_id": self.env.ref("base.partner_demo").id, + "res_model": "res.partner", + "referential": "global", + } + ) + with self.assertRaisesRegex(UserError, "Url Key already exists in other model"): + self.product._update_url_key("global", "en_US") diff --git a/base_url/views/url_view.xml b/base_url/views/url_view.xml new file mode 100644 index 00000000000..30b0e3205f1 --- /dev/null +++ b/base_url/views/url_view.xml @@ -0,0 +1,54 @@ + + + + url.url + +
+ + + + + + + + + + + + +
+
+
+ + + url.url + + + + + + + + + + + + + + + + Url List + url.url + list,form + + + +
diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index c94327ed083..ee260b3a46f 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,9 +1,10 @@ [project] name = "odoo-addons-oca-server-tools" -version = "18.0.20260815.0" +version = "18.0.20260928.0" dependencies = [ "odoo-addon-attachment_delete_restrict==18.0.*", "odoo-addon-attachment_queue==18.0.*", + "odoo-addon-attachment_queue_email==18.0.*", "odoo-addon-attachment_synchronize==18.0.*", "odoo-addon-attachment_unindex_content==18.0.*", "odoo-addon-auditlog==18.0.*", @@ -28,6 +29,7 @@ dependencies = [ "odoo-addon-base_temporary_action==18.0.*", "odoo-addon-base_time_parameter==18.0.*", "odoo-addon-base_time_window==18.0.*", + "odoo-addon-base_url==18.0.*", "odoo-addon-base_view_inheritance_extension==18.0.*", "odoo-addon-bus_alt_connection==18.0.*", "odoo-addon-database_autovacuum_tuning==18.0.*", diff --git a/upgrade_analysis/README.rst b/upgrade_analysis/README.rst index 12e5a548122..e345ef5f376 100644 --- a/upgrade_analysis/README.rst +++ b/upgrade_analysis/README.rst @@ -11,7 +11,7 @@ Upgrade Analysis !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:815ea5ed7d7a8648454af112d95a6ab85b9908afd52ad06eddd7c987d77afa55 + !! source digest: sha256:bc3e8dea1961a004afbcb8834bbba7bf2916748d45a398b398c06aa7a3a88d4d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -53,7 +53,8 @@ recommended to have this module in a production database. Usage ===== -`Usage instructions `__ +`Usage +instructions `__ Known issues / Roadmap ====================== diff --git a/upgrade_analysis/__manifest__.py b/upgrade_analysis/__manifest__.py index d1f388402b1..800685c5e5f 100644 --- a/upgrade_analysis/__manifest__.py +++ b/upgrade_analysis/__manifest__.py @@ -5,7 +5,7 @@ "name": "Upgrade Analysis", "summary": "Performs a difference analysis between modules" " installed on two different Odoo instances", - "version": "18.0.1.4.6", + "version": "18.0.1.4.7", "category": "Migration", "author": "Therp BV, Opener B.V., GRAP, Odoo Community Association (OCA)", "maintainers": ["StefanRijnhart", "legalsylvain"], diff --git a/upgrade_analysis/readme/USAGE.md b/upgrade_analysis/readme/USAGE.md index 34452556f22..1574a7e88f9 100644 --- a/upgrade_analysis/readme/USAGE.md +++ b/upgrade_analysis/readme/USAGE.md @@ -1 +1 @@ -[Usage instructions](https://oca.github.io/OpenUpgrade/analyse.html) +[Usage instructions](https://oca.github.io/OpenUpgrade/070_migration_files.html#generate-the-difference-analysis-files) diff --git a/upgrade_analysis/static/description/index.html b/upgrade_analysis/static/description/index.html index 55cb482e571..6758357782b 100644 --- a/upgrade_analysis/static/description/index.html +++ b/upgrade_analysis/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Upgrade Analysis