diff --git a/README.md b/README.md index 1a20b078..ceb41ab0 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ addon | version | maintainers | summary [g2p_registry_addl_info_rest_api](g2p_registry_addl_info_rest_api/) | 17.0.1.4.0 | | G2P Registry: Additional Info REST API [g2p_registry_base](g2p_registry_base/) | 17.0.1.4.0 | | G2P Registry: Base [g2p_registry_datashare_websub](g2p_registry_datashare_websub/) | 17.0.1.4.0 | | G2P Registry Datashare: WebSub +[g2p_registry_deduplication_deduplicator](g2p_registry_deduplication_deduplicator/) | 17.0.0.0.0 | | OpenG2P Registry Deduplication - Deduplicator [g2p_registry_documents](g2p_registry_documents/) | 17.0.1.4.0 | | G2P Registry: Documents [g2p_registry_encryption](g2p_registry_encryption/) | 17.0.1.4.0 | | G2P Registry: Encryption [g2p_registry_group](g2p_registry_group/) | 17.0.1.4.0 | | G2P Registry: Groups diff --git a/g2p_registry_deduplication_deduplicator/README.md b/g2p_registry_deduplication_deduplicator/README.md new file mode 100644 index 00000000..ac158263 --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/README.md @@ -0,0 +1,3 @@ +# OpenG2P Registry Deduplication - Deduplicator + +Refer to https://docs.openg2p.org. diff --git a/g2p_registry_deduplication_deduplicator/__init__.py b/g2p_registry_deduplication_deduplicator/__init__.py new file mode 100644 index 00000000..396d48de --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenG2P Social Registry. See LICENSE file for full copyright and licensing details. +from . import models diff --git a/g2p_registry_deduplication_deduplicator/__manifest__.py b/g2p_registry_deduplication_deduplicator/__manifest__.py new file mode 100644 index 00000000..ad2b2971 --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/__manifest__.py @@ -0,0 +1,32 @@ +# Part of OpenG2P Social Registry. See LICENSE file for full copyright and licensing details. +{ + "name": "OpenG2P Registry Deduplication - Deduplicator", + "category": "G2P", + "version": "17.0.0.0.0", + "sequence": 1, + "author": "OpenG2P", + "website": "https://openg2p.org", + "license": "LGPL-3", + "depends": [ + "g2p_registry_individual", + ], + "external_dependencies": {}, + "data": [ + "security/ir.model.access.csv", + "data/default_deduplicator_config.xml", + "views/deduplicator_config_view.xml", + "views/individual_view.xml", + "views/res_config_view.xml", + ], + "assets": { + "web.assets_backend": [ + "g2p_registry_deduplication_deduplicator/static/src/js/view_duplicates.js", + "g2p_registry_deduplication_deduplicator/static/src/xml/view_duplicates_template.xml", + ], + }, + "demo": [], + "images": [], + "application": True, + "installable": True, + "auto_install": False, +} diff --git a/g2p_registry_deduplication_deduplicator/data/default_deduplicator_config.xml b/g2p_registry_deduplication_deduplicator/data/default_deduplicator_config.xml new file mode 100644 index 00000000..831429d5 --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/data/default_deduplicator_config.xml @@ -0,0 +1,12 @@ + + + + Default + + + + diff --git a/g2p_registry_deduplication_deduplicator/models/__init__.py b/g2p_registry_deduplication_deduplicator/models/__init__.py new file mode 100644 index 00000000..bf9e5820 --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/models/__init__.py @@ -0,0 +1,4 @@ +# Part of OpenG2P. See LICENSE file for full copyright and licensing details. +from . import registrant +from . import dedupe_config +from . import res_config_settings diff --git a/g2p_registry_deduplication_deduplicator/models/dedupe_config.py b/g2p_registry_deduplication_deduplicator/models/dedupe_config.py new file mode 100644 index 00000000..f3a14ccb --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/models/dedupe_config.py @@ -0,0 +1,106 @@ +import logging +import os + +import requests + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class G2PDedupeConfigField(models.Model): + _name = "g2p.registry.deduplication.deduplicator.config.field" + _description = "Deduplicator Config Field" + + name = fields.Char(required=True) + fuzziness = fields.Char() + weightage = fields.Float() + exact = fields.Boolean() + + dedupe_config_id = fields.Many2one( + "g2p.registry.deduplication.deduplicator.config", ondelete="cascade", required=True + ) + + +class G2PDedupeConfig(models.Model): + _name = "g2p.registry.deduplication.deduplicator.config" + _description = "Deduplicator Config" + + name = fields.Char(required=True) + + config_name = fields.Char(required=True, default="default") + + dedupe_service_base_url = fields.Char( + default=os.getenv( + "DEDUPLICATOR_SERVICE_BASE_URL", "http://socialregistry-deduplicator-openg2p-deduplicator" + ) + ) + dedupe_service_api_timeout = fields.Integer(default=10) + + config_index_name = fields.Char(default="res_partner") + config_fields = fields.One2many( + "g2p.registry.deduplication.deduplicator.config.field", "dedupe_config_id" + ) + config_score_threshold = fields.Float() + + active = fields.Boolean(required=True) + + _sql_constraints = [ + ("unique_config_name", "unique (config_name)", "Dedupe Config with same config name already exists !") + ] + + def save_upload_config(self): + for rec in self: + res = requests.put( + f"{rec.dedupe_service_base_url.rstrip('/')}/config/{rec.config_name}", + timeout=rec.dedupe_service_api_timeout, + json={ + "index": rec.config_index_name, + "fields": [ + { + "name": rec_field.name, + "fuzziness": rec_field.fuzziness, + "boost": rec_field.weightage, + **({"query_type": "term"} if rec_field.exact else {}), + } + for rec_field in rec.config_fields + ], + "score_threshold": rec.config_score_threshold, + "active": rec.active, + }, + ) + try: + res.raise_for_status() + except Exception as e: + _logger.exception("Error uploading config") + raise ValidationError(_("Error uploading config")) from e + + @api.model + def get_configured_deduplicator(self): + dedupe_config_id = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("g2p_registry_deduplication_deduplicator.deduplicator_config_id", None) + ) + return self.browse(int(dedupe_config_id)) if dedupe_config_id else None + + @api.model + def get_duplicates_by_record_id(self, record_id, config_id=None): + if config_id: + dedupe_config = self.browse(config_id) + else: + dedupe_config = self.get_configured_deduplicator() + res = requests.get( + f"{dedupe_config.dedupe_service_base_url.rstrip('/')}/getDuplicates/{record_id}", + timeout=dedupe_config.dedupe_service_api_timeout, + ) + try: + res.raise_for_status() + except Exception as e: + raise ValidationError(_("Error retrieving duplicates")) from e + duplicates = res.json().get("duplicates") + for entry in duplicates: + duplicate_record = self.env["res.partner"].sudo().browse(entry.get("id")) + entry["name"] = duplicate_record.name + return duplicates diff --git a/g2p_registry_deduplication_deduplicator/models/registrant.py b/g2p_registry_deduplication_deduplicator/models/registrant.py new file mode 100644 index 00000000..faa0b22d --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/models/registrant.py @@ -0,0 +1,18 @@ +from odoo import _, models + + +class Registrant(models.Model): + _inherit = "res.partner" + + def view_deduplicator_duplicates(self): + self.ensure_one() + return { + "type": "ir.actions.client", + "tag": "g2p_registry_deduplication_deduplicator.view_duplicates_client_action", + "target": "new", + "name": _("Duplicates"), + "params": { + "record_id": self.id, + }, + "context": {}, + } diff --git a/g2p_registry_deduplication_deduplicator/models/res_config_settings.py b/g2p_registry_deduplication_deduplicator/models/res_config_settings.py new file mode 100644 index 00000000..ff3021b7 --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/models/res_config_settings.py @@ -0,0 +1,12 @@ +# Part of OpenG2P. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + deduplicator_config_id = fields.Many2one( + "g2p.registry.deduplication.deduplicator.config", + config_parameter="g2p_registry_deduplication_deduplicator.deduplicator_config_id", + ) diff --git a/g2p_registry_deduplication_deduplicator/pyproject.toml b/g2p_registry_deduplication_deduplicator/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/g2p_registry_deduplication_deduplicator/security/ir.model.access.csv b/g2p_registry_deduplication_deduplicator/security/ir.model.access.csv new file mode 100644 index 00000000..ee955c99 --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +g2p_registry_deduplication_deduplicator_config_admin,Deduplicator Config Admin Access,g2p_registry_deduplication_deduplicator.model_g2p_registry_deduplication_deduplicator_config,g2p_registry_base.group_g2p_admin,1,1,1,1 +g2p_registry_deduplication_deduplicator_config_registrar,Deduplicator Config Registrar Access,g2p_registry_deduplication_deduplicator.model_g2p_registry_deduplication_deduplicator_config,g2p_registry_base.group_g2p_registrar,1,1,1,0 +g2p_registry_deduplication_deduplicator_config_user,Deduplicator Config User Access,g2p_registry_deduplication_deduplicator.model_g2p_registry_deduplication_deduplicator_config,base.group_user,1,0,0,0 + +g2p_registry_deduplication_deduplicator_config_field_admin,Deduplicator Config Field Admin Access,g2p_registry_deduplication_deduplicator.model_g2p_registry_deduplication_deduplicator_config_field,g2p_registry_base.group_g2p_admin,1,1,1,1 +g2p_registry_deduplication_deduplicator_config_field_registrar,Deduplicator Config Field Registrar Access,g2p_registry_deduplication_deduplicator.model_g2p_registry_deduplication_deduplicator_config_field,g2p_registry_base.group_g2p_registrar,1,1,1,0 +g2p_registry_deduplication_deduplicator_config_field_user,Deduplicator Config Field User Access,g2p_registry_deduplication_deduplicator.model_g2p_registry_deduplication_deduplicator_config_field,base.group_user,1,0,0,0 diff --git a/g2p_registry_deduplication_deduplicator/static/description/icon.png b/g2p_registry_deduplication_deduplicator/static/description/icon.png new file mode 100644 index 00000000..5ecb429e Binary files /dev/null and b/g2p_registry_deduplication_deduplicator/static/description/icon.png differ diff --git a/g2p_registry_deduplication_deduplicator/static/src/js/view_duplicates.js b/g2p_registry_deduplication_deduplicator/static/src/js/view_duplicates.js new file mode 100644 index 00000000..c305093a --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/static/src/js/view_duplicates.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ + +import {Component, useState} from "@odoo/owl"; +import {_t} from "@web/core/l10n/translation"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; + +export class ViewDeduplicatorDuplicates extends Component { + setup() { + this.recordId = this.props.action.params.record_id; + + this.state = useState({dataLoading: "not_loaded"}); + this.displayError = ""; + this.duplicatesData = []; + + this.ormService = useService("orm"); + const self = this; + this.ormService + .call("g2p.registry.deduplication.deduplicator.config", "get_duplicates_by_record_id", [ + this.recordId, + ]) + .then((res) => { + self.duplicatesData = res; + self.state.dataLoading = "loaded"; + }) + .catch((err) => { + console.error("Cannot retrieve duplicates", err); + self.displayError = _t("Cannot retrieve duplicates") + err; + self.state.dataLoading = "error"; + }); + } +} + +ViewDeduplicatorDuplicates.template = "g2p_registry_deduplicator_view_duplicates_tpl"; + +registry + .category("actions") + .add("g2p_registry_deduplication_deduplicator.view_duplicates_client_action", ViewDeduplicatorDuplicates); diff --git a/g2p_registry_deduplication_deduplicator/static/src/xml/view_duplicates_template.xml b/g2p_registry_deduplication_deduplicator/static/src/xml/view_duplicates_template.xml new file mode 100644 index 00000000..c65e4be2 --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/static/src/xml/view_duplicates_template.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + ID + + + + Name + + + + Score + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/g2p_registry_deduplication_deduplicator/tests/__init__.py b/g2p_registry_deduplication_deduplicator/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/g2p_registry_deduplication_deduplicator/views/deduplicator_config_view.xml b/g2p_registry_deduplication_deduplicator/views/deduplicator_config_view.xml new file mode 100644 index 00000000..5bc5274c --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/views/deduplicator_config_view.xml @@ -0,0 +1,76 @@ + + + + view_g2p_registry_deduplicator_config_tree + g2p.registry.deduplication.deduplicator.config + + + + + + + + + + + + view_g2p_registry_deduplicator_config_form + g2p.registry.deduplication.deduplicator.config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Deduplicator Config + ir.actions.act_window + g2p.registry.deduplication.deduplicator.config + tree,form + {} + [] + + + Add an Deduplicator Config! + + Click the create button to configure a new Deduplicator. + + + + + + diff --git a/g2p_registry_deduplication_deduplicator/views/individual_view.xml b/g2p_registry_deduplication_deduplicator/views/individual_view.xml new file mode 100644 index 00000000..367b1e38 --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/views/individual_view.xml @@ -0,0 +1,25 @@ + + + + + view_deduplicates_individual_form + res.partner + + + + + + Get Duplicates + + + + + + diff --git a/g2p_registry_deduplication_deduplicator/views/res_config_view.xml b/g2p_registry_deduplication_deduplicator/views/res_config_view.xml new file mode 100644 index 00000000..e60c0913 --- /dev/null +++ b/g2p_registry_deduplication_deduplicator/views/res_config_view.xml @@ -0,0 +1,23 @@ + + + + res_config_settings_updated_form + res.config.settings + + + + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml index d959ff8c..81d3881a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "odoo-addon-g2p_registry_addl_info @ {root:uri}/g2p_registry_addl_info", "odoo-addon-g2p_registry_addl_info_rest_api @ {root:uri}/g2p_registry_addl_info_rest_api", "odoo-addon-g2p_registry_base @ {root:uri}/g2p_registry_base", + "odoo-addon-g2p_registry_deduplication_deduplicator @ {root:uri}/g2p_registry_deduplication_deduplicator", "odoo-addon-g2p_registry_documents @ {root:uri}/g2p_registry_documents", "odoo-addon-g2p_registry_encryption @ {root:uri}/g2p_registry_encryption", "odoo-addon-g2p_registry_group @ {root:uri}/g2p_registry_group",
+ Add an Deduplicator Config! +
+ Click the create button to configure a new Deduplicator. +