diff --git a/partner_stage_only_confirmed/README.rst b/partner_stage_only_confirmed/README.rst new file mode 100644 index 00000000000..9d53ba1975a --- /dev/null +++ b/partner_stage_only_confirmed/README.rst @@ -0,0 +1,316 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=============================================== +Partner Stage - Display only confirmed partners +=============================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6798741b40f265ab2535dc965b0d110aa92306c31190a3fd93a60a9fc2cc6c67 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_stage_only_confirmed + :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_stage_only_confirmed + :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 extends the partner stage functionality by implementing a +filtering mechanism that ensures only confirmed partners are displayed +in Many2one fields on form views. This is particularly useful in +business scenarios where you want to prevent users from selecting +partners that are not yet confirmed in your system. + +Problem Solved +~~~~~~~~~~~~~~ + +In standard Odoo, when using Many2one fields that reference partners +(like ``parent_id``, ``contact_id``, etc.), all partners are available +for selection regardless of their state or confirmation status. This can +lead to: + +- Selection of partners that are not yet properly validated +- Accidental linking to partners that are in draft or unconfirmed + states +- Data integrity issues when business processes require confirmed + partners + +Solution +~~~~~~~~ + +The module automatically modifies form views at runtime to filter +partner-related Many2one fields, showing only partners in the +'confirmed' state. This filtering is configurable via: + +- Context parameters +- System configuration parameters +- Default behavior that can be overridden as needed + +The filtering is applied transparently without requiring manual domain +updates on individual views, making it a robust solution that works +across the entire application for any partner-related Many2one field. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +Business Context +~~~~~~~~~~~~~~~~ + +Many organizations implement partner lifecycle management where contacts +move through various stages before being fully confirmed and eligible +for business transactions. This workflow typically includes: + +- **Draft Stage:** Initial partner entry, possibly pending validation +- **Validation Stage:** Partner details being verified +- **Confirmed Stage:** Fully validated partner ready for business +- **Other Stages:** Potentially archived or suspended partners + +Problem Statement +~~~~~~~~~~~~~~~~~ + +Without proper filtering mechanisms, users can accidentally select +unconfirmed partners in critical business operations: + +- Creating sales orders for draft partners +- Generating invoices for unverified contacts +- Assigning projects to partners not yet confirmed +- Linking transactions to partners that may not exist legally + +This leads to potential business disruption, data quality issues, and +compliance problems. + +Industry Scenarios +~~~~~~~~~~~~~~~~~~ + +Financial Services +~~~~~~~~~~~~~~~~~~ + +Banks and financial institutions must ensure that all customer +references in transactions are to properly verified and confirmed +clients. This filter prevents linking to customers who may not have +completed the required KYC (Know Your Customer) processes. + +E-commerce and Retail +~~~~~~~~~~~~~~~~~~~~~ + +Online retailers often have a customer validation process before +allowing full purchasing capabilities. This module ensures that business +transactions only reference validated customers. + +Professional Services +~~~~~~~~~~~~~~~~~~~~~ + +Consulting firms and professional services often have a client +onboarding process. This module ensures that only properly onboarded +clients appear in selection lists for new projects or contracts. + +Manufacturing and Supply Chain +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Manufacturers often have supplier qualification processes. The filtering +ensures that only qualified suppliers appear in procurement operations. + +Regulatory Compliance +~~~~~~~~~~~~~~~~~~~~~ + +In many jurisdictions, businesses must maintain proper verification of +their partner relationships. This module supports: + +- **GDPR Compliance:** Ensuring personal data is properly validated + before use +- **Financial Regulations:** Meeting requirements for customer + verification in financial transactions +- **Contract Law:** Ensuring legal capacity of contracting partners +- **Industry Standards:** Meeting sector-specific requirements for + partner validation + +Technical Context +~~~~~~~~~~~~~~~~~ + +The module addresses the gap between partner state management (handled +by the ``partner_stage`` module) and user interface behavior. While +partner states may be properly managed in the backend, the user +interface previously provided no automatic filtering mechanism. + +Integration Considerations +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module works in conjunction with: + +- The ``partner_stage`` module for state management +- Standard Odoo partner management functionality +- Custom partner validation workflows +- Existing business processes that depend on partner confirmation + status + +Solution Scope +~~~~~~~~~~~~~~ + +The module provides an elegant solution that: + +- Maintains data integrity without restricting functionality +- Provides configuration flexibility for different business needs +- Integrates seamlessly with existing user interfaces +- Supports both global and granular control over the filtering behavior + +Usage +===== + +Default Behavior +~~~~~~~~~~~~~~~~ + +By default, the module applies the partner filtering automatically to +all applicable Many2one fields that reference partners. When selecting a +partner in any form view, only partners in the 'confirmed' state will be +available. + +Configuration Methods +~~~~~~~~~~~~~~~~~~~~~ + +The filtering behavior can be controlled in several ways: + +1. Context Parameter +~~~~~~~~~~~~~~~~~~~~ + +You can disable the filtering for specific views by adding +``only_confirmed_partners: false`` to the context: + +**In views (XML):** + +.. code:: xml + + + +**In Python code:** + +.. code:: python + + record.with_context(only_confirmed_partners=False).get_view(...) + +2. System Configuration Parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can set the global filtering behavior using system parameters: + +**Enable filtering:** + +- Go to Settings > Technical > Parameters > System Parameters +- Create or update parameter: ``partner_stage.only_confirmed_partners`` +- Set value to any non-false value (e.g., "True", "1", "yes") + +**Disable filtering:** + +- Set parameter value to: "False", "false", "0", or empty string + +3. Explicit Enable +~~~~~~~~~~~~~~~~~~ + +To explicitly enable the filtering globally, set the system parameter to +any true-like value. + +Common Use Cases +~~~~~~~~~~~~~~~~ + +Case 1: Sales Orders +~~~~~~~~~~~~~~~~~~~~ + +When creating sales orders, ensure that only confirmed customers can be +selected as the main partner, preventing orders from being created for +unconfirmed/draft partners. + +Case 2: Invoicing +~~~~~~~~~~~~~~~~~ + +When creating invoices, ensure that only confirmed partners are +available as the billing address partner. + +Case 3: Project Management +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When assigning projects to contacts, restrict the selection to only +confirmed partners to maintain data quality. + +Technical Implementation +~~~~~~~~~~~~~~~~~~~~~~~~ + +The module uses the ``get_view`` method override to dynamically modify +form view architectures at runtime. This approach ensures that: + +- Existing views don't require modifications +- The filtering applies to all partner-related Many2one fields +- Performance impact is minimal and only affects form views +- The filtering is transparent to end users (they simply see fewer + options) + +Limitations +~~~~~~~~~~~ + +- The filtering only applies to form views +- Only affects Many2one fields with comodel_name "res.partner" +- Other view types (tree, search, kanban) are not affected +- Context and system parameters provide override mechanisms for + exceptions + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Silvio Gregorini +- Maksym Yankin +- Ruchir Shukla + +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_stage_only_confirmed/__init__.py b/partner_stage_only_confirmed/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/partner_stage_only_confirmed/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/partner_stage_only_confirmed/__manifest__.py b/partner_stage_only_confirmed/__manifest__.py new file mode 100644 index 00000000000..072019d6fae --- /dev/null +++ b/partner_stage_only_confirmed/__manifest__.py @@ -0,0 +1,14 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +{ + "name": "Partner Stage - Display only confirmed partners", + "summary": "Adds filters on form views to display only confirmed partners", + "author": "Odoo Community Association (OCA), Camptocamp", + "website": "https://github.com/OCA/partner-contact", + "category": "Partner Management", + "version": "19.0.1.0.0", + "license": "AGPL-3", + "depends": ["partner_stage"], + "installable": True, +} diff --git a/partner_stage_only_confirmed/i18n/it.po b/partner_stage_only_confirmed/i18n/it.po new file mode 100644 index 00000000000..9c5c453f947 --- /dev/null +++ b/partner_stage_only_confirmed/i18n/it.po @@ -0,0 +1,22 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_stage_only_confirmed +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-05-07 10:38+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: partner_stage_only_confirmed +#: model:ir.model,name:partner_stage_only_confirmed.model_base +msgid "Base" +msgstr "Base" diff --git a/partner_stage_only_confirmed/i18n/partner_stage_only_confirmed.pot b/partner_stage_only_confirmed/i18n/partner_stage_only_confirmed.pot new file mode 100644 index 00000000000..701cb2afba9 --- /dev/null +++ b/partner_stage_only_confirmed/i18n/partner_stage_only_confirmed.pot @@ -0,0 +1,19 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_stage_only_confirmed +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: partner_stage_only_confirmed +#: model:ir.model,name:partner_stage_only_confirmed.model_base +msgid "Base" +msgstr "" diff --git a/partner_stage_only_confirmed/models/__init__.py b/partner_stage_only_confirmed/models/__init__.py new file mode 100644 index 00000000000..0e44449338c --- /dev/null +++ b/partner_stage_only_confirmed/models/__init__.py @@ -0,0 +1 @@ +from . import base diff --git a/partner_stage_only_confirmed/models/base.py b/partner_stage_only_confirmed/models/base.py new file mode 100644 index 00000000000..bd40ead61e9 --- /dev/null +++ b/partner_stage_only_confirmed/models/base.py @@ -0,0 +1,65 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from lxml import etree + +from odoo import api, models + + +class Base(models.AbstractModel): + _inherit = "base" + + @api.model + def get_view(self, view_id=None, view_type="form", **options): + # OVERRIDE: display only confirmed partners on Many2one fields related + # to ``res.partner`` on form views. + res = super().get_view(view_id, view_type, **options) + if view_type == "form" and self._filter_only_confirmed(): + doc = etree.XML(res["arch"]) + for model_name, model_fields in res["models"].items(): + for fname in model_fields: + model = self.env[model_name] + field_obj = model._fields.get(fname) + if field_obj: + ftype, fcomodel_name = field_obj.type, field_obj.comodel_name + if (ftype, fcomodel_name) != ("many2one", "res.partner"): + continue + for node in doc.xpath(f"//field[@name='{fname}']"): + domain = node.get("domain") + if not domain: + domain = "[('state', '=', 'confirmed')]" + else: + # domain is always a string due to XML parsing + if domain in ("", "[]"): + domain = "[('state', '=', 'confirmed')]" + else: + domain = domain[:-1] + domain += ", ('state', '=', 'confirmed')]" + node.set("domain", domain) + res["arch"] = etree.tostring(doc) + return res + + @api.model + def _filter_only_confirmed(self) -> bool: + """Determines whether only confirmed partners should be shown + + Retrieves condition based on context or system parameters (in this + order). + Else, defaults to True. + """ + # Retrieve value from context (which can be defined in views field by + # field) + if "only_confirmed_partners" in self.env.context: + return bool(self.env.context["only_confirmed_partners"]) + + # Retrieve value from system parameters + val = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("partner_stage.only_confirmed_partners", default=None) + ) + if val is not None: + return val not in ("False", "false", "", "0") + + # Return default value + return True diff --git a/partner_stage_only_confirmed/pyproject.toml b/partner_stage_only_confirmed/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/partner_stage_only_confirmed/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/partner_stage_only_confirmed/readme/CONFIGURATION.md b/partner_stage_only_confirmed/readme/CONFIGURATION.md new file mode 100644 index 00000000000..a9ca8192edc --- /dev/null +++ b/partner_stage_only_confirmed/readme/CONFIGURATION.md @@ -0,0 +1,80 @@ +### System-Level Configuration + +The module behavior can be configured using system parameters without code changes: + +1. **Access System Parameters:** + - Navigate to: `Settings > Technical > Parameters > System Parameters` + - Or access directly: `[[Technical Settings]] > Parameters > System Parameters` + +2. **Configuration Parameter:** + - Parameter name: `partner_stage.only_confirmed_partners` + - Default behavior: When not set, the module defaults to `True` (filtering enabled) + +3. **Setting Values:** + - **Enable filtering:** Set value to any non-false string (e.g., "True", "1", "yes") + - **Disable filtering:** Set value to "False", "false", "0", "", or other false-like strings + +### Per-View Configuration + +Individual views can override the system default using context: + +### In XML Views: +```xml + + + + + +``` + +### In Action Definitions: +```xml + + + {'only_confirmed_partners': false} + +``` + +### Programmatic Configuration + +In Python code, you can control the behavior: + +```python +# Disable filtering for specific operations +partners = self.env['res.partner'].with_context( + only_confirmed_partners=False +) + +# Enable filtering explicitly +partners = self.env['res.partner'].with_context( + only_confirmed_partners=True +) +``` + +### Testing Configuration + +For testing purposes, you can temporarily modify the behavior: + +```python +# In test methods +def test_with_filtering_disabled(self): + self.env['ir.config_parameter'].sudo().set_param( + 'partner_stage.only_confirmed_partners', 'false' + ) + # Test code here +``` + +### Migration from Previous Versions + +If you're upgrading from a previous version of this module or have custom configurations: +1. Review existing system parameters related to partner filtering +2. Test the new configuration behavior in a development environment +3. Update any custom code that manually handled partner filtering +4. Ensure user permissions for system parameter access if needed + +### Performance Considerations + +- The filtering is applied at view generation time (in `get_view` method) +- Only affects form views and only for partner-related Many2one fields +- Minimal performance impact since it only adds a domain condition +- The filtering process is optimized to avoid unnecessary processing diff --git a/partner_stage_only_confirmed/readme/CONFIGURATION.rst b/partner_stage_only_confirmed/readme/CONFIGURATION.rst new file mode 100644 index 00000000000..bd62e651fec --- /dev/null +++ b/partner_stage_only_confirmed/readme/CONFIGURATION.rst @@ -0,0 +1,11 @@ +To define global behavior: + +#. create a system parameter with key "partner_stage.only_confirmed_partners" + +#. set its value as "True" or "1" to enable filtering or as "False" or "0" to disable it + +To define specific behavior for single fields on form views: + +#. add key "only_confirmed_partners" to field's context in form view + +#. set its value as "True" or "1" to enable filtering or as "False" or "0" to disable it diff --git a/partner_stage_only_confirmed/readme/CONTEXT.md b/partner_stage_only_confirmed/readme/CONTEXT.md new file mode 100644 index 00000000000..c0da0a22a58 --- /dev/null +++ b/partner_stage_only_confirmed/readme/CONTEXT.md @@ -0,0 +1,62 @@ +### Business Context + +Many organizations implement partner lifecycle management where contacts move through various stages before being fully confirmed and eligible for business transactions. This workflow typically includes: + +- **Draft Stage:** Initial partner entry, possibly pending validation +- **Validation Stage:** Partner details being verified +- **Confirmed Stage:** Fully validated partner ready for business +- **Other Stages:** Potentially archived or suspended partners + +### Problem Statement + +Without proper filtering mechanisms, users can accidentally select unconfirmed partners in critical business operations: + +- Creating sales orders for draft partners +- Generating invoices for unverified contacts +- Assigning projects to partners not yet confirmed +- Linking transactions to partners that may not exist legally + +This leads to potential business disruption, data quality issues, and compliance problems. + +### Industry Scenarios + +### Financial Services +Banks and financial institutions must ensure that all customer references in transactions are to properly verified and confirmed clients. This filter prevents linking to customers who may not have completed the required KYC (Know Your Customer) processes. + +### E-commerce and Retail +Online retailers often have a customer validation process before allowing full purchasing capabilities. This module ensures that business transactions only reference validated customers. + +### Professional Services +Consulting firms and professional services often have a client onboarding process. This module ensures that only properly onboarded clients appear in selection lists for new projects or contracts. + +### Manufacturing and Supply Chain +Manufacturers often have supplier qualification processes. The filtering ensures that only qualified suppliers appear in procurement operations. + +### Regulatory Compliance + +In many jurisdictions, businesses must maintain proper verification of their partner relationships. This module supports: + +- **GDPR Compliance:** Ensuring personal data is properly validated before use +- **Financial Regulations:** Meeting requirements for customer verification in financial transactions +- **Contract Law:** Ensuring legal capacity of contracting partners +- **Industry Standards:** Meeting sector-specific requirements for partner validation + +### Technical Context + +The module addresses the gap between partner state management (handled by the `partner_stage` module) and user interface behavior. While partner states may be properly managed in the backend, the user interface previously provided no automatic filtering mechanism. + +### Integration Considerations + +This module works in conjunction with: +- The `partner_stage` module for state management +- Standard Odoo partner management functionality +- Custom partner validation workflows +- Existing business processes that depend on partner confirmation status + +### Solution Scope + +The module provides an elegant solution that: +- Maintains data integrity without restricting functionality +- Provides configuration flexibility for different business needs +- Integrates seamlessly with existing user interfaces +- Supports both global and granular control over the filtering behavior diff --git a/partner_stage_only_confirmed/readme/CONTRIBUTORS.md b/partner_stage_only_confirmed/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..03d1a16cd4e --- /dev/null +++ b/partner_stage_only_confirmed/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Silvio Gregorini \<\> +- Maksym Yankin \<\> +- Ruchir Shukla \<\> diff --git a/partner_stage_only_confirmed/readme/DESCRIPTION.md b/partner_stage_only_confirmed/readme/DESCRIPTION.md new file mode 100644 index 00000000000..bb4c57b2f73 --- /dev/null +++ b/partner_stage_only_confirmed/readme/DESCRIPTION.md @@ -0,0 +1,17 @@ +This module extends the partner stage functionality by implementing a filtering mechanism that ensures only confirmed partners are displayed in Many2one fields on form views. This is particularly useful in business scenarios where you want to prevent users from selecting partners that are not yet confirmed in your system. + +### Problem Solved + +In standard Odoo, when using Many2one fields that reference partners (like `parent_id`, `contact_id`, etc.), all partners are available for selection regardless of their state or confirmation status. This can lead to: +- Selection of partners that are not yet properly validated +- Accidental linking to partners that are in draft or unconfirmed states +- Data integrity issues when business processes require confirmed partners + +### Solution + +The module automatically modifies form views at runtime to filter partner-related Many2one fields, showing only partners in the 'confirmed' state. This filtering is configurable via: +- Context parameters +- System configuration parameters +- Default behavior that can be overridden as needed + +The filtering is applied transparently without requiring manual domain updates on individual views, making it a robust solution that works across the entire application for any partner-related Many2one field. diff --git a/partner_stage_only_confirmed/readme/USAGE.md b/partner_stage_only_confirmed/readme/USAGE.md new file mode 100644 index 00000000000..81b6880ff28 --- /dev/null +++ b/partner_stage_only_confirmed/readme/USAGE.md @@ -0,0 +1,60 @@ +### Default Behavior + +By default, the module applies the partner filtering automatically to all applicable Many2one fields that reference partners. When selecting a partner in any form view, only partners in the 'confirmed' state will be available. + +### Configuration Methods + +The filtering behavior can be controlled in several ways: + +### 1. Context Parameter +You can disable the filtering for specific views by adding `only_confirmed_partners: false` to the context: + +**In views (XML):** +```xml + +``` + +**In Python code:** +```python +record.with_context(only_confirmed_partners=False).get_view(...) +``` + +### 2. System Configuration Parameter +You can set the global filtering behavior using system parameters: + +**Enable filtering:** +- Go to Settings > Technical > Parameters > System Parameters +- Create or update parameter: `partner_stage.only_confirmed_partners` +- Set value to any non-false value (e.g., "True", "1", "yes") + +**Disable filtering:** +- Set parameter value to: "False", "false", "0", or empty string + +### 3. Explicit Enable +To explicitly enable the filtering globally, set the system parameter to any true-like value. + +### Common Use Cases + +### Case 1: Sales Orders +When creating sales orders, ensure that only confirmed customers can be selected as the main partner, preventing orders from being created for unconfirmed/draft partners. + +### Case 2: Invoicing +When creating invoices, ensure that only confirmed partners are available as the billing address partner. + +### Case 3: Project Management +When assigning projects to contacts, restrict the selection to only confirmed partners to maintain data quality. + +### Technical Implementation + +The module uses the `get_view` method override to dynamically modify form view architectures at runtime. This approach ensures that: +- Existing views don't require modifications +- The filtering applies to all partner-related Many2one fields +- Performance impact is minimal and only affects form views +- The filtering is transparent to end users (they simply see fewer options) + +### Limitations + +- The filtering only applies to form views +- Only affects Many2one fields with comodel_name "res.partner" +- Other view types (tree, search, kanban) are not affected +- Context and system parameters provide override mechanisms for exceptions diff --git a/partner_stage_only_confirmed/static/description/icon.png b/partner_stage_only_confirmed/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/partner_stage_only_confirmed/static/description/icon.png differ diff --git a/partner_stage_only_confirmed/static/description/index.html b/partner_stage_only_confirmed/static/description/index.html new file mode 100644 index 00000000000..52a9f4892bd --- /dev/null +++ b/partner_stage_only_confirmed/static/description/index.html @@ -0,0 +1,641 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Partner Stage - Display only confirmed partners

+ +

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

+

This module extends the partner stage functionality by implementing a +filtering mechanism that ensures only confirmed partners are displayed +in Many2one fields on form views. This is particularly useful in +business scenarios where you want to prevent users from selecting +partners that are not yet confirmed in your system.

+
+

Problem Solved

+

In standard Odoo, when using Many2one fields that reference partners +(like parent_id, contact_id, etc.), all partners are available +for selection regardless of their state or confirmation status. This can +lead to:

+
    +
  • Selection of partners that are not yet properly validated
  • +
  • Accidental linking to partners that are in draft or unconfirmed +states
  • +
  • Data integrity issues when business processes require confirmed +partners
  • +
+
+
+

Solution

+

The module automatically modifies form views at runtime to filter +partner-related Many2one fields, showing only partners in the +‘confirmed’ state. This filtering is configurable via:

+
    +
  • Context parameters
  • +
  • System configuration parameters
  • +
  • Default behavior that can be overridden as needed
  • +
+

The filtering is applied transparently without requiring manual domain +updates on individual views, making it a robust solution that works +across the entire application for any partner-related Many2one field.

+

Table of contents

+ + +
+
+

Business Context

+

Many organizations implement partner lifecycle management where contacts +move through various stages before being fully confirmed and eligible +for business transactions. This workflow typically includes:

+
    +
  • Draft Stage: Initial partner entry, possibly pending validation
  • +
  • Validation Stage: Partner details being verified
  • +
  • Confirmed Stage: Fully validated partner ready for business
  • +
  • Other Stages: Potentially archived or suspended partners
  • +
+
+
+

Problem Statement

+

Without proper filtering mechanisms, users can accidentally select +unconfirmed partners in critical business operations:

+
    +
  • Creating sales orders for draft partners
  • +
  • Generating invoices for unverified contacts
  • +
  • Assigning projects to partners not yet confirmed
  • +
  • Linking transactions to partners that may not exist legally
  • +
+

This leads to potential business disruption, data quality issues, and +compliance problems.

+
+
+

Industry Scenarios

+
+
+

Financial Services

+

Banks and financial institutions must ensure that all customer +references in transactions are to properly verified and confirmed +clients. This filter prevents linking to customers who may not have +completed the required KYC (Know Your Customer) processes.

+
+
+

E-commerce and Retail

+

Online retailers often have a customer validation process before +allowing full purchasing capabilities. This module ensures that business +transactions only reference validated customers.

+
+
+

Professional Services

+

Consulting firms and professional services often have a client +onboarding process. This module ensures that only properly onboarded +clients appear in selection lists for new projects or contracts.

+
+
+

Manufacturing and Supply Chain

+

Manufacturers often have supplier qualification processes. The filtering +ensures that only qualified suppliers appear in procurement operations.

+
+
+

Regulatory Compliance

+

In many jurisdictions, businesses must maintain proper verification of +their partner relationships. This module supports:

+
    +
  • GDPR Compliance: Ensuring personal data is properly validated +before use
  • +
  • Financial Regulations: Meeting requirements for customer +verification in financial transactions
  • +
  • Contract Law: Ensuring legal capacity of contracting partners
  • +
  • Industry Standards: Meeting sector-specific requirements for +partner validation
  • +
+
+
+

Technical Context

+

The module addresses the gap between partner state management (handled +by the partner_stage module) and user interface behavior. While +partner states may be properly managed in the backend, the user +interface previously provided no automatic filtering mechanism.

+
+
+

Integration Considerations

+

This module works in conjunction with:

+
    +
  • The partner_stage module for state management
  • +
  • Standard Odoo partner management functionality
  • +
  • Custom partner validation workflows
  • +
  • Existing business processes that depend on partner confirmation +status
  • +
+
+
+

Solution Scope

+

The module provides an elegant solution that:

+
    +
  • Maintains data integrity without restricting functionality
  • +
  • Provides configuration flexibility for different business needs
  • +
  • Integrates seamlessly with existing user interfaces
  • +
  • Supports both global and granular control over the filtering behavior
  • +
+
+

Usage

+
+
+
+

Default Behavior

+

By default, the module applies the partner filtering automatically to +all applicable Many2one fields that reference partners. When selecting a +partner in any form view, only partners in the ‘confirmed’ state will be +available.

+
+
+

Configuration Methods

+

The filtering behavior can be controlled in several ways:

+
+
+

1. Context Parameter

+

You can disable the filtering for specific views by adding +only_confirmed_partners: false to the context:

+

In views (XML):

+
+<field name="parent_id" context="{'only_confirmed_partners': false}"/>
+
+

In Python code:

+
+record.with_context(only_confirmed_partners=False).get_view(...)
+
+
+
+

2. System Configuration Parameter

+

You can set the global filtering behavior using system parameters:

+

Enable filtering:

+
    +
  • Go to Settings > Technical > Parameters > System Parameters
  • +
  • Create or update parameter: partner_stage.only_confirmed_partners
  • +
  • Set value to any non-false value (e.g., “True”, “1”, “yes”)
  • +
+

Disable filtering:

+
    +
  • Set parameter value to: “False”, “false”, “0”, or empty string
  • +
+
+
+

3. Explicit Enable

+

To explicitly enable the filtering globally, set the system parameter to +any true-like value.

+
+
+

Common Use Cases

+
+
+

Case 1: Sales Orders

+

When creating sales orders, ensure that only confirmed customers can be +selected as the main partner, preventing orders from being created for +unconfirmed/draft partners.

+
+
+

Case 2: Invoicing

+

When creating invoices, ensure that only confirmed partners are +available as the billing address partner.

+
+
+

Case 3: Project Management

+

When assigning projects to contacts, restrict the selection to only +confirmed partners to maintain data quality.

+
+
+

Technical Implementation

+

The module uses the get_view method override to dynamically modify +form view architectures at runtime. This approach ensures that:

+
    +
  • Existing views don’t require modifications
  • +
  • The filtering applies to all partner-related Many2one fields
  • +
  • Performance impact is minimal and only affects form views
  • +
  • The filtering is transparent to end users (they simply see fewer +options)
  • +
+
+
+

Limitations

+
    +
  • The filtering only applies to form views
  • +
  • Only affects Many2one fields with comodel_name “res.partner”
  • +
  • Other view types (tree, search, kanban) are not affected
  • +
  • Context and system parameters provide override mechanisms for +exceptions
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

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_stage_only_confirmed/tests/__init__.py b/partner_stage_only_confirmed/tests/__init__.py new file mode 100644 index 00000000000..02aebe77d9f --- /dev/null +++ b/partner_stage_only_confirmed/tests/__init__.py @@ -0,0 +1 @@ +from . import test_partner_filter diff --git a/partner_stage_only_confirmed/tests/test_partner_filter.py b/partner_stage_only_confirmed/tests/test_partner_filter.py new file mode 100644 index 00000000000..db1f45486c7 --- /dev/null +++ b/partner_stage_only_confirmed/tests/test_partner_filter.py @@ -0,0 +1,466 @@ +from lxml import etree + +from odoo.addons.base.tests.common import BaseCommon + + +class TestConfirmedPartnerFilter(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Partner = cls.env["res.partner"] + cls.View = cls.env["ir.ui.view"] + cls.ConfigParam = cls.env["ir.config_parameter"] + cls.partner_form_view = cls.env.ref("base.view_partner_form") + cls.view_1 = cls.View.create( + { + "name": "test.partner.empty_domain", + "model": "res.partner", + "arch": """ +
+ + + +
+ """, + "type": "form", + } + ) + cls.view_2 = cls.View.create( + { + "name": "test.partner.empty_list_domain", + "model": "res.partner", + "arch": """ +
+ + + +
+ """, + "type": "form", + } + ) + + def _get_parent_field_domain(self, context): + view = self.Partner.with_context(**context).get_view( + view_id=self.partner_form_view.id, view_type="form" + ) + xml = etree.XML(view["arch"]) + parent_field = xml.xpath("//field[@name='parent_id']") + self.assertTrue(parent_field, "Expected 'parent_id' field in partner form view") + return parent_field[0].get("domain") or "" + + def test_confirmed_partner_filter_enabled(self): + """ + Test that domain is applied to Many2one fields (e.g., parent_id) when filter + is enabled + """ + domain = self._get_parent_field_domain({"only_confirmed_partners": True}) + self.assertIn("'state'", domain) + self.assertIn("'confirmed'", domain) + + def test_confirmed_partner_filter_disabled(self): + """ + Test that domain is not applied when filtering is explicitly disabled in + context + """ + domain = self._get_parent_field_domain({"only_confirmed_partners": False}) + self.assertNotIn("'state'", domain) + self.assertNotIn("'confirmed'", domain) + + def test_domain_empty_string(self): + """Test field with domain='' gets replaced with [('state', '=', 'confirmed')]""" + + result = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=self.view_1.id, view_type="form" + ) + xml = etree.XML(result["arch"]) + field = xml.xpath("//field[@name='parent_id']")[0] + domain = field.get("domain") + self.assertEqual(domain, "[('state', '=', 'confirmed')]") + + def test_domain_empty_list_string(self): + """Test field with domain='[]' gets replaced with + [('state', '=',''confirmed')]""" + + result = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=self.view_2.id, view_type="form" + ) + xml = etree.XML(result["arch"]) + field = xml.xpath("//field[@name='parent_id']")[0] + domain = field.get("domain") + self.assertEqual(domain, "[('state', '=', 'confirmed')]") + + def test_filter_respects_config_param_true(self): + """Test system parameter enabling partner filter""" + self.ConfigParam.set_param("partner_stage.only_confirmed_partners", "1") + + view = self.Partner.get_view( + view_id=self.partner_form_view.id, view_type="form" + ) + xml = etree.XML(view["arch"]) + parent_field = xml.xpath("//field[@name='parent_id']") + self.assertTrue(parent_field, "Expected 'parent_id' field in partner form view") + + domain = parent_field[0].get("domain") + self.assertIn("'state'", domain) + self.assertIn("'confirmed'", domain) + + def test_filter_respects_config_param_false(self): + """Test system parameter disabling partner filter""" + self.ConfigParam.set_param("partner_stage.only_confirmed_partners", "false") + + view = self.Partner.get_view( + view_id=self.partner_form_view.id, view_type="form" + ) + xml = etree.XML(view["arch"]) + parent_field = xml.xpath("//field[@name='parent_id']") + self.assertTrue(parent_field, "Expected 'parent_id' field in partner form view") + + domain = parent_field[0].get("domain") + self.assertNotIn("'state'", domain) + self.assertNotIn("'confirmed'", domain) + + def test_filter_respects_config_param_default(self): + """Test default behavior when system parameter is not set""" + self.ConfigParam.set_param("partner_stage.only_confirmed_partners", "") + + view = self.Partner.get_view( + view_id=self.partner_form_view.id, view_type="form" + ) + xml = etree.XML(view["arch"]) + parent_field = xml.xpath("//field[@name='parent_id']") + self.assertTrue(parent_field, "Expected 'parent_id' field in partner form view") + + domain = parent_field[0].get("domain") or "" + self.assertIn("'state'", domain) + self.assertIn("'confirmed'", domain) + + def test_filter_with_existing_domain(self): + """Test that existing domain is extended with state condition""" + # Create a view with an existing domain + view_with_existing_domain = self.View.create( + { + "name": "test.partner.existing_domain", + "model": "res.partner", + "arch": """ +
+ + + +
+ """, + "type": "form", + } + ) + + result = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=view_with_existing_domain.id, view_type="form" + ) + xml = etree.XML(result["arch"]) + field = xml.xpath("//field[@name='parent_id']")[0] + domain = field.get("domain") + + # Should contain both the original domain and the state condition + self.assertIn("'name'", domain) + self.assertIn("'test'", domain) + self.assertIn("'state'", domain) + self.assertIn("'confirmed'", domain) + + def test_filter_non_partner_many2one(self): + """Test that non-partner Many2one fields are not modified""" + # Create a view with non-partner Many2one field + test_view = self.View.create( + { + "name": "test.non_partner_field", + "model": "res.partner", + "arch": """ +
+ + + +
+ """, + "type": "form", + } + ) + + result = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=test_view.id, view_type="form" + ) + xml = etree.XML(result["arch"]) + field = xml.xpath("//field[@name='user_id']")[0] + domain = field.get("domain") or "" + + # Should not contain state condition for non-partner field + self.assertNotIn("'state'", domain) + self.assertNotIn("'confirmed'", domain) + + def test_filter_partner_many2one(self): + """Test that partner Many2one fields are modified""" + # Create a view with partner field + test_view = self.View.create( + { + "name": "test.partner_field", + "model": "res.partner", + "arch": """ +
+ + + +
+ """, + "type": "form", + } + ) + + result = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=test_view.id, view_type="form" + ) + xml = etree.XML(result["arch"]) + field = xml.xpath("//field[@name='parent_id']")[0] + domain = field.get("domain") + + # Should contain state condition for partner field + self.assertIn("'state'", domain) + self.assertIn("'confirmed'", domain) + + def test_filter_various_view_types(self): + """Test that filtering only occurs on form views""" + # Test form view (should be filtered) + result_form = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=self.partner_form_view.id, view_type="form" + ) + xml_form = etree.XML(result_form["arch"]) + parent_field_form = xml_form.xpath("//field[@name='parent_id']") + self.assertTrue(parent_field_form) + domain_form = parent_field_form[0].get("domain") or "" + self.assertIn("'state'", domain_form) + + # In Odoo 19.0, tree views are now called list views + # We'll test that the method properly handles different view types by checking + # that the filtering is only applied to form views + # Verify _filter_only_confirmed works correctly with different contexts + partner_with_filter = self.Partner.with_context(only_confirmed_partners=True) + # This should return True based on the context + self.assertTrue(partner_with_filter._filter_only_confirmed()) + + # Test with explicit false context + partner_without_filter = self.Partner.with_context( + only_confirmed_partners=False + ) + self.assertFalse(partner_without_filter._filter_only_confirmed()) + + def test_get_view_non_form_type(self): + """Test that get_view doesn't modify non-form views""" + # When view_type is not 'form', the filtering logic should not execute + # This means the method should call super().get_view without modifications + result = self.Partner.get_view(view_type="search") + self.assertIn("arch", result) # Should return a valid result + # The filtering should not apply to search views + + def test_get_view_filter_disabled(self): + """Test that get_view doesn't modify views when filtering is disabled""" + # When _filter_only_confirmed returns False, no modifications should occur + result = self.Partner.with_context(only_confirmed_partners=False).get_view( + view_id=self.partner_form_view.id, view_type="form" + ) + + # Parse the result to check that no domain was added + xml = etree.XML(result["arch"]) + parent_field = xml.xpath("//field[@name='parent_id']") + if parent_field: # If the field exists in the view + domain = parent_field[0].get("domain") or "" + # Should not contain the state condition if filtering is disabled + self.assertNotIn("'state'", domain) + self.assertNotIn("'confirmed'", domain) + + def test_get_view_non_partner_many2one_field(self): + """Test that non-partner Many2one fields are not modified""" + # Create a view with a non-partner Many2one field + test_view = self.View.create( + { + "name": "test.non_partner_field", + "model": "res.partner", + "arch": """ +
+ + + +
+ """, + "type": "form", + } + ) + + result = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=test_view.id, view_type="form" + ) + xml = etree.XML(result["arch"]) + field = xml.xpath("//field[@name='user_id']")[0] + domain = field.get("domain") or "" + + # Should not contain state condition for non-partner field + self.assertNotIn("'state'", domain) + self.assertNotIn("'confirmed'", domain) + + def test_get_view_domain_extension(self): + """Test that existing domains are properly extended""" + # Create a view with a field that has an existing domain + view_with_domain = self.View.create( + { + "name": "test.partner.with_domain", + "model": "res.partner", + "arch": """ +
+ + + +
+ """, + "type": "form", + } + ) + + result = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=view_with_domain.id, view_type="form" + ) + xml = etree.XML(result["arch"]) + field = xml.xpath("//field[@name='parent_id']")[0] + domain = field.get("domain") + + # Should contain both the original domain and the new condition + self.assertIn("'name'", domain) + self.assertIn("'state'", domain) + self.assertIn("'confirmed'", domain) + + def test_get_view_empty_domain_list(self): + """Test that empty list domain [] gets replaced properly""" + view_with_empty_domain = self.View.create( + { + "name": "test.partner.empty_domain_list", + "model": "res.partner", + "arch": """ +
+ + + +
+ """, + "type": "form", + } + ) + + result = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=view_with_empty_domain.id, view_type="form" + ) + xml = etree.XML(result["arch"]) + field = xml.xpath("//field[@name='parent_id']")[0] + domain = field.get("domain") + + # Should be replaced with the confirmed state condition + self.assertEqual(domain, "[('state', '=', 'confirmed')]") + + def test_get_view_no_domain(self): + """Test field with no initial domain gets the state condition""" + view_with_no_domain = self.View.create( + { + "name": "test.partner.no_domain", + "model": "res.partner", + "arch": """ +
+ + + +
+ """, + "type": "form", + } + ) + + result = self.Partner.with_context(only_confirmed_partners=True).get_view( + view_id=view_with_no_domain.id, view_type="form" + ) + xml = etree.XML(result["arch"]) + field = xml.xpath("//field[@name='parent_id']")[0] + domain = field.get("domain") + + # Should have the confirmed state condition added + self.assertEqual(domain, "[('state', '=', 'confirmed')]") + + def test_filter_with_non_boolean_context_value(self): + """Test behavior with non-boolean context value""" + # Test with context value that converts to True in bool() + result = self.Partner.with_context( + only_confirmed_partners="any_string" + )._filter_only_confirmed() + self.assertTrue(result) + + # Test with context value that converts to False in bool() + result = self.Partner.with_context( + only_confirmed_partners="" + )._filter_only_confirmed() + self.assertFalse(result) + + result = self.Partner.with_context( + only_confirmed_partners=0 + )._filter_only_confirmed() + self.assertFalse(result) + + def test_empty_context_config_parameter(self): + """Test behavior when config parameter is empty string""" + self.ConfigParam.set_param("partner_stage.only_confirmed_partners", "") + + # Should return default value (True) + result = self.Partner._filter_only_confirmed() + self.assertTrue(result) + + def test_false_config_parameter_values(self): + """Test behavior with false config parameter values""" + # Only these values are considered falsy according to the implementation + # Note: empty string is treated as "not set" by ir.config_parameter and + # follows the default path (returning True), so it's not included here + false_values = ["False", "false", "0"] + + for false_val in false_values: + with self.subTest(false_val=false_val): + self.ConfigParam.set_param( + "partner_stage.only_confirmed_partners", false_val + ) + result = self.Partner._filter_only_confirmed() + self.assertFalse(result, f"Failed for value: {false_val}") + + def test_true_config_parameter_values(self): + """Test behavior with various true-like config parameter values""" + # Note: "no" and "off" are treated as truthy by the current implementation + # Only "False", "false", "0" are considered falsy + # (empty string follows default path) + true_values = ["True", "true", "1", "yes", "on", "no", "off", "anything_else"] + + for true_val in true_values: + with self.subTest(true_val=true_val): + self.ConfigParam.set_param( + "partner_stage.only_confirmed_partners", true_val + ) + result = self.Partner._filter_only_confirmed() + self.assertTrue(result, f"Failed for value: {true_val}") + + def test_filter_with_integer_context(self): + """Test behavior with integer context values""" + # Test with integer 1 (should be truthy) + result = self.Partner.with_context( + only_confirmed_partners=1 + )._filter_only_confirmed() + self.assertTrue(result) + + # Test with integer 0 (should be falsy) + result = self.Partner.with_context( + only_confirmed_partners=0 + )._filter_only_confirmed() + self.assertFalse(result) + + def test_filter_with_none_context(self): + """Test behavior with None context value""" + result = self.Partner.with_context( + only_confirmed_partners=None + )._filter_only_confirmed() + self.assertFalse(result) # bool(None) is False diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000000..24d02c347a3 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-partner_stage @ git+https://github.com/OCA/partner-contact.git@refs/pull/2215/head#subdirectory=partner_stage