diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py index 046789f..48c3bb4 100644 --- a/.docker_files/main/__manifest__.py +++ b/.docker_files/main/__manifest__.py @@ -15,7 +15,7 @@ "resize_observer_error_catcher", "web_custom_label", "web_custom_modifier", - + "web_search_date_range" ], "installable": True, } diff --git a/Dockerfile b/Dockerfile index 2e488b7..2ad01d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,7 @@ USER odoo COPY resize_observer_error_catcher /mnt/extra-addons/resize_observer_error_catcher COPY web_custom_label /mnt/extra-addons/web_custom_label COPY web_custom_modifier /mnt/extra-addons/web_custom_modifier +COPY web_search_date_range /mnt/extra-addons/web_search_date_range COPY .docker_files/main /mnt/extra-addons/main COPY .docker_files/odoo.conf /etc/odoo diff --git a/web_search_date_range/README.rst b/web_search_date_range/README.rst new file mode 100644 index 0000000..47c7242 --- /dev/null +++ b/web_search_date_range/README.rst @@ -0,0 +1,100 @@ +===================== +Web Search Date Range +===================== + +.. contents:: Table of Contents + +Context +------- +Vanilla Odoo comes with a predefined set of date range filters. + +.. image:: static/description/vanilla_odoo_filters.png + +There are multiple issues with these filters: + +* They can not be customized (at least without extra javascript code). +* They are based on static dates, which is not suitable for dashboards (you don't want to update your dashboard every month). +* Quarters are only relevant for a company with a fiscal exercise from January to December. + +Overview +-------- +This module allows to easily add contextual date range filters. + +Contextual means that the filter does not need to be updated. +It always filters records based on the current date. + +You may add one of these filters to your favorites or your dashboard and it will not need to be refreshed. + +.. image:: static/description/quotations_list.png + +Configuration +------------- + +Date Filters +************ +To edit the list of filters that appear in the search view of a model: + +* Go to: Settings / Technical / User Interface / Date Filters + +.. image:: static/description/date_filters.png + +After editing the filters, you need to refresh your page for the changes to be applied. + +Date Ranges +*********** +The module comes with the following predefined date ranges: + +* Before Today +* Today +* Next Fifteen Days +* Previous Week +* Current Week +* Next Week +* Previous Month +* Current Month +* Next Month +* Previous Year +* Current Year +* Next Year + +To add a custom range type: + +* Go to: Settings / Technical / User Interface / Date Ranges + +.. image:: static/description/date_range_list.png + +* Click on `Create`. + +.. image:: static/description/date_range_form.png + +* Enter a label for your range type. +* Enter a domain filter for your new range type. + +.. image:: static/description/date_range_with_domain.png + +The following variables can be used inside the domain: + +* field: the technical name of the field being queried +* today: the current date in the timezone of the user +* datetime: the datetime.datetime class +* relativedelta: the dateutil.relativedelta.relativedelta class +* MO, TU, WE, TH, FR, SA, SU: dateutil.relativedelta.weekdays + +See the library `relativedelta `_ for more info. + +Weekly Date Ranges +------------------ +Weekly date ranges are implemented from monday to sunday. + +If you prefer from sunday to saturday: + +* Go to: Settings / Technical / User Interface / Date Range Types. +* For each weekly range type: + 1. Adapt the domain. + 2. Check the `No Update` checkbox. + +.. image:: static/description/demo_date_range.png + +Contributors +------------ +* Numigi (tm) and all its contributors (https://bit.ly/numigiens) \ No newline at end of file diff --git a/web_search_date_range/__init__.py b/web_search_date_range/__init__.py new file mode 100644 index 0000000..1a9d411 --- /dev/null +++ b/web_search_date_range/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/web_search_date_range/__manifest__.py b/web_search_date_range/__manifest__.py new file mode 100644 index 0000000..18d25b0 --- /dev/null +++ b/web_search_date_range/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Web Search Date Range", + "version": "16.0.1.0.0", + "author": "Numigi", + "maintainer": "Numigi", + "website": "https://bit.ly/numigi-com", + "license": "LGPL-3", + "category": "Project", + "summary": "Add date range filters to the search filters dropdown menu.", + "depends": [ + "web", + ], + "data": [ + "security/ir.model.access.csv", + "data/search_date_range.xml", + "views/search_date_range_views.xml", + "views/search_date_range_filter_views.xml", + ], + "assets": { + "web.assets_backend": [ + "/web_search_date_range/static/src/js/*", + ], + }, + "installable": True, +} diff --git a/web_search_date_range/data/search_date_range.xml b/web_search_date_range/data/search_date_range.xml new file mode 100644 index 0000000..bbd600b --- /dev/null +++ b/web_search_date_range/data/search_date_range.xml @@ -0,0 +1,123 @@ + + + + + Before Today + [ + (field, '<', today.strftime('%Y-%m-%d')), + ] + + + + + Today + [ + '&', + (field, '>=', today.strftime('%Y-%m-%d')), + (field, '<', (today + relativedelta(days=1)).strftime('%Y-%m-%d')), + ] + + + + + Next Fifteen Days + [ + '&', + (field, '>=', today.strftime('%Y-%m-%d')), + (field, '<', (today + relativedelta(days=15)).strftime('%Y-%m-%d')), + ] + + + + + + Previous Week + [ + '&', + (field, '>=', (today + relativedelta(days=-today.weekday() - 7)).strftime('%Y-%m-%d')), + (field, '<', (today + relativedelta(days=-today.weekday())).strftime('%Y-%m-%d')), + ] + + + + + Current Week + [ + '&', + (field, '>=', (today + relativedelta(days=-today.weekday())).strftime('%Y-%m-%d')), + (field, '<', (today + relativedelta(days=-today.weekday() + 7)).strftime('%Y-%m-%d')), + ] + + + + + Next Week + [ + '&', + (field, '>=', (today + relativedelta(days=-today.weekday() + 7)).strftime('%Y-%m-%d')), + (field, '<', (today + relativedelta(days=-today.weekday() + 14)).strftime('%Y-%m-%d')), + ] + + + + + Previous Month + [ + '&', + (field, '>=', (today - relativedelta(days=today.day - 1) + relativedelta(months=-1)).strftime('%Y-%m-%d')), + (field, '<', (today - relativedelta(days=today.day - 1)).strftime('%Y-%m-%d')), + ] + + + + + Current Month + [ + '&', + (field, '>=', (today - relativedelta(days=today.day - 1)).strftime('%Y-%m-%d')), + (field, '<', (today - relativedelta(days=today.day - 1) + relativedelta(months=1)).strftime('%Y-%m-%d')), + ] + + + + + Next Month + [ + '&', + (field, '>=', (today - relativedelta(days=today.day - 1) + relativedelta(months=1)).strftime('%Y-%m-%d')), + (field, '<', (today - relativedelta(days=today.day - 1) + relativedelta(months=2)).strftime('%Y-%m-%d')), + ] + + + + + Previous Year + [ + '&', + (field, '>=', datetime(today.year - 1, 1, 1).strftime('%Y-%m-%d')), + (field, '<', datetime(today.year, 1, 1).strftime('%Y-%m-%d')), + ] + + + + + Current Year + [ + '&', + (field, '>=', datetime(today.year, 1, 1).strftime('%Y-%m-%d')), + (field, '<', datetime(today.year + 1, 1, 1).strftime('%Y-%m-%d')), + ] + + + + + Next Year + [ + '&', + (field, '>=', datetime(today.year + 1, 1, 1).strftime('%Y-%m-%d')), + (field, '<', datetime(today.year + 2, 1, 1).strftime('%Y-%m-%d')), + ] + + + + diff --git a/web_search_date_range/i18n/fr.po b/web_search_date_range/i18n/fr.po new file mode 100644 index 0000000..1a6ee52 --- /dev/null +++ b/web_search_date_range/i18n/fr.po @@ -0,0 +1,192 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_search_date_range +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e-20240910\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-09-15 16:48+0000\n" +"PO-Revision-Date: 2024-09-15 16:48+0000\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: web_search_date_range +#: model:ir.model,name:web_search_date_range.model_base +msgid "Base" +msgstr "" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_before_today +msgid "Before Today" +msgstr "Avant aujourd'hui" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__create_uid +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__create_date +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_current_month +msgid "Current Month" +msgstr "Mois courant" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_current_week +msgid "Current Week" +msgstr "Semaine courante" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_current_year +msgid "Current Year" +msgstr "Année courante" + +#. module: web_search_date_range +#: model:ir.model,name:web_search_date_range.model_search_date_range_filter +msgid "Date Filter" +msgstr "Filtre de dates" + +#. module: web_search_date_range +#: model:ir.actions.act_window,name:web_search_date_range.date_range_filters_action +#: model:ir.ui.menu,name:web_search_date_range.date_range_filters_menu +#: model_terms:ir.ui.view,arch_db:web_search_date_range.date_range_filters_list +#: model_terms:ir.ui.view,arch_db:web_search_date_range.date_range_filters_search +msgid "Date Filters" +msgstr "Filtres de dates" + +#. module: web_search_date_range +#: model:ir.model,name:web_search_date_range.model_search_date_range +#: model_terms:ir.ui.view,arch_db:web_search_date_range.date_range_form +msgid "Date Range" +msgstr "Intervalle de dates" + +#. module: web_search_date_range +#: model:ir.actions.act_window,name:web_search_date_range.date_range_action +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter__range_ids +#: model:ir.ui.menu,name:web_search_date_range.date_range_menu +#: model_terms:ir.ui.view,arch_db:web_search_date_range.date_range_list +msgid "Date Ranges" +msgstr "Intervalles de dates" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__display_name +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__domain +msgid "Domain" +msgstr "Domaine" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter__field_id +#: model_terms:ir.ui.view,arch_db:web_search_date_range.date_range_filters_search +msgid "Field" +msgstr "Champ" + +#. module: web_search_date_range +#: model_terms:ir.ui.view,arch_db:web_search_date_range.date_range_filters_search +msgid "Group By" +msgstr "Grouper par" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__id +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter__id +msgid "ID" +msgstr "ID" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__label +msgid "Label" +msgstr "Libellé" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range____last_update +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter____last_update +msgid "Last Modified on" +msgstr "Modifié le" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__write_uid +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter__write_uid +msgid "Last Updated by" +msgstr "Modifié par" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__write_date +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range_filter__model_id +#: model_terms:ir.ui.view,arch_db:web_search_date_range.date_range_filters_search +msgid "Model" +msgstr "Modèle" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_next_fifteen_days +msgid "Next Fifteen Days" +msgstr "Prochains 15 jours" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_next_month +msgid "Next Month" +msgstr "Mois suivant" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_next_week +msgid "Next Week" +msgstr "Semaine suivante" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_next_year +msgid "Next Year" +msgstr "Année suivante" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__noupdate +msgid "No Update" +msgstr "Mise à jour désactivée" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_previous_month +msgid "Previous Month" +msgstr "Mois précédent" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_previous_week +msgid "Previous Week" +msgstr "Semaine précédente" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_previous_year +msgid "Previous Year" +msgstr "Année précédente" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__sequence +msgid "Sequence" +msgstr "Séquence" + +#. module: web_search_date_range +#: model:search.date.range,label:web_search_date_range.range_today +msgid "Today" +msgstr "Aujourd'hui" + +#. module: web_search_date_range +#: model:ir.model.fields,field_description:web_search_date_range.field_search_date_range__xml_id +msgid "XML ID" +msgstr "XML ID" diff --git a/web_search_date_range/models/__init__.py b/web_search_date_range/models/__init__.py new file mode 100644 index 0000000..da802ab --- /dev/null +++ b/web_search_date_range/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import base +from . import search_date_range_filter +from . import web_search_date_range diff --git a/web_search_date_range/models/base.py b/web_search_date_range/models/base.py new file mode 100644 index 0000000..06b058a --- /dev/null +++ b/web_search_date_range/models/base.py @@ -0,0 +1,41 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, models + + +class Base(models.AbstractModel): + + _inherit = "base" + + @api.model + def _where_calc(self, domain, active_test=True): + domain = _convert_domain(self.env, domain) + return super()._where_calc(domain, active_test) + + +def _convert_domain(env, domain): + if not isinstance(domain, list): + return domain + + return list(_iter_leaves(env, domain)) + + +def _iter_leaves(env, domain): + for leaf in domain: + if _is_date_range(leaf): + for range_leaf in _to_date_range(env, leaf): + yield range_leaf + else: + yield leaf + + +def _is_date_range(leaf): + return isinstance(leaf, (list, tuple)) and len(leaf) == 3 and leaf[1] == "range" + + +def _to_date_range(env, leaf): + field_name = leaf[0] + range_id = leaf[2] + range_ = env["search.date.range"].browse(range_id) + return range_.generate_domain(field_name) diff --git a/web_search_date_range/models/search_date_range_filter.py b/web_search_date_range/models/search_date_range_filter.py new file mode 100644 index 0000000..b143935 --- /dev/null +++ b/web_search_date_range/models/search_date_range_filter.py @@ -0,0 +1,63 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class SearchDateRangeFilter(models.Model): + _name = "search.date.range.filter" + _description = "Date Filter" + _order = "model_id, field_id" + + model_id = fields.Many2one("ir.model", "Model", ondelete="cascade", required=True) + field_id = fields.Many2one( + "ir.model.fields", + "Field", + ondelete="cascade", + required=True, + domain="[('model_id', '=', model_id), ('ttype', 'in', ['date', 'datetime'])]", + ) + range_ids = fields.Many2many( + "search.date.range", + "search_date_range_filter_rel", + "filter_id", + "range_id", + string="Date Ranges", + required=True, + ) + + @api.onchange("model_id") + def _onchange_model_id_empty_field_id(self): + self.field_id = None + + @api.model + def get_filter_list(self): + filters = [line._get_filter() for line in self.search([])] + return sorted(filters, key=lambda f: f["description"]) + + def _get_filter(self): + description = self._get_translation(self.field_id, "field_description") + return { + "isRelativeDateFilter": True, + "custom_options": [self._get_option(range_) for range_ in self.range_ids], + "description": description or self.field_id.field_description, + "type": "dateFilter", + "model": self.field_id.model, + "fieldName": self.field_id.name, + "fieldType": self.field_id.ttype, + } + + def _get_option(self, range_): + descritpion = self._get_translation(range_, "label") + return { + "id": f"date_range_filter_{self.id}_{range_.id}", + "domain": self._get_domain(range_), + "description": descritpion or range_.label, + } + + def _get_domain(self, range_): + return f'[("{self.field_id.name}", "range", {range_.id})]' + + def _get_translation(self, record, field_name): + lang = self.env.context.get("lang", self.env.user.lang) + return record.with_context(lang=lang)[field_name] diff --git a/web_search_date_range/models/web_search_date_range.py b/web_search_date_range/models/web_search_date_range.py new file mode 100644 index 0000000..4be0953 --- /dev/null +++ b/web_search_date_range/models/web_search_date_range.py @@ -0,0 +1,64 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from datetime import datetime +from dateutil.relativedelta import relativedelta, MO, TU, WE, TH, FR, SA, SU +from odoo import api, fields, models +from odoo.tools.safe_eval import safe_eval + + +class SearchDateRange(models.Model): + + _name = "search.date.range" + _description = "Date Range" + _rec_name = "label" + _order = "sequence" + + sequence = fields.Integer() + label = fields.Char(translate=True, required=True) + domain = fields.Text(required=True) + xml_id = fields.Many2one("ir.model.data", "XML ID", compute="_compute_xml_id") + noupdate = fields.Boolean( + "No Update", compute="_compute_noupdate", inverse="_set_noupdate" + ) + + def _compute_xml_id(self): + for range_type in self: + range_type.xml_id = self.env["ir.model.data"].search( + [ + ("model", "=", range_type._name), + ("res_id", "=", range_type.id), + ("module", "!=", False), + ], + limit=1, + ) + + def _compute_noupdate(self): + for range_type in self: + range_type.noupdate = ( + range_type.xml_id.noupdate if range_type.xml_id else False + ) + + def _set_noupdate(self): + range_types_with_xml_id = self.filtered(lambda r: r.xml_id) + for range_type in range_types_with_xml_id: + range_type.xml_id.noupdate = range_type.noupdate + + def generate_domain(self, field): + return safe_eval(self.domain, self._get_domain_context(field)) + + @api.model + def _get_domain_context(self, field): + return { + "field": field, + "today": fields.Date.context_today(self), + "datetime": datetime, + "relativedelta": relativedelta, + "MO": MO, + "TU": TU, + "WE": WE, + "TH": TH, + "FR": FR, + "SA": SA, + "SU": SU, + } diff --git a/web_search_date_range/security/ir.model.access.csv b/web_search_date_range/security/ir.model.access.csv new file mode 100644 index 0000000..ac1b3fc --- /dev/null +++ b/web_search_date_range/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_search_date_range,access_search_date_range,model_search_date_range,,1,0,0,0 +access_search_date_range_filter,access_search_date_range_filter,model_search_date_range_filter,,1,0,0,0 +access_search_date_range_admin,access_search_date_range_admin,model_search_date_range,base.group_system,1,1,1,1 +access_search_date_range_filter_admin,access_search_date_range_filter_admin,model_search_date_range_filter,base.group_system,1,1,1,1 diff --git a/web_search_date_range/static/description/date_filters.png b/web_search_date_range/static/description/date_filters.png new file mode 100644 index 0000000..eb3c48b Binary files /dev/null and b/web_search_date_range/static/description/date_filters.png differ diff --git a/web_search_date_range/static/description/date_range_form.png b/web_search_date_range/static/description/date_range_form.png new file mode 100644 index 0000000..cffb682 Binary files /dev/null and b/web_search_date_range/static/description/date_range_form.png differ diff --git a/web_search_date_range/static/description/date_range_list.png b/web_search_date_range/static/description/date_range_list.png new file mode 100644 index 0000000..893aaca Binary files /dev/null and b/web_search_date_range/static/description/date_range_list.png differ diff --git a/web_search_date_range/static/description/date_range_with_domain.png b/web_search_date_range/static/description/date_range_with_domain.png new file mode 100644 index 0000000..01cab66 Binary files /dev/null and b/web_search_date_range/static/description/date_range_with_domain.png differ diff --git a/web_search_date_range/static/description/demo_date_range.png b/web_search_date_range/static/description/demo_date_range.png new file mode 100644 index 0000000..2c858e8 Binary files /dev/null and b/web_search_date_range/static/description/demo_date_range.png differ diff --git a/web_search_date_range/static/description/icon.png b/web_search_date_range/static/description/icon.png new file mode 100644 index 0000000..92a86b1 Binary files /dev/null and b/web_search_date_range/static/description/icon.png differ diff --git a/web_search_date_range/static/description/quotations_list.png b/web_search_date_range/static/description/quotations_list.png new file mode 100644 index 0000000..ab573c6 Binary files /dev/null and b/web_search_date_range/static/description/quotations_list.png differ diff --git a/web_search_date_range/static/description/vanilla_odoo_filters.png b/web_search_date_range/static/description/vanilla_odoo_filters.png new file mode 100644 index 0000000..83b84c4 Binary files /dev/null and b/web_search_date_range/static/description/vanilla_odoo_filters.png differ diff --git a/web_search_date_range/static/src/js/search_filter.js b/web_search_date_range/static/src/js/search_filter.js new file mode 100644 index 0000000..1185ca3 --- /dev/null +++ b/web_search_date_range/static/src/js/search_filter.js @@ -0,0 +1,27 @@ +/** @odoo-module **/ + +import rpc from 'web.rpc'; + +export class SearchDateRange { + constructor() { + this._filtersByModel = new Map(); + } + + async fetchFilters(model) { + const result = await rpc.query({ + model: "search.date.range.filter", + method: "get_filter_list", + args: [], + kwargs: {}, + }); + const filteredResults = result.filter(filter => filter.model === model); + + if (!this._filtersByModel.has(model)) { + this._filtersByModel.set(model, []); + } + + this._filtersByModel.get(model).push(...filteredResults); + + return filteredResults; + } +} diff --git a/web_search_date_range/static/src/js/search_model.js b/web_search_date_range/static/src/js/search_model.js new file mode 100644 index 0000000..2ae69b3 --- /dev/null +++ b/web_search_date_range/static/src/js/search_model.js @@ -0,0 +1,104 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { SearchModel } from "@web/search/search_model"; +import { SearchDateRange } from "./search_filter"; +import pyUtils from "web.py_utils"; + +const searchDateRange = new SearchDateRange(); + +patch(SearchModel.prototype, "web_search_date_range.SearchModel", { + _enrichItem(searchItem) { + const item = this._super(...arguments); + if (item.isRelativeDateFilter !== true) { + return item; + } + + const queryElements = this.query.filter( + (queryElem) => queryElem.searchItemId === searchItem.id + ); + const isActive = Boolean(queryElements.length); + const enrichSearchItem = { ...searchItem, isActive }; + + const _enrichOptions = (options = [], selectedIds = []) => + options.map(({ description, id, groupNumber }) => ({ + description, + id, + groupNumber, + isActive: selectedIds.includes(id), + })); + + enrichSearchItem.options = _enrichOptions( + this.custom_options || [], + queryElements.map((queryElem) => queryElem.generatorId) + ); + + enrichSearchItem.options = _enrichOptions( + item.custom_options || [], + queryElements.map((queryElem) => queryElem.generatorId) + ); + + return enrichSearchItem; + }, + + async load() { + await this._super(...arguments); + const filteredItems = await searchDateRange.fetchFilters(this.resModel); + + if (filteredItems.length) { + const searchItemsArray = Array.isArray(this.searchItems) ? this.searchItems : Object.values(this.searchItems); + + filteredItems.forEach(item => { + if (item.isRelativeDateFilter) { + const exists = searchItemsArray.some(searchItem => + searchItem.description === item.description && searchItem.type === item.type + ); + + if (!exists) { + this._createGroupOfSearchItems([item]); + } + } + }); + } + }, + + toggleDateFilter(searchItemId, generatorId) { + const searchItem = this.searchItems[searchItemId]; + if (searchItem.type !== "dateFilter" || searchItem.isRelativeDateFilter !== true) { + return this._super(...arguments); + } + + const index = this.query.findIndex( + (queryElem) => + queryElem.searchItemId === searchItemId && + queryElem.generatorId === generatorId + ); + + if (index >= 0) { + this.query.splice(index, 1); + } else { + this.query.push({ searchItemId, generatorId }); + } + this._checkComparisonStatus(); + this._notify(); + }, + + _getDateFilterDomain(dateFilter, generatorIds, key = "domain") { + if (dateFilter.isRelativeDateFilter !== true) { + return this._super(...arguments); + } + const options = this._getSelectedDateRangeOptions(dateFilter, generatorIds); + const domains = options.map(o => o.domain); + + if (key === "domain") { + return pyUtils.assembleDomains(domains, 'OR'); + } else if (key === "description") { + return options.map(o => o.description).join(" | "); + } + }, + + _getSelectedDateRangeOptions(dateFilter, generatorIds) { + const optionIds = new Set(generatorIds); + return dateFilter.custom_options.filter(option => optionIds.has(option.id)); + } +}); diff --git a/web_search_date_range/tests/__init__.py b/web_search_date_range/tests/__init__.py new file mode 100644 index 0000000..c5fc8c2 --- /dev/null +++ b/web_search_date_range/tests/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import test_search_date_range +from . import test_res_partner diff --git a/web_search_date_range/tests/test_res_partner.py b/web_search_date_range/tests/test_res_partner.py new file mode 100644 index 0000000..7b26dca --- /dev/null +++ b/web_search_date_range/tests/test_res_partner.py @@ -0,0 +1,35 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from dateutil.relativedelta import relativedelta +from freezegun import freeze_time +from datetime import datetime +from odoo.tests.common import TransactionCase + + +class TestPartner(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env["res.partner"].search([]).write({"date": False}) + + cls.date_1 = datetime.now().date() + cls.date_2 = cls.date_1 - relativedelta(days=30) + + cls.partner_1 = cls.env["res.partner"].create( + {"name": "Partner 1", "date": cls.date_1} + ) + cls.partner_2 = cls.env["res.partner"].create( + {"name": "Partner 1", "date": cls.date_2} + ) + + def test_search_partner(self): + range_today = self.env.ref("web_search_date_range.range_today") + + with freeze_time(self.date_1): + partners = self.env["res.partner"].search( + [("date", "range", range_today.id)] + ) + + assert partners == self.partner_1 diff --git a/web_search_date_range/tests/test_search_date_range.py b/web_search_date_range/tests/test_search_date_range.py new file mode 100644 index 0000000..c1cfad5 --- /dev/null +++ b/web_search_date_range/tests/test_search_date_range.py @@ -0,0 +1,173 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from ddt import data, ddt +from freezegun import freeze_time +from odoo.tests.common import TransactionCase + + +@ddt +class TestSearchDateRange(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.model = cls.env.ref("base.model_res_partner") + cls.field = cls.env.ref("base.field_res_partner__create_date") + + def _eval_filter_domain(self, range_ref): + date_range = self.env.ref( + "web_search_date_range.{range_ref}".format(range_ref=range_ref) + ) + return date_range.generate_domain(self.field.name) + + def test_eval_domain_for_range_before_today(self): + with freeze_time("2018-05-18"): + domain = self._eval_filter_domain("range_before_today") + self.assertEqual( + domain, + [ + ("create_date", "<", "2018-05-18"), + ], + ) + + def test_eval_domain_for_range_today(self): + with freeze_time("2018-05-18"): + domain = self._eval_filter_domain("range_today") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2018-05-18"), + ("create_date", "<", "2018-05-19"), + ], + ) + + def test_eval_domain_for_next_fifteen_days(self): + with freeze_time("2018-05-18"): + domain = self._eval_filter_domain("range_next_fifteen_days") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2018-05-18"), + ("create_date", "<", "2018-06-02"), + ], + ) + + @data("2018-05-14", "2018-05-18", "2018-05-20") + def test_eval_domain_for_range_current_week(self, today): + with freeze_time(today): + domain = self._eval_filter_domain("range_current_week") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2018-05-14"), + ("create_date", "<", "2018-05-21"), + ], + ) + + @data("2018-05-14", "2018-05-18", "2018-05-20") + def test_eval_domain_for_range_next_week(self, today): + with freeze_time(today): + domain = self._eval_filter_domain("range_next_week") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2018-05-21"), + ("create_date", "<", "2018-05-28"), + ], + ) + + @data("2018-05-14", "2018-05-18", "2018-05-20") + def test_eval_domain_for_range_previous_week(self, today): + with freeze_time(today): + domain = self._eval_filter_domain("range_previous_week") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2018-05-07"), + ("create_date", "<", "2018-05-14"), + ], + ) + + @data("2018-05-01", "2018-05-18", "2018-05-31") + def test_eval_domain_for_range_current_month(self, today): + with freeze_time(today): + domain = self._eval_filter_domain("range_current_month") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2018-05-01"), + ("create_date", "<", "2018-06-01"), + ], + ) + + @data("2018-05-01", "2018-05-18", "2018-05-31") + def test_eval_domain_for_range_next_month(self, today): + with freeze_time(today): + domain = self._eval_filter_domain("range_next_month") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2018-06-01"), + ("create_date", "<", "2018-07-01"), + ], + ) + + @data("2018-05-01", "2018-05-18", "2018-05-31") + def test_eval_domain_for_range_previous_month(self, today): + with freeze_time(today): + domain = self._eval_filter_domain("range_previous_month") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2018-04-01"), + ("create_date", "<", "2018-05-01"), + ], + ) + + @data("2018-01-01", "2018-05-18", "2018-12-31") + def test_eval_domain_for_range_current_year(self, today): + with freeze_time(today): + domain = self._eval_filter_domain("range_current_year") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2018-01-01"), + ("create_date", "<", "2019-01-01"), + ], + ) + + @data("2018-01-01", "2018-05-18", "2018-12-31") + def test_eval_domain_for_range_next_year(self, today): + with freeze_time(today): + domain = self._eval_filter_domain("range_next_year") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2019-01-01"), + ("create_date", "<", "2020-01-01"), + ], + ) + + @data("2018-01-01", "2018-05-18", "2018-12-31") + def test_eval_domain_for_range_previous_year(self, today): + with freeze_time(today): + domain = self._eval_filter_domain("range_previous_year") + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", "2017-01-01"), + ("create_date", "<", "2018-01-01"), + ], + ) diff --git a/web_search_date_range/views/search_date_range_filter_views.xml b/web_search_date_range/views/search_date_range_filter_views.xml new file mode 100644 index 0000000..8c6f8c3 --- /dev/null +++ b/web_search_date_range/views/search_date_range_filter_views.xml @@ -0,0 +1,41 @@ + + + + + Date Filter List View + search.date.range.filter + + + + + + + + + + + Date Filter Search View + search.date.range.filter + + + + + + + + + + + + + + + Date Filters + ir.actions.act_window + search.date.range.filter + + + + + + diff --git a/web_search_date_range/views/search_date_range_views.xml b/web_search_date_range/views/search_date_range_views.xml new file mode 100644 index 0000000..44e88ba --- /dev/null +++ b/web_search_date_range/views/search_date_range_views.xml @@ -0,0 +1,43 @@ + + + + + Date Range Form View + search.date.range + +
+ + + + + + + + + +
+
+
+ + + Date Range List View + search.date.range + + + + + + + + + + + Date Ranges + ir.actions.act_window + search.date.range + + + + + +