19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:53 +01:00
parent dc68f80d3f
commit 7221b9ac46
610 changed files with 135477 additions and 161677 deletions

View file

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.tools.mail import is_html_empty
@ -9,20 +10,21 @@ class CrmLeadLost(models.TransientModel):
_name = 'crm.lead.lost'
_description = 'Get Lost Reason'
lead_ids = fields.Many2many('crm.lead', string='Leads', context={"active_test": False})
lost_reason_id = fields.Many2one('crm.lost.reason', 'Lost Reason')
lost_feedback = fields.Html(
'Closing Note', sanitize=True
)
def action_lost_reason_apply(self):
"""Mark lead as lost and apply the loss reason"""
self.ensure_one()
leads = self.env['crm.lead'].browse(self.env.context.get('active_ids'))
if not is_html_empty(self.lost_feedback):
leads._track_set_log_message(
'<div style="margin-bottom: 4px;"><p>%s:</p>%s<br /></div>' % (
self.lead_ids._track_set_log_message(
Markup('<div style="margin-bottom: 4px;"><p>%s:</p>%s<br /></div>') % (
_('Lost Comment'),
self.lost_feedback
)
)
res = leads.action_set_lost(lost_reason_id=self.lost_reason_id.id)
res = self.lead_ids.action_set_lost(lost_reason_id=self.lost_reason_id.id)
return res

View file

@ -4,28 +4,31 @@
<field name="name">crm.lead.lost.form</field>
<field name="model">crm.lead.lost</field>
<field name="arch" type="xml">
<form string="Lost Reason">
<form string="Lost Lead">
<field name="lead_ids" invisible="1"></field>
<field name="lost_reason_id" placeholder="Select a Lost Reason..." widget="selection_badge" />
<group>
<field name="lost_reason_id" options="{'no_create_edit': True}" />
<label for="lost_feedback" class="o_form_label">Closing Note</label>
<field name="lost_feedback" placeholder="What went wrong?" nolabel="1" colspan="2"/>
</group>
<field name="lost_feedback" placeholder="What went wrong ?"/>
<footer>
<button name="action_lost_reason_apply" string="Submit" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button name="action_lost_reason_apply" string="Mark as Lost" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
</record>
<record id="crm_lead_lost_action" model="ir.actions.act_window">
<field name="name">Lost Reason</field>
<field name="type">ir.actions.act_window</field>
<field name="name">Mark Lost</field>
<field name="res_model">crm.lead.lost</field>
<field name="view_mode">form</field>
<field name="view_id" ref="crm_lead_lost_view_form"/>
<field name="target">new</field>
<field name="binding_model_id" ref="crm.model_crm_lead"/>
<field name="context">{
'dialog_size' : 'medium',
'default_lead_ids': active_ids,
}</field>
</record>
</odoo>

View file

@ -4,7 +4,7 @@
from odoo import fields, models
class CrmUpdateProbabilities(models.TransientModel):
class CrmLeadPlsUpdate(models.TransientModel):
_name = 'crm.lead.pls.update'
_description = "Update the probabilities"

View file

@ -9,15 +9,15 @@
The success rate is computed based on the stage, but you can add more fields in the statistical analysis.
</p>
<p>
<field name="pls_fields" widget="many2many_tags" placeholder="Extra fields..."/>
<field name="pls_fields" widget="many2many_tags" options="{'color_field': 'color', 'no_edit_color': True}" placeholder="Extra fields..."/>
</p>
<p>
Consider leads created as of the: <field name="pls_start_date"/>
Consider leads created as of the: <field name="pls_start_date" class="o_field_highlight"/>
</p>
<footer>
<button name="action_update_crm_lead_probabilities" type="object"
string="Confirm" class="btn-primary" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Cancel"/>
string="Update" class="btn-primary" data-hotkey="q"/>
<button special="cancel" data-hotkey="x" string="Discard"/>
</footer>
</form>
</field>

View file

@ -6,7 +6,7 @@ from odoo.exceptions import UserError
from odoo.tools.translate import _
class Lead2OpportunityPartner(models.TransientModel):
class CrmLead2opportunityPartner(models.TransientModel):
_name = 'crm.lead2opportunity.partner'
_description = 'Convert Lead to Opportunity (not in mass)'
@ -14,9 +14,9 @@ class Lead2OpportunityPartner(models.TransientModel):
def default_get(self, fields):
""" Allow support of active_id / active_model instead of jut default_lead_id
to ease window action definitions, and be backward compatible. """
result = super(Lead2OpportunityPartner, self).default_get(fields)
result = super().default_get(fields)
if not result.get('lead_id') and self.env.context.get('active_id'):
if 'lead_id' in fields and not result.get('lead_id') and self.env.context.get('active_id'):
result['lead_id'] = self.env.context.get('active_id')
if result.get('lead_id'):
@ -32,12 +32,17 @@ class Lead2OpportunityPartner(models.TransientModel):
action = fields.Selection([
('create', 'Create a new customer'),
('exist', 'Link to an existing customer'),
('nothing', 'Do not link to a customer')
], string='Related Customer', compute='_compute_action', readonly=False, store=True, compute_sudo=False)
], string='Related Customer', compute='_compute_action',
precompute=True, readonly=False, required=True, store=True, compute_sudo=False)
lead_id = fields.Many2one('crm.lead', 'Associated Lead', required=True)
lead_partner_name = fields.Char(related='lead_id.partner_name', help=False)
lead_contact_name = fields.Char(related='lead_id.contact_name', help=False)
duplicated_lead_ids = fields.Many2many(
'crm.lead', string='Opportunities', context={'active_test': False},
compute='_compute_duplicated_lead_ids', readonly=False, store=True, compute_sudo=False)
commercial_partner_id = fields.Many2one(
'res.partner', 'Company', domain=[('is_company', '=', True)],
compute='_compute_commercial_partner_id', readonly=False, store=True, compute_sudo=False)
partner_id = fields.Many2one(
'res.partner', 'Customer',
compute='_compute_partner_id', readonly=False, store=True, compute_sudo=False)
@ -60,16 +65,8 @@ class Lead2OpportunityPartner(models.TransientModel):
@api.depends('lead_id')
def _compute_action(self):
for convert in self:
if not convert.lead_id:
convert.action = 'nothing'
else:
partner = convert.lead_id._find_matching_partner()
if partner:
convert.action = 'exist'
elif convert.lead_id.contact_name:
convert.action = 'create'
else:
convert.action = 'nothing'
partner = convert.lead_id and convert.lead_id._find_matching_partner()
convert.action = 'exist' if partner else 'create'
@api.depends('lead_id', 'partner_id')
def _compute_duplicated_lead_ids(self):
@ -82,6 +79,18 @@ class Lead2OpportunityPartner(models.TransientModel):
convert.lead_id.partner_id.email if convert.lead_id.partner_id.email else convert.lead_id.email_from,
include_lost=True).ids
@api.depends('partner_id')
def _compute_commercial_partner_id(self):
for convert in self.filtered('partner_id'):
if (
not convert.commercial_partner_id
or (
convert.commercial_partner_id
and not convert.commercial_partner_id.filtered_domain([('id', 'parent_of', convert.commercial_partner_id)])
)
) and convert.partner_id.parent_id:
convert.commercial_partner_id = convert.partner_id.parent_id
@api.depends('action', 'lead_id')
def _compute_partner_id(self):
for convert in self:
@ -118,7 +127,7 @@ class Lead2OpportunityPartner(models.TransientModel):
return result_opportunity.redirect_lead_opportunity_view()
def _action_merge(self):
to_merge = self.duplicated_lead_ids
to_merge = (self.duplicated_lead_ids | self.lead_id)
result_opportunity = to_merge.merge_opportunity(auto_unlink=False)
result_opportunity.action_unarchive()
@ -138,7 +147,7 @@ class Lead2OpportunityPartner(models.TransientModel):
def _action_convert(self):
""" """
result_opportunities = self.env['crm.lead'].browse(self._context.get('active_ids', []))
result_opportunities = self.env['crm.lead'].browse(self.env.context.get('active_ids', []))
self._convert_and_allocate(result_opportunities, [self.user_id.id], team_id=self.team_id.id)
return result_opportunities[0]
@ -146,7 +155,7 @@ class Lead2OpportunityPartner(models.TransientModel):
self.ensure_one()
for lead in leads:
if lead.active and self.action != 'nothing':
if lead.active:
self._convert_handle_partner(
lead, self.action, self.partner_id.id or lead.partner_id.id)
@ -163,5 +172,6 @@ class Lead2OpportunityPartner(models.TransientModel):
# used to propagate user_id (salesman) on created partners during conversion
lead.with_context(default_user_id=self.user_id.id)._handle_partner_assignment(
force_partner_id=partner_id,
create_missing=(action == 'create')
create_missing=action == 'create',
with_parent=self.commercial_partner_id,
)

View file

@ -4,10 +4,10 @@
from odoo import api, fields, models
class Lead2OpportunityMassConvert(models.TransientModel):
class CrmLead2opportunityPartnerMass(models.TransientModel):
_name = 'crm.lead2opportunity.partner.mass'
_description = 'Convert Lead to Opportunity (in mass)'
_inherit = 'crm.lead2opportunity.partner'
_inherit = ['crm.lead2opportunity.partner']
lead_id = fields.Many2one(required=False)
lead_tomerge_ids = fields.Many2many(
@ -39,6 +39,10 @@ class Lead2OpportunityMassConvert(models.TransientModel):
for convert in self:
convert.partner_id = False
def _compute_commercial_partner_id(self):
"""Setting a company for each lead in mass mode is not supported."""
self.commercial_partner_id = False
@api.depends('user_ids')
def _compute_team_id(self):
""" When changing the user, also set a team_id or restrict team id
@ -75,17 +79,17 @@ class Lead2OpportunityMassConvert(models.TransientModel):
salesmen_ids = []
if self.user_ids:
salesmen_ids = self.user_ids.ids
return super(Lead2OpportunityMassConvert, self)._convert_and_allocate(leads, salesmen_ids, team_id=team_id)
return super()._convert_and_allocate(leads, salesmen_ids, team_id=team_id)
def action_mass_convert(self):
self.ensure_one()
if self.name == 'convert' and self.deduplicate:
# TDE CLEANME: still using active_ids from context
active_ids = self._context.get('active_ids', [])
active_ids = self.env.context.get('active_ids', [])
merged_lead_ids = set()
remaining_lead_ids = set()
for lead in self.lead_tomerge_ids:
if lead not in merged_lead_ids:
if lead.id not in merged_lead_ids:
duplicated_leads = self.env['crm.lead']._get_lead_duplicates(
partner=lead.partner_id,
email=lead.partner_id.email or lead.email_from,
@ -104,6 +108,6 @@ class Lead2OpportunityMassConvert(models.TransientModel):
def _convert_handle_partner(self, lead, action, partner_id):
if self.action == 'each_exist_or_create':
partner_id = lead._find_matching_partner(email_only=True).id
partner_id = lead._find_matching_partner().id
action = 'create'
return super(Lead2OpportunityMassConvert, self)._convert_handle_partner(lead, action, partner_id)
return super()._convert_handle_partner(lead, action, partner_id)

View file

@ -12,39 +12,40 @@
<field name="deduplicate" class="oe_inline"/>
</group>
<group string="Assign these opportunities to">
<field name="team_id" kanban_view_ref="%(sales_team.crm_team_view_kanban)s"/>
<field name="user_ids" widget="many2many_tags" domain="[('share', '=', False)]"/>
<field name="team_id" context="{'kanban_view_ref': 'sales_team.crm_team_view_kanban'}"/>
<field name="user_ids" widget="many2many_avatar_user" domain="[('share', '=', False)]"/>
<field name="force_assignment"/>
</group>
<label for="duplicated_lead_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/>
<group attrs="{'invisible': [('deduplicate', '=', False)]}">
<field name="duplicated_lead_ids" colspan="4" nolabel="1" readonly="1">
<tree create="false" delete="false">
<field name="create_date" widget="date"/>
<label for="duplicated_lead_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" invisible="not deduplicate"/>
<group invisible="not deduplicate">
<field name="duplicated_lead_ids" nolabel="1" readonly="1">
<list create="false" delete="false">
<field name="create_date"/>
<field name="name"/>
<field name="type"/>
<field name="contact_name"/>
<field name="country_id" invisible="context.get('invisible_country', True)" options="{'no_open': True, 'no_create': True}"/>
<field name="email_from"/>
<field name="type" optional="hide"/>
<field name="contact_name" optional="show"/>
<field name="country_id" column_invisible="context.get('invisible_country', True)" options="{'no_open': True, 'no_create': True}"/>
<field name="email_from" optional="show"/>
<field name="stage_id"/>
<field name="user_id"/>
<field name="team_id"/>
</tree>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="team_id" optional="hide"/>
</list>
</field>
</group>
<group attrs="{'invisible': [('name', '!=', 'convert')]}" string="Customers" col="1">
<group invisible="name != 'convert'" string="Customers" col="1">
<field name="action" class="oe_inline" widget="radio"/>
<group col="2">
<field name="partner_id"
widget="res_partner_many2one"
attrs="{'required': [('action', '=', 'exist')], 'invisible':[('action','!=','exist')]}"
invisible="action != 'exist'"
required="action == 'exist'"
context="{'show_vat': True}"
class="oe_inline"/>
</group>
</group>
<footer>
<button string="Convert to Opportunities" name="action_mass_convert" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
@ -58,6 +59,6 @@
<field name="target">new</field>
<field name="context">{}</field>
<field name="binding_model_id" ref="model_crm_lead"/>
<field name="binding_view_types">list</field>
<field name="binding_view_types">list,kanban</field>
</record>
</odoo>

View file

@ -9,34 +9,54 @@
<field name="name" widget="radio"/>
</group>
<group string="Assign this opportunity to">
<field name="user_id" domain="[('share', '=', False)]"/>
<field name="team_id" options="{'no_open': True, 'no_create': True}" kanban_view_ref="%(sales_team.crm_team_view_kanban)s"/>
<field name="user_id" widget="many2one_avatar_user" domain="[('share', '=', False)]"/>
<field name="team_id" options="{'no_open': True, 'no_create': True}" context="{'kanban_view_ref': 'sales_team.crm_team_view_kanban'}"/>
</group>
<group string="Opportunities" attrs="{'invisible': [('name', '!=', 'merge')]}">
<group string="Opportunities" invisible="name != 'merge'" col="4">
<field name="lead_id" invisible="1"/>
<field name="duplicated_lead_ids" colspan="2" nolabel="1">
<tree>
<field name="create_date" widget="date"/>
<field name="duplicated_lead_ids" nolabel="1" colspan="4"
context="{'search_default_filter_won_status_pending': 1, 'crm_lead_view_list_short': 1}">
<list>
<field name="create_date"/>
<field name="name"/>
<field name="type"/>
<field name="contact_name"/>
<field name="country_id" invisible="context.get('invisible_country', True)" options="{'no_open': True, 'no_create': True}"/>
<field name="email_from"/>
<field name="type" optional="hide"/>
<field name="contact_name" optional="show"/>
<field name="country_id" column_invisible="context.get('invisible_country', True)" options="{'no_open': True, 'no_create': True}"/>
<field name="email_from" optional="show"/>
<field name="stage_id"/>
<field name="user_id"/>
<field name="team_id" kanban_view_ref="%(sales_team.crm_team_view_kanban)s"/>
</tree>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="team_id" context="{'kanban_view_ref': 'sales_team.crm_team_view_kanban'}" optional="hide"/>
</list>
</field>
</group>
<group name="action" attrs="{'invisible': [('name', '!=', 'convert')]}" string="Customer" col="1">
<field name="action" nolabel="1" widget="radio"/>
<group col="2">
<field name="partner_id" widget="res_partner_many2one" context="{'res_partner_search_mode': 'customer', 'show_vat': True}" attrs="{'required': [('action', '=', 'exist')], 'invisible':[('action','!=','exist')]}"/>
</group>
</group>
<div name="action" invisible="name != 'convert'" class="row">
<div class="col-3">
<field name="action" nolabel="1" widget="radio"/>
</div>
<div class="col-4">
<div class="row h-50">
<t invisible="lead_partner_name or action != 'create'">
<label for="commercial_partner_id" class="col-3 p-0"/>
<field name="commercial_partner_id" class="col p-0" string="Company" placeholder="Don't link to a company"/>
</t>
<t invisible="not lead_partner_name or not lead_contact_name or action != 'create'">
<label for="lead_partner_name" class="col-3 p-0"/>
<field name="lead_partner_name" class="col p-0" string="Company" placeholder="Don't link to a company"/>
</t>
</div>
<div class="row h-50">
<t invisible="action != 'exist'">
<label for="partner_id" class="col-3 p-0"/>
<field name="partner_id" class="col p-0" widget="res_partner_many2one"
context="{'res_partner_search_mode': 'customer', 'show_vat': True}"
required="action == 'exist'"/>
</t>
</div>
</div>
</div>
<footer>
<button name="action_apply" string="Create Opportunity" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
@ -44,7 +64,6 @@
<record id="action_crm_lead2opportunity_partner" model="ir.actions.act_window">
<field name="name">Convert to opportunity</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">crm.lead2opportunity.partner</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_crm_lead2opportunity_partner"/>

View file

@ -4,7 +4,7 @@
from odoo import api, fields, models
class MergeOpportunity(models.TransientModel):
class CrmMergeOpportunity(models.TransientModel):
"""
Merge opportunities together.
If we're talking about opportunities, it's just because it makes more sense
@ -20,19 +20,22 @@ class MergeOpportunity(models.TransientModel):
@api.model
def default_get(self, fields):
""" Use active_ids from the context to fetch the leads/opps to merge.
In order to get merged, these leads/opps can't be in 'Dead' or 'Closed'
In order to get merged, these leads/opps cannot be already 'Won' (closed)
"""
record_ids = self._context.get('active_ids')
result = super(MergeOpportunity, self).default_get(fields)
record_ids = self.env.context.get('active_ids')
result = super().default_get(fields)
if record_ids:
if 'opportunity_ids' in fields:
opp_ids = self.env['crm.lead'].browse(record_ids).filtered(lambda opp: opp.probability < 100).ids
opp_ids = self.env['crm.lead'].browse(record_ids).filtered(lambda opp: opp.won_status != 'won').ids
result['opportunity_ids'] = [(6, 0, opp_ids)]
return result
opportunity_ids = fields.Many2many('crm.lead', 'merge_opportunity_rel', 'merge_id', 'opportunity_id', string='Leads/Opportunities')
opportunity_ids = fields.Many2many(
'crm.lead', 'merge_opportunity_rel', 'merge_id', 'opportunity_id',
string='Leads/Opportunities',
context={'active_test': False})
user_id = fields.Many2one('res.users', 'Salesperson', domain="[('share', '=', False)]")
team_id = fields.Many2one(
'crm.team', 'Sales Team',

View file

@ -7,27 +7,25 @@
<field name="arch" type="xml">
<form string="Merge Leads/Opportunities">
<group string="Assign opportunities to">
<field name="user_id" class="oe_inline"/>
<field name="team_id" class="oe_inline" kanban_view_ref="%(sales_team.crm_team_view_kanban)s"/>
</group>
<group string="Select Leads/Opportunities">
<field name="opportunity_ids" nolabel="1">
<tree>
<field name="create_date"/>
<field name="name"/>
<field name="type"/>
<field name="contact_name"/>
<field name="email_from" optional="hide"/>
<field name="phone" class="o_force_ltr" optional="hide"/>
<field name="stage_id"/>
<field name="user_id"/>
<field name="team_id"/>
</tree>
</field>
<field name="user_id" class="oe_inline" widget="many2one_avatar_user" domain="[('share', '=', False)]"/>
<field name="team_id" class="oe_inline" context="{'kanban_view_ref': 'sales_team.crm_team_view_kanban'}"/>
</group>
<field name="opportunity_ids" nolabel="1">
<list>
<field name="create_date"/>
<field name="name"/>
<field name="type" optional="hide"/>
<field name="contact_name" optional="show"/>
<field name="email_from" optional="show"/>
<field name="phone" class="o_force_ltr" optional="hide"/>
<field name="stage_id"/>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="team_id" optional="hide"/>
</list>
</field>
<footer>
<button name="action_merge" type="object" string="Merge" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
@ -39,7 +37,7 @@
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="model_crm_lead"/>
<field name="binding_view_types">list</field>
<field name="binding_view_types">list,kanban</field>
</record>
</odoo>