diff --git a/README.md b/README.md index 38f59b33..ceb41ab0 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ addon | version | maintainers | summary [g2p_registry_addl_info](g2p_registry_addl_info/) | 17.0.1.4.0 | | G2P Registry: Additional Info [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_openid_vci_group/models/vci_issuer.py b/g2p_openid_vci_group/models/vci_issuer.py index 02f808b4..390d70e9 100644 --- a/g2p_openid_vci_group/models/vci_issuer.py +++ b/g2p_openid_vci_group/models/vci_issuer.py @@ -42,6 +42,7 @@ def issue_vc_Registry_Group(self, auth_claims, credential_request): raise ValueError("ID not found in DB. Invalid Subject Received in auth claims") head_kind = self.env.ref("g2p_registry_membership.group_membership_kind_head") + # Searches for the first group which in the individual is a HEAD. individual_group_membership = ( self.env["g2p.group.membership"] .sudo() diff --git a/g2p_registry_datashare_websub/README.md b/g2p_registry_datashare_websub/README.md new file mode 100644 index 00000000..44252cfd --- /dev/null +++ b/g2p_registry_datashare_websub/README.md @@ -0,0 +1,3 @@ +# G2P Registry Datashare: WebSub + +Refer to https://docs.openg2p.org. diff --git a/g2p_registry_datashare_websub/__init__.py b/g2p_registry_datashare_websub/__init__.py new file mode 100644 index 00000000..2c0aa80c --- /dev/null +++ b/g2p_registry_datashare_websub/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenG2P. See LICENSE file for full copyright and licensing details. +from . import models diff --git a/g2p_registry_datashare_websub/__manifest__.py b/g2p_registry_datashare_websub/__manifest__.py new file mode 100644 index 00000000..6993b620 --- /dev/null +++ b/g2p_registry_datashare_websub/__manifest__.py @@ -0,0 +1,30 @@ +# Part of OpenG2P. See LICENSE file for full copyright and licensing details. +{ + "name": "G2P Registry Datashare: WebSub", + "category": "G2P", + "version": "17.0.1.4.0", + "sequence": 1, + "author": "OpenG2P", + "website": "https://openg2p.org", + "license": "LGPL-3", + "depends": [ + "queue_job", + "g2p_registry_base", + "g2p_registry_individual", + "g2p_registry_group", + "g2p_registry_membership", + ], + "external_dependencies": {"python": ["jq"]}, + "data": [ + "views/datashare_config_websub.xml", + "security/ir.model.access.csv", + ], + "assets": { + "web.assets_backend": [], + }, + "demo": [], + "images": [], + "application": True, + "installable": True, + "auto_install": False, +} diff --git a/g2p_registry_datashare_websub/json_encoder.py b/g2p_registry_datashare_websub/json_encoder.py new file mode 100644 index 00000000..1eea05a1 --- /dev/null +++ b/g2p_registry_datashare_websub/json_encoder.py @@ -0,0 +1,20 @@ +import base64 +import json +from datetime import date, datetime, timezone + + +class WebSubJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, bytes): + return base64.b64encode(obj).decode() + if isinstance(obj, datetime): + return ( + f'{obj.astimezone(tz=timezone.utc).replace(tzinfo=None).isoformat(timespec="milliseconds")}Z' + ) + if isinstance(obj, date): + return obj.isoformat() + return json.JSONEncoder.default(self, obj) + + @classmethod + def python_dict_to_json_dict(cls, data: dict) -> dict: + return json.loads(json.dumps(data, cls=cls)) diff --git a/g2p_registry_datashare_websub/models/__init__.py b/g2p_registry_datashare_websub/models/__init__.py new file mode 100644 index 00000000..6b257d1b --- /dev/null +++ b/g2p_registry_datashare_websub/models/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenG2P. See LICENSE file for full copyright and licensing details. +from . import registrant +from . import datashare_config_websub diff --git a/g2p_registry_datashare_websub/models/datashare_config_websub.py b/g2p_registry_datashare_websub/models/datashare_config_websub.py new file mode 100644 index 00000000..380625ae --- /dev/null +++ b/g2p_registry_datashare_websub/models/datashare_config_websub.py @@ -0,0 +1,235 @@ +import logging +import os +from datetime import datetime, timedelta +from urllib.parse import parse_qs + +import jq +import requests + +from odoo import api, fields, models, tools + +from ..json_encoder import WebSubJSONEncoder + +_logger = logging.getLogger(__name__) + +WEBSUB_BASE_URL = os.getenv("WEBSUB_BASE_URL", "http://websub/hub") +WEBSUB_AUTH_URL = os.getenv( + "WEBSUB_AUTH_URL", + "http://keycloak.keycloak/realms/master/protocol/openid-connect/token", +) +WEBSUB_AUTH_CLIENT_ID = os.getenv("WEBSUB_AUTH_CLIENT_ID", "openg2p-admin-client") +WEBSUB_AUTH_CLIENT_SECRET = os.getenv("WEBSUB_AUTH_CLIENT_SECRET", "") +WEBSUB_AUTH_GRANT_TYPE = os.getenv("WEBSUB_AUTH_GRANT_TYPE", "client_credentials") + + +class G2PDatashareConfigWebsub(models.Model): + _name = "g2p.datashare.config.websub" + _description = "G2P Datashare Config WebSub" + + name = fields.Char(required=True) + + partner_id = fields.Char(string="Partner ID") + + event_type = fields.Selection( + [ + ("GROUP_CREATED", "GROUP_CREATED"), + ("GROUP_UPDATED", "GROUP_UPDATED"), + ("GROUP_DELETED", "GROUP_DELETED"), + ("INDIVIDUAL_CREATED", "INDIVIDUAL_CREATED"), + ("INDIVIDUAL_UPDATED", "INDIVIDUAL_UPDATED"), + ("INDIVIDUAL_DELETED", "INDIVIDUAL_DELETED"), + ], + required=True, + ) + topic_joiner = fields.Char(default="/") + + transform_data_jq = fields.Text( + string="Data Transform JQ Expression", + default="""{ + ts_ms: .curr_datetime, + event: .publisher.event_type, + groupData: .record_data +}""", + ) + condition_jq = fields.Text(string="Condition JQ Expression", default="true") + + websub_base_url = fields.Char("WebSub Base URL", default=WEBSUB_BASE_URL) + websub_auth_url = fields.Char("WebSub Auth URL (Token Endpoint)", default=WEBSUB_AUTH_URL) + websub_auth_client_id = fields.Char("WebSub Auth Client ID", default=WEBSUB_AUTH_CLIENT_ID) + websub_auth_client_secret = fields.Char(default=WEBSUB_AUTH_CLIENT_SECRET) + websub_auth_grant_type = fields.Char(default=WEBSUB_AUTH_GRANT_TYPE) + websub_api_timeout = fields.Integer("WebSub API Timeout", default=10) + + websub_access_token = fields.Char() + websub_access_token_expiry = fields.Datetime() + + active = fields.Boolean(required=True, default=True) + + @api.model_create_multi + def create(self, vals): + res = super().create(vals) + for rec in res: + rec.register_websub_event() + return res + + def write(self, vals): + try: + if isinstance(vals, dict) and ("event_type" in vals or "partner_id" in vals): + for rec in self: + rec.deregister_websub_event() + except Exception: + _logger.exception("WebSub - Changed event: couldnt deregister") + res = super().write(vals) + if isinstance(vals, dict) and ("event_type" in vals or "partner_id" in vals): + for rec in self: + rec.register_websub_event() + return res + + def unlink(self): + for rec in self: + rec.deregister_websub_event() + return super().unlink() + + @api.model + def publish_event(self, event_type, data: dict): + publishers = self.get_publishers(event_type) + if not publishers: + return + for publisher in publishers: + publisher.publish_by_publisher(data) + + def publish_by_publisher(self, data: dict): + self.ensure_one() + web_base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url").rstrip("/") + curr_datetime = f'{datetime.now().isoformat(timespec = "milliseconds")}Z' + + record_id = data["id"] + record = self.env["res.partner"].browse(record_id) + record_data = self.get_full_record_data(record) + record_data = WebSubJSONEncoder.python_dict_to_json_dict( + { + "web_base_url": web_base_url, + "publisher": self.read()[0], + "curr_datetime": curr_datetime, + "data": data, + "record_data": record_data, + }, + ) + + if not jq.first(self.condition_jq, record_data): + return + + data_transformed = jq.first( + self.transform_data_jq, + record_data, + ) + self.publish_event_websub(data_transformed) + + def publish_event_websub(self, data): + self.ensure_one() + token = self.get_access_token() + res = requests.post( + self.websub_base_url, + params={ + "hub.mode": "publish", + "hub.topic": ( + f"{self.partner_id}" + f"{self.topic_joiner if self.partner_id else ''}" + f"{self.event_type}" + ), + }, + headers={"Authorization": f"Bearer {token}"}, + json=data, + timeout=self.websub_api_timeout, + ) + res.raise_for_status() + _logger.info("WebSub Publish Success. Response: %s. Headers: %s", res.text, res.headers) + res = parse_qs(res.text) + if not len(res.get("hub.mode", [])) or res["hub.mode"][0].lower() != "accepted": + raise ValueError("WebSub Publish: Invalid hub response") + + def register_websub_event(self, mode="register"): + self.ensure_one() + token = self.get_access_token() + res = requests.post( + self.websub_base_url, + headers={"Authorization": f"Bearer {token}"}, + data={ + "hub.mode": mode, + "hub.topic": ( + f"{self.partner_id}" + f"{self.topic_joiner if self.partner_id else ''}" + f"{self.event_type}" + ), + }, + timeout=self.websub_api_timeout, + ) + res.raise_for_status() + _logger.info( + "WebSub Topic Registration/Deregistration Successful. Response: %s. Headers: %s", + res.text, + res.headers, + ) + res = parse_qs(res.text) + if not len(res.get("hub.mode", [])) or res["hub.mode"][0].lower() != "accepted": + raise ValueError("WebSub Topic Register: Invalid hub response") + + def deregister_websub_event(self): + return self.register_websub_event(mode="deregister") + + def get_access_token(self): + self.ensure_one() + if ( + self.websub_access_token + and self.websub_access_token_expiry + and self.websub_access_token_expiry > datetime.now() + ): + return self.websub_access_token + data = { + "client_id": self.websub_auth_client_id, + "client_secret": self.websub_auth_client_secret, + "grant_type": self.websub_auth_grant_type, + } + response = requests.post(self.websub_auth_url, data=data, timeout=self.websub_api_timeout) + _logger.debug("WebSub Token response: %s", response.text) + response.raise_for_status() + response = response.json() + access_token = response.get("access_token", None) + token_exp = response.get("expires_in", None) + self.sudo().write( + { + "websub_access_token": access_token, + "websub_access_token_expiry": ( + (datetime.now() + timedelta(seconds=token_exp)) + if isinstance(token_exp, int) + else (datetime.fromisoformat(token_exp) if isinstance(token_exp, str) else token_exp) + ), + } + ) + return access_token + + @tools.ormcache("event_type") + def get_publishers(self, event_type): + return self.search([("event_type", "=", event_type), ("active", "=", True)]) + + def get_full_record_data(self, records): + response = [] + record_data = records.read() + for i, rec in enumerate(records): + record_data[i]["image"] = self.get_image_base64_data_in_url((rec.image_1920 or b"").decode()) + record_data[i]["reg_ids"] = {reg_id.id_type.name: reg_id.value for reg_id in rec.reg_ids} + if rec.is_group: + members = rec.group_membership_ids + members_data = members.read() + for i, member in enumerate(members): + members_data[i]["individual"] = self.get_full_record_data(member.individual) + record_data[i]["group_membership_ids"] = members_data + response.append(record_data) + return response + + @api.model + def get_image_base64_data_in_url(self, image_base64: str) -> str: + if not image_base64: + return None + image = tools.base64_to_image(image_base64) + return f"data:image/{image.format.lower()};base64,{image_base64}" diff --git a/g2p_registry_datashare_websub/models/registrant.py b/g2p_registry_datashare_websub/models/registrant.py new file mode 100644 index 00000000..37a8fc86 --- /dev/null +++ b/g2p_registry_datashare_websub/models/registrant.py @@ -0,0 +1,40 @@ +from odoo import api, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + @api.model_create_multi + def create(self, vals): + res = super().create(vals) + if not isinstance(vals, list): + vals = [ + vals, + ] + for i in range(len(res)): + if res[i].is_registrant: + new_vals = vals[i].copy() + new_vals["id"] = res[i].id + self.env["g2p.datashare.config.websub"].with_delay().publish_event( + "GROUP_CREATED" if res[i].is_group else "INDIVIDUAL_CREATED", new_vals + ) + return res + + def write(self, vals): + res = super().write(vals) + for rec in self: + if rec.is_registrant: + new_vals = vals.copy() + new_vals["id"] = rec.id + self.env["g2p.datashare.config.websub"].with_delay().publish_event( + "GROUP_UPDATED" if rec.is_group else "INDIVIDUAL_UPDATED", new_vals + ) + return res + + def unlink(self): + for rec in self: + if rec.is_registrant: + self.env["g2p.datashare.config.websub"].with_delay().publish_event( + "GROUP_DELETED" if rec.is_group else "INDIVIDUAL_DELETED", dict(id=rec.id) + ) + return super().unlink() diff --git a/g2p_registry_datashare_websub/pyproject.toml b/g2p_registry_datashare_websub/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/g2p_registry_datashare_websub/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/g2p_registry_datashare_websub/security/ir.model.access.csv b/g2p_registry_datashare_websub/security/ir.model.access.csv new file mode 100644 index 00000000..5441aefe --- /dev/null +++ b/g2p_registry_datashare_websub/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 +datashare_websub_config_admin,Datashare Config WebSub Admin,g2p_registry_datashare_websub.model_g2p_datashare_config_websub,g2p_registry_base.group_g2p_admin,1,1,1,1 +datashare_websub_config_registrar,Datashare Config WebSub Registrar,g2p_registry_datashare_websub.model_g2p_datashare_config_websub,g2p_registry_base.group_g2p_registrar,1,0,0,0 diff --git a/g2p_registry_datashare_websub/static/description/icon.png b/g2p_registry_datashare_websub/static/description/icon.png new file mode 100644 index 00000000..5ecb429e Binary files /dev/null and b/g2p_registry_datashare_websub/static/description/icon.png differ diff --git a/g2p_registry_datashare_websub/tests/__init__.py b/g2p_registry_datashare_websub/tests/__init__.py new file mode 100644 index 00000000..d3bdf22e --- /dev/null +++ b/g2p_registry_datashare_websub/tests/__init__.py @@ -0,0 +1 @@ +from . import test_datashare_websub diff --git a/g2p_registry_datashare_websub/tests/test_datashare_websub.py b/g2p_registry_datashare_websub/tests/test_datashare_websub.py new file mode 100644 index 00000000..d8cb05f9 --- /dev/null +++ b/g2p_registry_datashare_websub/tests/test_datashare_websub.py @@ -0,0 +1,6 @@ +from odoo.addons.component.tests.common import TransactionComponentCase + + +class TestG2PRegistryDatashareWebsub(TransactionComponentCase): + def setUp(self): + super().setUp() diff --git a/g2p_registry_datashare_websub/views/datashare_config_websub.xml b/g2p_registry_datashare_websub/views/datashare_config_websub.xml new file mode 100644 index 00000000..c85f51b6 --- /dev/null +++ b/g2p_registry_datashare_websub/views/datashare_config_websub.xml @@ -0,0 +1,54 @@ + + + + view_datashare_config_websub_tree + g2p.datashare.config.websub + + + + + + + + + + + view_datashare_config_websub_form + g2p.datashare.config.websub + + + + + + + + + + + + + + + + + + + + + + + + WebSub Datashare Configs + g2p.datashare.config.websub + tree,form + Manage datashare configs. + + + + 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 233c49c9..81d3881a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,12 +26,14 @@ 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", "odoo-addon-g2p_registry_individual @ {root:uri}/g2p_registry_individual", "odoo-addon-g2p_registry_membership @ {root:uri}/g2p_registry_membership", "odoo-addon-g2p_registry_rest_api @ {root:uri}/g2p_registry_rest_api", + "odoo-addon-g2p_registry_datashare_websub @ {root:uri}/g2p_registry_datashare_websub", "odoo-addon-g2p_superset_dashboard @ {root:uri}/g2p_superset_dashboard", "odoo-addon-mts_connector @ {root:uri}/mts_connector", ]
+ Add an Deduplicator Config! +
+ Click the create button to configure a new Deduplicator. +