Skip to content

[16.0][ADD] sale_partner_sale_contact[_on_project]: Add sale contact to orders, invoices and projects#4039

Open
maisim wants to merge 2 commits intoOCA:16.0from
maisim:16.0-add-sale_partner_sale_contact
Open

[16.0][ADD] sale_partner_sale_contact[_on_project]: Add sale contact to orders, invoices and projects#4039
maisim wants to merge 2 commits intoOCA:16.0from
maisim:16.0-add-sale_partner_sale_contact

Conversation

@maisim
Copy link
Copy Markdown

@maisim maisim commented Nov 26, 2025

New modules: Sale Contact Management

This PR introduces two complementary modules for managing sale contacts across the sales workflow:

sale_partner_sale_contact

  • Adds a sale_contact_partner_id field to sales orders and invoices
  • Allows selecting a specific contact person from the customer's organization
  • Automatically propagates the contact from order to invoice
  • Optional display on PDF reports (configurable in settings)
  • Auto-switches customer to parent company when a contact person is selected

sale_partner_sale_contact_on_project

  • Extends the contact management to projects
  • Automatically copies the sale contact when creating projects from sales orders
  • Maintains contact information throughout the project lifecycle

Use case

In B2B scenarios, you often deal with specific contacts within a customer organization (procurement managers, project leads, etc.). While the invoice must go to the company, you need to track who you're actually working with for follow-ups, communication, and relationship management.

Design choices

  • Uses child contact hierarchy (existing Odoo pattern)
  • Auto-switch behavior prevents accidental invoicing to individuals while preserving contact info
  • Optional restriction modules available via settings for stricter enforcement
  • No modification of core partner/invoice logic - purely additive

Benefits

  • Better contact tracking across sales → invoice → project workflow
  • Cleaner reports with relevant contact information
  • Improve global consistncy and avoid to get stuffs like Analytic Account linked to a person
  • Respects commercial entity relationships while maintaining operational contacts
  • All features are backward compatible and don't affect existing workflows. 🤞

Related PRs

You can optionally use this module to limit partner_id selection to companies

@maisim maisim changed the title [16.0][ADD] sale_partner_sale_contact: Add sale contact to orders and invoices [16.0][ADD] sale_partner_sale_contact[_on_project]: Add sale contact to orders, invoices and projects Nov 26, 2025
@maisim maisim marked this pull request as draft December 2, 2025 13:52
@maisim maisim force-pushed the 16.0-add-sale_partner_sale_contact branch from 3d8b2e9 to 27c72b2 Compare December 3, 2025 10:14
@maisim maisim marked this pull request as ready for review December 3, 2025 14:16
@rrebollo
Copy link
Copy Markdown

@maisim are you sure we can achieve similar features with built-in?

@maisim
Copy link
Copy Markdown
Author

maisim commented Dec 15, 2025

@maisim are you sure we can achieve similar features with built-in?

I looked, I didn't found anything

@rousseldenis
Copy link
Copy Markdown
Contributor

@maisim As @rrebollo said, you can use the contact in standard.

Here, I've just add an invoice address if different (tested in runbot):

image

Copy link
Copy Markdown

@alexey-pelykh alexey-pelykh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this contribution, Simon. Useful B2B use case. I went through both modules in detail and have feedback on a few areas.

Functional overlap question

I see @rousseldenis and @rrebollo already raised this -- standard Odoo 16 does have invoice/delivery/contact addresses on partners, and the SO form shows invoice and delivery address fields. However, I understand this module targets a different concept: a named commercial contact person (procurement manager, project lead) that travels across SO → invoice → project, distinct from the billing/shipping addresses. If that understanding is correct, it would help to explain that distinction explicitly in the module description to avoid future confusion.

Code-level findings

1. Duplicated onchange logic across 3 models

The _onchange_partner_id_clear_sale_contact method is copy-pasted identically in sale.order, account.move, and project.project. This is a maintenance burden -- any bug fix or behavior change would need to be applied in three places.

Consider extracting this into a mixin model (e.g. sale.contact.mixin) that all three inherit. The field definition and onchange would live in one place.

2. Domain uses child_of but onchange checks parent_id

The field domain uses ('id', 'child_of', partner_id) which includes all descendants (children, grandchildren, etc.), but the onchange validation only checks self.sale_contact_partner_id.parent_id != self.partner_id -- this would incorrectly clear a valid grandchild contact. The validation should match the domain, e.g. using commercial_partner_id or walking up the parent chain.

3. Auto-switch onchange may conflict with standard onchange_partner_id

In sale.order, the standard onchange_partner_id already fires when partner_id changes and updates fiscal position, pricelist, payment terms, etc. Your onchange modifies partner_id during an onchange on partner_id, which can cause infinite recursion or unpredictable sequencing depending on the Odoo version. This is fragile -- the auto-switch from contact → company should probably be handled differently, perhaps via an @api.onchange('partner_id') with a guard flag, or by overriding the standard onchange_partner_id instead of adding a second one.

Same concern applies to account.move where the standard _onchange_partner_id is already complex.

4. Report templates use different HTML elements

The sale order report uses a <p> tag while the invoice report uses a <div> tag for the same content block. Should be consistent.

5. module_* fields in res.config.settings reference non-existent modules

The settings form includes checkboxes for module_sale_partner_id_company_only, module_account_invoice_partner_id_company_only, and module_project_partner_id_company_only. These reference modules from other PRs that aren't merged yet (and project_partner_id_company_only is in a completely different repo). Including auto-install triggers for modules that don't exist in this repo's dependency tree is risky -- if a user toggles the checkbox, Odoo will try to install a module that may not be available, resulting in an error. These should either be removed from this PR or the related modules should be merged first.

6. Missing copy=False consideration

When duplicating a sale order or invoice, sale_contact_partner_id will be copied by default. This might be desired, but it's worth being explicit about the choice -- add copy=True (to document intent) or copy=False if the contact should not carry over to duplicates.

7. account.move view uses attrs for readonly instead of states

The invoice view uses attrs="{'readonly': [('state', '!=', 'draft')]}" but the sale order view does not have any readonly condition. Both should be consistent. For the SO view, you'd typically want readonly in locked/done states too.

Test coverage

The tests are solid for the happy path. A couple of gaps:

  • No test for the auto-switch behavior (selecting a contact person as partner → auto-switch to company)
  • No test for the _prepare_invoice propagation when sale_contact_partner_id is False (the if guard -- the test test_06 covers this indirectly but doesn't assert the dict key is absent)
  • The codecov/patch check is failing, so some lines aren't covered

OCA conventions

  • Missing i18n/*.pot export via oca-gen-addon-readme (the .pot files look hand-crafted rather than exported)
  • The copyright header says "Odoo Community Association (OCA)" but the author is "OpenStudio SAS" -- for OCA contributions, copyright typically belongs to the contributing company. The copyright line in source files should match the author in manifest.

Overall a useful feature, but the duplicated onchange logic and the domain/validation mismatch should be addressed before merge.

@JordiBForgeFlow
Copy link
Copy Markdown
Member

@maisim Can you change the module to LGPL? Being a quite low level feature can be benefitial to both CE and EE users.

@maisim
Copy link
Copy Markdown
Author

maisim commented Mar 23, 2026

@maisim Can you change the module to LGPL? Being a quite low level feature can be benefitial to both CE and EE users.

No worries, I'll do it right away

@maisim
Copy link
Copy Markdown
Author

maisim commented Mar 23, 2026

Hi @alexey-pelykh,
Thank you so much for your feedback; I've started making the changes accordingly. I'll commit each point individually, which will make reviewing easier. If necessary, I'll then merge them into a single commit per module, as is currently the case.

@maisim maisim force-pushed the 16.0-add-sale_partner_sale_contact branch from 27c72b2 to 1e57658 Compare March 24, 2026 11:33
sale_contact_partner_id = fields.Many2one(
# Copied on duplication: the sale contact is part of the commercial
# context of the order and should carry over when duplicating.
copy=True,
Copy link
Copy Markdown
Contributor

@MiquelRForgeFlow MiquelRForgeFlow Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default, this already has copy=True.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @MiquelRForgeFlow,
It's a request from @alexey-pelykh be be explicit on the behavior

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, you can put it in the mixin instead :S

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes sorry

sale_contact_partner_id = fields.Many2one(
# Copied on duplication: when cloning a project for the same customer
# the sale contact should carry over, just like the partner itself.
copy=True,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as previous comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

# Copied on duplication: when cloning a project for the same customer
# the sale contact should carry over, just like the partner itself.
copy=True,
help="Contact person for this project from the sale order. "
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, the point of the mixing is avoid declaring this field everywhere, but just to customize this help, you are redeclaring it again anyway, so the mixin loses part of its purpose.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help in the mixin could be: Contact person. Only child contacts of the customer can be selected., this way you can avoid customizing for each model.

Copy link
Copy Markdown
Author

@maisim maisim Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it is better to just remove the field here?
I readd it here because I had a db error, the orm try to recreate the field, but I think I change the way I call the mixin between, let me do some tests to double check

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be ok, let me do some functional tests from the ui

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last changes broke the "auto switch" feature from the UI.
I first tried to re-add @api.onchange decorator but it seems to brake the MRO.
I had to use a new method name (instead of override _onchange_partner_id) , then remove the super()
Both onchange are now called on partner_id change.
It works.
Do you think this is the good way to do that?

@maisim maisim force-pushed the 16.0-add-sale_partner_sale_contact branch from 54316c1 to 14a3f2f Compare March 24, 2026 13:01
@maisim
Copy link
Copy Markdown
Author

maisim commented Mar 24, 2026

  1. module_* fields in res.config.settings reference non-existent modules

Any change to get concerned module merged instead of remove the refs?

@maisim maisim force-pushed the 16.0-add-sale_partner_sale_contact branch 3 times, most recently from e80d565 to 9284df8 Compare March 25, 2026 08:57
@MiquelRForgeFlow
Copy link
Copy Markdown
Contributor

Repo was greenified in #4230 so please rebase. Also, don't add last commit, thus modules are not dependencies. Also, I think it would be better to squash commits, there are too many.

@maisim
Copy link
Copy Markdown
Author

maisim commented Mar 25, 2026

Repo was greenified in #4230 so please rebase. Also, don't add last commit, thus modules are not dependencies.

It was to ease functionnal test of modules installation from the settings. I will remove it

Also, I think it would be better to squash commits, there are too many.

I did it it seprate commits to get one per asked modification. I will squash them in one commit per addon

@maisim maisim force-pushed the 16.0-add-sale_partner_sale_contact branch 2 times, most recently from 66efc34 to ea7df6b Compare March 26, 2026 11:01
Comment on lines +20 to +29
# Module installation checkboxes
module_sale_order_partner_company_only = fields.Boolean(
string="Restrict customer selection to company only in quotation and order",
)
module_account_invoice_partner_company_only = fields.Boolean(
string="Restrict customer selection to company only on invoice",
)
module_project_partner_company_only = fields.Boolean(
string="Restrict customer selection to company only in projects",
)
Copy link
Copy Markdown
Contributor

@MiquelRForgeFlow MiquelRForgeFlow Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this have to be removed, this should be added when merging the corresponding PRs.

Copy link
Copy Markdown
Author

@maisim maisim Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There, it's done.

I took a quick look and don't see any other references to the additional modules in the commits.

I think it's not the right approach anyway. It's more of a configuration issue and doesn't really make sense, as @rrebollo kindly pointed out to me. OCA/account-invoicing#2180

Either we do without it here and let everyone override their views to limit the display to companies, or I'll look into integrating it later as a configuration option in this module.
What do you think?

@maisim maisim force-pushed the 16.0-add-sale_partner_sale_contact branch from ea7df6b to 8a197e9 Compare April 10, 2026 07:15
… invoices

This module adds the ability to specify a contact person for sale orders
and invoices, separate from the main customer.

Features:
* Add Sale Contact field on sale orders and invoices
* Propagate sale contact from sale order to invoice
* Display sale contact on PDF reports (configurable)
* Configuration option in Settings > Sales to enable/disable display
* Domain restriction: only child contacts of the customer can be selected
* OnChange logic to clear incompatible contacts when customer changes

Technical implementation:
* New field sale_contact_partner_id on sale.order and account.move
* Company-level configuration field sale_display_contact_on_reports
* Report template inheritance for sale orders and invoices
* French translations included
…act to projects

This module extends sale_partner_sale_contact to automatically propagate
the sale contact from sale orders to the projects created from those orders.

Features:
* Propagate sale contact from sale order to project
* Display sale contact field on project form view
* Automatic propagation when project is created from sale order line

Technical implementation:
* Override _timesheet_create_project_prepare_values on sale.order.line
* Add sale_contact_partner_id field on project.project
* Tree and form view customization for projects
* French translations included

Depends on: sale_partner_sale_contact (in this PR)
@maisim maisim force-pushed the 16.0-add-sale_partner_sale_contact branch from 8a197e9 to 161880c Compare April 10, 2026 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants