From b4f604d2d975f730a2e29adffa07a94c95847ca5 Mon Sep 17 00:00:00 2001 From: bosd <5e2fd43-d292-4c90-9d1f-74ff3436329a@anonaddy.me> Date: Sat, 6 Jun 2026 14:04:19 +0200 Subject: [PATCH 1/6] [ADD] partner_identification_kyc Add a module providing Know Your Customer (KYC) identification for partners, built on top of partner_identification: - a dedicated "KYC" identification category with its check activity type - a "Request KYC" button on the partner form and a helper to ensure a KYC record exists (e.g. for API-created partners) - computed KYC validity / button-visibility, optionally enabled on child contacts - a KYC filter on the id_numbers views and demo data --- partner_identification_kyc/README.rst | 166 ++++ partner_identification_kyc/__init__.py | 1 + partner_identification_kyc/__manifest__.py | 25 + .../data/activity_type_data.xml | 18 + .../data/identification_category_data.xml | 16 + .../demo/identification_number_demo.xml | 103 ++ partner_identification_kyc/i18n/de.po | 20 + partner_identification_kyc/i18n/es.po | 20 + partner_identification_kyc/i18n/fr.po | 20 + partner_identification_kyc/i18n/it.po | 20 + partner_identification_kyc/i18n/nl.po | 20 + partner_identification_kyc/models/__init__.py | 2 + .../models/res_partner.py | 165 ++++ .../models/res_partner_id_category.py | 11 + partner_identification_kyc/pyproject.toml | 3 + .../readme/CONFIGURE.md | 17 + .../readme/CONTRIBUTORS.md | 2 + .../readme/DESCRIPTION.md | 1 + partner_identification_kyc/readme/HISTORY.md | 9 + partner_identification_kyc/readme/INSTALL.md | 1 + partner_identification_kyc/readme/USAGE.md | 19 + .../static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/index.html | 528 ++++++++++ partner_identification_kyc/tests/__init__.py | 1 + .../tests/test_partner_identification_kyc.py | 923 ++++++++++++++++++ .../views/identification_number_views.xml | 22 + .../views/res_partner_id_category_view.xml | 16 + .../views/res_partner_views.xml | 39 + 28 files changed, 2188 insertions(+) create mode 100644 partner_identification_kyc/README.rst create mode 100644 partner_identification_kyc/__init__.py create mode 100644 partner_identification_kyc/__manifest__.py create mode 100644 partner_identification_kyc/data/activity_type_data.xml create mode 100644 partner_identification_kyc/data/identification_category_data.xml create mode 100644 partner_identification_kyc/demo/identification_number_demo.xml create mode 100644 partner_identification_kyc/i18n/de.po create mode 100644 partner_identification_kyc/i18n/es.po create mode 100644 partner_identification_kyc/i18n/fr.po create mode 100644 partner_identification_kyc/i18n/it.po create mode 100644 partner_identification_kyc/i18n/nl.po create mode 100644 partner_identification_kyc/models/__init__.py create mode 100644 partner_identification_kyc/models/res_partner.py create mode 100644 partner_identification_kyc/models/res_partner_id_category.py create mode 100644 partner_identification_kyc/pyproject.toml create mode 100644 partner_identification_kyc/readme/CONFIGURE.md create mode 100644 partner_identification_kyc/readme/CONTRIBUTORS.md create mode 100644 partner_identification_kyc/readme/DESCRIPTION.md create mode 100644 partner_identification_kyc/readme/HISTORY.md create mode 100644 partner_identification_kyc/readme/INSTALL.md create mode 100644 partner_identification_kyc/readme/USAGE.md create mode 100644 partner_identification_kyc/static/description/icon.png create mode 100644 partner_identification_kyc/static/description/index.html create mode 100644 partner_identification_kyc/tests/__init__.py create mode 100644 partner_identification_kyc/tests/test_partner_identification_kyc.py create mode 100644 partner_identification_kyc/views/identification_number_views.xml create mode 100644 partner_identification_kyc/views/res_partner_id_category_view.xml create mode 100644 partner_identification_kyc/views/res_partner_views.xml diff --git a/partner_identification_kyc/README.rst b/partner_identification_kyc/README.rst new file mode 100644 index 00000000000..27da60b3450 --- /dev/null +++ b/partner_identification_kyc/README.rst @@ -0,0 +1,166 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +========================== +Partner Identification KYC +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:10c73c95886256911541d6f9b7ed42f8f1cbddd83a56b784b533ee8334d66525 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fpartner--contact-lightgray.png?logo=github + :target: https://github.com/OCA/partner-contact/tree/19.0/partner_identification_kyc + :alt: OCA/partner-contact +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/partner-contact-19-0/partner-contact-19-0-partner_identification_kyc + :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/partner-contact&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds Know Your Customer (KYC) identification functionality +to the partner identification automation system. It provides a +specialized KYC identification category with associated activities and +workflows to manage customer verification processes. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +This module depends on ``partner_identification_automation_activity`` +which must be installed first. The module will automatically create the +KYC identification category and activity type during installation. + +Configuration +============= + +Identification Category +----------------------- + +The module automatically creates a "KYC" identification category with +the following default settings: + +- Initial Activity Type: "Perform KYC Check" +- Renew Activity Type: "Perform KYC Check" +- Create Activity on New: True +- Default Validity: 1 year +- Renewal Lead Number: 2 +- Renewal Lead Unit: Months + +Activity Types +-------------- + +The module creates the "Perform KYC Check" activity type which is +assigned to both the initial and renewal activities for KYC +identification records. + +Filters +------- + +A filter is added to the id_numbers views to allow filtering on the KYC +Category. + +Usage +===== + +Request KYC Button +------------------ + +On the partner form, a "Request KYC" button will appear when: + +- There is no KYC identification record in the 'new', 'running', or + 'to_renew' states +- OR there is no ID number record of the KYC category at all + +The button will be hidden if: + +- There is a 'running' or 'to_renew' KYC record +- OR there is already a 'new' status KYC record (to prevent redundant + records) + +Clicking the button will create a new KYC identification record in the +'new' status, triggering the associated activity for KYC officers to +process. + +API Function +------------ + +For partners created via API, there is a function to trigger the KYC +process automatically. If no KYC identification record exists for a +partner, calling this function will create a record in the 'new' status. + +Activity Management +------------------- + +KYC identification records are associated with the "Perform KYC Check" +activity type, which appears in the activity views for tracking and +processing by responsible officers. + +Changelog +========= + +[1.0.0] - 2025-01-01 +-------------------- + +Added +~~~~~ + +- Initial implementation of KYC identification category +- "Request KYC" button on partner form +- API function to trigger KYC flow for API-created partners +- Dedicated "Perform KYC Check" activity type +- Filters for KYC category in id_numbers views + +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 +======= + +Contributors +------------ + +- OCA Community +- Emiel van Bokhoven + +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. + +This module is part of the `OCA/partner-contact `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/partner_identification_kyc/__init__.py b/partner_identification_kyc/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/partner_identification_kyc/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/partner_identification_kyc/__manifest__.py b/partner_identification_kyc/__manifest__.py new file mode 100644 index 00000000000..3f24f7aeb6e --- /dev/null +++ b/partner_identification_kyc/__manifest__.py @@ -0,0 +1,25 @@ +{ + "name": "Partner Identification KYC", + "summary": "Know Your Customer identification for partners", + "version": "19.0.1.0.0", + "category": "Customer Relationship Management", + "author": "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/partner-contact", + "license": "AGPL-3", + "depends": [ + "partner_identification_automation_activity", + "partner_identification", + ], + "data": [ + "data/activity_type_data.xml", + "data/identification_category_data.xml", + "views/res_partner_views.xml", + "views/identification_number_views.xml", + "views/res_partner_id_category_view.xml", + ], + "demo": [ + "demo/identification_number_demo.xml", + ], + "installable": True, + "auto_install": False, +} diff --git a/partner_identification_kyc/data/activity_type_data.xml b/partner_identification_kyc/data/activity_type_data.xml new file mode 100644 index 00000000000..c544ad8fb84 --- /dev/null +++ b/partner_identification_kyc/data/activity_type_data.xml @@ -0,0 +1,18 @@ + + + + + KYC Identification Sequence + kyc.identification + 3 + + + + + Perform KYC Check + 2 + days + Perform KYC verification + fa-user-secret + + diff --git a/partner_identification_kyc/data/identification_category_data.xml b/partner_identification_kyc/data/identification_category_data.xml new file mode 100644 index 00000000000..2dbf1f9815a --- /dev/null +++ b/partner_identification_kyc/data/identification_category_data.xml @@ -0,0 +1,16 @@ + + + + + KYC + KYC + + + + 1 + years + 2 + months + + + diff --git a/partner_identification_kyc/demo/identification_number_demo.xml b/partner_identification_kyc/demo/identification_number_demo.xml new file mode 100644 index 00000000000..8ded091697d --- /dev/null +++ b/partner_identification_kyc/demo/identification_number_demo.xml @@ -0,0 +1,103 @@ + + + + + Demo Partner for KYC Testing + demo.kyc@example.com + +1 234 567 8900 + + + + KYC Verification Authority + + info@kyc-authority.example.com + + + + + John KYC Customer + john.customer@example.com + + + + + Jane Running KYC + jane.running@example.com + + + + Bob To-Renew KYC + bob.torenew@example.com + + + + Alice Expired KYC + alice.expired@example.com + + + + + + + + KYC-001-DRAFT + draft + + + + + + + + + + KYC-002-RUNNING + open + + + + + + + + + + KYC-003-TORENEW + pending + + + + + + + + + + KYC-004-EXPIRED + close + + + + + diff --git a/partner_identification_kyc/i18n/de.po b/partner_identification_kyc/i18n/de.po new file mode 100644 index 00000000000..f92fceffa22 --- /dev/null +++ b/partner_identification_kyc/i18n/de.po @@ -0,0 +1,20 @@ +# German translation for partner_identification_kyc +# Copyright (C) 2025 +# This file is distributed under the same license as the partner_identification_kyc module. +#, fuzzy +msgid "" +msgstr "" +"Content-Type: text/plain; charset=utf-8\n" +"Language: de\n" + +#. module: partner_identification_kyc +msgid "Request KYC" +msgstr "KYC anfordern" + +#. module: partner_identification_kyc +msgid "Perform KYC Check" +msgstr "KYC-Prüfung durchführen" + +#. module: partner_identification_kyc +msgid "KYC" +msgstr "KYC" \ No newline at end of file diff --git a/partner_identification_kyc/i18n/es.po b/partner_identification_kyc/i18n/es.po new file mode 100644 index 00000000000..f799f28a612 --- /dev/null +++ b/partner_identification_kyc/i18n/es.po @@ -0,0 +1,20 @@ +# Spanish translation for partner_identification_kyc +# Copyright (C) 2025 +# This file is distributed under the same license as the partner_identification_kyc module. +#, fuzzy +msgid "" +msgstr "" +"Content-Type: text/plain; charset=utf-8\n" +"Language: es\n" + +#. module: partner_identification_kyc +msgid "Request KYC" +msgstr "Solicitar KYC" + +#. module: partner_identification_kyc +msgid "Perform KYC Check" +msgstr "Realizar verificación KYC" + +#. module: partner_identification_kyc +msgid "KYC" +msgstr "KYC" \ No newline at end of file diff --git a/partner_identification_kyc/i18n/fr.po b/partner_identification_kyc/i18n/fr.po new file mode 100644 index 00000000000..0fb1e1f3e06 --- /dev/null +++ b/partner_identification_kyc/i18n/fr.po @@ -0,0 +1,20 @@ +# French translation for partner_identification_kyc +# Copyright (C) 2025 +# This file is distributed under the same license as the partner_identification_kyc module. +#, fuzzy +msgid "" +msgstr "" +"Content-Type: text/plain; charset=utf-8\n" +"Language: fr\n" + +#. module: partner_identification_kyc +msgid "Request KYC" +msgstr "Demander KYC" + +#. module: partner_identification_kyc +msgid "Perform KYC Check" +msgstr "Effectuer une vérification KYC" + +#. module: partner_identification_kyc +msgid "KYC" +msgstr "KYC" \ No newline at end of file diff --git a/partner_identification_kyc/i18n/it.po b/partner_identification_kyc/i18n/it.po new file mode 100644 index 00000000000..43598faec3e --- /dev/null +++ b/partner_identification_kyc/i18n/it.po @@ -0,0 +1,20 @@ +# Italian translation for partner_identification_kyc +# Copyright (C) 2025 +# This file is distributed under the same license as the partner_identification_kyc module. +#, fuzzy +msgid "" +msgstr "" +"Content-Type: text/plain; charset=utf-8\n" +"Language: it\n" + +#. module: partner_identification_kyc +msgid "Request KYC" +msgstr "Richiedi KYC" + +#. module: partner_identification_kyc +msgid "Perform KYC Check" +msgstr "Effettua controllo KYC" + +#. module: partner_identification_kyc +msgid "KYC" +msgstr "KYC" \ No newline at end of file diff --git a/partner_identification_kyc/i18n/nl.po b/partner_identification_kyc/i18n/nl.po new file mode 100644 index 00000000000..8f7dcd1b3ee --- /dev/null +++ b/partner_identification_kyc/i18n/nl.po @@ -0,0 +1,20 @@ +# Dutch translation for partner_identification_kyc +# Copyright (C) 2025 +# This file is distributed under the same license as the partner_identification_kyc module. +#, fuzzy +msgid "" +msgstr "" +"Content-Type: text/plain; charset=utf-8\n" +"Language: nl\n" + +#. module: partner_identification_kyc +msgid "Request KYC" +msgstr "Verzoek KYC" + +#. module: partner_identification_kyc +msgid "Perform KYC Check" +msgstr "Voer KYC-controle uit" + +#. module: partner_identification_kyc +msgid "KYC" +msgstr "KYC" \ No newline at end of file diff --git a/partner_identification_kyc/models/__init__.py b/partner_identification_kyc/models/__init__.py new file mode 100644 index 00000000000..21153e6be40 --- /dev/null +++ b/partner_identification_kyc/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_partner +from . import res_partner_id_category diff --git a/partner_identification_kyc/models/res_partner.py b/partner_identification_kyc/models/res_partner.py new file mode 100644 index 00000000000..89616d97481 --- /dev/null +++ b/partner_identification_kyc/models/res_partner.py @@ -0,0 +1,165 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class ResPartner(models.Model): + _inherit = "res.partner" + + # Computed field to determine if the KYC button should be visible + show_kyc_button = fields.Boolean( + string="Show KYC Button", + compute="_compute_show_kyc_button", + store=False, + depends=["id_numbers", "id_numbers.category_id", "id_numbers.status"], + ) + + def _compute_show_kyc_button(self): + """Compute whether to show the KYC request button.""" + kyc_category = self.env.ref( + "partner_identification_kyc.kyc_identification_category", + raise_if_not_found=False, + ) + if not kyc_category: + for partner in self: + partner.show_kyc_button = False + return + + # Find all partners in self that have an ongoing KYC record + ongoing_kyc_records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "in", self.ids), + ("category_id", "=", kyc_category.id), + ("status", "in", ["draft", "open", "pending"]), + ] + ) + + ongoing_kyc_partner_ids = { + record.partner_id.id for record in ongoing_kyc_records + } + + for partner in self: + show_button = True + if not kyc_category.enable_on_child_contacts and not partner.is_company: + show_button = False + + if partner.id in ongoing_kyc_partner_ids: + show_button = False + + partner.show_kyc_button = show_button + + kyc_valid_until = fields.Date( + string="KYC Valid Until", + compute="_compute_kyc_valid_until", + store=True, + ) + + @api.depends( + "id_numbers.valid_until", "id_numbers.category_id", "id_numbers.status" + ) + def _compute_kyc_valid_until(self): + kyc_category = self.env.ref( + "partner_identification_kyc.kyc_identification_category", + raise_if_not_found=False, + ) + for partner in self: + valid_until_date = False + if kyc_category: + kyc_records = partner.id_numbers.filtered( + lambda r: r.category_id == kyc_category and r.status == "open" + ) + # Collect valid_until dates from records that have them + valid_dates = [ + rec.valid_until for rec in kyc_records if rec.valid_until + ] + if valid_dates: + valid_until_date = min(valid_dates) + partner.kyc_valid_until = valid_until_date + + def action_view_kyc_records(self): + self.ensure_one() + kyc_category = self.env.ref( + "partner_identification_kyc.kyc_identification_category", + raise_if_not_found=False, + ) + return { + "name": "KYC Records", + "type": "ir.actions.act_window", + "res_model": "res.partner.id_number", + "view_mode": "list,form", + "domain": [ + ("partner_id", "=", self.id), + ("category_id", "=", kyc_category.id if kyc_category else False), + ], + } + + def _create_kyc_record(self): + """Private helper method to create a new KYC identification record.""" + self.ensure_one() + kyc_category = self.env.ref( + "partner_identification_kyc.kyc_identification_category" + ) + identification_model = self.env["res.partner.id_number"] + sequence_code = ( + self.env["ir.sequence"].next_by_code("kyc.identification") or "001" + ) + return identification_model.create( + { + "partner_id": self.id, + "category_id": kyc_category.id, + "name": f"KYC-{self.id}-{sequence_code}", + "status": "draft", + } + ) + + def action_request_kyc(self): + """Create a new KYC identification record in the 'draft' status.""" + self.ensure_one() # Ensure single record operation + kyc_category = self.env.ref( + "partner_identification_kyc.kyc_identification_category" + ) + + # Check if there's already any active ('open', 'pending') or 'draft' status + # KYC record to prevent duplicates + existing_kyc = self.id_numbers.filtered( + lambda r: r.category_id == kyc_category + and r.status in ["draft", "open", "pending"] + ) + + if existing_kyc: + raise UserError( + self.env._("A KYC request has already been submitted for this partner.") + ) + + # Create a new identification number record for the KYC category using helper + # method + self._create_kyc_record() + + # Return action to trigger a refresh of the view + return { + "type": "ir.actions.client", + "tag": "reload", + } + + def ensure_kyc_record(self): + """ + API function to ensure a KYC record exists for the partner if none currently + exists. + If no active KYC identification record exists for a partner (not in 'draft', + 'open', or 'pending' status), this function creates a record in the 'draft' + status. + """ + kyc_category = self.env.ref( + "partner_identification_kyc.kyc_identification_category" + ) + + for partner in self: + # Check if there's already an active or pending KYC record for this partner + existing_kyc = partner.id_numbers.filtered( + lambda r: r.category_id == kyc_category + and r.status in ["draft", "open", "pending"] + ) + + if not existing_kyc: + # Create a new identification number record for the KYC category using + # helper method + partner.sudo()._create_kyc_record() diff --git a/partner_identification_kyc/models/res_partner_id_category.py b/partner_identification_kyc/models/res_partner_id_category.py new file mode 100644 index 00000000000..7132f2a8896 --- /dev/null +++ b/partner_identification_kyc/models/res_partner_id_category.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResPartnerIdCategory(models.Model): + _inherit = "res.partner.id_category" + + enable_on_child_contacts = fields.Boolean( + string="Enable on Child Contacts", + default=False, + help="If checked, KYC checks can be performed on child contacts of a company.", + ) diff --git a/partner_identification_kyc/pyproject.toml b/partner_identification_kyc/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/partner_identification_kyc/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/partner_identification_kyc/readme/CONFIGURE.md b/partner_identification_kyc/readme/CONFIGURE.md new file mode 100644 index 00000000000..a291bd696fc --- /dev/null +++ b/partner_identification_kyc/readme/CONFIGURE.md @@ -0,0 +1,17 @@ +## Identification Category + +The module automatically creates a "KYC" identification category with the following default settings: +- Initial Activity Type: "Perform KYC Check" +- Renew Activity Type: "Perform KYC Check" +- Create Activity on New: True +- Default Validity: 1 year +- Renewal Lead Number: 2 +- Renewal Lead Unit: Months + +## Activity Types + +The module creates the "Perform KYC Check" activity type which is assigned to both the initial and renewal activities for KYC identification records. + +## Filters + +A filter is added to the id_numbers views to allow filtering on the KYC Category. diff --git a/partner_identification_kyc/readme/CONTRIBUTORS.md b/partner_identification_kyc/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..738bb13829a --- /dev/null +++ b/partner_identification_kyc/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- OCA Community +- Emiel van Bokhoven diff --git a/partner_identification_kyc/readme/DESCRIPTION.md b/partner_identification_kyc/readme/DESCRIPTION.md new file mode 100644 index 00000000000..5874d1809a0 --- /dev/null +++ b/partner_identification_kyc/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module adds Know Your Customer (KYC) identification functionality to the partner identification automation system. It provides a specialized KYC identification category with associated activities and workflows to manage customer verification processes. \ No newline at end of file diff --git a/partner_identification_kyc/readme/HISTORY.md b/partner_identification_kyc/readme/HISTORY.md new file mode 100644 index 00000000000..23b5beefb0b --- /dev/null +++ b/partner_identification_kyc/readme/HISTORY.md @@ -0,0 +1,9 @@ +## [1.0.0] - 2025-01-01 + +### Added + +- Initial implementation of KYC identification category +- "Request KYC" button on partner form +- API function to trigger KYC flow for API-created partners +- Dedicated "Perform KYC Check" activity type +- Filters for KYC category in id_numbers views \ No newline at end of file diff --git a/partner_identification_kyc/readme/INSTALL.md b/partner_identification_kyc/readme/INSTALL.md new file mode 100644 index 00000000000..f19896a1b82 --- /dev/null +++ b/partner_identification_kyc/readme/INSTALL.md @@ -0,0 +1 @@ +This module depends on `partner_identification_automation_activity` which must be installed first. The module will automatically create the KYC identification category and activity type during installation. diff --git a/partner_identification_kyc/readme/USAGE.md b/partner_identification_kyc/readme/USAGE.md new file mode 100644 index 00000000000..79b2dbf5c0c --- /dev/null +++ b/partner_identification_kyc/readme/USAGE.md @@ -0,0 +1,19 @@ +## Request KYC Button + +On the partner form, a "Request KYC" button will appear when: +- There is no KYC identification record in the 'new', 'running', or 'to_renew' states +- OR there is no ID number record of the KYC category at all + +The button will be hidden if: +- There is a 'running' or 'to_renew' KYC record +- OR there is already a 'new' status KYC record (to prevent redundant records) + +Clicking the button will create a new KYC identification record in the 'new' status, triggering the associated activity for KYC officers to process. + +## API Function + +For partners created via API, there is a function to trigger the KYC process automatically. If no KYC identification record exists for a partner, calling this function will create a record in the 'new' status. + +## Activity Management + +KYC identification records are associated with the "Perform KYC Check" activity type, which appears in the activity views for tracking and processing by responsible officers. diff --git a/partner_identification_kyc/static/description/icon.png b/partner_identification_kyc/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Partner Identification KYC

+ +

Beta License: AGPL-3 OCA/partner-contact Translate me on Weblate Try me on Runboat

+

This module adds Know Your Customer (KYC) identification functionality +to the partner identification automation system. It provides a +specialized KYC identification category with associated activities and +workflows to manage customer verification processes.

+

Table of contents

+ +
+

Installation

+

This module depends on partner_identification_automation_activity +which must be installed first. The module will automatically create the +KYC identification category and activity type during installation.

+
+
+

Configuration

+
+

Identification Category

+

The module automatically creates a “KYC” identification category with +the following default settings:

+
    +
  • Initial Activity Type: “Perform KYC Check”
  • +
  • Renew Activity Type: “Perform KYC Check”
  • +
  • Create Activity on New: True
  • +
  • Default Validity: 1 year
  • +
  • Renewal Lead Number: 2
  • +
  • Renewal Lead Unit: Months
  • +
+
+
+

Activity Types

+

The module creates the “Perform KYC Check” activity type which is +assigned to both the initial and renewal activities for KYC +identification records.

+
+
+

Filters

+

A filter is added to the id_numbers views to allow filtering on the KYC +Category.

+
+
+
+

Usage

+
+

Request KYC Button

+

On the partner form, a “Request KYC” button will appear when:

+
    +
  • There is no KYC identification record in the ‘new’, ‘running’, or +‘to_renew’ states
  • +
  • OR there is no ID number record of the KYC category at all
  • +
+

The button will be hidden if:

+
    +
  • There is a ‘running’ or ‘to_renew’ KYC record
  • +
  • OR there is already a ‘new’ status KYC record (to prevent redundant +records)
  • +
+

Clicking the button will create a new KYC identification record in the +‘new’ status, triggering the associated activity for KYC officers to +process.

+
+
+

API Function

+

For partners created via API, there is a function to trigger the KYC +process automatically. If no KYC identification record exists for a +partner, calling this function will create a record in the ‘new’ status.

+
+
+

Activity Management

+

KYC identification records are associated with the “Perform KYC Check” +activity type, which appears in the activity views for tracking and +processing by responsible officers.

+
+
+
+

Changelog

+
+

[1.0.0] - 2025-01-01

+
+

Added

+
    +
  • Initial implementation of KYC identification category
  • +
  • “Request KYC” button on partner form
  • +
  • API function to trigger KYC flow for API-created partners
  • +
  • Dedicated “Perform KYC Check” activity type
  • +
  • Filters for KYC category in id_numbers views
  • +
+
+
+
+
+

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

+
+

Contributors

+
    +
  • OCA Community
  • +
  • Emiel van Bokhoven
  • +
+
+
+

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.

+

This module is part of the OCA/partner-contact project on GitHub.

+

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

+
+
+
+
+ + diff --git a/partner_identification_kyc/tests/__init__.py b/partner_identification_kyc/tests/__init__.py new file mode 100644 index 00000000000..ec8c19a195d --- /dev/null +++ b/partner_identification_kyc/tests/__init__.py @@ -0,0 +1 @@ +from . import test_partner_identification_kyc diff --git a/partner_identification_kyc/tests/test_partner_identification_kyc.py b/partner_identification_kyc/tests/test_partner_identification_kyc.py new file mode 100644 index 00000000000..4785e626bbb --- /dev/null +++ b/partner_identification_kyc/tests/test_partner_identification_kyc.py @@ -0,0 +1,923 @@ +import odoo +from odoo import fields +from odoo.tests import common + + +@odoo.tests.tagged("post_install", "-at_install") +class TestPartnerIdentificationKYC(common.TransactionCase): + def setUp(self): + super().setUp() + + # Get the KYC category + self.kyc_category = self.env.ref( + "partner_identification_kyc.kyc_identification_category" + ) + + # Create a test partner + self.test_partner = self.env["res.partner"].create( + { + "name": "Test Partner for KYC", + "email": "test.kyc@example.com", + } + ) + + # Create a test issuer + self.test_issuer = self.env["res.partner"].create( + { + "name": "Test KYC Issuer", + "is_company": True, + } + ) + + def test_kyc_category_creation(self): + """Test that the KYC category was created with correct settings.""" + self.assertEqual(self.kyc_category.name, "KYC") + self.assertEqual(self.kyc_category.code, "KYC") + self.assertTrue(self.kyc_category.create_activity_on_new) + self.assertEqual(self.kyc_category.default_validity_number, 1) + self.assertEqual(self.kyc_category.default_validity_unit, "years") + self.assertEqual(self.kyc_category.renewal_lead_number, 2) + self.assertEqual(self.kyc_category.renewal_lead_unit, "months") + + # Check that activity types are set correctly + activity_type = self.env.ref( + "partner_identification_kyc.activity_type_kyc_check" + ) + self.assertEqual(self.kyc_category.initial_activity_type_id, activity_type) + self.assertEqual(self.kyc_category.renew_activity_type_id, activity_type) + + def test_action_request_kyc_creates_record(self): + """Test that the action_request_kyc creates a new KYC identification record.""" + initial_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + # Call the action + self.test_partner.action_request_kyc() + + # Check that a new record was created + final_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + self.assertEqual(final_count, initial_count + 1) + + # Get the newly created record + new_record = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ], + order="create_date desc", + limit=1, + ) + + self.assertEqual(new_record.status, "draft") + self.assertTrue(new_record.name.startswith("KYC-")) + + def test_action_request_kyc_duplicate_prevention(self): + """Test that action_request_kyc prevents duplicates when a 'draft' record + already exists.""" + # Create the first KYC record + self.test_partner.action_request_kyc() + + # Verify the first record exists + records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ("status", "=", "draft"), + ] + ) + self.assertEqual(len(records), 1) + + # Try to create another record - should raise an error + with self.assertRaises(odoo.exceptions.UserError): + self.test_partner.action_request_kyc() + + def test_ensure_kyc_record_creates_when_none_exists(self): + """Test that ensure_kyc_record creates a record when none exists.""" + initial_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + # Call the API function + self.test_partner.ensure_kyc_record() + + # Check that a new record was created + final_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + self.assertEqual(final_count, initial_count + 1) + + # Verify the status is 'draft' + new_record = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ], + order="create_date desc", + limit=1, + ) + + self.assertEqual(new_record.status, "draft") + + def test_ensure_kyc_record_no_duplicate_when_active_exists(self): + """Test that ensure_kyc_record does not create a record when an active one + already exists.""" + # Create the first record with 'draft' status (active) + self.test_partner.ensure_kyc_record() + + # Get the initial record + initial_records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + self.assertEqual(len(initial_records), 1) + + # Call the API function again + self.test_partner.ensure_kyc_record() + + # Verify that no duplicate was created (still has only 1 active record) + final_records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + self.assertEqual(len(final_records), 1) + + def test_ensure_kyc_record_creates_when_expired_exists(self): + """Test that ensure_kyc_record creates a record when only expired records + exist.""" + # Create an expired record + identification_model = self.env["res.partner.id_number"] + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-EXPIRED-TEST", + "status": "close", + } + ) + + initial_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + self.assertEqual(initial_count, 1) + + # Call the API function - should create a new record since existing one is + # expired + self.test_partner.ensure_kyc_record() + + # Verify that a new record was created (now has 2 records total) + final_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + self.assertEqual(final_count, 2) + + def test_ensure_kyc_record_multiple_scenarios(self): + """Test ensure_kyc_record with different status scenarios.""" + test_partner_multi = self.env["res.partner"].create( + { + "name": "Test Partner Multi", + "email": "test.multi@example.com", + } + ) + + identification_model = self.env["res.partner.id_number"] + + # Scenario 1: Add a 'close' record, then ensure_kyc_record should create new one + identification_model.create( + { + "partner_id": test_partner_multi.id, + "category_id": self.kyc_category.id, + "name": "KYC-CLOSE-TEST", + "status": "close", + } + ) + + initial_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", test_partner_multi.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + self.assertEqual(initial_count, 1) + + test_partner_multi.ensure_kyc_record() # Should create new record + count_after_ensure = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", test_partner_multi.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + self.assertEqual(count_after_ensure, 2) + + # Scenario 2: Add an 'open' record, then ensure_kyc_record should NOT create + # new one + test_partner_multi2 = self.env["res.partner"].create( + { + "name": "Test Partner Multi 2", + "email": "test.multi2@example.com", + } + ) + + identification_model.create( + { + "partner_id": test_partner_multi2.id, + "category_id": self.kyc_category.id, + "name": "KYC-OPEN-TEST", + "status": "open", + } + ) + + initial_count2 = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", test_partner_multi2.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + self.assertEqual(initial_count2, 1) + + test_partner_multi2.ensure_kyc_record() # Should NOT create new record + count_after_ensure2 = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", test_partner_multi2.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + self.assertEqual(count_after_ensure2, 1) + + def test_button_visibility_conditions(self): + """Test the visibility conditions for the Request KYC button.""" + # Initially, no KYC records exist, so button should be visible + # (This is tested indirectly by testing the functions that implement the logic) + + # Create a 'draft' status record + self.test_partner.action_request_kyc() + + # Now the button should be hidden (tested by trying to call the function + # and expect error) + with self.assertRaises(odoo.exceptions.UserError): + self.test_partner.action_request_kyc() + + # Create a running status record with a different partner for testing + partner2 = self.env["res.partner"].create( + { + "name": "Test Partner 2 for KYC", + "email": "test2.kyc@example.com", + } + ) + + identification_model = self.env["res.partner.id_number"] + identification_model.create( + { + "partner_id": partner2.id, + "category_id": self.kyc_category.id, + "name": "KYC-RUNNING-TEST", + "status": "open", + } + ) + + # With a running status, the button should be hidden + with self.assertRaises(odoo.exceptions.UserError): + partner2.action_request_kyc() + + def test_button_visibility_computed_field(self): + """Test the computed field show_kyc_button for different scenarios.""" + # Initially, no KYC records - button should be visible (show_kyc_button = True) + self.assertTrue(self.test_partner.show_kyc_button) + + # Create a draft status record - button should be hidden + self.test_partner.action_request_kyc() + # Reload the partner to get the updated computed field + self.test_partner.invalidate_recordset() + partner_reloaded = self.test_partner.browse(self.test_partner.id) + self.assertFalse(partner_reloaded.show_kyc_button) + + # Create a new partner with pending status - button should be hidden + partner_pending = self.env["res.partner"].create( + { + "name": "Test Partner Pending", + "email": "test.pending@example.com", + } + ) + identification_model = self.env["res.partner.id_number"] + identification_model.create( + { + "partner_id": partner_pending.id, + "category_id": self.kyc_category.id, + "name": "KYC-PENDING-TEST", + "status": "pending", + } + ) + partner_pending.invalidate_recordset() + partner_pending_reloaded = partner_pending.browse(partner_pending.id) + self.assertFalse(partner_pending_reloaded.show_kyc_button) + + # Create a new partner with close status - button should be visible + partner_close = self.env["res.partner"].create( + { + "name": "Test Partner Close", + "email": "test.close@example.com", + } + ) + identification_model.create( + { + "partner_id": partner_close.id, + "category_id": self.kyc_category.id, + "name": "KYC-CLOSE-TEST", + "status": "close", + } + ) + partner_close.invalidate_recordset() + partner_close_reloaded = partner_close.browse(partner_close.id) + self.assertTrue(partner_close_reloaded.show_kyc_button) + + def test_action_request_kyc_ensure_single_record(self): + """Test that action_request_kyc works correctly with single record.""" + # Test that the method properly calls ensure_one() + partners = self.test_partner | self.env["res.partner"].create( + { + "name": "Another Test Partner", + "email": "another.test@example.com", + } + ) + + # Calling the action on multiple records should raise an error + with self.assertRaises(ValueError): + partners.action_request_kyc() + + def test_ensure_kyc_record_idempotency(self): + """Test that ensure_kyc_record is idempotent when record exists.""" + # Call ensure_kyc_record multiple times - should not create duplicates + initial_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + # First call - creates the record + self.test_partner.ensure_kyc_record() + + # Second call - should not create a duplicate + self.test_partner.ensure_kyc_record() + + final_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + # Should have created only one record + self.assertEqual(final_count, initial_count + 1) + + def test_multiple_status_scenarios_for_button_visibility(self): + """Test all possible status combinations for button visibility.""" + # Create a new partner to test various scenarios + test_partner_3 = self.env["res.partner"].create( + { + "name": "Test Partner 3", + "email": "test3@example.com", + } + ) + + # Initially, with no records, button should be visible + self.assertTrue(test_partner_3.show_kyc_button) + + # Test each status combination + + # Test with 'open' (Running) status only - button should be hidden + identification_model = self.env["res.partner.id_number"] + identification_model.create( + { + "partner_id": test_partner_3.id, + "category_id": self.kyc_category.id, + "name": "KYC-OPEN-TEST", + "status": "open", + } + ) + test_partner_3.invalidate_recordset() + reloaded_partner = test_partner_3.browse(test_partner_3.id) + self.assertFalse(reloaded_partner.show_kyc_button) + + # Clean up for next test + identification_model.search( + [ + ("partner_id", "=", test_partner_3.id), + ("category_id", "=", self.kyc_category.id), + ] + ).unlink() + + # Test with 'pending' (To Renew) status only - button should be hidden + identification_model.create( + { + "partner_id": test_partner_3.id, + "category_id": self.kyc_category.id, + "name": "KYC-PENDING-TEST", + "status": "pending", + } + ) + test_partner_3.invalidate_recordset() + reloaded_partner = test_partner_3.browse(test_partner_3.id) + self.assertFalse(reloaded_partner.show_kyc_button) + + # Clean up for next test + identification_model.search( + [ + ("partner_id", "=", test_partner_3.id), + ("category_id", "=", self.kyc_category.id), + ] + ).unlink() + + # Test with 'close' (Expired) status only - button should be visible + identification_model.create( + { + "partner_id": test_partner_3.id, + "category_id": self.kyc_category.id, + "name": "KYC-CLOSE-TEST", + "status": "close", + } + ) + test_partner_3.invalidate_recordset() + reloaded_partner = test_partner_3.browse(test_partner_3.id) + self.assertTrue(reloaded_partner.show_kyc_button) + + def test_kyc_valid_until_computed_field_with_valid_dates(self): + """Test the kyc_valid_until computed field when records have valid dates.""" + # Create KYC record with a valid_until date + identification_model = self.env["res.partner.id_number"] + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-TEST-WITH-DATE", + "status": "open", + "valid_until": "2025-12-31", # Set a future date + } + ) + + # Reload partner to get updated computed field + self.test_partner.invalidate_recordset() + reloaded_partner = self.test_partner.browse(self.test_partner.id) + + # Should have a valid_until date + self.assertIsNotNone(reloaded_partner.kyc_valid_until) + self.assertEqual( + reloaded_partner.kyc_valid_until, fields.Date.from_string("2025-12-31") + ) + + def test_kyc_valid_until_computed_field_multiple_records(self): + """Test the kyc_valid_until computed field with multiple records + to get min date.""" + identification_model = self.env["res.partner.id_number"] + + # Create multiple KYC records with different valid_until dates + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-TEST-DATE1", + "status": "open", + "valid_until": "2025-12-31", # Later date + } + ) + + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-TEST-DATE2", + "status": "open", + "valid_until": "2025-06-15", # Earlier date - should be the min + } + ) + + # Reload partner to get updated computed field + self.test_partner.invalidate_recordset() + reloaded_partner = self.test_partner.browse(self.test_partner.id) + + # Should have the minimum (earliest) valid_until date + self.assertIsNotNone(reloaded_partner.kyc_valid_until) + self.assertEqual( + reloaded_partner.kyc_valid_until, fields.Date.from_string("2025-06-15") + ) + + def test_kyc_valid_until_computed_field_no_open_records(self): + """Test the kyc_valid_until computed field when no open records exist.""" + # Create a KYC record but with 'draft' status (not 'open') + identification_model = self.env["res.partner.id_number"] + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-TEST-DRAFT", + "status": "draft", # Not 'open' status + } + ) + + # Reload partner to get updated computed field + self.test_partner.invalidate_recordset() + reloaded_partner = self.test_partner.browse(self.test_partner.id) + + # Should be False since there are no 'open' status records + self.assertFalse(reloaded_partner.kyc_valid_until) + + def test_kyc_valid_until_computed_field_open_records_without_dates(self): + """Test the kyc_valid_until computed field when open records + have no valid_until dates.""" + identification_model = self.env["res.partner.id_number"] + + # Create KYC record with 'open' status but no valid_until date + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-TEST-NO-DATE", + "status": "open", # Open status but no valid_until + } + ) + + # Reload partner to get updated computed field + self.test_partner.invalidate_recordset() + reloaded_partner = self.test_partner.browse(self.test_partner.id) + + # Should be False since there are no valid_until dates in open records + self.assertFalse(reloaded_partner.kyc_valid_until) + + def test_kyc_valid_until_computed_field_mixed_records(self): + """Test the kyc_valid_until computed field with mixed records + (some with dates, some without).""" + identification_model = self.env["res.partner.id_number"] + + # Create one record with valid_until and one without + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-TEST-WITH-DATE", + "status": "open", + "valid_until": "2025-12-31", + } + ) + + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-TEST-NO-DATE", + "status": "open", + # No valid_until field + } + ) + + # Reload partner to get updated computed field + self.test_partner.invalidate_recordset() + reloaded_partner = self.test_partner.browse(self.test_partner.id) + + # Should have the valid_until from the record that has it + self.assertIsNotNone(reloaded_partner.kyc_valid_until) + self.assertEqual( + reloaded_partner.kyc_valid_until, fields.Date.from_string("2025-12-31") + ) + + def test_show_kyc_button_with_company_partner(self): + """Test show_kyc_button computed field with company partner + when child contacts disabled.""" + # First, disable child contacts on the KYC category + self.kyc_category.enable_on_child_contacts = False + + # Create a company partner (should still be able to see button + # regardless of enable_on_child_contacts) + company_partner = self.env["res.partner"].create( + { + "name": "Company Partner", + "email": "company@example.com", + "is_company": True, # Is a company + } + ) + + # The button should be visible for companies even when + # enable_on_child_contacts is False + company_partner.invalidate_recordset() + reloaded_company = company_partner.browse(company_partner.id) + self.assertTrue(reloaded_company.show_kyc_button) + + def test_button_visibility_computed_field_with_disabled_child_contacts(self): + """Test the computed field show_kyc_button when child contacts are disabled.""" + # First, disable child contacts on the KYC category + self.kyc_category.enable_on_child_contacts = False + + # Create a non-company partner (individual contact) + individual_contact = self.env["res.partner"].create( + { + "name": "Individual Contact", + "email": "individual@example.com", + "is_company": False, # Not a company + } + ) + + # The button should be hidden for individual contacts when + # enable_on_child_contacts is False + individual_contact.invalidate_recordset() + reloaded_contact = individual_contact.browse(individual_contact.id) + self.assertFalse(reloaded_contact.show_kyc_button) + + def test_button_visibility_computed_field_with_enabled_child_contacts(self): + """Test the computed field show_kyc_button when child contacts are enabled.""" + # Ensure child contacts are enabled on the KYC category (default) + self.kyc_category.enable_on_child_contacts = True + + # Create a non-company partner (individual contact) + individual_contact = self.env["res.partner"].create( + { + "name": "Individual Contact", + "email": "individual@example.com", + "is_company": False, # Not a company + } + ) + + # The button should be visible for individual contacts when + # enable_on_child_contacts is True + individual_contact.invalidate_recordset() + reloaded_contact = individual_contact.browse(individual_contact.id) + self.assertTrue(reloaded_contact.show_kyc_button) + + def test_kyc_valid_until_computed_field_empty_sequence_edge_case(self): + """Test that kyc_valid_until doesn't fail when open records exist + but none have valid_until.""" + # Create multiple open records without valid_until dates + # to test the min() edge case + identification_model = self.env["res.partner.id_number"] + + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-TEST-NO-DATE-1", + "status": "open", + # No valid_until + } + ) + + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-TEST-NO-DATE-2", + "status": "open", + # No valid_until + } + ) + + # This should not raise an error and should return False + self.test_partner.invalidate_recordset() + reloaded_partner = self.test_partner.browse(self.test_partner.id) + + # Should be False since no records have valid_until dates + self.assertFalse(reloaded_partner.kyc_valid_until) + + def test_action_request_kyc_with_custom_sequence(self): + """Test action_request_kyc creates record with proper sequence.""" + # Remove any existing KYC records first + existing_records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + existing_records.unlink() + + # Call action_request_kyc + self.test_partner.action_request_kyc() + + # Check that a record was created + kyc_records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + self.assertEqual(len(kyc_records), 1) + self.assertEqual(kyc_records[0].status, "draft") + self.assertTrue(kyc_records[0].name.startswith("KYC-")) + + def test_ensure_kyc_record_when_none_exist(self): + """Test ensure_kyc_record creates record when none exists.""" + # Remove any existing KYC records first + existing_records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + existing_records.unlink() + + # Call ensure_kyc_record + self.test_partner.ensure_kyc_record() + + # Check that a record was created + kyc_records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + self.assertEqual(len(kyc_records), 1) + self.assertEqual(kyc_records[0].status, "draft") + + def test_ensure_kyc_record_when_exists_with_active_status(self): + """Test ensure_kyc_record does nothing when active record exists.""" + # Create an existing 'open' status record + identification_model = self.env["res.partner.id_number"] + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-EXISTING-TEST", + "status": "open", + } + ) + + # Count existing records + initial_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + # Call ensure_kyc_record - should not create a new record + self.test_partner.ensure_kyc_record() + + # Count should remain the same + final_count = self.env["res.partner.id_number"].search_count( + [ + ("partner_id", "=", self.test_partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + + self.assertEqual(initial_count, final_count) + + def test_action_view_kyc_records(self): + """Test action_view_kyc_records returns proper action.""" + # Create a KYC record first + identification_model = self.env["res.partner.id_number"] + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-VIEW-TEST", + "status": "open", + } + ) + + # Call action_view_kyc_records + action = self.test_partner.action_view_kyc_records() + + # Check the action structure + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_model"], "res.partner.id_number") + self.assertIn("domain", action) + + # Check that domain contains partner_id filter + domain_has_partner = any( + isinstance(d, (list, tuple)) + and len(d) >= 3 + and d[0] == "partner_id" + and d[1] == "=" + and d[2] == self.test_partner.id + for d in action["domain"] + ) + self.assertTrue(domain_has_partner) + + def test_kyc_valid_until_with_expired_and_active_records(self): + """Test kyc_valid_until with mix of expired and active records.""" + identification_model = self.env["res.partner.id_number"] + + # Create an expired record (close status) with a date + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-EXPIRED-TEST", + "status": "close", # Not 'open' status, should be ignored + "valid_until": "2020-01-01", # Past date + } + ) + + # Create an open record with a future date + identification_model.create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "KYC-ACTIVE-TEST", + "status": "open", # This one should be considered + "valid_until": "2025-12-31", # Future date + } + ) + + # Reload partner to get updated computed field + self.test_partner.invalidate_recordset() + reloaded_partner = self.test_partner.browse(self.test_partner.id) + + # Should only consider 'open' status records + self.assertIsNotNone(reloaded_partner.kyc_valid_until) + expected_date = fields.Date.from_string("2025-12-31") + self.assertEqual(reloaded_partner.kyc_valid_until, expected_date) + + def test_button_visibility_with_multiple_partners(self): + """Test show_kyc_button computed field with multiple partners at once.""" + # Create multiple partners + partner1 = self.env["res.partner"].create( + { + "name": "Partner 1", + "email": "partner1@example.com", + } + ) + + partner2 = self.env["res.partner"].create( + { + "name": "Partner 2", + "email": "partner2@example.com", + } + ) + + # Get multiple partners at once to test computed field batch processing + partners = partner1 | partner2 + + # All should have button visible initially (no KYC records) + partners.invalidate_recordset() + for partner in partners: + self.assertTrue(partner.show_kyc_button) + + def test_ensure_kyc_record_batch_processing(self): + """Test ensure_kyc_record works with multiple partners.""" + # Create multiple partners + partner1 = self.env["res.partner"].create( + { + "name": "Batch Partner 1", + "email": "batch1@example.com", + } + ) + + partner2 = self.env["res.partner"].create( + { + "name": "Batch Partner 2", + "email": "batch2@example.com", + } + ) + + # Remove any existing KYC records for these partners + existing_records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "in", [partner1.id, partner2.id]), + ("category_id", "=", self.kyc_category.id), + ] + ) + existing_records.unlink() + + # Apply ensure_kyc_record to multiple partners at once + partners = partner1 | partner2 + partners.ensure_kyc_record() + + # Each partner should now have a KYC record + for partner in partners: + partner_records = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", partner.id), + ("category_id", "=", self.kyc_category.id), + ] + ) + self.assertEqual(len(partner_records), 1) + self.assertEqual(partner_records[0].status, "draft") diff --git a/partner_identification_kyc/views/identification_number_views.xml b/partner_identification_kyc/views/identification_number_views.xml new file mode 100644 index 00000000000..4c4a94e1d57 --- /dev/null +++ b/partner_identification_kyc/views/identification_number_views.xml @@ -0,0 +1,22 @@ + + + + + res.partner.id_number.search.kyc + res.partner.id_number + + + + + + + + + diff --git a/partner_identification_kyc/views/res_partner_id_category_view.xml b/partner_identification_kyc/views/res_partner_id_category_view.xml new file mode 100644 index 00000000000..01955eda849 --- /dev/null +++ b/partner_identification_kyc/views/res_partner_id_category_view.xml @@ -0,0 +1,16 @@ + + + + res.partner.id_category.form.kyc + res.partner.id_category + + + + + + + + diff --git a/partner_identification_kyc/views/res_partner_views.xml b/partner_identification_kyc/views/res_partner_views.xml new file mode 100644 index 00000000000..8a26e1844c0 --- /dev/null +++ b/partner_identification_kyc/views/res_partner_views.xml @@ -0,0 +1,39 @@ + + + + + res.partner.form.kyc + res.partner + + + + + + +
+
+
+
+
+
From 1f0cea67eb710b069e81c3d927557127ca8d8702 Mon Sep 17 00:00:00 2001 From: bosd <5e2fd43-d292-4c90-9d1f-74ff3436329a@anonaddy.me> Date: Sat, 6 Jun 2026 14:04:19 +0200 Subject: [PATCH 2/6] [ DO NOT MERGE TEST REQUIREMENTS] --- test-requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 test-requirements.txt diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000000..e48f3526a30 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +odoo-addon-partner-identification-automation @ git+https://github.com/OCA/partner-contact.git@refs/pull/2224/head#subdirectory=partner_identification_automation +odoo-addon-partner-identification-automation-activity @ git+https://github.com/OCA/partner-contact.git@refs/pull/2225/head#subdirectory=partner_identification_automation_activity From a26692c5b327bda28c25695c6acce102c77a2e42 Mon Sep 17 00:00:00 2001 From: bosd <5e2fd43-d292-4c90-9d1f-74ff3436329a@anonaddy.me> Date: Mon, 8 Jun 2026 13:17:21 +0200 Subject: [PATCH 3/6] [IMP] partner_identification_kyc: address review feedback - Add an is_kyc boolean on res.partner.id_category to mark the KYC category instead of hard-coding the 'KYC' code; enable_on_child_contacts is now only shown for KYC categories (invisible="not is_kyc"), and the search filter matches on is_kyc. - Auto-assign the identification number from the kyc.identification sequence in res.partner.id_number.create, so KYC records created manually via the one2many no longer require a hand-typed ID Number (name is relaxed for KYC in the form view). - Drop the duplicated 'KYC-' prefix from the generated ID Number (the category already conveys KYC). - Tests: cover manual auto-sequence and provided-name paths. --- .../data/identification_category_data.xml | 1 + partner_identification_kyc/models/__init__.py | 1 + .../models/res_partner.py | 9 ++---- .../models/res_partner_id_category.py | 7 +++++ .../models/res_partner_id_number.py | 31 +++++++++++++++++++ .../tests/test_partner_identification_kyc.py | 27 +++++++++++++++- .../views/identification_number_views.xml | 20 +++++++++++- .../views/res_partner_id_category_view.xml | 3 +- 8 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 partner_identification_kyc/models/res_partner_id_number.py diff --git a/partner_identification_kyc/data/identification_category_data.xml b/partner_identification_kyc/data/identification_category_data.xml index 2dbf1f9815a..429aefadfa0 100644 --- a/partner_identification_kyc/data/identification_category_data.xml +++ b/partner_identification_kyc/data/identification_category_data.xml @@ -4,6 +4,7 @@ KYC KYC + diff --git a/partner_identification_kyc/models/__init__.py b/partner_identification_kyc/models/__init__.py index 21153e6be40..6114a90ed68 100644 --- a/partner_identification_kyc/models/__init__.py +++ b/partner_identification_kyc/models/__init__.py @@ -1,2 +1,3 @@ from . import res_partner from . import res_partner_id_category +from . import res_partner_id_number diff --git a/partner_identification_kyc/models/res_partner.py b/partner_identification_kyc/models/res_partner.py index 89616d97481..34a23a739c3 100644 --- a/partner_identification_kyc/models/res_partner.py +++ b/partner_identification_kyc/models/res_partner.py @@ -98,15 +98,12 @@ def _create_kyc_record(self): kyc_category = self.env.ref( "partner_identification_kyc.kyc_identification_category" ) - identification_model = self.env["res.partner.id_number"] - sequence_code = ( - self.env["ir.sequence"].next_by_code("kyc.identification") or "001" - ) - return identification_model.create( + # The identification number (``name``) is auto-assigned from the + # ``kyc.identification`` sequence by ``res.partner.id_number.create``. + return self.env["res.partner.id_number"].create( { "partner_id": self.id, "category_id": kyc_category.id, - "name": f"KYC-{self.id}-{sequence_code}", "status": "draft", } ) diff --git a/partner_identification_kyc/models/res_partner_id_category.py b/partner_identification_kyc/models/res_partner_id_category.py index 7132f2a8896..bf01e406323 100644 --- a/partner_identification_kyc/models/res_partner_id_category.py +++ b/partner_identification_kyc/models/res_partner_id_category.py @@ -4,6 +4,13 @@ class ResPartnerIdCategory(models.Model): _inherit = "res.partner.id_category" + is_kyc = fields.Boolean( + string="Is KYC Category", + default=False, + help="Marks this category as the one used for KYC processes. It enables " + "KYC-specific behaviour such as the automatic identification number " + "sequence and child-contact handling.", + ) enable_on_child_contacts = fields.Boolean( string="Enable on Child Contacts", default=False, diff --git a/partner_identification_kyc/models/res_partner_id_number.py b/partner_identification_kyc/models/res_partner_id_number.py new file mode 100644 index 00000000000..cffed3c00c8 --- /dev/null +++ b/partner_identification_kyc/models/res_partner_id_number.py @@ -0,0 +1,31 @@ +from odoo import api, fields, models + + +class ResPartnerIdNumber(models.Model): + _inherit = "res.partner.id_number" + + category_is_kyc = fields.Boolean( + string="KYC Category", + related="category_id.is_kyc", + ) + + @api.model_create_multi + def create(self, vals_list): + """Auto-assign the identification number from the KYC sequence. + + The base ``name`` field (the ID Number) is required, but for KYC + records it can be left empty so it gets filled automatically from the + ``kyc.identification`` sequence, both for the ``Request KYC`` button + and for records created manually through the one2many. + """ + for vals in vals_list: + if vals.get("name"): + continue + category = self.env["res.partner.id_category"].browse( + vals.get("category_id") + ) + if category.is_kyc: + vals["name"] = ( + self.env["ir.sequence"].next_by_code("kyc.identification") or "/" + ) + return super().create(vals_list) diff --git a/partner_identification_kyc/tests/test_partner_identification_kyc.py b/partner_identification_kyc/tests/test_partner_identification_kyc.py index 4785e626bbb..7c0d6954378 100644 --- a/partner_identification_kyc/tests/test_partner_identification_kyc.py +++ b/partner_identification_kyc/tests/test_partner_identification_kyc.py @@ -79,7 +79,32 @@ def test_action_request_kyc_creates_record(self): ) self.assertEqual(new_record.status, "draft") - self.assertTrue(new_record.name.startswith("KYC-")) + # The ID Number is auto-assigned from the sequence and must not repeat + # the category name ("KYC"). + self.assertTrue(new_record.name) + self.assertFalse(new_record.name.startswith("KYC-")) + + def test_manual_id_number_gets_sequence(self): + """A KYC ID record created manually (no name) gets the sequence.""" + record = self.env["res.partner.id_number"].create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + } + ) + self.assertTrue(record.name) + self.assertTrue(record.category_is_kyc) + + def test_manual_id_number_keeps_provided_name(self): + """An explicit ID Number is not overwritten by the sequence.""" + record = self.env["res.partner.id_number"].create( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + "name": "MY-CUSTOM-ID", + } + ) + self.assertEqual(record.name, "MY-CUSTOM-ID") def test_action_request_kyc_duplicate_prevention(self): """Test that action_request_kyc prevents duplicates when a 'draft' record diff --git a/partner_identification_kyc/views/identification_number_views.xml b/partner_identification_kyc/views/identification_number_views.xml index 4c4a94e1d57..e68ac531d8e 100644 --- a/partner_identification_kyc/views/identification_number_views.xml +++ b/partner_identification_kyc/views/identification_number_views.xml @@ -14,9 +14,27 @@ + + + + res.partner.id_number.form.kyc + res.partner.id_number + + + + + + + not category_is_kyc + + + diff --git a/partner_identification_kyc/views/res_partner_id_category_view.xml b/partner_identification_kyc/views/res_partner_id_category_view.xml index 01955eda849..4bf8e1022c0 100644 --- a/partner_identification_kyc/views/res_partner_id_category_view.xml +++ b/partner_identification_kyc/views/res_partner_id_category_view.xml @@ -9,7 +9,8 @@ /> - + + From 7e97ad34b4048357c5f03b47a50a52c51de758c2 Mon Sep 17 00:00:00 2001 From: bosd <5e2fd43-d292-4c90-9d1f-74ff3436329a@anonaddy.me> Date: Mon, 8 Jun 2026 13:45:27 +0200 Subject: [PATCH 4/6] [FIX] partner_identification_kyc: update KYC name assertion in tests test_action_request_kyc_with_custom_sequence still asserted the old "KYC-" prefixed ID Number; the number now comes purely from the sequence. Assert the name is set and no longer duplicates the category. --- .../tests/test_partner_identification_kyc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/partner_identification_kyc/tests/test_partner_identification_kyc.py b/partner_identification_kyc/tests/test_partner_identification_kyc.py index 7c0d6954378..301258e13f9 100644 --- a/partner_identification_kyc/tests/test_partner_identification_kyc.py +++ b/partner_identification_kyc/tests/test_partner_identification_kyc.py @@ -753,7 +753,10 @@ def test_action_request_kyc_with_custom_sequence(self): self.assertEqual(len(kyc_records), 1) self.assertEqual(kyc_records[0].status, "draft") - self.assertTrue(kyc_records[0].name.startswith("KYC-")) + # The ID Number is auto-assigned from the sequence and must not repeat + # the category name ("KYC"). + self.assertTrue(kyc_records[0].name) + self.assertFalse(kyc_records[0].name.startswith("KYC-")) def test_ensure_kyc_record_when_none_exist(self): """Test ensure_kyc_record creates record when none exists.""" From 349dcd16d16ffc63fe716d39c404cffb9332d243 Mon Sep 17 00:00:00 2001 From: bosd <5e2fd43-d292-4c90-9d1f-74ff3436329a@anonaddy.me> Date: Mon, 8 Jun 2026 14:13:11 +0200 Subject: [PATCH 5/6] [IMP] partner_identification_kyc: prefix KYC sequence and prefill ID Number Addresses the second review round: - Configure the "KYC" prefix on seq_kyc_identification (instead of hard-coding it in create), so generated ID Numbers read e.g. KYC001. - Prefill the (required) ID Number from the sequence via an onchange when the KYC category is selected, so a KYC record can be created manually through the one2many without typing the number. create() keeps assigning it for the Request KYC button and programmatic creation. - Drop the now-unnecessary form-view required modifier and the unused category_is_kyc related field. - Tests: assert the KYC prefix and add an onchange/Form test. --- .../data/activity_type_data.xml | 1 + .../models/res_partner_id_number.py | 16 +++++++++---- .../tests/test_partner_identification_kyc.py | 23 +++++++++++++------ .../views/identification_number_views.xml | 18 --------------- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/partner_identification_kyc/data/activity_type_data.xml b/partner_identification_kyc/data/activity_type_data.xml index c544ad8fb84..c5a07e5c2c6 100644 --- a/partner_identification_kyc/data/activity_type_data.xml +++ b/partner_identification_kyc/data/activity_type_data.xml @@ -4,6 +4,7 @@ KYC Identification Sequence kyc.identification + KYC 3 diff --git a/partner_identification_kyc/models/res_partner_id_number.py b/partner_identification_kyc/models/res_partner_id_number.py index cffed3c00c8..c89be3b2c4f 100644 --- a/partner_identification_kyc/models/res_partner_id_number.py +++ b/partner_identification_kyc/models/res_partner_id_number.py @@ -1,13 +1,19 @@ -from odoo import api, fields, models +from odoo import api, models class ResPartnerIdNumber(models.Model): _inherit = "res.partner.id_number" - category_is_kyc = fields.Boolean( - string="KYC Category", - related="category_id.is_kyc", - ) + @api.onchange("category_id") + def _onchange_category_id_kyc_name(self): + """Prefill the ID Number from the sequence when the KYC category is set. + + The base ``name`` field is required; for KYC records the number is + generated automatically so it does not have to be typed when creating + a record manually through the one2many. + """ + if self.category_id.is_kyc and not self.name: + self.name = self.env["ir.sequence"].next_by_code("kyc.identification") @api.model_create_multi def create(self, vals_list): diff --git a/partner_identification_kyc/tests/test_partner_identification_kyc.py b/partner_identification_kyc/tests/test_partner_identification_kyc.py index 301258e13f9..8607fb2d851 100644 --- a/partner_identification_kyc/tests/test_partner_identification_kyc.py +++ b/partner_identification_kyc/tests/test_partner_identification_kyc.py @@ -79,10 +79,9 @@ def test_action_request_kyc_creates_record(self): ) self.assertEqual(new_record.status, "draft") - # The ID Number is auto-assigned from the sequence and must not repeat - # the category name ("KYC"). + # The ID Number is auto-assigned from the KYC sequence (prefix "KYC"). self.assertTrue(new_record.name) - self.assertFalse(new_record.name.startswith("KYC-")) + self.assertTrue(new_record.name.startswith("KYC")) def test_manual_id_number_gets_sequence(self): """A KYC ID record created manually (no name) gets the sequence.""" @@ -93,7 +92,18 @@ def test_manual_id_number_gets_sequence(self): } ) self.assertTrue(record.name) - self.assertTrue(record.category_is_kyc) + self.assertTrue(record.name.startswith("KYC")) + + def test_onchange_category_prefills_name(self): + """Selecting the KYC category in the form prefills the ID Number.""" + form = common.Form(self.env["res.partner.id_number"]) + form.partner_id = self.test_partner + form.category_id = self.kyc_category + # The required ID Number is filled automatically, so the form is savable. + self.assertTrue(form.name) + self.assertTrue(form.name.startswith("KYC")) + record = form.save() + self.assertEqual(record.name, form.name) def test_manual_id_number_keeps_provided_name(self): """An explicit ID Number is not overwritten by the sequence.""" @@ -753,10 +763,9 @@ def test_action_request_kyc_with_custom_sequence(self): self.assertEqual(len(kyc_records), 1) self.assertEqual(kyc_records[0].status, "draft") - # The ID Number is auto-assigned from the sequence and must not repeat - # the category name ("KYC"). + # The ID Number is auto-assigned from the KYC sequence (prefix "KYC"). self.assertTrue(kyc_records[0].name) - self.assertFalse(kyc_records[0].name.startswith("KYC-")) + self.assertTrue(kyc_records[0].name.startswith("KYC")) def test_ensure_kyc_record_when_none_exist(self): """Test ensure_kyc_record creates record when none exists.""" diff --git a/partner_identification_kyc/views/identification_number_views.xml b/partner_identification_kyc/views/identification_number_views.xml index e68ac531d8e..535d67dc0cd 100644 --- a/partner_identification_kyc/views/identification_number_views.xml +++ b/partner_identification_kyc/views/identification_number_views.xml @@ -19,22 +19,4 @@ - - - - res.partner.id_number.form.kyc - res.partner.id_number - - - - - - - not category_is_kyc - - - From a88d50e3a326502af7615c3609bb1f98af5a628b Mon Sep 17 00:00:00 2001 From: bosd <5e2fd43-d292-4c90-9d1f-74ff3436329a@anonaddy.me> Date: Mon, 8 Jun 2026 14:18:17 +0200 Subject: [PATCH 6/6] [FIX] partner_identification_kyc: test onchange directly instead of Form common.Form construction errored in the Odoo 19 CI; call the onchange method on a new() record instead, which tests the same prefill behaviour. --- .../tests/test_partner_identification_kyc.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/partner_identification_kyc/tests/test_partner_identification_kyc.py b/partner_identification_kyc/tests/test_partner_identification_kyc.py index 8607fb2d851..db9bb431e7b 100644 --- a/partner_identification_kyc/tests/test_partner_identification_kyc.py +++ b/partner_identification_kyc/tests/test_partner_identification_kyc.py @@ -95,15 +95,18 @@ def test_manual_id_number_gets_sequence(self): self.assertTrue(record.name.startswith("KYC")) def test_onchange_category_prefills_name(self): - """Selecting the KYC category in the form prefills the ID Number.""" - form = common.Form(self.env["res.partner.id_number"]) - form.partner_id = self.test_partner - form.category_id = self.kyc_category - # The required ID Number is filled automatically, so the form is savable. - self.assertTrue(form.name) - self.assertTrue(form.name.startswith("KYC")) - record = form.save() - self.assertEqual(record.name, form.name) + """Selecting the KYC category prefills the (required) ID Number.""" + record = self.env["res.partner.id_number"].new( + { + "partner_id": self.test_partner.id, + "category_id": self.kyc_category.id, + } + ) + self.assertFalse(record.name) + record._onchange_category_id_kyc_name() + # The required ID Number is filled automatically, so the record is savable. + self.assertTrue(record.name) + self.assertTrue(record.name.startswith("KYC")) def test_manual_id_number_keeps_provided_name(self): """An explicit ID Number is not overwritten by the sequence."""