summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorRavi Gohil <rgo@odoo.com>2016-04-26 11:56:24 +0200
committerJérome Maes <jem@openerp.com>2016-07-04 17:28:04 +0200
commitaa44700dccdc2618e0b8bc94252789264104047c (patch)
tree6c9ea08ef319ee0b9db18e315b4657b0da1cc9f7 /addons
parent768486634a90ead3d9a804f760dfe1b2b45ad633 (diff)
[MIG] crm : migration to new API
Also adapt code in crm_partner_assign according to migration of crm.lead
Diffstat (limited to 'addons')
-rw-r--r--addons/crm/data/base_action_rule_demo.xml4
-rw-r--r--addons/crm/data/crm_data.xml14
-rw-r--r--addons/crm/data/crm_lead_data.xml6
-rw-r--r--addons/crm/data/crm_lead_demo.xml22
-rw-r--r--addons/crm/data/mail_template_data.xml7
-rw-r--r--addons/crm/data/web_planner_data.xml12
-rw-r--r--addons/crm/models/calendar.py25
-rw-r--r--addons/crm/models/crm_activity.py2
-rw-r--r--addons/crm/models/crm_lead.py1716
-rw-r--r--addons/crm/models/crm_stage.py51
-rw-r--r--addons/crm/models/crm_team.py75
-rw-r--r--addons/crm/models/res_config.py112
-rw-r--r--addons/crm/models/res_partner.py79
-rw-r--r--addons/crm/models/res_users.py13
-rw-r--r--addons/crm/models/web_planner.py4
-rw-r--r--addons/crm/report/crm_activity_report.py12
-rw-r--r--addons/crm/report/crm_activity_report_views.xml15
-rw-r--r--addons/crm/report/crm_opportunity_report.py82
-rw-r--r--addons/crm/report/crm_opportunity_report_views.xml22
-rw-r--r--addons/crm/security/crm_security.xml8
-rw-r--r--addons/crm/test/crm_lead_find_stage.yml2
-rw-r--r--addons/crm/views/calendar_views.xml12
-rw-r--r--addons/crm/views/crm_activity_views.xml14
-rw-r--r--addons/crm/views/crm_lead_views.xml131
-rw-r--r--addons/crm/views/crm_stage_views.xml12
-rw-r--r--addons/crm/views/crm_team_views.xml29
-rw-r--r--addons/crm/views/crm_templates.xml7
-rw-r--r--addons/crm/views/crm_views.xml37
-rw-r--r--addons/crm/views/res_config_views.xml24
-rw-r--r--addons/crm/views/res_partner_views.xml22
-rw-r--r--addons/crm/wizard/base_partner_merge.py686
-rw-r--r--addons/crm/wizard/base_partner_merge_views.xml32
-rw-r--r--addons/crm/wizard/crm_activity_log.py19
-rw-r--r--addons/crm/wizard/crm_lead_to_opportunity.py342
-rw-r--r--addons/crm/wizard/crm_lead_to_opportunity_views.xml15
-rw-r--r--addons/crm/wizard/crm_merge_opportunities.py103
-rw-r--r--addons/crm/wizard/crm_merge_opportunities_views.xml29
-rw-r--r--addons/crm/wizard/crm_partner_binding.py112
-rw-r--r--addons/website_crm_partner_assign/models/crm_partner_assign.py7
39 files changed, 1939 insertions, 1977 deletions
diff --git a/addons/crm/data/base_action_rule_demo.xml b/addons/crm/data/base_action_rule_demo.xml
index ff83639703a..de11414c4ba 100644
--- a/addons/crm/data/base_action_rule_demo.xml
+++ b/addons/crm/data/base_action_rule_demo.xml
@@ -1,5 +1,5 @@
<?xml version="1.0"?>
-<openerp>
+<odoo>
<data noupdate="1">
<!-- demo automated actions add messge on new lead after 5 days -->
<record id="filter_draft_lead" model="ir.filters">
@@ -56,4 +56,4 @@ object.write({'team_id': sales_team.id})
<field name="server_action_ids" eval="[(6,0,[ref('action_set_team_sales_department')])]"/>
</record>
</data>
-</openerp>
+</odoo>
diff --git a/addons/crm/data/crm_data.xml b/addons/crm/data/crm_data.xml
index 683e1a6e285..c7e502e1b62 100644
--- a/addons/crm/data/crm_data.xml
+++ b/addons/crm/data/crm_data.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
+<odoo>
<data noupdate="1">
- <record model="crm.team" id="sales_team.team_sales_department">
+ <record id="sales_team.team_sales_department" model="crm.team">
<field name="use_leads">True</field>
<field name="alias_name">sales</field>
</record>
@@ -15,7 +15,7 @@
</record>
<!-- notify all employees of module installation -->
- <record model="mail.message" id="module_install_notification">
+ <record id="module_install_notification" model="mail.message">
<field name="model">mail.channel</field>
<field name="res_id" ref="mail.channel_all_employees"/>
<field name="message_type">email</field>
@@ -26,15 +26,15 @@
</record>
<!-- opportunities' lost causes -->
- <record model="crm.lost.reason" id="lost_reason_1">
+ <record id="lost_reason_1" model="crm.lost.reason">
<field name="name">Too expensive</field>
</record>
- <record model="crm.lost.reason" id="lost_reason_2">
+ <record id="lost_reason_2" model="crm.lost.reason">
<field name="name">We don't have people/skills</field>
</record>
- <record model="crm.lost.reason" id="lost_reason_3">
+ <record id="lost_reason_3" model="crm.lost.reason">
<field name="name">Not enough stock</field>
</record>
</data>
-</openerp>
+</odoo>
diff --git a/addons/crm/data/crm_lead_data.xml b/addons/crm/data/crm_lead_data.xml
index 03f5086b7b5..732d0ffa90b 100644
--- a/addons/crm/data/crm_lead_data.xml
+++ b/addons/crm/data/crm_lead_data.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
-<openerp>
+<odoo>
<data noupdate="1">
-
+
<!-- CRM-related subtypes for messaging / Chatter -->
<record id="mt_lead_create" model="mail.message.subtype">
<field name="name">Lead Created</field>
@@ -89,4 +89,4 @@
</record>
</data>
-</openerp>
+</odoo>
diff --git a/addons/crm/data/crm_lead_demo.xml b/addons/crm/data/crm_lead_demo.xml
index 8958543a761..b2895364069 100644
--- a/addons/crm/data/crm_lead_demo.xml
+++ b/addons/crm/data/crm_lead_demo.xml
@@ -1,44 +1,44 @@
<?xml version="1.0"?>
-<openerp>
+<odoo>
<data noupdate="1">
<!-- crm categories -->
- <record model="crm.lead.tag" id="categ_oppor1">
+ <record id="categ_oppor1" model="crm.lead.tag">
<field name="name">Product</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="color" eval="1"/>
</record>
- <record model="crm.lead.tag" id="categ_oppor2">
+ <record id="categ_oppor2" model="crm.lead.tag">
<field name="name">Software</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="color" eval="2"/>
</record>
- <record model="crm.lead.tag" id="categ_oppor3">
+ <record id="categ_oppor3" model="crm.lead.tag">
<field name="name">Services</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="color" eval="3"/>
</record>
- <record model="crm.lead.tag" id="categ_oppor4">
+ <record id="categ_oppor4" model="crm.lead.tag">
<field name="name">Information</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="color" eval="4"/>
</record>
- <record model="crm.lead.tag" id="categ_oppor5">
+ <record id="categ_oppor5" model="crm.lead.tag">
<field name="name">Design</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="color" eval="5"/>
</record>
- <record model="crm.lead.tag" id="categ_oppor6">
+ <record id="categ_oppor6" model="crm.lead.tag">
<field name="name">Training</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="color" eval="6"/>
</record>
- <record model="crm.lead.tag" id="categ_oppor7">
+ <record id="categ_oppor7" model="crm.lead.tag">
<field name="name">Consulting</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="color" eval="7"/>
</record>
- <record model="crm.lead.tag" id="categ_oppor8">
+ <record id="categ_oppor8" model="crm.lead.tag">
<field name="name">Other</field>
<field name="team_id" ref="sales_team.team_sales_department"/>
<field name="color" eval="8"/>
@@ -461,7 +461,7 @@ Andrew</p>]]></field>
<p>I am interested in your company's products and I plan to buy a new laptop having latest technologies as well as an affordable price.</p>
<p>Could you please send me the product catalog?</p>]]></field>
<field name="message_type">email</field>
- <field name="subtype_id" ref="mail.mt_comment"/>
+ <field name="subtype_id" ref="mail.mt_comment"/>
</record>
<record id="msg_case15_2" model="mail.message">
<field name="subject">Re: Plan to buy RedHat servers</field>
@@ -826,4 +826,4 @@ Andrew</p>]]></field>
{'install_mode': True}"/>
</data>
-</openerp>
+</odoo>
diff --git a/addons/crm/data/mail_template_data.xml b/addons/crm/data/mail_template_data.xml
index 99b6e280fb4..bd9407e1cf1 100644
--- a/addons/crm/data/mail_template_data.xml
+++ b/addons/crm/data/mail_template_data.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- notify to admin of module installation -->
@@ -90,9 +91,9 @@
<span style="font-style:italic;">You’re not alone</span>
</p>
<p dir="ltr" style="font-size:13px;margin-top:0pt;margin-bottom:8pt;">
- <span>We would be delighted to assist you along the way. Contact us at
+ <span>We would be delighted to assist you along the way. Contact us at
<a href="mailto:help@odoo.com"><span style="text-decoration:underline;">
- help@odoo.com</span></a> if you have any question. You can also discover
+ help@odoo.com</span></a> if you have any question. You can also discover
how to get the best out of Odoo CRM with our </span>
<a href="https://www.odoo.com/documentation/user/9.0/crm.html">
<span style="text-decoration:underline;">User Documentation</span></a>
@@ -114,7 +115,7 @@
<function
id="mail_template_function_module_install_crm"
- model="mail.template"
+ model="mail.template"
name="send_mail"
eval="[ref('mail_template_data_module_install_crm'), ref('base.module_crm')]"/>
diff --git a/addons/crm/data/web_planner_data.xml b/addons/crm/data/web_planner_data.xml
index f9674ae5fab..6b66ebc82e8 100644
--- a/addons/crm/data/web_planner_data.xml
+++ b/addons/crm/data/web_planner_data.xml
@@ -341,12 +341,12 @@
<t t-call="web_planner.page">
<t t-set="page_title">Incoming Emails</t>
<p class="text-muted">
- There are several ways for your company to generate leads with Odoo CRM.
- One of them is using your company's generic email address as a trigger
- to create a new lead in the system. In Odoo, each one of your sales teams
- is linked to its own email address from which prospects can reach them.
- For example, if the personal email address of your Direct team is
- direct@mycompany.example.com, every email sent when a new prospect
+ There are several ways for your company to generate leads with Odoo CRM.
+ One of them is using your company's generic email address as a trigger
+ to create a new lead in the system. In Odoo, each one of your sales teams
+ is linked to its own email address from which prospects can reach them.
+ For example, if the personal email address of your Direct team is
+ direct@mycompany.example.com, every email sent when a new prospect
contacts you will automatically create a new opportunity into the sales team.
That's the easiest way to integrate Odoo with third party apps.
diff --git a/addons/crm/models/calendar.py b/addons/crm/models/calendar.py
index 996f6681a51..6c704e2ce39 100644
--- a/addons/crm/models/calendar.py
+++ b/addons/crm/models/calendar.py
@@ -1,21 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import fields, osv
-import logging
-_logger = logging.getLogger(__name__)
+from odoo import api, fields, models
-class calendar_event(osv.Model):
- """ Model for Calendar Event """
+class CalendarEvent(models.Model):
+
_inherit = 'calendar.event'
- _columns = {
- 'opportunity_id': fields.many2one('crm.lead', 'Opportunity', domain="[('type', '=', 'opportunity')]"),
- }
- def create(self, cr, uid, vals, context=None):
- res = super(calendar_event, self).create(cr, uid, vals, context=context)
- obj = self.browse(cr, uid, res, context=context)
- if obj.opportunity_id:
- self.pool.get('crm.lead').log_meeting(cr, uid, [obj.opportunity_id.id], obj.name, obj.start, obj.duration, context=context)
- return res
+ opportunity_id = fields.Many2one('crm.lead', 'Opportunity', domain="[('type', '=', 'opportunity')]")
+
+ @api.model
+ def create(self, vals):
+ event = super(CalendarEvent, self).create(vals)
+ if event.opportunity_id:
+ event.opportunity_id.log_meeting(event.name, event.start, event.duration)
+ return event
diff --git a/addons/crm/models/crm_activity.py b/addons/crm/models/crm_activity.py
index 79dcf01795d..c5202785784 100644
--- a/addons/crm/models/crm_activity.py
+++ b/addons/crm/models/crm_activity.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp import api, fields, models
+from odoo import api, fields, models
class CrmActivity(models.Model):
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 3e5f0829d5f..c997249c453 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -1,24 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
from datetime import datetime, timedelta, date
from dateutil.relativedelta import relativedelta
-import logging
-from operator import itemgetter
-from werkzeug import url_encode
-
-from openerp import SUPERUSER_ID
-from openerp import tools, api
-from openerp.addons.base.res.res_partner import FormatAddress
-from openerp.addons.crm.models import crm_stage
-from openerp.osv import fields, osv
-from openerp.tools.translate import _
-from openerp.tools import email_re, email_split
-from openerp.exceptions import UserError, AccessError
+
+from odoo import api, fields, models, tools
+from odoo.tools.translate import _
+from odoo.tools import email_re, email_split
+from odoo.exceptions import UserError, AccessError
+
+from odoo.addons.base.res.res_partner import FormatAddress
+from odoo.addons.crm.models import crm_stage
_logger = logging.getLogger(__name__)
-CRM_LEAD_FIELDS_TO_MERGE = ['name',
+CRM_LEAD_FIELDS_TO_MERGE = [
+ 'name',
'partner_id',
'campaign_id',
'company_id',
@@ -51,31 +49,110 @@ CRM_LEAD_FIELDS_TO_MERGE = ['name',
'partner_name']
-class crm_lead(FormatAddress, osv.osv):
- """ CRM Lead Case """
+class Lead(FormatAddress, models.Model):
+
_name = "crm.lead"
_description = "Lead/Opportunity"
_order = "priority desc,date_action,id desc"
_inherit = ['mail.thread', 'ir.needaction_mixin', 'utm.mixin']
_mail_mass_mailing = _('Leads / Opportunities')
- def _get_default_probability(self, cr, uid, context=None):
- """ Gives default probability """
- stage_id = self._get_default_stage_id(cr, uid, context=context)
+ def _default_probability(self):
+ stage_id = self._default_stage_id()
if stage_id:
- return self.pool['crm.stage'].browse(cr, uid, stage_id, context=context).probability
- else:
- return 10
+ return self.env['crm.stage'].browse(stage_id).probability
+ return 10
+
+ def _default_stage_id(self):
+ team = self.env['crm.team'].sudo()._get_default_team_id(user_id=self.env.uid)
+ return self.stage_find(team.id, [('fold', '=', False)])
+
+ name = fields.Char('Opportunity', required=True, index=True)
+ partner_id = fields.Many2one('res.partner', string='Customer', track_visibility='onchange', index=True,
+ help="Linked partner (optional). Usually created when converting the lead.")
+ active = fields.Boolean('Active', default=True)
+ date_action_last = fields.Datetime('Last Action', readonly=True)
+ date_action_next = fields.Datetime('Next Action', readonly=True)
+ email_from = fields.Char('Email', help="Email address of the contact", index=True)
+ team_id = fields.Many2one('crm.team', string='Sales Team', oldname='section_id', default=lambda self: self.env['crm.team'].sudo()._get_default_team_id(user_id=self.env.uid),
+ index=True, track_visibility='onchange', help='When sending mails, the default email address is taken from the sales team.')
+ kanban_state = fields.Selection([('grey', 'No next activity planned'), ('red', 'Next activity late'), ('green', 'Next activity is planned')],
+ string='Activity State', compute='_compute_kanban_state')
+ email_cc = fields.Text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma")
+ description = fields.Text('Notes')
+ create_date = fields.Datetime('Create Date', readonly=True)
+ write_date = fields.Datetime('Update Date', readonly=True)
+ tag_ids = fields.Many2many('crm.lead.tag', 'crm_lead_tag_rel', 'lead_id', 'tag_id', string='Tags', help="Classify and analyze your lead/opportunity categories like: Training, Service")
+ contact_name = fields.Char('Contact Name')
+ partner_name = fields.Char("Customer Name", index=True, help='The name of the future partner company that will be created while converting the lead into opportunity')
+ opt_out = fields.Boolean(string='Opt-Out', oldname='optout',
+ help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
+ "Filter 'Available for Mass Mailing' allows users to filter the leads when performing mass mailing.")
+ type = fields.Selection([('lead', 'Lead'), ('opportunity', 'Opportunity')], index=True, required=True,
+ default=lambda self: 'lead' if self.env['res.users'].has_group('crm.group_use_lead') else 'opportunity',
+ help="Type is used to separate Leads and Opportunities")
+ priority = fields.Selection(crm_stage.AVAILABLE_PRIORITIES, string='Rating', index=True, default=lambda *a: crm_stage.AVAILABLE_PRIORITIES[0][0])
+ date_closed = fields.Datetime('Closed Date', readonly=True, copy=False)
+
+ stage_id = fields.Many2one('crm.stage', string='Stage', track_visibility='onchange', index=True,
+ domain="['|', ('team_id', '=', False), ('team_id', '=', team_id)]",
+ default=lambda self: self._default_stage_id())
+ user_id = fields.Many2one('res.users', string='Salesperson', index=True, track_visibility='onchange', default=lambda self: self.env.user)
+ referred = fields.Char('Referred By')
+
+ date_open = fields.Datetime('Assigned', readonly=True)
+ day_open = fields.Float(compute='_compute_day_open', string='Days to Assign', store=True)
+ day_close = fields.Float(compute='_compute_day_close', string='Days to Close', store=True)
+ date_last_stage_update = fields.Datetime(string='Last Stage Update', index=True, default=fields.Datetime.now)
+ date_conversion = fields.Datetime('Conversion Date', readonly=True)
+
+ # Messaging and marketing
+ message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact")
+
+ # Only used for type opportunity
+ probability = fields.Float('Probability', group_operator="avg", default=lambda self: self._default_probability())
+ planned_revenue = fields.Float('Expected Revenue', track_visibility='always')
+ date_deadline = fields.Date('Expected Closing', help="Estimate of the date on which the opportunity will be won.")
+
+ # CRM Actions
+ next_activity_id = fields.Many2one("crm.activity", string="Next Activity", index=True)
+ date_action = fields.Date('Next Activity Date', index=True)
+ title_action = fields.Char('Next Activity Summary')
+
+ color = fields.Integer('Color Index', default=0)
+ partner_address_name = fields.Char('Partner Contact Name', related='partner_id.name', readonly=True)
+ partner_address_email = fields.Char('Partner Contact Email', related='partner_id.email', readonly=True)
+ company_currency = fields.Many2one(string='Currency', related='company_id.currency_id', readonly=True, relation="res.currency")
+ user_email = fields.Char('User Email', related='user_id.email', readonly=True)
+ user_login = fields.Char('User Login', related='user_id.login', readonly=True)
+
+ # Fields for address, due to separation from crm and res.partner
+ street = fields.Char('Street')
+ street2 = fields.Char('Street2')
+ zip = fields.Char('Zip', change_default=True)
+ city = fields.Char('City')
+ state_id = fields.Many2one("res.country.state", string='State')
+ country_id = fields.Many2one('res.country', string='Country')
+ phone = fields.Char('Phone')
+ fax = fields.Char('Fax')
+ mobile = fields.Char('Mobile')
+ function = fields.Char('Job Position')
+ title = fields.Many2one('res.partner.title')
+ company_id = fields.Many2one('res.company', string='Company', index=True, default=lambda self: self.env.user.company_id.id)
+ meeting_count = fields.Integer('# Meetings', compute='_compute_meeting_count')
+ lost_reason = fields.Many2one('crm.lost.reason', string='Lost Reason', index=True, track_visibility='onchange')
- def _get_default_stage_id(self, cr, uid, context=None):
- """ Gives default stage_id """
- team_id = self.pool['crm.team']._get_default_team_id(cr, SUPERUSER_ID, context=context, user_id=uid)
- return self.stage_find(cr, uid, [], team_id, [('fold', '=', False)], context=context)
+ _sql_constraints = [
+ ('check_probability', 'check(probability >= 0 and probability <= 100)', 'The probability of closing the deal should be between 0% and 100%!')
+ ]
- def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
- access_rights_uid = access_rights_uid or uid
- stage_obj = self.pool.get('crm.stage')
- order = stage_obj._order
+ @api.multi
+ def _read_group_stage_ids(self, domain, read_group_order=None, access_rights_uid=None):
+ if access_rights_uid:
+ self = self.sudo(access_rights_uid)
+
+ Stage = self.env['crm.stage']
+ order = Stage._order
# lame hack to allow reverting search, should just work in the trivial case
if read_group_order == 'stage_id desc':
order = "%s desc" % order
@@ -83,206 +160,97 @@ class crm_lead(FormatAddress, osv.osv):
# - ('id', 'in', 'ids'): add columns that should be present
# - OR ('fold', '=', False): add default columns that are not folded
# - OR ('team_ids', '=', team_id), ('fold', '=', False) if team_id: add team columns that are not folded
- team_id = context and context.get('default_team_id') or False
+ team_id = self._context.get('default_team_id', False)
if team_id:
- search_domain = ['|', ('id', 'in', ids), '|', ('team_id', '=', False), ('team_id', '=', team_id)]
+ search_domain = ['|', ('id', 'in', self.ids), '|', ('team_id', '=', False), ('team_id', '=', team_id)]
else:
- search_domain = ['|', ('id', 'in', ids), ('team_id', '=', False)]
+ search_domain = ['|', ('id', 'in', self.ids), ('team_id', '=', False)]
# perform search
- stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
- result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
+ stage_ids = Stage._search(search_domain, order=order, access_rights_uid=access_rights_uid)
+ stages = Stage.browse(stage_ids)
+ result = stages.name_get()
# restore order of the search
- result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
-
+ order_mapping = dict((item[0], stage_ids.index(item[0])) for item in result) # match stage_id with its index
+ result.sort(key=lambda item: order_mapping.get(item[0]))
fold = {}
- for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
+ for stage in stages:
fold[stage.id] = stage.fold or False
return result, fold
- def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
- if context and context.get('opportunity_id'):
- action = self.get_formview_action(cr, user, context['opportunity_id'], context=context)
- if action.get('views') and any(view_id for view_id in action['views'] if view_id[1] == view_type):
- view_id = next(view_id[0] for view_id in action['views'] if view_id[1] == view_type)
- res = super(crm_lead, self).fields_view_get(cr, user, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
- if view_type == 'form':
- res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
- return res
-
_group_by_full = {
'stage_id': _read_group_stage_ids
}
- def _compute_kanban_state(self, cr, uid, ids, fields, args, context=None):
- result = {}
+ @api.multi
+ def _compute_kanban_state(self):
today = date.today()
- for lead in self.browse(cr, uid, ids, context=context):
- result[lead.id] = 'red'
+ for lead in self:
+ kanban_state = 'red'
if lead.date_action:
- lead_date = datetime.strptime(lead.date_action, tools.DEFAULT_SERVER_DATE_FORMAT).date()
+ lead_date = fields.Date.from_string(lead.date_action)
if lead_date >= today:
- result[lead.id] = 'green'
+ kanban_state = 'green'
elif lead_date < today:
- result[lead.id] = 'grey'
- return result
-
- def _compute_day(self, cr, uid, ids, fields, args, context=None):
- """
- :return dict: difference between current date and log date
- """
- res = {}
- for lead in self.browse(cr, uid, ids, context=context):
- for field in fields:
- res[lead.id] = {}
- duration = 0
- ans = False
- if field == 'day_open':
- if lead.date_open:
- date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
- date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
- ans = date_open - date_create
- elif field == 'day_close':
- if lead.date_closed:
- date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
- date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
- ans = date_close - date_create
- if ans:
- duration = abs(int(ans.days))
- res[lead.id][field] = duration
- return res
-
- def _meeting_count(self, cr, uid, ids, field_name, arg, context=None):
- Event = self.pool['calendar.event']
- return {
- opp_id: Event.search_count(cr,uid, [('opportunity_id', '=', opp_id)], context=context)
- for opp_id in ids
- }
-
- def _get_company_currency(self, cr, uid, context=None):
- if context is None:
- context = {}
- return self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id.id
-
- _columns = {
- 'partner_id': fields.many2one('res.partner', 'Customer', ondelete='set null', track_visibility='onchange',
- select=True, help="Linked partner (optional). Usually created when converting the lead."),
-
- 'id': fields.integer('ID', readonly=True),
- 'name': fields.char('Opportunity', required=True, select=1),
- 'active': fields.boolean('Active', required=False),
- 'date_action_last': fields.datetime('Last Action', readonly=1),
- 'date_action_next': fields.datetime('Next Action', readonly=1),
- 'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
- 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id',
- select=True, track_visibility='onchange', help='When sending mails, the default email address is taken from the sales team.'),
- 'kanban_state': fields.function(
- _compute_kanban_state, string='Activity State', type="selection",
- selection=[('grey', 'No next activity planned'), ('red', 'Next activity late'), ('green', 'Next activity is planned')]),
- 'create_date': fields.datetime('Creation Date', readonly=True),
- 'email_cc': fields.text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
- 'description': fields.text('Notes'),
- 'write_date': fields.datetime('Update Date', readonly=True),
- 'tag_ids': fields.many2many('crm.lead.tag', 'crm_lead_tag_rel', 'lead_id', 'tag_id', 'Tags', help="Classify and analyze your lead/opportunity categories like: Training, Service"),
- 'contact_name': fields.char('Contact Name', size=64),
- 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
- 'opt_out': fields.boolean('Opt-Out', oldname='optout',
- help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
- "Filter 'Available for Mass Mailing' allows users to filter the leads when performing mass mailing."),
- 'type': fields.selection(
- [('lead', 'Lead'), ('opportunity', 'Opportunity')],
- string='Type', select=True, required=True,
- help="Type is used to separate Leads and Opportunities"),
- 'priority': fields.selection(crm_stage.AVAILABLE_PRIORITIES, 'Rating', select=True),
- 'date_closed': fields.datetime('Closed Date', readonly=True, copy=False),
- 'stage_id': fields.many2one('crm.stage', 'Stage', track_visibility='onchange', select=True,
- domain="['|', ('team_id', '=', False), ('team_id', '=', team_id)]"),
- 'user_id': fields.many2one('res.users', 'Salesperson', select=True, track_visibility='onchange'),
- 'referred': fields.char('Referred By'),
- 'date_open': fields.datetime('Assigned', readonly=True),
- 'day_open': fields.function(_compute_day, string='Days to Assign',
- multi='day_open', type="float",
- store={'crm.lead': (lambda self, cr, uid, ids, c={}: ids, ['date_open'], 10)}),
- 'day_close': fields.function(_compute_day, string='Days to Close',
- multi='day_open', type="float",
- store={'crm.lead': (lambda self, cr, uid, ids, c={}: ids, ['date_closed'], 10)}),
- 'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
- 'date_conversion': fields.datetime('Conversion Date', readonly=1),
-
- # Messaging and marketing
- 'message_bounce': fields.integer('Bounce', help="Counter of the number of bounced emails for this contact"),
- # Only used for type opportunity
- 'probability': fields.float('Probability', group_operator="avg"),
- 'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
- 'date_deadline': fields.date('Expected Closing', help="Estimate of the date on which the opportunity will be won."),
- # CRM Actions
- 'next_activity_id': fields.many2one("crm.activity", "Next Activity", select=True),
- 'date_action': fields.date('Next Activity Date', select=True),
- 'title_action': fields.char('Next Activity Summary'),
-
- 'color': fields.integer('Color Index'),
- 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
- 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
- 'company_currency': fields.related('company_id', 'currency_id', type='many2one', string='Currency', readonly=True, relation="res.currency"),
- 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
- 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
-
- # Fields for address, due to separation from crm and res.partner
- 'street': fields.char('Street'),
- 'street2': fields.char('Street2'),
- 'zip': fields.char('Zip', change_default=True, size=24),
- 'city': fields.char('City'),
- 'state_id': fields.many2one("res.country.state", 'State'),
- 'country_id': fields.many2one('res.country', 'Country'),
- 'phone': fields.char('Phone'),
- 'fax': fields.char('Fax'),
- 'mobile': fields.char('Mobile'),
- 'function': fields.char('Job Position'),
- 'title': fields.many2one('res.partner.title', 'Title'),
- 'company_id': fields.many2one('res.company', 'Company', select=1),
- 'meeting_count': fields.function(_meeting_count, string='# Meetings', type='integer'),
- 'lost_reason': fields.many2one('crm.lost.reason', 'Lost Reason', select=True, track_visibility='onchange'),
- }
-
- _defaults = {
- 'active': 1,
- 'type': lambda s, cr, uid, c: 'lead' if s.pool['res.users'].has_group(cr, uid, 'crm.group_use_lead') else 'opportunity',
- 'company_currency': _get_company_currency,
- 'user_id': lambda s, cr, uid, c: uid,
- 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
- 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, SUPERUSER_ID, context=c, user_id=uid),
- 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
- 'priority': lambda *a: crm_stage.AVAILABLE_PRIORITIES[0][0],
- 'probability': lambda s, cr, uid, c: s._get_default_probability(cr, uid, c),
- 'color': 0,
- 'date_last_stage_update': fields.datetime.now,
- }
-
- _sql_constraints = [
- ('check_probability', 'check(probability >= 0 and probability <= 100)', 'The probability of closing the deal should be between 0% and 100%!')
- ]
-
- def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
+ kanban_state = 'grey'
+ lead.kanban_state = kanban_state
+
+ @api.depends('date_open')
+ def _compute_day_open(self):
+ """ Compute difference between create date and open date """
+ for lead in self.filtered(lambda l: l.date_open):
+ date_create = fields.Datetime.from_string(lead.create_date)
+ date_open = fields.Datetime.from_string(lead.date_open)
+ lead.day_open = abs((date_open - date_create).days)
+
+ @api.depends('date_closed')
+ def _compute_day_close(self):
+ """ Compute difference between current date and log date """
+ for lead in self.filtered(lambda l: l.date_closed):
+ date_create = fields.Datetime.from_string(lead.create_date)
+ date_close = fields.Datetime.from_string(lead.date_closed)
+ lead.day_close = abs((date_close - date_create).days)
+
+ @api.multi
+ def _compute_meeting_count(self):
+ meeting_data = self.env['calendar.event'].read_group([('opportunity_id', 'in', self.ids)], ['opportunity_id'], ['opportunity_id'])
+ mapped_data = dict([(m['opportunity_id'][0], m['opportunity_id_count']) for m in meeting_data])
+ for lead in self:
+ lead.meeting_count = mapped_data.get(lead.id, 0)
+
+ @api.model
+ def _onchange_stage_id_values(self, stage_id):
+ """ returns the new values when stage_id has changed """
if not stage_id:
- return {'value': {}}
- stage = self.pool['crm.stage'].browse(cr, uid, stage_id, context=context)
- if not stage.on_change:
- return {'value': {}}
- return {'value': {'probability': stage.probability}}
+ return {}
+ stage = self.env['crm.stage'].browse(stage_id)
+ if stage.on_change:
+ return {'probability': stage.probability}
+ return {}
- def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
+ @api.onchange('stage_id')
+ def _onchange_stage_id(self):
+ values = self._onchange_stage_id_values(self.stage_id.id if self.stage_id else False)
+ self.update(values)
+
+ def _onchange_partner_id_values(self, partner_id):
+ """ returns the new values when partner_id has changed """
values = {}
if partner_id:
- partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
- partner_name = (partner.parent_id and partner.parent_id.name) or (partner.is_company and partner.name) or False
+ partner = self.env['res.partner'].browse(partner_id)
+
+ partner_name = partner.parent_id.name
+ if not partner_name and partner.is_company:
+ partner_name = partner.name
+
values = {
'partner_name': partner_name,
- 'contact_name': (not partner.is_company and partner.name) or False,
- 'title': partner.title and partner.title.id or False,
+ 'contact_name': partner.name if not partner.is_company else False,
+ 'title': partner.title.id,
'street': partner.street,
'street2': partner.street2,
'city': partner.city,
- 'state_id': partner.state_id and partner.state_id.id or False,
- 'country_id': partner.country_id and partner.country_id.id or False,
+ 'state_id': partner.state_id.id,
+ 'country_id': partner.country_id.id,
'email_from': partner.email,
'phone': partner.phone,
'mobile': partner.mobile,
@@ -290,40 +258,178 @@ class crm_lead(FormatAddress, osv.osv):
'zip': partner.zip,
'function': partner.function,
}
- return {'value': values}
-
- def on_change_user(self, cr, uid, ids, user_id, context=None):
- """ When changing the user, also set a team_id or restrict team id
- to the ones user_id is member of. """
- if not context:
- context = {}
- if user_id and context.get('team_id'):
- team = self.pool['crm.team'].browse(cr, uid, context['team_id'], context=context)
+ return values
+
+ @api.onchange('partner_id')
+ def _onchange_partner_id(self):
+ values = self._onchange_partner_id_values(self.partner_id.id if self.partner_id else False)
+ self.update(values)
+
+ @api.model
+ def _onchange_user_values(self, user_id):
+ """ returns new values when user_id has changed """
+ if user_id and self._context.get('team_id'):
+ team = self.env['crm.team'].browse(self._context['team_id'])
if user_id in team.member_ids.ids:
return {}
- team_id = self.pool['crm.team']._get_default_team_id(cr, uid, context=context, user_id=user_id)
- return {'value': {'team_id': team_id}}
-
- def stage_find(self, cr, uid, cases, team_id, domain=None, order='sequence', context=None):
- """ Override of the base.stage method
- Parameter of the stage search taken from the lead:
- - type: stage type must be the same or 'both'
- - team_id: if set, stages must belong to this team or
- be a default stage; if not set, stages must be default
- stages
+ team_id = self.env['crm.team']._get_default_team_id(user_id=user_id)
+ return {'team_id': team_id}
+
+ @api.onchange('user_id')
+ def _onchange_user_id(self):
+ """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """
+ values = self._onchange_user_values(self.user_id.id)
+ self.update(values)
+
+ @api.onchange('state_id')
+ def _onchange_state(self):
+ if self.state_id:
+ self.country_id = self.state_id.country_id.id
+
+ @api.onchange('next_activity_id')
+ def _onchange_next_activity_id(self):
+ values = {
+ 'title_action': False,
+ 'date_action': False,
+ }
+ if self.next_activity_id:
+ values['title_action'] = self.next_activity_id.description
+ if self.next_activity_id.days:
+ values['date_action'] = fields.Datetime.to_string(datetime.now() + timedelta(days=self.next_activity_id.days))
+ self.update(values)
+
+ # ----------------------------------------
+ # ORM override (CRUD, fields_view_get, ...)
+ # ----------------------------------------
+
+ @api.model
+ def create(self, vals):
+ context = dict(self._context or {})
+ if vals.get('type') and not self._context.get('default_type'):
+ context['default_type'] = vals.get('type')
+ if vals.get('team_id') and not self._context.get('default_team_id'):
+ context['default_team_id'] = vals.get('team_id')
+ if vals.get('user_id') and 'date_open' not in vals:
+ vals['date_open'] = fields.Datetime.now()
+ # context: no_log, because subtype already handle this
+ return super(Lead, self.with_context(context, mail_create_nolog=True)).create(vals)
+
+ @api.multi
+ def write(self, vals):
+ # stage change: update date_last_stage_update
+ if 'stage_id' in vals:
+ vals['date_last_stage_update'] = fields.Datetime.now()
+ if vals.get('user_id') and 'date_open' not in vals:
+ vals['date_open'] = fields.Datetime.now()
+ # stage change with new stage: update probability and date_closed
+ if vals.get('stage_id') and 'probability' not in vals:
+ vals.update(self._onchange_stage_id_values(vals.get('stage_id')))
+ if vals.get('probability') >= 100 or not vals.get('active', True):
+ vals['date_closed'] = fields.Datetime.now()
+ elif vals.get('probability') < 100:
+ vals['date_closed'] = False
+ return super(Lead, self).write(vals)
+
+ @api.multi
+ def copy(self, default=None):
+ self.ensure_one()
+ # set default value in context, if not already set (Put stage to 'new' stage)
+ context = dict(self._context)
+ context.setdefault('default_type', self.type)
+ context.setdefault('default_team_id', self.team_id.id)
+ # Set date_open to today if it is an opp
+ default = default or {}
+ default['date_open'] = fields.Datetime.now() if self.type == 'opportunity' else False
+ return super(Lead, self.with_context(context)).copy(default=default)
+
+ @api.model
+ def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
+ if self._context.get('opportunity_id'):
+ opportunity = self.browse(self._context['opportunity_id'])
+ action = opportunity.get_formview_action()
+ if action.get('views') and any(view_id for view_id in action['views'] if view_id[1] == view_type):
+ view_id = next(view_id[0] for view_id in action['views'] if view_id[1] == view_type)
+ res = super(Lead, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
+ if view_type == 'form':
+ res['arch'] = self.fields_view_get_address(res['arch'])
+ return res
+
+ # ----------------------------------------
+ # Actions Methods
+ # ----------------------------------------
+
+ @api.multi
+ def action_set_lost(self):
+ """ Lost semantic: probability = 0, active = False """
+ return self.write({'probability': 0, 'active': False})
+
+ # TODO JEM : check, nad remove this
+ # Backward compatibility
+ case_mark_lost = action_set_lost
+
+ @api.multi
+ def action_set_active(self):
+ return self.write({'active': True})
+
+ @api.multi
+ def action_set_unactive(self):
+ return self.write({'active': False})
+
+ @api.multi
+ def action_set_won(self):
+ """ Won semantic: probability = 100 (active untouched) """
+ for lead in self:
+ stage_id = lead.stage_find(lead.team_id.id, [('probability', '=', 100.0), ('on_change', '=', True)])
+ lead.write({'stage_id': stage_id, 'probability': 100})
+ return True
+
+ # TODO JEM : check, nad remove this
+ # Backward compatibility
+ case_mark_won = action_set_won
+
+ @api.multi
+ def action_schedule_meeting(self):
+ """ Open meeting's calendar view to schedule meeting on current opportunity.
+ :return dict: dictionary value for created Meeting view
+ """
+ self.ensure_one()
+ action = self.env['ir.actions.act_window'].for_xml_id('calendar', 'action_calendar_event')
+ partner_ids = self.env.user.partner_id.ids
+ if self.partner_id:
+ partner_ids.append(self.partner_id.id)
+ action['context'] = {
+ 'search_default_opportunity_id': self.id if self.type == 'opportunity' else False,
+ 'default_opportunity_id': self.id if self.type == 'opportunity' else False,
+ 'default_partner_id': self.partner_id.id,
+ 'default_partner_ids': partner_ids,
+ 'default_team_id': self.team_id.id,
+ 'default_name': self.name,
+ }
+ return action
+
+ @api.multi
+ def close_dialog(self):
+ return {'type': 'ir.actions.act_window_close'}
+
+ # ----------------------------------------
+ # Business Methods
+ # ----------------------------------------
+ # TODO JEM : team_id should be optional
+ @api.multi
+ def stage_find(self, team_id, domain=None, order='sequence'):
+ """ Determine the stage of the current lead with its teams, the given domain and the given team_id
+ :param team_id
+ :param domain : base search domain for stage
+ :returns crm.stage record identifier
"""
- if isinstance(cases, (int, long)):
- cases = self.browse(cr, uid, cases, context=context)
- if context is None:
- context = {}
- # collect all team_ids
+ # collect all team_ids by adding given one, and the ones related to the current leads
team_ids = set()
if team_id:
team_ids.add(team_id)
- for lead in cases:
+ for lead in self:
if lead.team_id:
team_ids.add(lead.team_id.id)
- # OR all team_ids
+ # generate the domain
if team_ids:
search_domain = ['|', ('team_id', '=', False), ('team_id', 'in', list(team_ids))]
else:
@@ -331,102 +437,56 @@ class crm_lead(FormatAddress, osv.osv):
# AND with the domain in parameter
search_domain += list(domain)
# perform search, return the first found
- stage_ids = self.pool.get('crm.stage').search(cr, uid, search_domain, order=order, limit=1, context=context)
- if stage_ids:
- return stage_ids[0]
+ stage = self.env['crm.stage'].search(search_domain, order=order, limit=1)
+ if stage:
+ return stage.id
return False
- def action_set_lost(self, cr, uid, ids, context=None):
- """ Lost semantic: probability = 0, active = False """
- return self.write(cr, uid, ids, {'probability': 0, 'active': False}, context=context)
- # Backward compatibility
- case_mark_lost = action_set_lost
-
- def action_set_active(self, cr, uid, ids, context=None):
- return self.write(cr, uid, ids, {'active': True}, context=context)
-
- def action_set_unactive(self, cr, uid, ids, context=None):
- return self.write(cr, uid, ids, {'active': False}, context=context)
-
- def action_set_won(self, cr, uid, ids, context=None):
- """ Won semantic: probability = 100 (active untouched) """
- stages_leads = {}
- for lead in self.browse(cr, uid, ids, context=context):
- stage_id = self.stage_find(cr, uid, [lead], lead.team_id.id or False, [('probability', '=', 100.0), ('on_change', '=', True)], context=context)
- if stage_id:
- if stages_leads.get(stage_id):
- stages_leads[stage_id].append(lead.id)
- else:
- stages_leads[stage_id] = [lead.id]
- for stage_id, lead_ids in stages_leads.items():
- self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
- return self.write(cr, uid, ids, {'probability': 100}, context=context)
- # Backward compatibility
- case_mark_won = action_set_won
-
- def onchange_next_activity_id(self, cr, uid, ids, next_activity_id, context=None):
- if not next_activity_id:
- return {'value': {
- 'title_action': False,
- 'date_action': False,
- }}
- activity = self.pool['crm.activity'].browse(cr, uid, next_activity_id, context=context)
- date_action = False
- if activity.days:
- date_action = (datetime.now() + timedelta(days=activity.days)).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
- return {'value': {
- 'title_action': activity.description,
- 'date_action': date_action,
- }}
-
- def _merge_get_result_type(self, cr, uid, opps, context=None):
- """
- Define the type of the result of the merge. If at least one of the
- element to merge is an opp, the resulting new element will be an opp.
- Otherwise it will be a lead.
-
- We'll directly use a list of browse records instead of a list of ids
- for performances' sake: it will spare a second browse of the
- leads/opps.
-
- :param list opps: list of browse records containing the leads/opps to process
- :return string type: the type of the final element
+ @api.multi
+ def _merge_get_result_type(self):
+ """ Define the type of the result of the merge. If at least one of the
+ element to merge is an opp, the resulting new element will be an opp.
+ Otherwise it will be a lead.
+ We'll directly use a list of browse records instead of a list of ids
+ for performances' sake: it will spare a second browse of the
+ leads/opps.
+
+ :param list opps: list of browse records containing the leads/opps to process
+ :return string type: the type of the final element
"""
- for opp in opps:
- if (opp.type == 'opportunity'):
+ for record in self:
+ if record.type == 'opportunity':
return 'opportunity'
-
return 'lead'
- def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
- """
- Prepare lead/opp data into a dictionary for merging. Different types
- of fields are processed in different ways:
- - text: all the values are concatenated
- - m2m and o2m: those fields aren't processed
- - m2o: the first not null value prevails (the other are dropped)
- - any other type of field: same as m2o
-
- :param list ids: list of ids of the leads to process
- :param list fields: list of leads' fields to process
- :return dict data: contains the merged values
+ @api.multi
+ def _merge_data(self, fields):
+ """ Prepare lead/opp data into a dictionary for merging. Different types
+ of fields are processed in different ways:
+ - text: all the values are concatenated
+ - m2m and o2m: those fields aren't processed
+ - m2o: the first not null value prevails (the other are dropped)
+ - any other type of field: same as m2o
+
+ :param fields: list of fields to process
+ :return dict data: contains the merged values of the new opportunity
"""
- opportunities = self.browse(cr, uid, ids, context=context)
-
- def _get_first_not_null(attr):
+ # helpers
+ def _get_first_not_null(attr, opportunities):
for opp in opportunities:
- if hasattr(opp, attr) and bool(getattr(opp, attr)):
- return getattr(opp, attr)
+ val = opp[attr]
+ if val:
+ return val
return False
- def _get_first_not_null_id(attr):
- res = _get_first_not_null(attr)
- return res and res.id or False
+ def _get_first_not_null_id(attr, opportunities):
+ res = _get_first_not_null(attr, opportunities)
+ return res.id if res else False
- def _concat_all(attr):
- return '\n\n'.join(filter(lambda x: x, [getattr(opp, attr) or '' for opp in opportunities if hasattr(opp, attr)]))
+ def _concat_all(attr, opportunities):
+ return '\n\n'.join(filter(None, (opp[attr] for opp in opportunities)))
- # Process the fields' values
+ # process the fields' values
data = {}
for field_name in fields:
field = self._fields.get(field_name)
@@ -435,313 +495,339 @@ class crm_lead(FormatAddress, osv.osv):
if field.type in ('many2many', 'one2many'):
continue
elif field.type == 'many2one':
- data[field_name] = _get_first_not_null_id(field_name) # !!
+ data[field_name] = _get_first_not_null_id(field_name, self) # take the first not null
elif field.type == 'text':
- data[field_name] = _concat_all(field_name) #not lost
+ data[field_name] = _concat_all(field_name, self) # contact field of all opportunities
else:
- data[field_name] = _get_first_not_null(field_name) #not lost
+ data[field_name] = _get_first_not_null(field_name, self)
- # Define the resulting type ('lead' or 'opportunity')
- data['type'] = self._merge_get_result_type(cr, uid, opportunities, context)
+ # define the resulting type ('lead' or 'opportunity')
+ data['type'] = self._merge_get_result_type()
return data
- def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
- body = []
- if title:
- body.append("%s\n" % (title))
-
+ @api.one
+ def _mail_body(self, fields):
+ """ generate the message body with the changed values
+ :param fields : list of fields to track
+ :returns the body of the message for the current crm.lead
+ """
+ title = "%s : %s\n" % (_('Merged opportunity') if self.type == 'opportunity' else _('Merged lead'), self.name)
+ body = [title]
for field_name in fields:
field = self._fields.get(field_name)
if field is None:
continue
- value = ''
+ value = self[field_name]
if field.type == 'selection':
- if callable(field.selection):
- key = field.selection(self, cr, uid, context=context)
- else:
- key = field.selection
- value = dict(key).get(lead[field_name], lead[field_name])
+ value = dict(field.get_values(self.env)).get(value, value)
elif field.type == 'many2one':
- if lead[field_name]:
- value = lead[field_name].sudo().name_get()[0][1]
+ if value:
+ value = value.sudo().name_get()[0][1]
elif field.type == 'many2many':
- if lead[field_name]:
- for val in lead[field_name]:
- field_value = val.sudo().name_get()[0][1]
- value += field_value + ","
- else:
- value = lead[field_name]
-
+ if value:
+ value = ','.join(
+ val.name_get()[0][1]
+ for val in value.sudo()
+ )
body.append("%s: %s" % (field.string, value or ''))
return "<br/>".join(body + ['<br/>'])
- def _merge_notify(self, cr, uid, opportunity_id, opportunities, context=None):
+ @api.multi
+ def _merge_notify(self, opportunities):
+ """ Create a message gathering merged leads/opps informations. Using message_post, send a
+ message explaining which fields has been merged and their new value. `self` is the
+ resulting merge crm.lead record.
+ :param opportunities : recordset of merged crm.lead
+ :returns mail.message posted on resulting crm.lead
"""
- Create a message gathering merged leads/opps information.
+ # TODO JEM: mail template should be used instead of fix body, subject text
+ self.ensure_one()
+ # mail message's subject
+ result_type = opportunities._merge_get_result_type()
+ merge_message = _('Merged leads') if result_type == 'lead' else _('Merged opportunities')
+ subject = merge_message + ": " + ", ".join(opportunities.mapped('name'))
+ # message bodies
+ message_bodies = opportunities._mail_body(CRM_LEAD_FIELDS_TO_MERGE)
+ message_body = "\n\n".join(message_bodies)
+ return self.message_post(body=message_body, subject=subject)
+
+ @api.multi
+ def _merge_opportunity_history(self, opportunities):
+ """ Move mail.message from the given opportunities to the current one. `self` is the
+ crm.lead record destination for message of `opportunities`.
+ :param opportunities : recordset of crm.lead to move the messages
"""
- #TOFIX: mail template should be used instead of fix body, subject text
- details = []
- result_type = self._merge_get_result_type(cr, uid, opportunities, context)
- if result_type == 'lead':
- merge_message = _('Merged leads')
- else:
- merge_message = _('Merged opportunities')
- subject = [merge_message]
+ self.ensure_one()
for opportunity in opportunities:
- subject.append(opportunity.name)
- title = "%s : %s" % (opportunity.type == 'opportunity' and _('Merged opportunity') or _('Merged lead'), opportunity.name)
- fields = list(CRM_LEAD_FIELDS_TO_MERGE)
- details.append(self._mail_body(cr, uid, opportunity, fields, title=title, context=context))
-
- # Chatter message's subject
- subject = subject[0] + ": " + ", ".join(subject[1:])
- details = "\n\n".join(details)
- return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
-
- def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
- message = self.pool.get('mail.message')
- for opportunity in opportunities:
- for history in opportunity.message_ids:
- message.write(cr, uid, history.id, {
- 'res_id': opportunity_id,
- 'subject' : _("From %s : %s") % (opportunity.name, history.subject)
- }, context=context)
-
+ for message in opportunity.message_ids:
+ message.write({
+ 'res_id': self.id,
+ 'subject': _("From %s : %s") % (opportunity.name, message.subject)
+ })
return True
- def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
- attach_obj = self.pool.get('ir.attachment')
+ @api.multi
+ def _merge_opportunity_attachments(self, opportunities):
+ """ Move attachments of given opportunities to the current one `self`, and rename
+ the attachments having same name than native ones.
+ :param opportunities : recordset of merged crm.lead
+ """
+ self.ensure_one()
# return attachments of opportunity
def _get_attachments(opportunity_id):
- attachment_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
- return attach_obj.browse(cr, uid, attachment_ids, context=context)
+ return self.env['ir.attachment'].search([('res_model', '=', self._name), ('res_id', '=', opportunity_id)])
- first_attachments = _get_attachments(opportunity_id)
- #counter of all attachments to move. Used to make sure the name is different for all attachments
+ first_attachments = _get_attachments(self.id)
+ # counter of all attachments to move. Used to make sure the name is different for all attachments
count = 1
for opportunity in opportunities:
attachments = _get_attachments(opportunity.id)
for attachment in attachments:
- values = {'res_id': opportunity_id,}
+ values = {'res_id': self.id}
for attachment_in_first in first_attachments:
if attachment.name == attachment_in_first.name:
- values['name'] = "%s (%s)" % (attachment.name, count,),
- count+=1
+ values['name'] = "%s (%s)" % (attachment.name, count)
+ count += 1
attachment.write(values)
return True
- def get_duplicated_leads(self, cr, uid, ids, partner_id, include_lost=False, context=None):
+ @api.multi
+ def merge_dependences(self, opportunities):
+ """ Merge dependences (messages, attachments, ...). These dependences will be
+ transfered to `self`, the most important lead.
+ :param opportunities : recordset of opportunities to transfert. Does
+ not include `self`.
"""
- Search for opportunities that have the same partner and that arent done or cancelled
+ self.ensure_one()
+ self._merge_notify(opportunities)
+ self._merge_opportunity_history(opportunities)
+ self._merge_opportunity_attachments(opportunities)
+
+ #TODO JEM : make it return record
+ @api.multi
+ def merge_opportunity(self, user_id=False, team_id=False):
+ """ Merge opportunities in one. Different cases of merge:
+ - merge leads together = 1 new lead
+ - merge at least 1 opp with anything else (lead or opp) = 1 new opp
+ The resulting lead/opportunity will be the most important one (based on its confidence level)
+ updated with values from other opportunities to merge.
+ :param user_id : the id of the saleperson. If not given, will be determined by `_merge_data`.
+ :param team : the id of the sales team. If not given, will be determined by `_merge_data`.
+ :return int id: id of the resulting lead/opportunity
"""
- lead = self.browse(cr, uid, ids[0], context=context)
- email = lead.partner_id and lead.partner_id.email or lead.email_from
- return self.pool['crm.lead']._get_duplicated_leads_by_emails(cr, uid, partner_id, email, include_lost=include_lost, context=context)
-
- def _get_duplicated_leads_by_emails(self, cr, uid, partner_id, email, include_lost=False, context=None):
- """
- Search for opportunities that have the same partner and that arent done or cancelled
- """
- partner_match_domain = []
- for email in set(email_split(email) + [email]):
- partner_match_domain.append(('email_from', '=ilike', email))
- if partner_id:
- partner_match_domain.append(('partner_id', '=', partner_id))
- partner_match_domain = ['|'] * (len(partner_match_domain) - 1) + partner_match_domain
- if not partner_match_domain:
- return []
- domain = partner_match_domain
- if not include_lost:
- domain += ['&', ('active', '=', True), ('probability', '<', 100)]
- return self.search(cr, uid, domain, context=context)
-
- def merge_dependences(self, cr, uid, highest, opportunities, context=None):
- self._merge_notify(cr, uid, highest, opportunities, context=context)
- self._merge_opportunity_history(cr, uid, highest, opportunities, context=context)
- self._merge_opportunity_attachments(cr, uid, highest, opportunities, context=context)
-
- def merge_opportunity(self, cr, uid, ids, user_id=False, team_id=False, context=None):
- """
- Different cases of merge:
- - merge leads together = 1 new lead
- - merge at least 1 opp with anything else (lead or opp) = 1 new opp
-
- :param list ids: leads/opportunities ids to merge
- :return int id: id of the resulting lead/opp
- """
- if context is None:
- context = {}
-
- if len(ids) <= 1:
+ if len(self.ids) <= 1:
raise UserError(_('Please select more than one element (lead or opportunity) from the list view.'))
- opportunities = self.browse(cr, uid, ids, context=context)
- sequenced_opps = []
# Sorting the leads/opps according to the confidence level of its stage, which relates to the probability of winning it
# The confidence level increases with the stage sequence, except when the stage probability is 0.0 (Lost cases)
# An Opportunity always has higher confidence level than a lead, unless its stage probability is 0.0
- for opportunity in opportunities:
+ sequenced_opps = []
+ for opportunity in self:
sequence = -1
if opportunity.stage_id and opportunity.stage_id.on_change:
sequence = opportunity.stage_id.sequence
sequenced_opps.append(((int(sequence != -1 and opportunity.type == 'opportunity'), sequence, -opportunity.id), opportunity))
-
sequenced_opps.sort(reverse=True)
- opportunities = map(itemgetter(1), sequenced_opps)
- ids = [opportunity.id for opportunity in opportunities]
- highest = opportunities[0]
- opportunities_rest = opportunities[1:]
- tail_opportunities = opportunities_rest
+ sorted_opportunities = [item[1] for item in sequenced_opps] # get sorted list of crm.lead record
+
+ # get SORTED recordset of head and tail, and complete list
+ opportunities = self.browse([opportunity.id for opportunity in sorted_opportunities])
+ opportunities_head = sorted_opportunities[0]
+ opportunities_tail = self.browse([opportunity.id for opportunity in sorted_opportunities[1:]])
- fields = list(CRM_LEAD_FIELDS_TO_MERGE)
- merged_data = self._merge_data(cr, uid, ids, highest, fields, context=context)
+ # merge all the sorted opportunity. This means the value of
+ # the first (head opp) will be a priority.
+ merged_data = opportunities._merge_data(CRM_LEAD_FIELDS_TO_MERGE)
+ # force value for saleperson and sales team
if user_id:
merged_data['user_id'] = user_id
if team_id:
merged_data['team_id'] = team_id
- # Merge notifications about loss of information
- opportunities = [highest]
- opportunities.extend(opportunities_rest)
+ # merge other data (mail.message, attachments, ...) from tail into head
+ opportunities_head.merge_dependences(opportunities_tail)
- self.merge_dependences(cr, uid, highest.id, tail_opportunities, context=context)
-
- # Check if the stage is in the stages of the sales team. If not, assign the stage with the lowest sequence
+ # check if the stage is in the stages of the sales team. If not, assign the stage with the lowest sequence
if merged_data.get('team_id'):
- team_stage_ids = self.pool.get('crm.stage').search(cr, uid, ['|', ('team_id', '=', merged_data['team_id']), ('team_id', '=', False)], order='sequence', context=context)
+ team_stage_ids = self.env['crm.stage'].search(['|', ('team_id', '=', merged_data['team_id']), ('team_id', '=', False)], order='sequence')
+ if merged_data.get('stage_id') not in team_stage_ids.ids:
+ merged_data['stage_id'] = team_stage_ids[0].id if team_stage_ids else False
+
+ # write merged data into first opportunity
+ opportunities_head.write(merged_data)
- if merged_data.get('stage_id') not in team_stage_ids:
- merged_data['stage_id'] = team_stage_ids and team_stage_ids[0] or False
- # Write merged data into first opportunity
- self.write(cr, uid, [highest.id], merged_data, context=context)
- # Delete tail opportunities
- # We use the SUPERUSER to avoid access rights issues because as the user had the rights to see the records it should be safe to do so
- self.unlink(cr, SUPERUSER_ID, [x.id for x in tail_opportunities], context=context)
+ # delete tail opportunities
+ # we use the SUPERUSER to avoid access rights issues because as the user had the rights to see the records it should be safe to do so
+ opportunities_tail.sudo().unlink()
- return highest.id
+ return opportunities_head.id
- def _convert_opportunity_data(self, cr, uid, lead, customer, team_id=False, context=None):
+ @api.multi
+ def get_duplicated_leads(self, partner_id, include_lost=False):
+ """ Search for opportunities that have the same partner and that arent done or cancelled
+ :param partner_id : partner to search
+ """
+ self.ensure_one()
+ email = self.partner_id.email or self.email_from
+ return self._get_duplicated_leads_by_emails(partner_id, email, include_lost=include_lost)
+
+ @api.model
+ def _get_duplicated_leads_by_emails(self, partner_id, email, include_lost=False):
+ """ Search for opportunities that have the same partner and that arent done or cancelled """
+ partner_match_domain = []
+ for email in set(email_split(email) + [email]):
+ partner_match_domain.append(('email_from', '=ilike', email))
+ if partner_id:
+ partner_match_domain.append(('partner_id', '=', partner_id))
+ partner_match_domain = ['|'] * (len(partner_match_domain) - 1) + partner_match_domain
+ if not partner_match_domain:
+ return []
+ domain = partner_match_domain
+ if not include_lost:
+ domain += ['&', ('active', '=', True), ('probability', '<', 100)]
+ return self.search(domain)
+
+ @api.multi
+ def _convert_opportunity_data(self, customer, team_id=False):
+ """ Extract the data from a lead to create the opportunity
+ :param customer : res.partner record
+ :param team_id : identifier of the sales team to determine the stage
+ """
if not team_id:
- team_id = lead.team_id and lead.team_id.id or False
- val = {
- 'planned_revenue': lead.planned_revenue,
- 'probability': lead.probability,
- 'name': lead.name,
- 'partner_id': customer and customer.id or False,
+ team_id = self.team_id.id if self.team_id else False
+ value = {
+ 'planned_revenue': self.planned_revenue,
+ 'probability': self.probability,
+ 'name': self.name,
+ 'partner_id': customer.id if customer else False,
'type': 'opportunity',
- 'date_open': fields.datetime.now(),
- 'email_from': customer and customer.email or lead.email_from,
- 'phone': customer and customer.phone or lead.phone,
- 'date_conversion': fields.datetime.now(),
+ 'date_open': fields.Datetime.now(),
+ 'email_from': customer.email if customer else self.email_from,
+ 'phone': customer.phone if customer else self.phone,
+ 'date_conversion': fields.Datetime.now(),
}
- if not lead.stage_id:
- stage_id = self.stage_find(cr, uid, [lead], team_id, [], context=context)
- val['stage_id'] = stage_id
+ if not self.stage_id:
+ stage_id = self.stage_find(team_id, [])
+ value['stage_id'] = stage_id
if stage_id:
- val['probability'] = self.pool['crm.stage'].browse(cr, uid, stage_id, context=context).probability
- return val
+ value['probability'] = self.env['crm.stage'].browse(stage_id).probability
+ return value
- def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, team_id=False, context=None):
+ @api.multi
+ def convert_opportunity(self, partner_id, user_ids=False, team_id=False):
customer = False
if partner_id:
- partner = self.pool.get('res.partner')
- customer = partner.browse(cr, uid, partner_id, context=context)
- for lead in self.browse(cr, uid, ids, context=context):
+ customer = self.env['res.partner'].browse(partner_id)
+ for lead in self:
if not lead.active or lead.probability == 100:
continue
- vals = self._convert_opportunity_data(cr, uid, lead, customer, team_id, context=context)
- self.write(cr, uid, [lead.id], vals, context=context)
+ vals = lead._convert_opportunity_data(customer, team_id)
+ lead.write(vals)
if user_ids or team_id:
- self.allocate_salesman(cr, uid, ids, user_ids, team_id, context=context)
+ self.allocate_salesman(user_ids, team_id)
return True
- def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
- partner = self.pool.get('res.partner')
- vals = {'name': name,
- 'user_id': lead.user_id.id,
- 'comment': lead.description,
- 'team_id': lead.team_id.id or False,
+ @api.multi
+ def _lead_create_contact(self, name, is_company, parent_id=False):
+ """ extract data from lead to create a partner
+ :param name : furtur name of the partner
+ :param is_company : True if the partner is a company
+ :param parent_id : id of the parent partner (False if no parent)
+ :returns res.partner record
+ """
+ email_split = tools.email_split(self.email_from)
+ values = {
+ 'name': name,
+ 'user_id': self.user_id.id,
+ 'comment': self.description,
+ 'team_id': self.team_id.id,
'parent_id': parent_id,
- 'phone': lead.phone,
- 'mobile': lead.mobile,
- 'email': tools.email_split(lead.email_from) and tools.email_split(lead.email_from)[0] or False,
- 'fax': lead.fax,
- 'title': lead.title and lead.title.id or False,
- 'function': lead.function,
- 'street': lead.street,
- 'street2': lead.street2,
- 'zip': lead.zip,
- 'city': lead.city,
- 'country_id': lead.country_id and lead.country_id.id or False,
- 'state_id': lead.state_id and lead.state_id.id or False,
+ 'phone': self.phone,
+ 'mobile': self.mobile,
+ 'email': email_split[0] if email_split else False,
+ 'fax': self.fax,
+ 'title': self.title.id,
+ 'function': self.function,
+ 'street': self.street,
+ 'street2': self.street2,
+ 'zip': self.zip,
+ 'city': self.city,
+ 'country_id': self.country_id.id,
+ 'state_id': self.state_id.id,
'is_company': is_company,
'type': 'contact'
}
- partner = partner.create(cr, uid, vals, context=context)
- return partner
-
- def _create_lead_partner(self, cr, uid, lead, context=None):
- contact_id = False
- contact_name = lead.contact_name or lead.email_from and self.pool.get('res.partner')._parse_partner_name(lead.email_from, context=context)[0] or False
- if lead.partner_name:
- partner_company_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
- elif lead.partner_id:
- partner_company_id = lead.partner_id.id
+ return self.env['res.partner'].create(values)
+
+ @api.multi
+ def _create_lead_partner(self):
+ """ Create a partner from lead data
+ :returns res.partner record
+ """
+ contact_name = self.contact_name
+ if not contact_name:
+ contact_name = self.env['res.partner']._parse_partner_name(self.email_from)[0] if self.email_from else False
+
+ if self.partner_name:
+ partner_company = self._lead_create_contact(self.partner_name, True)
+ elif self.partner_id:
+ partner_company = self.partner_id
else:
- partner_company_id = False
+ partner_company = None
if contact_name:
- contact_id = self._lead_create_contact(cr, uid, lead, contact_name, False, partner_company_id, context=context)
-
- partner_id = contact_id or partner_company_id or self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
- return partner_id
-
- def handle_partner_assignation(self, cr, uid, ids, action='create', partner_id=False, context=None):
- """
- Handle partner assignation during a lead conversion.
- if action is 'create', create new partner with contact and assign lead to new partner_id.
- otherwise assign lead to the specified partner_id
-
- :param list ids: leads/opportunities ids to process
- :param string action: what has to be done regarding partners (create it, assign an existing one, or nothing)
- :param int partner_id: partner to assign if any
- :return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
+ return self._lead_create_contact(contact_name, False, partner_company.id if partner_company else False)
+
+ if partner_company:
+ return partner_company
+ return self._lead_create_contact(self.name, False)
+
+ @api.multi
+ def handle_partner_assignation(self, action='create', partner_id=False):
+ """ Handle partner assignation during a lead conversion.
+ if action is 'create', create new partner with contact and assign lead to new partner_id.
+ otherwise assign lead to the specified partner_id
+
+ :param list ids: leads/opportunities ids to process
+ :param string action: what has to be done regarding partners (create it, assign an existing one, or nothing)
+ :param int partner_id: partner to assign if any
+ :return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
"""
partner_ids = {}
- for lead in self.browse(cr, uid, ids, context=context):
+ for lead in self:
# If the action is set to 'create' and no partner_id is set, create a new one
if lead.partner_id:
partner_ids[lead.id] = lead.partner_id.id
if action == 'create':
- partner_id = self._create_lead_partner(cr, uid, lead, context)
- self.pool['res.partner'].write(cr, uid, partner_id, {'team_id': lead.team_id and lead.team_id.id or False})
+ partner = lead._create_lead_partner()
+ partner_id = partner.id
+ partner.write({'team_id': lead.team_id.id if lead.team_id else False})
if partner_id:
lead.write({'partner_id': partner_id})
partner_ids[lead.id] = partner_id
return partner_ids
- def allocate_salesman(self, cr, uid, ids, user_ids=None, team_id=False, context=None):
- """
- Assign salesmen and salesteam to a batch of leads. If there are more
- leads than salesmen, these salesmen will be assigned in round-robin.
- E.g.: 4 salesmen (S1, S2, S3, S4) for 6 leads (L1, L2, ... L6). They
- will be assigned as followed: L1 - S1, L2 - S2, L3 - S3, L4 - S4,
- L5 - S1, L6 - S2.
-
- :param list ids: leads/opportunities ids to process
- :param list user_ids: salesmen to assign
- :param int team_id: salesteam to assign
- :return bool
+ @api.multi
+ def allocate_salesman(self, user_ids=None, team_id=False):
+ """ Assign salesmen and salesteam to a batch of leads. If there are more
+ leads than salesmen, these salesmen will be assigned in round-robin.
+ E.g.: 4 salesmen (S1, S2, S3, S4) for 6 leads (L1, L2, ... L6). They
+ will be assigned as followed: L1 - S1, L2 - S2, L3 - S3, L4 - S4,
+ L5 - S1, L6 - S2.
+
+ :param list ids: leads/opportunities ids to process
+ :param list user_ids: salesmen to assign
+ :param int team_id: salesteam to assign
+ :return bool
"""
index = 0
-
- for lead_id in ids:
+ for lead in self:
value = {}
if team_id:
value['team_id'] = team_id
@@ -750,291 +836,95 @@ class crm_lead(FormatAddress, osv.osv):
# Cycle through user_ids
index = (index + 1) % len(user_ids)
if value:
- self.write(cr, uid, [lead_id], value, context=context)
+ lead.write(value)
return True
- def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
- models_data = self.pool.get('ir.model.data')
-
+ @api.multi
+ def redirect_opportunity_view(self):
+ self.ensure_one()
# Get opportunity views
- dummy, form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
- dummy, tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
+ form_view = self.env.ref('crm.crm_case_form_view_oppor')
+ tree_view = self.env.ref('crm.crm_case_tree_view_oppor')
return {
'name': _('Opportunity'),
'view_type': 'form',
'view_mode': 'tree, form',
'res_model': 'crm.lead',
'domain': [('type', '=', 'opportunity')],
- 'res_id': int(opportunity_id),
+ 'res_id': self.id,
'view_id': False,
- 'views': [(form_view or False, 'form'),
- (tree_view or False, 'tree'), (False, 'kanban'),
- (False, 'calendar'), (False, 'graph')],
+ 'views': [
+ (form_view.id if form_view else False, 'form'),
+ (tree_view.id if tree_view else False, 'tree'),
+ (False, 'kanban'),
+ (False, 'calendar'),
+ (False, 'graph')
+ ],
'type': 'ir.actions.act_window',
'context': {'default_type': 'opportunity'}
}
- def redirect_lead_view(self, cr, uid, lead_id, context=None):
- models_data = self.pool.get('ir.model.data')
-
+ @api.multi
+ def redirect_lead_view(self):
+ self.ensure_one()
# Get lead views
- dummy, form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_leads')
- dummy, tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_leads')
+ form_view = self.env.ref('crm.crm_case_form_view_leads')
+ tree_view = self.env.ref('crm.crm_case_tree_view_leads')
return {
'name': _('Lead'),
'view_type': 'form',
'view_mode': 'tree, form',
'res_model': 'crm.lead',
'domain': [('type', '=', 'lead')],
- 'res_id': int(lead_id),
+ 'res_id': self.id,
'view_id': False,
- 'views': [(form_view or False, 'form'),
- (tree_view or False, 'tree'),
- (False, 'calendar'), (False, 'graph')],
+ 'views': [
+ (form_view.id if form_view else False, 'form'),
+ (tree_view.id if form_view else False, 'tree'),
+ (False, 'calendar'),
+ (False, 'graph')
+ ],
'type': 'ir.actions.act_window',
}
- def action_schedule_meeting(self, cr, uid, ids, context=None):
- """
- Open meeting's calendar view to schedule meeting on current opportunity.
- :return dict: dictionary value for created Meeting view
- """
- lead = self.browse(cr, uid, ids[0], context)
- res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'calendar', 'action_calendar_event', context)
- partner_ids = [self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id]
- if lead.partner_id:
- partner_ids.append(lead.partner_id.id)
- res['context'] = {
- 'search_default_opportunity_id': lead.type == 'opportunity' and lead.id or False,
- 'default_opportunity_id': lead.type == 'opportunity' and lead.id or False,
- 'default_partner_id': lead.partner_id and lead.partner_id.id or False,
- 'default_partner_ids': partner_ids,
- 'default_team_id': lead.team_id and lead.team_id.id or False,
- 'default_name': lead.name,
+ @api.model
+ def get_empty_list_help(self, help):
+ additionnal_context = {
+ 'empty_list_help_model': 'crm.team',
+ 'empty_list_help_id': self._context.get('default_team_id', False),
+ 'empty_list_help_document_name': _("opportunities")
}
- return res
-
- def create(self, cr, uid, vals, context=None):
- context = dict(context or {})
- if vals.get('type') and not context.get('default_type'):
- context['default_type'] = vals.get('type')
- if vals.get('team_id') and not context.get('default_team_id'):
- context['default_team_id'] = vals.get('team_id')
- if vals.get('user_id') and 'date_open' not in vals:
- vals['date_open'] = fields.datetime.now()
-
- # context: no_log, because subtype already handle this
- create_context = dict(context, mail_create_nolog=True)
- return super(crm_lead, self).create(cr, uid, vals, context=create_context)
-
- def write(self, cr, uid, ids, vals, context=None):
- # stage change: update date_last_stage_update
- if 'stage_id' in vals:
- vals['date_last_stage_update'] = fields.datetime.now()
- if vals.get('user_id') and 'date_open' not in vals:
- vals['date_open'] = fields.datetime.now()
- # stage change with new stage: update probability and date_closed
- if vals.get('stage_id') and 'probability' not in vals:
- onchange_stage_values = self.onchange_stage_id(cr, uid, ids, vals.get('stage_id'), context=context)['value']
- vals.update(onchange_stage_values)
- if vals.get('probability') >= 100 or not vals.get('active', True):
- vals['date_closed'] = fields.datetime.now()
- elif 'probability' in vals and vals['probability'] < 100:
- vals['date_closed'] = False
- return super(crm_lead, self).write(cr, uid, ids, vals, context=context)
-
- def copy(self, cr, uid, id, default=None, context=None):
- if not default:
- default = {}
- if not context:
- context = {}
- lead = self.browse(cr, uid, id, context=context)
- local_context = dict(context)
- local_context.setdefault('default_type', lead.type)
- local_context.setdefault('default_team_id', lead.team_id.id)
- if lead.type == 'opportunity':
- default['date_open'] = fields.datetime.now()
- else:
- default['date_open'] = False
- return super(crm_lead, self).copy(cr, uid, id, default, context=local_context)
-
- def get_empty_list_help(self, cr, uid, help, context=None):
- context = dict(context or {})
- context['empty_list_help_model'] = 'crm.team'
- context['empty_list_help_id'] = context.get('default_team_id', None)
- context['empty_list_help_document_name'] = _("opportunities")
if help:
- alias_record = self.pool['ir.model.data'].xmlid_to_object(cr, uid, "crm.mail_alias_lead_info")
+ alias_record = self.env.ref("crm.mail_alias_lead_info", raise_if_not_found=False)
if alias_record and alias_record.alias_domain and alias_record.alias_name:
- dynamic_help = '<p>%s</p>' % _("""All email incoming to %(link)s will automatically create new opportunity.
-Update your business card, phone book, social media,... Send an email right now and see it here.""") % {
- 'link': "<a href='mailto:%(email)s'>%(email)s</a>" % {'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)}
- }
- return '<p class="oe_view_nocontent_create">%s</p>%s%s' % (
- _('Click to add a new opportunity'),
- help,
- dynamic_help)
- return super(crm_lead, self).get_empty_list_help(cr, uid, help, context=context)
-
- # ----------------------------------------
- # Mail Gateway
- # ----------------------------------------
-
- def _track_subtype(self, cr, uid, ids, init_values, context=None):
- record = self.browse(cr, uid, ids[0], context=context)
- if 'stage_id' in init_values and record.probability == 100 and record.stage_id and record.stage_id.on_change:
- return 'crm.mt_lead_won'
- elif 'active' in init_values and record.probability == 0 and not record.active:
- return 'crm.mt_lead_lost'
- elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1:
- return 'crm.mt_lead_create'
- elif 'stage_id' in init_values:
- return 'crm.mt_lead_stage'
- return super(crm_lead, self)._track_subtype(cr, uid, ids, init_values, context=context)
-
- def _notification_group_recipients(self, cr, uid, ids, message, recipients, done_ids, group_data, context=None):
- """ Override the mail.thread method to handle salesman recipients.
- Indeed those will have specific action in their notification emails. """
- group_sale_salesman = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'sales_team.group_sale_salesman')
- for recipient in recipients:
- if recipient.id in done_ids:
- continue
- if recipient.user_ids and group_sale_salesman in recipient.user_ids[0].groups_id.ids:
- group_data['group_sale_salesman'] |= recipient
- done_ids.add(recipient.id)
- return super(crm_lead, self)._notification_group_recipients(cr, uid, ids, message, recipients, done_ids, group_data, context=context)
-
- def _notification_get_recipient_groups(self, cr, uid, ids, message, recipients, context=None):
- res = super(crm_lead, self)._notification_get_recipient_groups(cr, uid, ids, message, recipients, context=context)
-
- lead = self.browse(cr, uid, ids[0], context=context)
-
- won_action = self._notification_link_helper(cr, uid, ids, 'method', context=context, method='case_mark_won')
- lost_action = self._notification_link_helper(cr, uid, ids, 'method', context=context, method='case_mark_lost')
- convert_action = self._notification_link_helper(cr, uid, ids, 'method', context=context, method='convert_opportunity', partner_id=lead.partner_id.id)
-
- if lead.type == 'lead':
- res['group_sale_salesman'] = {
- 'actions': [{'url': convert_action, 'title': 'Convert to opportunity'}]
- }
- else:
- res['group_sale_salesman'] = {
- 'actions': [
- {'url': won_action, 'title': 'Won'},
- {'url': lost_action, 'title': 'Lost'}]
- }
- return res
-
- @api.cr_uid_context
- def message_get_reply_to(self, cr, uid, ids, default=None, context=None):
- """ Override to get the reply_to of the parent project. """
- leads = self.browse(cr, SUPERUSER_ID, ids, context=context)
- team_ids = set([lead.team_id.id for lead in leads if lead.team_id])
- aliases = self.pool['crm.team'].message_get_reply_to(cr, uid, list(team_ids), default=default, context=context)
- return dict((lead.id, aliases.get(lead.team_id and lead.team_id.id or 0, False)) for lead in leads)
-
- def get_formview_id(self, cr, uid, id, context=None):
- obj = self.browse(cr, uid, id, context=context)
- if obj.type == 'opportunity':
- model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
- else:
- view_id = super(crm_lead, self).get_formview_id(cr, uid, id, context=context)
- return view_id
-
- def message_get_suggested_recipients(self, cr, uid, ids, context=None):
- recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context)
- try:
- for lead in self.browse(cr, uid, ids, context=context):
- if lead.partner_id:
- lead._message_add_suggested_recipient(recipients, partner=lead.partner_id, reason=_('Customer'))
- elif lead.email_from:
- lead._message_add_suggested_recipient(recipients, email=lead.email_from, reason=_('Customer Email'))
- except AccessError: # no read access rights -> just ignore suggested recipients because this imply modifying followers
- pass
- return recipients
-
- def message_new(self, cr, uid, msg, custom_values=None, context=None):
- """ Overrides mail_thread message_new that is called by the mailgateway
- through message_process.
- This override updates the document according to the email.
- """
- # remove default author when going through the mail gateway. Indeed we
- # do not want to explicitly set user_id to False; however we do not
- # want the gateway user to be responsible if no other responsible is
- # found.
- create_context = dict(context or {})
- create_context['default_user_id'] = False
- if custom_values is None:
- custom_values = {}
- defaults = {
- 'name': msg.get('subject') or _("No Subject"),
- 'email_from': msg.get('from'),
- 'email_cc': msg.get('cc'),
- 'partner_id': msg.get('author_id', False),
- }
- if msg.get('author_id'):
- defaults.update(self.on_change_partner_id(cr, uid, None, msg.get('author_id'), context=context)['value'])
- if msg.get('priority') in dict(crm_stage.AVAILABLE_PRIORITIES):
- defaults['priority'] = msg.get('priority')
- defaults.update(custom_values)
- return super(crm_lead, self).message_new(cr, uid, msg, custom_values=defaults, context=create_context)
-
- def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
- """ Overrides mail_thread message_update that is called by the mailgateway
- through message_process.
- This method updates the document according to the email.
- """
- if isinstance(ids, (str, int, long)):
- ids = [ids]
- if update_vals is None: update_vals = {}
-
- if msg.get('priority') in dict(crm_stage.AVAILABLE_PRIORITIES):
- update_vals['priority'] = msg.get('priority')
- maps = {
- 'revenue': 'planned_revenue',
- 'probability':'probability',
- }
- for line in msg.get('body', '').split('\n'):
- line = line.strip()
- res = tools.command_re.match(line)
- if res and maps.get(res.group(1).lower()):
- key = maps.get(res.group(1).lower())
- update_vals[key] = res.group(2).lower()
-
- return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
-
- def log_meeting(self, cr, uid, ids, meeting_subject, meeting_date, duration, context=None):
+ email = '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
+ email_link = "<a href='mailto:%s'>%s</a>" % (email, email)
+ dynamic_help = _("""All email incoming to %s will automatically
+ create new opportunity. Update your business card, phone book, social media,...
+ Send an email right now and see it here.""") % (email_link,)
+ return '<p class="oe_view_nocontent_create">%s</p>%s<p>%s</p>' % (_('Click to add a new opportunity'), help, dynamic_help)
+ return super(Lead, self.with_context(**additionnal_context)).get_empty_list_help(help)
+
+ @api.multi
+ def log_meeting(self, meeting_subject, meeting_date, duration):
if not duration:
duration = _('unknown')
else:
duration = str(duration)
- meet_date = datetime.strptime(meeting_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
- meeting_usertime = fields.datetime.context_timestamp(cr, uid, meet_date, context=context).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ meet_date = fields.Datetime.from_string(meeting_date)
+ meeting_usertime = fields.Datetime.to_string(fields.Datetime.context_timestamp(self, meet_date))
html_time = "<time datetime='%s+00:00'>%s</time>" % (meeting_date, meeting_usertime)
message = _("Meeting scheduled at '%s'<br> Subject: %s <br> Duration: %s hour(s)") % (html_time, meeting_subject, duration)
- return self.message_post(cr, uid, ids, body=message, context=context)
-
- def onchange_state(self, cr, uid, ids, state_id, context=None):
- if state_id:
- country_id=self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id
- return {'value':{'country_id':country_id}}
- return {}
-
- def message_partner_info_from_emails(self, cr, uid, ids, emails, link_mail=False, context=None):
- res = super(crm_lead, self).message_partner_info_from_emails(cr, uid, ids, emails, link_mail=link_mail, context=context)
- lead = self.browse(cr, uid, ids[0], context=context)
- for partner_info in res:
- if not partner_info.get('partner_id') and (lead.partner_name or lead.contact_name):
- emails = email_re.findall(partner_info['full_name'] or '')
- email = emails and emails[0] or ''
- if email and lead.email_from and email.lower() == lead.email_from.lower():
- partner_info['full_name'] = '%s <%s>' % (lead.partner_name or lead.contact_name, email)
- break
- return res
+ return self.message_post(body=message)
- def retrieve_sales_dashboard(self, cr, uid, context=None):
+ # ----------------------------------------
+ # Sales Team Dashboard
+ # ----------------------------------------
- res = {
+ @api.model
+ def retrieve_sales_dashboard(self):
+ """ Fetch data to setup Sales Dashboard """
+ result = {
'meeting': {
'today': 0,
'next_7_days': 0,
@@ -1060,136 +950,260 @@ Update your business card, phone book, social media,... Send an email right now
'nb_opportunities': 0,
}
- opportunities = self.search_read(
- cr, uid,
- [('type', '=', 'opportunity'), ('user_id', '=', uid)],
- ['date_deadline', 'next_activity_id', 'date_action', 'date_closed', 'planned_revenue'], context=context)
+ opportunities = self.search_read([('type', '=', 'opportunity'), ('user_id', '=', self._uid)], ['date_deadline', 'next_activity_id', 'date_action', 'date_closed', 'planned_revenue'])
for opp in opportunities:
-
# Expected closing
if opp['date_deadline']:
- date_deadline = datetime.strptime(opp['date_deadline'], tools.DEFAULT_SERVER_DATE_FORMAT).date()
-
+ date_deadline = fields.Date.from_string(opp['date_deadline'])
if date_deadline == date.today():
- res['closing']['today'] += 1
+ result['closing']['today'] += 1
if date_deadline >= date.today() and date_deadline <= date.today() + timedelta(days=7):
- res['closing']['next_7_days'] += 1
+ result['closing']['next_7_days'] += 1
if date_deadline < date.today():
- res['closing']['overdue'] += 1
-
+ result['closing']['overdue'] += 1
# Next activities
if opp['next_activity_id'] and opp['date_action']:
- date_action = datetime.strptime(opp['date_action'], tools.DEFAULT_SERVER_DATE_FORMAT).date()
-
+ date_action = fields.Date.from_string(opp['date_action'])
if date_action == date.today():
- res['activity']['today'] += 1
+ result['activity']['today'] += 1
if date_action >= date.today() and date_action <= date.today() + timedelta(days=7):
- res['activity']['next_7_days'] += 1
+ result['activity']['next_7_days'] += 1
if date_action < date.today():
- res['activity']['overdue'] += 1
-
+ result['activity']['overdue'] += 1
# Won in Opportunities
if opp['date_closed']:
- date_closed = datetime.strptime(opp['date_closed'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
-
+ date_closed = fields.Date.from_string(opp['date_closed'])
if date_closed <= date.today() and date_closed >= date.today().replace(day=1):
if opp['planned_revenue']:
- res['won']['this_month'] += opp['planned_revenue']
+ result['won']['this_month'] += opp['planned_revenue']
elif date_closed < date.today().replace(day=1) and date_closed >= date.today().replace(day=1) - relativedelta(months=+1):
if opp['planned_revenue']:
- res['won']['last_month'] += opp['planned_revenue']
+ result['won']['last_month'] += opp['planned_revenue']
+
+ result['nb_opportunities'] = len(opportunities)
# crm.activity is a very messy model so we need to do that in order to retrieve the actions done.
- cr.execute("""
+ self._cr.execute("""
SELECT
m.id,
m.subtype_id,
m.date,
l.user_id,
l.type
- FROM
- "mail_message" m
- LEFT JOIN
- "crm_lead" l
- ON
- (m.res_id = l.id)
- INNER JOIN
- "crm_activity" a
- ON
- (m.subtype_id = a.subtype_id)
+ FROM mail_message M
+ LEFT JOIN crm_lead L ON (M.res_id = L.id)
+ INNER JOIN crm_activity A ON (M.subtype_id = A.subtype_id)
WHERE
- (m.model = 'crm.lead') AND (l.user_id = %s) AND (l.type = 'opportunity')
- """, (uid,))
- activites_done = cr.dictfetchall()
+ (M.model = 'crm.lead') AND (L.user_id = %s) AND (L.type = 'opportunity')
+ """, (self._uid,))
+ activites_done = self._cr.dictfetchall()
- for act in activites_done:
- if act['date']:
- date_act = datetime.strptime(act['date'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
+ for activity in activites_done:
+ if activity['date']:
+ date_act = fields.Date.from_string(activity['date'])
if date_act <= date.today() and date_act >= date.today().replace(day=1):
- res['done']['this_month'] += 1
+ result['done']['this_month'] += 1
elif date_act < date.today().replace(day=1) and date_act >= date.today().replace(day=1) - relativedelta(months=+1):
- res['done']['last_month'] += 1
+ result['done']['last_month'] += 1
# Meetings
- min_date = datetime.now().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
- max_date = (datetime.now() + timedelta(days=8)).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ min_date = fields.Datetime.now()
+ max_date = fields.Datetime.to_string(datetime.now() + timedelta(days=8))
meetings_domain = [
('start', '>=', min_date),
('start', '<=', max_date)
]
+ #TODO JEM : remove this stupid context key
# We need to add 'mymeetings' in the context for the search to be correct.
- meetings = self.pool.get('calendar.event').search_read(cr, uid, meetings_domain, ['start'], context=context.update({'mymeetings': 1}) if context else {'mymeetings': 1})
+ meetings = self.env['calendar.event'].with_context(mymeetings=1).search(meetings_domain)
for meeting in meetings:
if meeting['start']:
start = datetime.strptime(meeting['start'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
-
if start == date.today():
- res['meeting']['today'] += 1
+ result['meeting']['today'] += 1
if start >= date.today() and start <= date.today() + timedelta(days=7):
- res['meeting']['next_7_days'] += 1
+ result['meeting']['next_7_days'] += 1
- res['nb_opportunities'] = len(opportunities)
+ result['done']['target'] = self.env.user.target_sales_done
+ result['won']['target'] = self.env.user.target_sales_won
+ result['currency_id'] = self.env.user.company_id.currency_id.id
- user = self.pool('res.users').browse(cr, uid, uid, context=context)
- res['done']['target'] = user.target_sales_done
- res['won']['target'] = user.target_sales_won
+ return result
- res['currency_id'] = user.company_id.currency_id.id
+ @api.model
+ def modify_target_sales_dashboard(self, target_name, target_value):
+ """ Update the user objectives (`target_sales_done`, target_sales_won`
+ and `target_sales_invoiced` fields).
+ :param target_name : part of the fields name to update
+ :param target_value : value of the field to update
+ """
+ if target_name in ['won', 'done', 'invoiced']:
+ # bypass rights, since self.env.user is browsed as SUPERUSER_ID
+ self.env.user.write({'target_sales_' + target_name: target_value})
+ else:
+ raise UserError(_('This target does not exist.'))
- return res
+ # ----------------------------------------
+ # Mail Gateway
+ # ----------------------------------------
- def modify_target_sales_dashboard(self, cr, uid, target_name, target_value, context=None):
+ @api.multi
+ def _track_subtype(self, init_values):
+ self.ensure_one()
+ if 'stage_id' in init_values and self.probability == 100 and self.stage_id and self.stage_id.on_change:
+ return 'crm.mt_lead_won'
+ elif 'active' in init_values and self.probability == 0 and not self.active:
+ return 'crm.mt_lead_lost'
+ elif 'stage_id' in init_values and self.stage_id and self.stage_id.sequence <= 1:
+ return 'crm.mt_lead_create'
+ elif 'stage_id' in init_values:
+ return 'crm.mt_lead_stage'
+ return super(Lead, self)._track_subtype(init_values)
- if target_name in ['won', 'done', 'invoiced']:
- # bypass rights (with superuser_id)
- self.pool('res.users').write(cr, SUPERUSER_ID, [uid], {'target_sales_' + target_name: target_value}, context=context)
+ @api.multi
+ def _notification_group_recipients(self, message, recipients, done_ids, group_data):
+ """ Override the mail.thread method to handle salesman recipients.
+ Indeed those will have specific action in their notification emails.
+ """
+ group_sale_salesman = self.env.ref('sales_team.group_sale_salesman')
+ for recipient in recipients:
+ if recipient.id in done_ids:
+ continue
+ # FIXME: why not recipient.user_ids[0].has_group(sales_team.group_sale_salesman)?
+ if recipient.user_ids and group_sale_salesman in recipient.user_ids[0].groups_id:
+ group_data['group_sale_salesman'] |= recipient
+ done_ids.add(recipient.id)
+ return super(Lead, self)._notification_group_recipients(message, recipients, done_ids, group_data)
+
+ @api.multi
+ def _notification_get_recipient_groups(self, message, recipients):
+ result = super(Lead, self)._notification_get_recipient_groups(message, recipients)
+
+ lead = self[0]
+ won_action = self._notification_link_helper('method', method='action_set_won')
+ lost_action = self._notification_link_helper('method', method='action_set_lost')
+ convert_action = self._notification_link_helper('method', method='convert_opportunity', partner_id=lead.partner_id.id)
+
+ if lead.type == 'lead':
+ result['group_sale_salesman'] = {
+ 'actions': [{'url': convert_action, 'title': 'Convert to opportunity'}]
+ }
else:
- raise UserError(_('This target does not exist.'))
+ result['group_sale_salesman'] = {
+ 'actions': [
+ {'url': won_action, 'title': 'Won'},
+ {'url': lost_action, 'title': 'Lost'}]
+ }
+ return result
- def close_dialog(self, cr, uid, ids, context=None):
- return {'type': 'ir.actions.act_window_close'}
+ @api.model
+ def message_get_reply_to(self, res_ids, default=None):
+ leads = self.sudo().browse(res_ids)
+ aliases = self.env['crm.team'].message_get_reply_to(leads.mapped('team_id').ids, default=default)
+ return dict((lead.id, aliases.get(lead.team_id and lead.team_id.id or 0, False)) for lead in leads)
+
+ @api.multi
+ def get_formview_id(self):
+ self.ensure_one()
+ if self.type == 'opportunity':
+ view_id = self.env.ref('crm.crm_case_form_view_oppor').id
+ else:
+ view_id = super(Lead, self).get_formview_id()
+ return view_id
+
+ @api.multi
+ def message_get_suggested_recipients(self):
+ recipients = super(Lead, self).message_get_suggested_recipients()
+ try:
+ for lead in self:
+ if lead.partner_id:
+ lead._message_add_suggested_recipient(recipients, partner=lead.partner_id, reason=_('Customer'))
+ elif lead.email_from:
+ lead._message_add_suggested_recipient(recipients, email=lead.email_from, reason=_('Customer Email'))
+ except AccessError: # no read access rights -> just ignore suggested recipients because this imply modifying followers
+ pass
+ return recipients
+
+ @api.model
+ def message_new(self, msg_dict, custom_values=None):
+ """ Overrides mail_thread message_new that is called by the mailgateway
+ through message_process.
+ This override updates the document according to the email.
+ """
+ # remove default author when going through the mail gateway. Indeed we
+ # do not want to explicitly set user_id to False; however we do not
+ # want the gateway user to be responsible if no other responsible is
+ # found.
+ self = self.with_context(default_user_id=False)
+
+ if custom_values is None:
+ custom_values = {}
+ defaults = {
+ 'name': msg_dict.get('subject') or _("No Subject"),
+ 'email_from': msg_dict.get('from'),
+ 'email_cc': msg_dict.get('cc'),
+ 'partner_id': msg_dict.get('author_id', False),
+ }
+ if msg_dict.get('author_id'):
+ defaults.update(self._onchange_partner_id_values(msg_dict.get('author_id')))
+ if msg_dict.get('priority') in dict(crm_stage.AVAILABLE_PRIORITIES):
+ defaults['priority'] = msg_dict.get('priority')
+ defaults.update(custom_values)
+ return super(Lead, self).message_new(msg_dict, custom_values=defaults)
+
+ @api.multi
+ def message_update(self, msg_dict, update_vals=None):
+ """ Overrides mail_thread message_update that is called by the mailgateway
+ through message_process.
+ This method updates the document according to the email.
+ """
+ if update_vals is None:
+ update_vals = {}
+ if msg_dict.get('priority') in dict(crm_stage.AVAILABLE_PRIORITIES):
+ update_vals['priority'] = msg_dict.get('priority')
+ maps = {
+ 'revenue': 'planned_revenue',
+ 'probability': 'probability',
+ }
+ for line in msg_dict.get('body', '').split('\n'):
+ line = line.strip()
+ res = tools.command_re.match(line)
+ if res and maps.get(res.group(1).lower()):
+ key = maps.get(res.group(1).lower())
+ update_vals[key] = res.group(2).lower()
+ return super(Lead, self).message_update(msg_dict, update_vals=update_vals)
+
+ @api.multi
+ def message_partner_info_from_emails(self, emails, link_mail=False):
+ result = super(Lead, self).message_partner_info_from_emails(emails, link_mail=link_mail)
+ for partner_info in result:
+ if not partner_info.get('partner_id') and (self.partner_name or self.contact_name):
+ emails = email_re.findall(partner_info['full_name'] or '')
+ email = emails and emails[0] or ''
+ if email and self.email_from and email.lower() == self.email_from.lower():
+ partner_info['full_name'] = '%s <%s>' % (self.partner_name or self.contact_name, email)
+ break
+ return result
+
+
+class Tag(models.Model):
-class crm_lead_tag(osv.Model):
_name = "crm.lead.tag"
_description = "Category of lead"
- _columns = {
- 'name': fields.char('Name', required=True),
- 'color': fields.integer('Color Index'),
- 'team_id': fields.many2one('crm.team', 'Sales Team'),
- }
+
+ name = fields.Char('Name', required=True)
+ color = fields.Integer('Color Index')
+ team_id = fields.Many2one('crm.team', 'Sales Team')
+
_sql_constraints = [
- ('name_uniq', 'unique (name)', "Tag name already exists !"),
+ ('name_uniq', 'unique (name)', "Tag name already exists !"),
]
-class crm_lost_reason(osv.Model):
+class LostReason(models.Model):
_name = "crm.lost.reason"
_description = 'Reason for loosing leads'
- _columns = {
- 'name': fields.char('Name', required=True, translate=True),
- 'active': fields.boolean('Active'),
- }
- _defaults = {
- 'active': True,
- }
+
+ name = fields.Char('Name', required=True, translate=True)
+ active = fields.Boolean('Active', default=True)
diff --git a/addons/crm/models/crm_stage.py b/addons/crm/models/crm_stage.py
index ac453ddc2ff..9b8769e9e08 100644
--- a/addons/crm/models/crm_stage.py
+++ b/addons/crm/models/crm_stage.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import osv, fields
+from odoo import api, fields, models
AVAILABLE_PRIORITIES = [
('0', 'Normal'),
@@ -11,7 +11,7 @@ AVAILABLE_PRIORITIES = [
]
-class crm_stage(osv.Model):
+class Stage(models.Model):
""" Model for case stages. This models the main stages of a document
management flow. Main CRM objects (leads, opportunities, project
issues, ...) will now use only stages, instead of state and stages.
@@ -22,31 +22,24 @@ class crm_stage(osv.Model):
_rec_name = 'name'
_order = "sequence, name, id"
- _columns = {
- 'name': fields.char('Stage Name', required=True, translate=True),
- 'sequence': fields.integer('Sequence', help="Used to order stages. Lower is better."),
- 'probability': fields.float('Probability (%)', required=True, help="This percentage depicts the default/average probability of the Case for this stage to be a success"),
- 'on_change': fields.boolean('Change Probability Automatically', help="Setting this stage will change the probability automatically on the opportunity."),
- 'requirements': fields.text('Requirements', help="Enter here the internal requirements for this stage (ex: Offer sent to customer). It will appear as a tooltip over the stage's name."),
- 'team_id': fields.many2one('crm.team', 'Team',
- ondelete='set null',
- help='Specific team that uses this stage. Other teams will not ne able to see or use this stage.'),
- 'legend_priority': fields.text(
- 'Priority Management Explanation', translate=True,
- help='Explanation text to help users using the star and priority mechanism on stages or issues that are in this stage.'),
- 'fold': fields.boolean('Folded in Pipeline',
- help='This stage is folded in the kanban view when '
- 'there are no records in that stage to display.'),
- }
+ @api.model
+ def default_get(self, fields):
+ """ Hack : when going from the pipeline, creating a stage with a sales team in
+ context should not create a stage for the current sales team only
+ """
+ ctx = dict(self.env.context)
+ if ctx.get('default_team_id') and not ctx.get('crm_team_mono'):
+ ctx.pop('default_team_id')
+ return super(Stage, self.with_context(ctx)).default_get(fields)
- _defaults = {
- 'sequence': 1,
- 'probability': 10.0,
- 'fold': False,
- }
-
- def default_get(self, cr, uid, fields, context=None):
- if context and context.get('default_team_id') and not context.get('crm_team_mono', False):
- context = dict(context)
- context.pop('default_team_id')
- return super(crm_stage, self).default_get(cr, uid, fields, context=context)
+ name = fields.Char('Stage Name', required=True, translate=True)
+ sequence = fields.Integer('Sequence', default=1, help="Used to order stages. Lower is better.")
+ probability = fields.Float('Probability (%)', required=True, default=10.0, help="This percentage depicts the default/average probability of the Case for this stage to be a success")
+ on_change = fields.Boolean('Change Probability Automatically', help="Setting this stage will change the probability automatically on the opportunity.")
+ requirements = fields.Text('Requirements', help="Enter here the internal requirements for this stage (ex: Offer sent to customer). It will appear as a tooltip over the stage's name.")
+ team_id = fields.Many2one('crm.team', string='Team', ondelete='set null',
+ help='Specific team that uses this stage. Other teams will not ne able to see or use this stage.')
+ legend_priority = fields.Text('Priority Management Explanation', translate=True,
+ help='Explanation text to help users using the star and priority mechanism on stages or issues that are in this stage.')
+ fold = fields.Boolean('Folded in Pipeline',
+ help='This stage is folded in the kanban view when there are no records in that stage to display.')
diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py
index bb048b66789..c8e74ead04e 100644
--- a/addons/crm/models/crm_team.py
+++ b/addons/crm/models/crm_team.py
@@ -1,67 +1,60 @@
# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import fields, osv
-from openerp.tools.safe_eval import safe_eval as eval
-from openerp.tools.translate import _
+from odoo import api, fields, models
+from odoo.tools.safe_eval import safe_eval as eval
+from odoo.tools.translate import _
-class crm_team(osv.Model):
+class Team(models.Model):
_name = 'crm.team'
_inherit = ['mail.alias.mixin', 'crm.team']
- _columns = {
- 'resource_calendar_id': fields.many2one('resource.calendar', "Working Time", help="Used to compute open days"),
- 'use_leads': fields.boolean('Leads',
- help="The first contact you get with a potential customer is a lead you qualify before converting it into a real business opportunity. Check this box to manage leads in this sales team."),
- 'use_opportunities': fields.boolean('Opportunities', help="Check this box to manage opportunities in this sales team."),
- 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True, help="The email address associated with this team. New emails received will automatically create new leads assigned to the team."),
- }
+ resource_calendar_id = fields.Many2one('resource.calendar', string="Working Time", help="Used to compute open days")
+ use_leads = fields.Boolean('Leads',
+ help="The first contact you get with a potential customer is a lead you qualify before converting it into a real business opportunity. Check this box to manage leads in this sales team.")
+ use_opportunities = fields.Boolean('Opportunities', default=True, help="Check this box to manage opportunities in this sales team.")
+ alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=True, help="The email address associated with this team. New emails received will automatically create new leads assigned to the team.")
- _defaults = {
- 'use_opportunities': True,
- }
def get_alias_model_name(self, vals):
return 'crm.lead'
def get_alias_values(self):
has_group_use_lead = self.env['res.users'].has_group('crm.group_use_lead')
- values = super(crm_team, self).get_alias_values()
+ values = super(Team, self).get_alias_values()
values['alias_defaults'] = defaults = eval(self.alias_defaults or "{}")
defaults['type'] = 'lead' if has_group_use_lead and self.use_leads else 'opportunity'
defaults['team_id'] = self.id
return values
- def onchange_use_leads_opportunities(self, cr, uid, ids, use_leads, use_opportunities, context=None):
- if use_leads or use_opportunities:
- return {'value': {}}
- return {'value': {'alias_name': False}}
+ @api.onchange('use_leads', 'use_opportunities')
+ def _onchange_use_leads_opportunities(self):
+ if not self.use_leads and not self.use_opportunities:
+ self.alias_name = False
- def create(self, cr, uid, vals, context=None):
- generate_alias_name = self.pool['ir.values'].get_default(cr, uid, 'sales.config.settings', 'generate_sales_team_alias')
+ @api.model
+ def create(self, vals):
+ generate_alias_name = self.env['ir.values'].get_default('sales.config.settings', 'generate_sales_team_alias')
if generate_alias_name and not vals.get('alias_name'):
vals['alias_name'] = vals.get('name')
- return super(crm_team, self).create(cr, uid, vals, context=context)
+ return super(Team, self).create(vals)
- def write(self, cr, uid, ids, vals, context=None):
- res = super(crm_team, self).write(cr, uid, ids, vals, context=context)
+ @api.multi
+ def write(self, vals):
+ result = super(Team, self).write(vals)
if vals.get('use_leads') or vals.get('alias_defaults'):
- for team in self.browse(cr, uid, ids, context=context):
+ for team in self:
team.alias_id.write(team.get_alias_values())
- return res
+ return result
- def action_your_pipeline(self, cr, uid, context=None):
- IrModelData = self.pool['ir.model.data']
- action = IrModelData.xmlid_to_object(cr, uid, 'crm.crm_lead_opportunities_tree_view', context=context).read(['name', 'help', 'res_model', 'target', 'domain', 'context', 'type', 'search_view_id'])
- if not action:
- action = {}
- else:
- action = action[0]
-
- user_team_id = self.pool['res.users'].browse(cr, uid, uid, context=context).sale_team_id.id
+ #TODO JEM : refactor this stuff with xml action, proper customization, ...
+ @api.model
+ def action_your_pipeline(self):
+ action = self.env.ref('crm.crm_lead_opportunities_tree_view').read()[0]
+ user_team_id = self.env.user.sale_team_id.id
if not user_team_id:
- user_team_id = self.search(cr, uid, [], context=context, limit=1)
- user_team_id = user_team_id and user_team_id[0] or False
+ user_team_id = self.search([], limit=1).id
action['help'] = """<p class='oe_view_nocontent_create'>Click here to add new opportunities</p><p>
Looks like you are not a member of a sales team. You should add yourself
as a member of one of the sales team.
@@ -69,15 +62,15 @@ class crm_team(osv.Model):
if user_team_id:
action['help'] += "<p>As you don't belong to any sales team, Odoo opens the first one by default.</p>"
- action_context = eval(action['context'], {'uid': uid})
+ action_context = eval(action['context'], {'uid': self.env.uid})
if user_team_id:
action_context.update({
'default_team_id': user_team_id,
})
- tree_view_id = IrModelData.xmlid_to_res_id(cr, uid, 'crm.crm_case_tree_view_oppor')
- form_view_id = IrModelData.xmlid_to_res_id(cr, uid, 'crm.crm_case_form_view_oppor')
- kanb_view_id = IrModelData.xmlid_to_res_id(cr, uid, 'crm.crm_case_kanban_view_leads')
+ tree_view_id = self.env.ref('crm.crm_case_tree_view_oppor').id
+ form_view_id = self.env.ref('crm.crm_case_form_view_oppor').id
+ kanb_view_id = self.env.ref('crm.crm_case_kanban_view_leads').id
action.update({
'views': [
[kanb_view_id, 'kanban'],
diff --git a/addons/crm/models/res_config.py b/addons/crm/models/res_config.py
index fac463016a0..e9a311a26ef 100644
--- a/addons/crm/models/res_config.py
+++ b/addons/crm/models/res_config.py
@@ -1,71 +1,61 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp import SUPERUSER_ID
-from openerp.osv import fields, osv
+from odoo import api, fields, models
-class crm_configuration(osv.TransientModel):
+class CRMSettings(models.TransientModel):
+
_name = 'sale.config.settings'
_inherit = ['sale.config.settings']
- _columns = {
- 'generate_sales_team_alias': fields.boolean(
- "Automatically generate an email alias at the sales team creation",
- help="Odoo will generate an email alias based on the sales team name"),
- 'alias_prefix': fields.char('Default Alias Name for Leads'),
- 'alias_domain' : fields.char('Alias Domain'),
- 'group_use_lead': fields.selection([
- (0, "Each mail sent to the alias creates a new opportunity"),
- (1, "Use leads if you need a qualification step before creating an opportunity or a customer")
- ], "Leads",
- implied_group='crm.group_use_lead'),
- 'module_crm_voip': fields.boolean("VoIP integration",
- help="Integration with Asterisk"),
- 'module_website_sign': fields.boolean("Odoo Sign"),
- }
-
- _defaults = {
- 'alias_domain': lambda self, cr, uid, context: self.pool["ir.config_parameter"].get_param(cr, uid, "mail.catchall.domain", context=context),
- }
-
- def _find_default_lead_alias_id(self, cr, uid, context=None):
- alias_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'crm.mail_alias_lead_info')
- if not alias_id:
- alias_ids = self.pool['mail.alias'].search(
- cr, uid, [
- ('alias_model_id.model', '=', 'crm.lead'),
- ('alias_force_thread_id', '=', False),
- ('alias_parent_model_id.model', '=', 'crm.team'),
- ('alias_parent_thread_id', '=', False),
- ('alias_defaults', '=', '{}')
- ], context=context)
- alias_id = alias_ids and alias_ids[0] or False
- return alias_id
-
- def get_default_generate_sales_team_alias(self, cr, uid, fields, context=None):
- return {'generate_sales_team_alias': self.pool['ir.values'].get_default(
- cr, uid, 'sales.config.settings', 'generate_sales_team_alias')}
-
- def set_default_generate_sales_team_alias(self, cr, uid, ids, context=None):
- config_value = self.browse(cr, uid, ids, context=context).generate_sales_team_alias
- user_id = SUPERUSER_ID if self.user_has_groups(cr, uid, 'base.group_erp_manager') else uid
- self.pool['ir.values'].set_default(cr, user_id, 'sales.config.settings', 'generate_sales_team_alias', config_value)
-
- def get_default_alias_prefix(self, cr, uid, fields, context=None):
- alias_name = False
- alias_id = self._find_default_lead_alias_id(cr, uid, context=context)
- if alias_id:
- alias_name = self.pool['mail.alias'].browse(cr, uid, alias_id, context=context).alias_name
- return {'alias_prefix': alias_name}
-
- def set_default_alias_prefix(self, cr, uid, ids, context=None):
- mail_alias = self.pool['mail.alias']
- for record in self.browse(cr, uid, ids, context=context):
- alias_id = self._find_default_lead_alias_id(cr, uid, context=context)
- if not alias_id:
- create_ctx = dict(context, alias_model_name='crm.lead', alias_parent_model_name='crm.team')
- alias_id = self.pool['mail.alias'].create(cr, uid, {'alias_name': record.alias_prefix}, context=create_ctx)
+ generate_sales_team_alias = fields.Boolean("Automatically generate an email alias at the sales team creation",
+ help="Odoo will generate an email alias based on the sales team name")
+ alias_prefix = fields.Char('Default Alias Name for Leads')
+ alias_domain = fields.Char('Alias Domain', default=lambda self: self.env["ir.config_parameter"].get_param("mail.catchall.domain"))
+ group_use_lead = fields.Selection([
+ (0, "Each mail sent to the alias creates a new opportunity"),
+ (1, "Use leads if you need a qualification step before creating an opportunity or a customer")
+ ], string="Leads", implied_group='crm.group_use_lead')
+ module_crm_voip = fields.Boolean("VoIP integration", help="Integration with Asterisk")
+ module_website_sign = fields.Boolean("Odoo Sign")
+
+ def _find_default_lead_alias_id(self):
+ alias = self.env.ref('crm.mail_alias_lead_info', False)
+ if not alias:
+ alias = self.env['mail.alias'].search([
+ ('alias_model_id.model', '=', 'crm.lead'),
+ ('alias_force_thread_id', '=', False),
+ ('alias_parent_model_id.model', '=', 'crm.team'),
+ ('alias_parent_thread_id', '=', False),
+ ('alias_defaults', '=', '{}')
+ ], limit=1)
+ return alias
+
+ @api.model
+ def get_default_generate_sales_team_alias(self, fields):
+ return {
+ 'generate_sales_team_alias': self.env['ir.values'].get_default('sales.config.settings', 'generate_sales_team_alias')
+ }
+
+ @api.multi
+ def set_default_generate_sales_team_alias(self):
+ IrValues = self.env['ir.values']
+ if self.env['res.users'].has_group('base.group_erp_manager'):
+ IrValues = IrValues.sudo()
+ IrValues.set_default('sales.config.settings', 'generate_sales_team_alias', self.generate_sales_team_alias)
+
+ @api.model
+ def get_default_alias_prefix(self, fields):
+ alias = self._find_default_lead_alias_id()
+ return {'alias_prefix': alias.alias_name if alias else False}
+
+ @api.multi
+ def set_default_alias_prefix(self):
+ for record in self:
+ alias = self._find_default_lead_alias_id()
+ if not alias:
+ alias = self.env['mail.alias'].with_context(alias_model_name='crm.lead', alias_parent_model_name='crm.team').create({'alias_name': record.alias_prefix})
else:
- mail_alias.write(cr, uid, alias_id, {'alias_name': record.alias_prefix}, context=context)
+ alias.write({'alias_name': record.alias_prefix})
return True
diff --git a/addons/crm/models/res_partner.py b/addons/crm/models/res_partner.py
index eb1c3a65f1b..1ed44567065 100644
--- a/addons/crm/models/res_partner.py
+++ b/addons/crm/models/res_partner.py
@@ -1,48 +1,44 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import fields,osv
+from odoo import api, fields, models
+
+
+class Partner(models.Model):
-class res_partner(osv.osv):
- """ Inherits partner and adds CRM information in the partner form """
_inherit = 'res.partner'
- def _opportunity_meeting_count(self, cr, uid, ids, field_name, arg, context=None):
- res = dict(map(lambda x: (x,{'opportunity_count': 0, 'meeting_count': 0}), ids))
- # the user may not have access rights for opportunities or meetings
+ team_id = fields.Many2one('crm.team', string='Sales Team', oldname='section_id')
+ opportunity_ids = fields.One2many('crm.lead', 'partner_id', string='Opportunities', domain=[('type', '=', 'opportunity')])
+ meeting_ids = fields.Many2many('calendar.event', 'calendar_event_res_partner_rel', 'res_partner_id', 'calendar_event_id', string='Meetings')
+ opportunity_count = fields.Integer("Opportunity", compute='_compute_opportunity_count')
+ meeting_count = fields.Integer("# Meetings", compute='_compute_meeting_count')
+ activities_count = fields.Integer("Activities", compute='_compute_activities_count')
+
+ @api.multi
+ def _compute_opportunity_count(self):
+ # TODO JEM : remove the try/except by putting a group on button
try:
- for partner in self.browse(cr, uid, ids, context):
- if partner.is_company:
- operator = 'child_of'
- else:
- operator = '='
- opp_ids = self.pool['crm.lead'].search(cr, uid, [('partner_id', operator, partner.id), ('type', '=', 'opportunity')], context=context)
- res[partner.id] = {
- 'opportunity_count': len(opp_ids),
- 'meeting_count': len(partner.meeting_ids),
- }
+ for partner in self:
+ operator = 'child_of' if partner.is_company else '=' # the opportunity count should counts the opportunities of this company and all its contacts
+ partner.opportunity_count = self.env['crm.lead'].search_count([('partner_id', operator, partner.id), ('type', '=', 'opportunity')])
except:
pass
- return res
- def _compute_activities_count(self, cr, uid, ids, field_name, arg, context=None):
- res = dict.fromkeys(ids, None)
- activities = self.pool['crm.activity.report'].read_group(cr, uid, [('partner_id', 'in', ids)], ['partner_id'], ['partner_id'], context=context)
- for activity in activities:
- res[activity['partner_id'][0]] = activity['partner_id_count']
- return res
+ @api.multi
+ def _compute_meeting_count(self):
+ for partner in self:
+ partner.meeting_count = len(partner.meeting_ids)
- _columns = {
- 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id'),
- 'opportunity_ids': fields.one2many('crm.lead', 'partner_id',\
- 'Opportunities', domain=[('type', '=', 'opportunity')]),
- 'meeting_ids': fields.many2many('calendar.event', 'calendar_event_res_partner_rel','res_partner_id', 'calendar_event_id',
- 'Meetings'),
- 'opportunity_count': fields.function(_opportunity_meeting_count, string="Opportunity", type='integer', multi='opp_meet'),
- 'meeting_count': fields.function(_opportunity_meeting_count, string="# Meetings", type='integer', multi='opp_meet'),
- 'activities_count': fields.function(_compute_activities_count, string="Activities", type="integer"),
- }
+ @api.multi
+ def _compute_activities_count(self):
+ activity_data = self.env['crm.activity.report'].read_group([('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id'])
+ mapped_data = {act['partner_id'][0]: act['partner_id_count'] for act in activity_data}
+ for partner in self:
+ partner.activities_count = mapped_data.get(partner.id, 0)
+ # TODO JEM : it is still used ?
+ @api.v7
def redirect_partner_form(self, cr, uid, partner_id, context=None):
search_view = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'view_res_partner_filter')
value = {
@@ -58,6 +54,8 @@ class res_partner(osv.osv):
}
return value
+ # TODO JEM : it is still used ?
+ @api.v7
def make_opportunity(self, cr, uid, ids, opportunity_summary, planned_revenue=0.0, probability=0.0, partner_id=None, context=None):
lead_obj = self.pool.get('crm.lead')
tag_ids = self.pool['crm.lead.tag'].search(cr, uid, [])
@@ -76,12 +74,13 @@ class res_partner(osv.osv):
opportunity_ids[partner_id] = opportunity_id
return opportunity_ids
- def schedule_meeting(self, cr, uid, ids, context=None):
- partner_ids = list(ids)
- partner_ids.append(self.pool.get('res.users').browse(cr, uid, uid).partner_id.id)
- res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'calendar', 'action_calendar_event', context)
- res['context'] = {
- 'search_default_partner_ids': context['partner_name'],
+ @api.multi
+ def schedule_meeting(self):
+ partner_ids = self.ids
+ partner_ids.append(self.env.user.partner_id.id)
+ action = self.env['ir.actions.act_window'].for_xml_id('calendar', 'action_calendar_event')
+ action['context'] = {
+ 'search_default_partner_ids': self._context['partner_name'],
'default_partner_ids': partner_ids,
}
- return res
+ return action
diff --git a/addons/crm/models/res_users.py b/addons/crm/models/res_users.py
index b4997d32424..2c94f1c05ab 100644
--- a/addons/crm/models/res_users.py
+++ b/addons/crm/models/res_users.py
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import osv, fields
+from odoo import fields, models
-class res_users(osv.osv):
+class Users(models.Model):
+
_inherit = 'res.users'
- _columns = {
- 'target_sales_won': fields.integer('Won in Opportunities Target'),
- 'target_sales_done': fields.integer('Activities Done Target'),
- }
+
+ target_sales_won = fields.Integer('Won in Opportunities Target')
+ target_sales_done = fields.Integer('Activities Done Target')
diff --git a/addons/crm/models/web_planner.py b/addons/crm/models/web_planner.py
index 2f0d539aa01..8a15c6e947b 100644
--- a/addons/crm/models/web_planner.py
+++ b/addons/crm/models/web_planner.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
-from openerp import api, models
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
class PlannerCrm(models.Model):
diff --git a/addons/crm/report/crm_activity_report.py b/addons/crm/report/crm_activity_report.py
index 6337e81f055..489838e0686 100644
--- a/addons/crm/report/crm_activity_report.py
+++ b/addons/crm/report/crm_activity_report.py
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp import fields, models
-from openerp import tools
+from odoo import fields, models, tools
-class crm_activity_report(models.Model):
+class ActivityReport(models.Model):
""" CRM Lead Analysis """
+
_name = "crm.activity.report"
_auto = False
_description = "CRM Activity Analysis"
@@ -30,9 +30,9 @@ class crm_activity_report(models.Model):
active = fields.Boolean('Active', readonly=True)
probability = fields.Float('Probability', group_operator='avg', readonly=True)
- def init(self, cr):
- tools.drop_view_if_exists(cr, 'crm_activity_report')
- cr.execute("""
+ def init(self):
+ tools.drop_view_if_exists(self._cr, 'crm_activity_report')
+ self._cr.execute("""
CREATE OR REPLACE VIEW crm_activity_report AS (
select
m.id,
diff --git a/addons/crm/report/crm_activity_report_views.xml b/addons/crm/report/crm_activity_report_views.xml
index cc114dbbf15..6be30b1d3b2 100644
--- a/addons/crm/report/crm_activity_report_views.xml
+++ b/addons/crm/report/crm_activity_report_views.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
- <data>
+<odoo>
<record id="crm_activity_report_view_graph" model="ir.ui.view">
<field name="name">crm.activity.report.graph</field>
@@ -57,9 +56,14 @@
<field name="context">{}</field>
<field name="domain">[]</field>
</record>
- <menuitem name="Activities" id="crm_activity_report_menu"
+
+ <menuitem
+ id="crm_activity_report_menu"
+ name="Activities"
groups="sales_team.group_sale_manager"
- parent="base.menu_sale_report" action="crm_activity_report_action" sequence="7"/>
+ parent="base.menu_sale_report"
+ action="crm_activity_report_action"
+ sequence="7"/>
<record id="crm_activity_report_action_team" model="ir.actions.act_window">
<field name="name">Activities Analysis</field>
@@ -70,5 +74,4 @@
<field name="domain">[]</field>
</record>
- </data>
-</openerp>
+</odoo>
diff --git a/addons/crm/report/crm_opportunity_report.py b/addons/crm/report/crm_opportunity_report.py
index 38494cf06d1..f8627bfeb76 100644
--- a/addons/crm/report/crm_opportunity_report.py
+++ b/addons/crm/report/crm_opportunity_report.py
@@ -1,59 +1,57 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.addons.crm.models import crm_stage
-from openerp.osv import fields, osv
-from openerp import tools
+from odoo.addons.crm.models import crm_stage
+from odoo import fields, models, tools
-class crm_opportunity_report(osv.Model):
+class OpportunityReport(models.Model):
""" CRM Opportunity Analysis """
+
_name = "crm.opportunity.report"
_auto = False
_description = "CRM Opportunity Analysis"
_rec_name = 'date_deadline'
- _columns = {
- 'date_deadline': fields.date('Expected Closing', readonly=True),
- 'create_date': fields.datetime('Creation Date', readonly=True),
- 'opening_date': fields.datetime('Assignation Date', readonly=True),
- 'date_closed': fields.datetime('Close Date', readonly=True),
- 'date_last_stage_update': fields.datetime('Last Stage Update', readonly=True),
- 'active': fields.boolean('Active', readonly=True),
+ date_deadline = fields.Date('Expected Closing', readonly=True)
+ create_date = fields.Datetime('Creation Date', readonly=True)
+ opening_date = fields.Datetime('Assignation Date', readonly=True)
+ date_closed = fields.Datetime('Close Date', readonly=True)
+ date_last_stage_update = fields.Datetime('Last Stage Update', readonly=True)
+ active = fields.Boolean('Active', readonly=True)
- # durations
- 'delay_open': fields.float('Delay to Assign',digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to open the case"),
- 'delay_close': fields.float('Delay to Close',digits=(16,2),readonly=True, group_operator="avg",help="Number of Days to close the case"),
- 'delay_expected': fields.float('Overpassed Deadline',digits=(16,2),readonly=True, group_operator="avg"),
+ # durations
+ delay_open = fields.Float('Delay to Assign', digits=(16, 2), readonly=True, group_operator="avg", help="Number of Days to open the case")
+ delay_close = fields.Float('Delay to Close', digits=(16, 2), readonly=True, group_operator="avg", help="Number of Days to close the case")
+ delay_expected = fields.Float('Overpassed Deadline', digits=(16, 2), readonly=True, group_operator="avg")
- 'user_id':fields.many2one('res.users', 'User', readonly=True),
- 'team_id':fields.many2one('crm.team', 'Sales Team', oldname='section_id', readonly=True),
- 'nbr_activities': fields.integer('# of Activities', readonly=True),
- 'city': fields.char('City'),
- 'country_id':fields.many2one('res.country', 'Country', readonly=True),
- 'company_id': fields.many2one('res.company', 'Company', readonly=True),
- 'probability': fields.float('Probability',digits=(16,2),readonly=True, group_operator="avg"),
- 'total_revenue': fields.float('Total Revenue',digits=(16,2),readonly=True),
- 'expected_revenue': fields.float('Probable Turnover', digits=(16,2),readonly=True),
- 'stage_id': fields.many2one ('crm.stage', 'Stage', readonly=True, domain="['|', ('team_id', '=', False), ('team_id', '=', team_id)]"),
- 'stage_name': fields.char('Stage Name', readonly=True),
- 'partner_id': fields.many2one('res.partner', 'Partner' , readonly=True),
- 'company_id': fields.many2one('res.company', 'Company', readonly=True),
- 'priority': fields.selection(crm_stage.AVAILABLE_PRIORITIES, 'Priority'),
- 'type':fields.selection([
- ('lead','Lead'),
- ('opportunity','Opportunity'),
- ],'Type', help="Type is used to separate Leads and Opportunities"),
- 'lost_reason': fields.many2one('crm.lost.reason', 'Lost Reason', readonly=True),
- 'date_conversion': fields.datetime('Conversion Date', readonly=True),
- 'campaign_id': fields.many2one('utm.campaign', 'Campaign', readonly=True),
- 'source_id':fields.many2one('utm.source', 'Source', readonly=True),
- 'medium_id': fields.many2one('utm.medium', 'Medium', readonly=True),
- }
+ user_id = fields.Many2one('res.users', string='User', readonly=True)
+ team_id = fields.Many2one('crm.team', 'Sales Team', oldname='section_id', readonly=True)
+ nbr_activities = fields.Integer('# of Activities', readonly=True)
+ city = fields.Char('City')
+ country_id = fields.Many2one('res.country', string='Country', readonly=True)
+ company_id = fields.Many2one('res.company', string='Company', readonly=True)
+ probability = fields.Float(string='Probability', digits=(16, 2), readonly=True, group_operator="avg")
+ total_revenue = fields.Float(string='Total Revenue', digits=(16, 2), readonly=True)
+ expected_revenue = fields.Float(string='Probable Turnover', digits=(16, 2), readonly=True)
+ stage_id = fields.Many2one('crm.stage', string='Stage', readonly=True, domain="['|', ('team_id', '=', False), ('team_id', '=', team_id)]")
+ stage_name = fields.Char(string='Stage Name', readonly=True)
+ partner_id = fields.Many2one('res.partner', string='Partner', readonly=True)
+ company_id = fields.Many2one('res.company', string='Company', readonly=True)
+ priority = fields.Selection(crm_stage.AVAILABLE_PRIORITIES, string='Priority', group_operator="avg")
+ type = fields.Selection([
+ ('lead', 'Lead'),
+ ('opportunity', 'Opportunity'),
+ ], help="Type is used to separate Leads and Opportunities")
+ lost_reason = fields.Many2one('crm.lost.reason', string='Lost Reason', readonly=True)
+ date_conversion = fields.Datetime(string='Conversion Date', readonly=True)
+ campaign_id = fields.Many2one('utm.campaign', string='Campaign', readonly=True)
+ source_id = fields.Many2one('utm.source', string='Source', readonly=True)
+ medium_id = fields.Many2one('utm.medium', string='Medium', readonly=True)
- def init(self, cr):
- tools.drop_view_if_exists(cr, 'crm_opportunity_report')
- cr.execute("""
+ def init(self):
+ tools.drop_view_if_exists(self._cr, 'crm_opportunity_report')
+ self._cr.execute("""
CREATE OR REPLACE VIEW crm_opportunity_report AS (
SELECT
c.id,
diff --git a/addons/crm/report/crm_opportunity_report_views.xml b/addons/crm/report/crm_opportunity_report_views.xml
index 19a018b9970..9e2e40cbde3 100644
--- a/addons/crm/report/crm_opportunity_report_views.xml
+++ b/addons/crm/report/crm_opportunity_report_views.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
- <data>
+<odoo>
<!-- Opportunities by user and team pivot View -->
<record id="crm_opportunity_report_view_pivot" model="ir.ui.view">
@@ -154,7 +153,7 @@
<field name="view_type">form</field>
<field name="view_mode">pivot,graph</field>
<field name="context">{'search_default_opportunity': True, 'search_default_current': True, 'active_test': True, 'search_default_my': True}</field>
- <field name="view_ids"
+ <field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('crm_opportunity_report_view_pivot')}),
(0, 0, {'view_mode': 'graph', 'view_id': ref('crm_opportunity_report_view_graph')})]"/>
@@ -173,7 +172,7 @@ teams of the sales pipeline.</field>
<field name="view_type">form</field>
<field name="view_mode">graph,pivot</field>
<field name="context">{'search_default_opportunity': True, 'search_default_current': True, 'active_test': True, 'search_default_my': True}</field>
- <field name="view_ids"
+ <field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'graph', 'view_id': ref('crm_opportunity_report_view_graph')}),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('crm_opportunity_report_view_pivot')})]"/>
@@ -190,15 +189,20 @@ with the the sales teams.</field>
<field name="view_type">form</field>
<field name="view_mode">pivot,graph</field>
<field name="context">{'search_default_current': True, 'active_test': True}</field>
- <field name="view_ids"
+ <field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'pivot', 'view_id': ref('crm_opportunity_report_view_pivot_lead')}),
(0, 0, {'view_mode': 'graph', 'view_id': ref('crm_opportunity_report_view_graph_lead')})]"/>
<field name="help">This report analyses the source of your leads
(including those converted into opportunities).</field>
</record>
- <menuitem id="crm_opportunity_report_menu_lead" name="Leads"
- parent="base.menu_sale_report" groups="crm.group_use_lead" action="crm_opportunity_report_action_lead" sequence="6"/>
- </data>
-</openerp>
+ <menuitem
+ id="crm_opportunity_report_menu_lead"
+ name="Leads"
+ parent="base.menu_sale_report"
+ action="crm_opportunity_report_action_lead"
+ groups="crm.group_use_lead"
+ sequence="6"/>
+
+</odoo>
diff --git a/addons/crm/security/crm_security.xml b/addons/crm/security/crm_security.xml
index 7f9803cbee3..61641b4f0e9 100644
--- a/addons/crm/security/crm_security.xml
+++ b/addons/crm/security/crm_security.xml
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
-<data noupdate="0">
+
+<odoo>
+<data>
<record id="group_use_lead" model="res.groups">
<field name="name">Show Lead Menu</field>
@@ -71,4 +72,5 @@
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
</record>
</data>
-</openerp>
+
+</odoo>
diff --git a/addons/crm/test/crm_lead_find_stage.yml b/addons/crm/test/crm_lead_find_stage.yml
index 9522924960d..040b29a2317 100644
--- a/addons/crm/test/crm_lead_find_stage.yml
+++ b/addons/crm/test/crm_lead_find_stage.yml
@@ -28,6 +28,6 @@
-
!python {model: crm.lead}: |
opp = self.browse(cr, uid, ref('test_crm_lead_new'))
- stage_id = self.stage_find(cr, uid , [opp], opp.team_id.id or False,[('probability','=',100.0)], context)
+ stage_id = opp.stage_find(opp.team_id.id or False,[('probability','=',100.0)])
assert stage_id == opp.stage_id.id, "Stage of opportunity is incorrect!"
diff --git a/addons/crm/views/calendar_views.xml b/addons/crm/views/calendar_views.xml
index c9d43050387..0186955335a 100644
--- a/addons/crm/views/calendar_views.xml
+++ b/addons/crm/views/calendar_views.xml
@@ -1,12 +1,13 @@
<?xml version="1.0"?>
-<openerp>
-<data>
+<odoo>
- <menuitem name="Import &amp; Synchronize" id="base.menu_import_crm"
+ <menuitem
+ id="base.menu_import_crm"
+ name="Import &amp; Synchronize"
parent="sales_team.menu_base_partner"/>
<record id="view_crm_meeting_search" model="ir.ui.view">
- <field name="name">CRM - Meetings Search</field>
+ <field name="name">calendar.event.form.inherit</field>
<field name="model">calendar.event</field>
<field name="inherit_id" ref="calendar.view_calendar_event_search"/>
<field name="arch" type="xml">
@@ -16,5 +17,4 @@
</field>
</record>
-</data>
-</openerp>
+</odoo>
diff --git a/addons/crm/views/crm_activity_views.xml b/addons/crm/views/crm_activity_views.xml
index cf3ade757ae..f42580d1ba2 100644
--- a/addons/crm/views/crm_activity_views.xml
+++ b/addons/crm/views/crm_activity_views.xml
@@ -1,7 +1,8 @@
<?xml version="1.0"?>
<odoo>
+
<record id="crm_activity_view_search" model="ir.ui.view">
- <field name="name">crm.activity.view.search</field>
+ <field name="name">crm.activity.search</field>
<field name="model">crm.activity</field>
<field name="arch" type="xml">
<search string="Action Search">
@@ -12,7 +13,7 @@
</record>
<record id="crm_activity_view_form" model="ir.ui.view">
- <field name="name">crm.activity.view.form</field>
+ <field name="name">crm.activity.form</field>
<field name="model">crm.activity</field>
<field name="arch" type="xml">
<form string="Activities">
@@ -36,7 +37,7 @@
</record>
<record id="crm_activity_view_tree" model="ir.ui.view">
- <field name="name">crm.activity.view.tree</field>
+ <field name="name">crm.activity.tree</field>
<field name="model">crm.activity</field>
<field name="arch" type="xml">
<tree string="Activities">
@@ -58,6 +59,9 @@
</field>
</record>
- <menuitem id="crm_activity_menu" action="crm_activity_action"
- name="Activities" parent="base.menu_crm_config_lead"/>
+ <menuitem
+ id="crm_activity_menu"
+ name="Activities"
+ action="crm_activity_action"
+ parent="sales_team.menu_crm_config_lead"/>
</odoo>
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index dd0de811f3d..b2d545b68ee 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -1,6 +1,5 @@
<?xml version="1.0"?>
-<openerp>
- <data>
+<odoo>
<!--
crm.lead.tag views
-->
@@ -38,10 +37,8 @@
context="{'default_duration': 4.0, 'default_opportunity_id': active_id}"
view_type="form"/>
-
- <!-- CRM Lead Form View -->
- <record model="ir.ui.view" id="crm_case_form_view_leads">
- <field name="name">CRM - Leads Form</field>
+ <record id="crm_case_form_view_leads" model="ir.ui.view">
+ <field name="name">crm.lead.form.lead</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<form string="Leads Form">
@@ -64,7 +61,6 @@
<group>
<!-- Preload all the partner's information -->
<field name="partner_id" string="Customer"
- on_change="on_change_partner_id(partner_id)"
options='{"create_name_field": "name"}'
context="{'default_name': contact_name, 'default_street': street, 'default_city': city, 'default_state_id': state_id, 'default_zip': zip, 'default_country_id': country_id, 'default_function': function, 'default_phone': phone, 'default_mobile': mobile, 'default_fax': fax, 'default_email': email_from, 'default_user_id': user_id, 'default_team_id': team_id}" groups="base.group_no_one"/>
<field name="partner_name" string="Company Name"/>
@@ -73,7 +69,7 @@
<field name="street" placeholder="Street..." class="o_address_street"/>
<field name="street2" placeholder="Street 2..." class="o_address_street"/>
<field name="city" placeholder="City" class="o_address_city"/>
- <field name="state_id" class="o_address_state" placeholder="State" options='{"no_open": True}' on_change="onchange_state(state_id)"/>
+ <field name="state_id" class="o_address_state" placeholder="State" options='{"no_open": True}'/>
<field name="zip" placeholder="ZIP" class="o_address_zip"/>
<field name="country_id" placeholder="Country" class="o_address_country" options='{"no_open": True, "no_create": True}'/>
</div>
@@ -91,7 +87,7 @@
<field name="fax"/>
</group>
<group>
- <field name="user_id" on_change="on_change_user(user_id, context)" domain="[('share', '=', False)]"
+ <field name="user_id" domain="[('share', '=', False)]"
context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'sales_team.group_sale_salesman_all_leads'], 'team_id': team_id}"/>
<field name="team_id" widget="selection" domain="[('use_leads','=',True)]"/>
<field name="type" invisible="1"/>
@@ -137,9 +133,8 @@
</field>
</record>
- <!-- CRM Lead Pivot View -->
- <record model="ir.ui.view" id="crm_case_pivot_view_leads">
- <field name="name">Opportunities</field>
+ <record id="crm_case_pivot_view_leads" model="ir.ui.view">
+ <field name="name">crm.lead.pivot.lead</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<pivot string="Opportunities" display_quantity="true">
@@ -149,9 +144,8 @@
</field>
</record>
- <!-- CRM Lead Graph View -->
- <record model="ir.ui.view" id="crm_case_graph_view_leads">
- <field name="name">Opportunities</field>
+ <record id="crm_case_graph_view_leads" model="ir.ui.view">
+ <field name="name">crm.lead.graph.lead</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<graph string="Opportunities" type="bar">
@@ -161,9 +155,8 @@
</field>
</record>
- <!-- CRM Lead Tree View -->
- <record model="ir.ui.view" id="crm_case_tree_view_leads">
- <field name="name">Leads</field>
+ <record id="crm_case_tree_view_leads" model="ir.ui.view">
+ <field name="name">crm.lead.tree.lead</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<tree string="Leads" decoration-bf="message_needaction==True" decoration-muted="probability == 100">
@@ -189,22 +182,18 @@
</field>
</record>
-
- <!-- CRM Lead Calendar View -->
- <record model="ir.ui.view" id="crm_case_calendar_view_leads">
- <field name="name">CRM - Leads Calendar</field>
+ <record id="crm_case_calendar_view_leads" model="ir.ui.view">
+ <field name="name">crm.lead.calendar.lead</field>
<field name="model">crm.lead</field>
<field name="priority" eval="2"/>
<field name="arch" type="xml">
- <calendar string="Leads Generation"
- date_start="date_action" color="user_id">
+ <calendar string="Leads Generation" date_start="date_action" color="user_id">
<field name="name"/>
<field name="partner_name"/>
</calendar>
</field>
</record>
- <!-- CRM Lead simplified Form View -->
<record id="view_create_opportunity_simplified" model="ir.ui.view">
<field name="name">crm.lead.form.simple</field>
<field name="model">crm.lead</field>
@@ -232,6 +221,7 @@
</form>
</field>
</record>
+
<record id="create_opportunity_simplified" model="ir.actions.act_window">
<field name="name">Create an Opportunity</field>
<field name="res_model">crm.lead</field>
@@ -241,9 +231,8 @@
<field name="target">new</field>
</record>
- <!-- CRM Lead Kanban View -->
- <record model="ir.ui.view" id="crm_case_kanban_view_leads">
- <field name="name">CRM - Leads Kanban</field>
+ <record id="crm_case_kanban_view_leads" model="ir.ui.view">
+ <field name="name">crm.lead.kanban.lead</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<kanban default_group_by="stage_id" class="o_kanban_small_column o_opportunity_kanban" on_create="crm.create_opportunity_simplified">
@@ -317,9 +306,8 @@
</field>
</record>
- <!-- CRM Lead Search View -->
<record id="view_crm_case_leads_filter" model="ir.ui.view">
- <field name="name">CRM - Leads Search</field>
+ <field name="name">crm.lead.search.lead</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<search string="Search Leads">
@@ -362,9 +350,8 @@
<!--
crm.lead (as Opportunity) views
-->
- <!-- Opportunities Form View -->
- <record model="ir.ui.view" id="crm_case_form_view_oppor">
- <field name="name">Opportunities</field>
+ <record id="crm_case_form_view_oppor" model="ir.ui.view">
+ <field name="name">crm.lead.form.opportunity</field>
<field name="model">crm.lead</field>
<field name="priority">20</field>
<field name="arch" type="xml">
@@ -426,15 +413,14 @@
<group>
<group>
<field name="partner_id"
- on_change="on_change_partner_id(partner_id)"
string="Customer" domain="[('customer', '=', True)]"
context="{'search_default_customer': 1,
- 'default_name': partner_name, 'default_street': street,
- 'default_street2': street2, 'default_city': city,
- 'default_state_id': state_id, 'default_zip': zip,
- 'default_country_id': country_id, 'default_function': function,
- 'default_phone': phone, 'default_mobile': mobile,
- 'default_fax': fax, 'default_email': email_from,
+ 'default_name': partner_name, 'default_street': street,
+ 'default_street2': street2, 'default_city': city,
+ 'default_state_id': state_id, 'default_zip': zip,
+ 'default_country_id': country_id, 'default_function': function,
+ 'default_phone': phone, 'default_mobile': mobile,
+ 'default_fax': fax, 'default_email': email_from,
'default_user_id': user_id, 'default_team_id': team_id}"/>
<field name="email_from" string="Email" widget="email"/>
<field name="phone" widget="phone"/>
@@ -446,8 +432,7 @@
<div class="o_row mr16">
<field name="next_activity_id"
options="{'no_create': True, 'no_open': True}"
- domain="['|',('team_id','=',False),('team_id','=',team_id)]"
- on_change="onchange_next_activity_id(next_activity_id)"/>
+ domain="['|',('team_id','=',False),('team_id','=',team_id)]"/>
<span class="oe_form_field"
attrs="{'invisible': ['|', ('next_activity_id', '=', False), ('date_action', '=', False)]}">&#160;on&#160;</span>
<field name="date_action" attrs="{'invisible': [('next_activity_id', '=', False)]}" placeholder="Choose a date..."/>
@@ -461,7 +446,7 @@
</group>
<group>
- <field name="user_id" on_change="on_change_user(user_id, context)" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'sales_team.group_sale_salesman_all_leads'], 'team_id': team_id}" domain="[('share', '=', False)]"/>
+ <field name="user_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'sales_team.group_sale_salesman_all_leads'], 'team_id': team_id}" domain="[('share', '=', False)]"/>
<field name="team_id" widget="selection"/>
</group>
<group>
@@ -485,7 +470,7 @@
<field name="street" placeholder="Street..." class="o_address_street"/>
<field name="street2" placeholder="Street 2..." class="o_address_street"/>
<field name="city" placeholder="City" class="o_address_city"/>
- <field name="state_id" class="o_address_state" placeholder="State" options='{"no_open": True}' on_change="onchange_state(state_id)"/>
+ <field name="state_id" class="o_address_state" placeholder="State" options='{"no_open": True}'/>
<field name="zip" placeholder="ZIP" class="o_address_zip"/>
<field name="country_id" placeholder="Country" class="o_address_country" options='{"no_open": True, "no_create": True}'/>
</div>
@@ -525,9 +510,8 @@
</field>
</record>
- <!-- Opportunities Tree View -->
- <record model="ir.ui.view" id="crm_case_tree_view_oppor">
- <field name="name">Opportunities Tree</field>
+ <record id="crm_case_tree_view_oppor" model="ir.ui.view">
+ <field name="name">crm.lead.tree.opportunity</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<tree string="Opportunities" decoration-bf="message_needaction==True" decoration-muted="probability == 100" decoration-danger="date_action and (date_action &lt; current_date)">
@@ -572,9 +556,8 @@
</field>
</record>
- <!-- Opportunities Search View -->
<record id="view_crm_case_opportunities_filter" model="ir.ui.view">
- <field name="name">CRM - Opportunities Search</field>
+ <field name="name">crm.lead.search.opportunity</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<search string="Search Opportunities">
@@ -704,7 +687,11 @@
<!--
Menu Actions
-->
- <menuitem id="base.menu_sales" name="Sales" parent="base.menu_base_partner" sequence="5"/>
+ <menuitem
+ id="sales_team.menu_sales"
+ name="Sales"
+ parent="sales_team.menu_base_partner"
+ sequence="5"/>
<!-- Lead Menu -->
<record model="ir.actions.act_window" id="crm_lead_all_leads">
@@ -734,8 +721,13 @@
</field>
</record>
- <menuitem name="Leads" id="menu_crm_leads" parent="base.menu_sales" sequence="1"
- action="crm_lead_all_leads" groups="crm.group_use_lead"/>
+ <menuitem
+ id="menu_crm_leads"
+ name="Leads"
+ parent="sales_team.menu_sales"
+ sequence="1"
+ action="crm_lead_all_leads"
+ groups="crm.group_use_lead"/>
<!-- 'My Pipeline' menu : Server action, act_window_views and act_windows -->
<record model="ir.actions.act_window" id="crm_lead_opportunities">
@@ -800,6 +792,7 @@
'default_user_id': uid,
'search_default_assigned_to_me': 1,
}</field>
+ <field name="view_id" ref="crm.crm_case_form_view_oppor"/>
<field name="search_view_id" ref="crm.view_crm_case_opportunities_filter"/>
<field name="help" type="html">
<p>
@@ -813,7 +806,11 @@
</field>
</record>
- <menuitem id="menu_crm_opportunities" action="action_your_pipeline" parent="base.menu_sales" sequence="4"/>
+ <menuitem
+ id="menu_crm_opportunities"
+ action="action_your_pipeline"
+ parent="sales_team.menu_sales"
+ sequence="4"/>
<!-- Next Activities Menu -->
<record id="crm_lead_action_activities" model="ir.actions.act_window">
@@ -841,8 +838,12 @@
</field>
</record>
- <menuitem id="crm_lead_menu_activities" name="Next Activities" sequence="5"
- parent="base.menu_sales" action="crm_lead_action_activities" />
+ <menuitem
+ id="crm_lead_menu_activities"
+ name="Next Activities"
+ parent="sales_team.menu_sales"
+ sequence="5"
+ action="crm_lead_action_activities" />
<!-- Configuration/Lead & Opporotunities/Lead Tags Menu -->
<record id="crm_lead_tag_action" model="ir.actions.act_window">
@@ -862,9 +863,13 @@
</field>
</record>
- <menuitem action="crm_lead_tag_action"
- id="menu_crm_lead_categ" name="Lead Tags"
- parent="base.menu_crm_config_lead" sequence="1" groups="base.group_no_one"/>
+ <menuitem
+ id="menu_crm_lead_categ"
+ name="Lead Tags"
+ action="crm_lead_tag_action"
+ parent="sales_team.menu_crm_config_lead"
+ sequence="1"
+ groups="base.group_no_one"/>
<!-- Configuration/Lead & Opporotunities/Lost Reasons Menu -->
<record id="crm_lost_reason_action" model="ir.actions.act_window">
@@ -883,7 +888,11 @@
</field>
</record>
- <menuitem action="crm_lost_reason_action" id="menu_crm_lost_reason" parent="base.menu_crm_config_lead" sequence="6" groups="base.group_no_one"/>
+ <menuitem
+ id="menu_crm_lost_reason"
+ action="crm_lost_reason_action"
+ parent="sales_team.menu_crm_config_lead"
+ sequence="6"
+ groups="base.group_no_one"/>
- </data>
-</openerp>
+</odoo>
diff --git a/addons/crm/views/crm_stage_views.xml b/addons/crm/views/crm_stage_views.xml
index 95d0535a0f1..c4d9d98c6ce 100644
--- a/addons/crm/views/crm_stage_views.xml
+++ b/addons/crm/views/crm_stage_views.xml
@@ -14,7 +14,7 @@
</field>
</record>
- <record model="ir.ui.view" id="crm_stage_tree">
+ <record id="crm_stage_tree" model="ir.ui.view">
<field name="name">crm.stage.tree</field>
<field name="model">crm.stage</field>
<field name="arch" type="xml">
@@ -27,7 +27,7 @@
</field>
</record>
- <record model="ir.ui.view" id="crm_stage_form">
+ <record id="crm_stage_form" model="ir.ui.view">
<field name="name">crm.stage.form</field>
<field name="model">crm.stage</field>
<field name="priority" eval="1"/>
@@ -65,6 +65,12 @@
</field>
</record>
- <menuitem action="crm_stage_action" id="menu_crm_lead_stage_act" name="Stages" sequence="0" parent="base.menu_crm_config_lead" groups="base.group_no_one"/>
+ <menuitem
+ id="menu_crm_lead_stage_act"
+ name="Stages"
+ sequence="0"
+ action="crm_stage_action"
+ parent="sales_team.menu_crm_config_lead"
+ groups="base.group_no_one"/>
</odoo>
diff --git a/addons/crm/views/crm_team_views.xml b/addons/crm/views/crm_team_views.xml
index 30b6b1d9a4a..87e2c025bee 100644
--- a/addons/crm/views/crm_team_views.xml
+++ b/addons/crm/views/crm_team_views.xml
@@ -1,10 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
- <data>
+<odoo>
<!-- CRM lead search by Salesteams -->
-
- <record model="ir.actions.act_window" id="crm_case_form_view_salesteams_lead">
+ <record id="crm_case_form_view_salesteams_lead" model="ir.actions.act_window">
<field name="name">Leads</field>
<field name="res_model">crm.lead</field>
<field name="view_mode">tree,form</field>
@@ -31,8 +29,7 @@
</record>
<!-- CRM opportunity search by Salesteams -->
-
- <record model="ir.actions.act_window" id="crm_case_form_view_salesteams_opportunity">
+ <record id="crm_case_form_view_salesteams_opportunity" model="ir.actions.act_window">
<field name="name">Opportunities</field>
<field name="res_model">crm.lead</field>
<field name="view_mode">kanban,tree,graph,form,calendar,pivot</field>
@@ -79,18 +76,18 @@
<field name="help">Opportunities Analysis gives you an instant access to your opportunities with information such as the expected revenue, planned cost, missed deadlines or the number of interactions per opportunity. This report is mainly used by the sales manager in order to do the periodic review with the teams of the sales pipeline.</field>
</record>
- <record model="ir.ui.view" id="sales_team_form_view_in_crm">
- <field name="name">crm.team.form</field>
+ <record id="sales_team_form_view_in_crm" model="ir.ui.view">
+ <field name="name">crm.team.form.inherit</field>
<field name="model">crm.team</field>
<field name="inherit_id" ref="sales_team.crm_team_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='options_active']" position="inside">
<div class="o_row">
<span groups="crm.group_use_lead" class="o_row">
- <field name="use_leads" on_change="onchange_use_leads_opportunities(use_leads, use_opportunities)"/>
+ <field name="use_leads"/>
<label for="use_leads" string="Leads"/>
</span>
- <field name="use_opportunities" on_change="onchange_use_leads_opportunities(use_leads, use_opportunities)"/>
+ <field name="use_opportunities"/>
<label for="use_opportunities"/>
</div>
</xpath>
@@ -114,21 +111,21 @@
</record>
<!-- Case Teams Action -->
- <record model="ir.actions.act_window.view" id="action_crm_tag_kanban_view_salesteams_oppor11">
+ <record id="action_crm_tag_kanban_view_salesteams_oppor11" model="ir.actions.act_window.view">
<field name="sequence" eval="0"/>
<field name="view_mode">kanban</field>
<field name="view_id" ref="crm_case_kanban_view_leads"/>
<field name="act_window_id" ref="crm_case_form_view_salesteams_opportunity"/>
</record>
- <record model="ir.actions.act_window.view" id="action_crm_tag_tree_view_salesteams_oppor11">
+ <record id="action_crm_tag_tree_view_salesteams_oppor11" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="crm_case_tree_view_oppor"/>
<field name="act_window_id" ref="crm_case_form_view_salesteams_opportunity"/>
</record>
- <record model="ir.actions.act_window.view" id="action_crm_tag_form_view_salesteams_oppor11">
+ <record id="action_crm_tag_form_view_salesteams_oppor11" model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="crm_case_form_view_oppor"/>
@@ -142,7 +139,6 @@
<field name="view_id" ref="sales_team.crm_team_view_tree"/>
</record>
-
<record id="action_opportunity_form" model="ir.actions.act_window">
<field name="name">New Opportunity</field>
<field name="res_model">crm.lead</field>
@@ -160,7 +156,7 @@
<field name="search_view_id" ref="crm.view_crm_case_opportunities_filter"/>
</record>
- <record model="ir.ui.view" id="crm_team_salesteams_view_kanban">
+ <record id="crm_team_salesteams_view_kanban" model="ir.ui.view">
<field name="name">crm.team.kanban</field>
<field name="model">crm.team</field>
<field name="inherit_id" ref="sales_team.crm_team_salesteams_view_kanban"/>
@@ -234,5 +230,4 @@
</field>
</record>
- </data>
-</openerp>
+</odoo>
diff --git a/addons/crm/views/crm_templates.xml b/addons/crm/views/crm_templates.xml
index 8d3a81a3a17..47b588487df 100644
--- a/addons/crm/views/crm_templates.xml
+++ b/addons/crm/views/crm_templates.xml
@@ -1,16 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
- <data>
+<odoo>
<template id="assets_backend" name="crm assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/crm/static/src/css/crm.css"/>
<!-- Planner assets -->
<script type="text/javascript" src="/crm/static/src/js/web_planner_crm.js"></script>
-
<!-- Salesteam dashboard asset -->
<script type="text/javascript" src="/crm/static/src/js/sales_team_dashboard.js"></script>
<script type="text/javascript" src="/crm/static/src/js/tour.js"></script>
</xpath>
</template>
- </data>
-</openerp>
+</odoo>
diff --git a/addons/crm/views/crm_views.xml b/addons/crm/views/crm_views.xml
index 933e39417e2..2510f3bccf8 100644
--- a/addons/crm/views/crm_views.xml
+++ b/addons/crm/views/crm_views.xml
@@ -1,24 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
- <data>
+<odoo>
<!-- Top menu item -->
<!--
This menu item's purpose is to overwrite another one defined in
the base module in order to set new groups.
-->
- <menuitem name="CRM"
+ <menuitem
id="sales_team.menu_base_partner"
- web_icon="crm,static/description/icon.png"/>
+ name="CRM"
+ groups="sales_team.group_sale_manager,sales_team.group_sale_salesman"
+ web_icon="crm,static/description/icon.png"
+ sequence="6"/>
- <menuitem id="base.menu_crm_config_lead" name="Leads &amp; Opportunities"
- parent="sales_team.menu_sale_config" sequence="5" groups="sales_team.group_sale_manager"/>
+ <menuitem
+ id="sales_team.menu_crm_config_lead"
+ name="Leads &amp; Opportunities"
+ parent="sales_team.menu_sale_config"
+ sequence="5"
+ groups="sales_team.group_sale_manager"/>
- <menuitem id="base.menu_crm_config_opportunity" name="Opportunities"
- parent="sales_team.menu_sale_config" sequence="1" groups="sales_team.group_sale_manager"/>
+ <menuitem
+ id="sales_team.menu_crm_config_opportunity"
+ name="Opportunities"
+ parent="sales_team.menu_sale_config"
+ sequence="1"
+ groups="sales_team.group_sale_manager"/>
- <menuitem id="sales_team.menu_sale_config_sales" name="Sales"
- parent="sales_team.menu_sale_config" sequence="0" groups="sales_team.group_sale_manager"/>
+ <menuitem
+ id="sales_team.menu_sale_config_sales"
+ name="Sales"
+ parent="sales_team.menu_sale_config"
+ sequence="0"
+ groups="sales_team.group_sale_manager"/>
- </data>
-</openerp>
+</odoo>
diff --git a/addons/crm/views/res_config_views.xml b/addons/crm/views/res_config_views.xml
index 1e9cc22ae15..dc244230e0a 100644
--- a/addons/crm/views/res_config_views.xml
+++ b/addons/crm/views/res_config_views.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
- <data>
+<odoo>
<record id="view_sale_config_settings" model="ir.ui.view">
<field name="name">crm settings</field>
<field name="model">sale.config.settings</field>
@@ -41,15 +40,24 @@
<field name="module_website_sign" widget="upgrade_boolean" class="oe_inline"/>
<label for="module_website_sign" invisible="1"/>
<a href="https://www.odoo.com/page/sign" target="_blank"> More Info </a>
- </div>
+ </div>
</group>
</div>
</field>
</record>
- <menuitem id="sales_team.menu_sale_config" name="Configuration" parent="sales_team.menu_base_partner"
+ <menuitem
+ id="sales_team.menu_sale_config"
+ name="Configuration"
+ parent="sales_team.menu_base_partner"
sequence="100"/>
- <menuitem id="sales_team.menu_sale_general_settings" name="Settings" parent="sales_team.menu_sale_config"
- sequence="0" action="sales_team.action_sale_config" groups="base.group_system"/>
- </data>
-</openerp>
+
+ <menuitem
+ id="sales_team.menu_sale_general_settings"
+ name="Settings"
+ parent="sales_team.menu_sale_config"
+ sequence="0"
+ action="sales_team.action_sale_config"
+ groups="base.group_system"/>
+
+</odoo>
diff --git a/addons/crm/views/res_partner_views.xml b/addons/crm/views/res_partner_views.xml
index 73c04a4ea0a..1208663ef78 100644
--- a/addons/crm/views/res_partner_views.xml
+++ b/addons/crm/views/res_partner_views.xml
@@ -1,6 +1,5 @@
<?xml version="1.0"?>
-<openerp>
- <data>
+<odoo>
<!-- open meetings related to given partner -->
<record id="calendar_event_partner" model="ir.actions.act_window">
@@ -12,7 +11,7 @@
</record>
<!-- open opportunities related to given partner -->
- <record model="ir.actions.act_window" id="relate_partner_opportunities">
+ <record id="relate_partner_opportunities" model="ir.actions.act_window">
<field name="name">Opportunities</field>
<field name="res_model">crm.lead</field>
<field name="view_mode">kanban,tree,form,graph,calendar</field>
@@ -36,19 +35,22 @@
</p>
</field>
</record>
- <record model="ir.actions.act_window.view" id="relate_partner_opportunities_kanban">
+
+ <record id="relate_partner_opportunities_kanban" model="ir.actions.act_window.view">
<field name="sequence" eval="0"/>
<field name="view_mode">kanban</field>
<field name="view_id" ref="crm_case_kanban_view_leads"/>
<field name="act_window_id" ref="relate_partner_opportunities"/>
</record>
- <record model="ir.actions.act_window.view" id="relate_partner_opportunities_tree">
+
+ <record id="relate_partner_opportunities_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="crm_case_tree_view_oppor"/>
<field name="act_window_id" ref="relate_partner_opportunities"/>
</record>
- <record model="ir.actions.act_window.view" id="relate_partner_opportunities_form">
+
+ <record id="relate_partner_opportunities_form" model="ir.actions.act_window.view">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="crm_case_form_view_oppor"/>
@@ -56,7 +58,7 @@
</record>
<!-- Partner kanban view inherte -->
- <record model="ir.ui.view" id="crm_lead_partner_kanban_view">
+ <record id="crm_lead_partner_kanban_view" model="ir.ui.view">
<field name="name">res.partner.kanban.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.res_partner_kanban_view"/>
@@ -104,9 +106,6 @@
<field eval="18" name="priority"/>
<field name="arch" type="xml">
<data>
- <!-- <field name="user_id" position="after">
- <field name="team_id" completion="1" invisible="1"/>
- </field> -->
<div name="button_box" position="inside">
<button class="oe_stat_button o_res_partner_tip_opp" type="action"
attrs="{'invisible': [('customer', '=', False)]}"
@@ -131,5 +130,4 @@
</field>
</record>
- </data>
-</openerp>
+</odoo>
diff --git a/addons/crm/wizard/base_partner_merge.py b/addons/crm/wizard/base_partner_merge.py
index 1fedd1dac80..17220184c7d 100644
--- a/addons/crm/wizard/base_partner_merge.py
+++ b/addons/crm/wizard/base_partner_merge.py
@@ -1,5 +1,7 @@
-#!/usr/bin/env python
-from __future__ import absolute_import
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from ast import literal_eval
from email.utils import parseaddr
import functools
import htmlentitydefs
@@ -8,21 +10,15 @@ import logging
import operator
import psycopg2
import re
-from ast import literal_eval
-from openerp.exceptions import ValidationError
-from openerp.tools import mute_logger
# Validation Library https://pypi.python.org/pypi/validate_email/1.1
from .validate_email import validate_email
-import openerp
-from openerp.osv import osv, orm
-from openerp.osv import fields
-from openerp.osv.orm import browse_record
-from openerp.tools.translate import _
-from openerp.exceptions import UserError
+from odoo import api, fields, models
+from odoo import SUPERUSER_ID, _
+from odoo.exceptions import ValidationError, UserError
+from odoo.tools import mute_logger
-pattern = re.compile("&(\w+?);")
_logger = logging.getLogger('base.partner.merge')
@@ -36,14 +32,14 @@ def html_entity_decode_char(m, defs=htmlentitydefs.entitydefs):
def html_entity_decode(string):
+ pattern = re.compile("&(\w+?);")
return pattern.sub(html_entity_decode_char, string)
def sanitize_email(email):
assert isinstance(email, basestring) and email
- result = re.subn(r';|/|:', ',',
- html_entity_decode(email or ''))[0].split(',')
+ result = re.subn(r';|/|:', ',', html_entity_decode(email or ''))[0].split(',')
emails = [parseaddr(email)[1]
for item in result
@@ -54,119 +50,110 @@ def sanitize_email(email):
if validate_email(email)]
-def is_integer_list(ids):
- return all(isinstance(i, (int, long)) for i in ids)
-
-
-class ResPartner(osv.Model):
- _inherit = 'res.partner'
+class MergePartnerLine(models.TransientModel):
- _columns = {
- 'id': fields.integer('Id', readonly=True),
- 'create_date': fields.datetime('Create Date', readonly=True),
- }
-
-class MergePartnerLine(osv.TransientModel):
_name = 'base.partner.merge.line'
-
- _columns = {
- 'wizard_id': fields.many2one('base.partner.merge.automatic.wizard',
- 'Wizard'),
- 'min_id': fields.integer('MinID'),
- 'aggr_ids': fields.char('Ids', required=True),
- }
-
_order = 'min_id asc'
+ wizard_id = fields.Many2one('base.partner.merge.automatic.wizard', 'Wizard')
+ min_id = fields.Integer('MinID')
+ aggr_ids = fields.Char('Ids', required=True)
+
-class MergePartnerAutomatic(osv.TransientModel):
+class MergePartnerAutomatic(models.TransientModel):
"""
The idea behind this wizard is to create a list of potential partners to
merge. We use two objects, the first one is the wizard for the end-user.
And the second will contain the partner list to merge.
"""
+
_name = 'base.partner.merge.automatic.wizard'
- _columns = {
- # Group by
- 'group_by_email': fields.boolean('Email'),
- 'group_by_name': fields.boolean('Name'),
- 'group_by_is_company': fields.boolean('Is Company'),
- 'group_by_vat': fields.boolean('VAT'),
- 'group_by_parent_id': fields.boolean('Parent Company'),
-
- 'state': fields.selection([('option', 'Option'),
- ('selection', 'Selection'),
- ('finished', 'Finished')],
- 'State',
- readonly=True,
- required=True),
- 'number_group': fields.integer("Group of Contacts", readonly=True),
- 'current_line_id': fields.many2one('base.partner.merge.line', 'Current Line'),
- 'line_ids': fields.one2many('base.partner.merge.line', 'wizard_id', 'Lines'),
- 'partner_ids': fields.many2many('res.partner', string='Contacts'),
- 'dst_partner_id': fields.many2one('res.partner', string='Destination Contact'),
-
- 'exclude_contact': fields.boolean('A user associated to the contact'),
- 'exclude_journal_item': fields.boolean('Journal Items associated to the contact'),
- 'maximum_group': fields.integer("Maximum of Group of Contacts"),
- }
-
- def default_get(self, cr, uid, fields, context=None):
- if context is None:
- context = {}
- res = super(MergePartnerAutomatic, self).default_get(cr, uid, fields, context)
- if context.get('active_model') == 'res.partner' and context.get('active_ids'):
- partner_ids = context['active_ids']
+ @api.model
+ def default_get(self, fields):
+ res = super(MergePartnerAutomatic, self).default_get(fields)
+ active_ids = self.env.context.get('active_ids')
+ if self.env.context.get('active_model') == 'res.partner' and active_ids:
res['state'] = 'selection'
- res['partner_ids'] = partner_ids
- res['dst_partner_id'] = self._get_ordered_partner(cr, uid, partner_ids, context=context)[-1].id
+ res['partner_ids'] = active_ids
+ res['dst_partner_id'] = self._get_ordered_partner(active_ids)[-1].id
return res
- _defaults = {
- 'state': 'option'
- }
-
- def get_fk_on(self, cr, table):
- q = """ SELECT cl1.relname as table,
- att1.attname as column
- FROM pg_constraint as con, pg_class as cl1, pg_class as cl2,
- pg_attribute as att1, pg_attribute as att2
- WHERE con.conrelid = cl1.oid
- AND con.confrelid = cl2.oid
- AND array_lower(con.conkey, 1) = 1
- AND con.conkey[1] = att1.attnum
- AND att1.attrelid = cl1.oid
- AND cl2.relname = %s
- AND att2.attname = 'id'
- AND array_lower(con.confkey, 1) = 1
- AND con.confkey[1] = att2.attnum
- AND att2.attrelid = cl2.oid
- AND con.contype = 'f'
+ # Group by
+ group_by_email = fields.Boolean('Email')
+ group_by_name = fields.Boolean('Name')
+ group_by_is_company = fields.Boolean('Is Company')
+ group_by_vat = fields.Boolean('VAT')
+ group_by_parent_id = fields.Boolean('Parent Company')
+
+ state = fields.Selection([
+ ('option', 'Option'),
+ ('selection', 'Selection'),
+ ('finished', 'Finished')
+ ], readonly=True, required=True, string='State', default='option')
+
+ number_group = fields.Integer('Group of Contacts', readonly=True)
+ current_line_id = fields.Many2one('base.partner.merge.line', string='Current Line')
+ line_ids = fields.One2many('base.partner.merge.line', 'wizard_id', string='Lines')
+ partner_ids = fields.Many2many('res.partner', string='Contacts')
+ dst_partner_id = fields.Many2one('res.partner', string='Destination Contact')
+
+ exclude_contact = fields.Boolean('A user associated to the contact')
+ exclude_journal_item = fields.Boolean('Journal Items associated to the contact')
+ maximum_group = fields.Integer('Maximum of Group of Contacts')
+
+ # ----------------------------------------
+ # Update method. Core methods to merge steps
+ # ----------------------------------------
+
+ def _get_fk_on(self, table):
+ """ return a list of many2one relation with the given table.
+ :param table : the name of the sql table to return relations
+ :returns a list of tuple 'table name', 'column name'.
"""
- return cr.execute(q, (table,))
-
- def _update_foreign_keys(self, cr, uid, src_partners, dst_partner, context=None):
- _logger.debug('_update_foreign_keys for dst_partner: %s for src_partners: %r', dst_partner.id, list(map(operator.attrgetter('id'), src_partners)))
+ query = """
+ SELECT cl1.relname as table, att1.attname as column
+ FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, pg_attribute as att1, pg_attribute as att2
+ WHERE con.conrelid = cl1.oid
+ AND con.confrelid = cl2.oid
+ AND array_lower(con.conkey, 1) = 1
+ AND con.conkey[1] = att1.attnum
+ AND att1.attrelid = cl1.oid
+ AND cl2.relname = %s
+ AND att2.attname = 'id'
+ AND array_lower(con.confkey, 1) = 1
+ AND con.confkey[1] = att2.attnum
+ AND att2.attrelid = cl2.oid
+ AND con.contype = 'f'
+ """
+ self._cr.execute(query, (table,))
+ return self._cr.fetchall()
+
+ @api.model
+ def _update_foreign_keys(self, src_partners, dst_partner):
+ """ Update all foreign key from the src_partner to dst_partner. All many2one fields will be updated.
+ :param src_partners : merge source res.partner recordset (does not include destination one)
+ :param dst_partner : record of destination res.partner
+ """
+ _logger.debug('_update_foreign_keys for dst_partner: %s for src_partners: %s', dst_partner.id, str(src_partners.ids))
# find the many2one relation to a partner
- proxy = self.pool.get('res.partner')
- self.get_fk_on(cr, 'res_partner')
-
- # ignore two tables
+ Partner = self.env['res.partner']
+ relations = self._get_fk_on('res_partner')
- for table, column in cr.fetchall():
- if 'base_partner_merge_' in table:
+ for table, column in relations:
+ if 'base_partner_merge_' in table: # ignore two tables
continue
- partner_ids = tuple(map(int, src_partners))
+ # get list of columns of current table (exept the current fk column)
query = "SELECT column_name FROM information_schema.columns WHERE table_name LIKE '%s'" % (table)
- cr.execute(query, ())
+ self._cr.execute(query, ())
columns = []
- for data in cr.fetchall():
+ for data in self._cr.fetchall():
if data[0] != column:
columns.append(data[0])
+ # do the update for the current table/column in SQL
query_dic = {
'table': table,
'column': column,
@@ -186,15 +173,16 @@ class MergePartnerAutomatic(osv.TransientModel):
%(column)s = %%s AND
___tu.%(value)s = ___tw.%(value)s
)""" % query_dic
- for partner_id in partner_ids:
- cr.execute(query, (dst_partner.id, partner_id, dst_partner.id))
+ for partner in src_partners:
+ self._cr.execute(query, (dst_partner.id, partner.id, dst_partner.id))
else:
try:
- with mute_logger('openerp.sql_db'), cr.savepoint():
+ with mute_logger('openerp.sql_db'), self._cr.savepoint():
query = 'UPDATE "%(table)s" SET %(column)s = %%s WHERE %(column)s IN %%s' % query_dic
- cr.execute(query, (dst_partner.id, partner_ids,))
+ self._cr.execute(query, (dst_partner.id, tuple(src_partners.ids),))
- if column == proxy._parent_name and table == 'res_partner':
+ # handle the recursivity with parent relation
+ if column == Partner._parent_name and table == 'res_partner':
query = """
WITH RECURSIVE cycle(id, parent_id) AS (
SELECT id, parent_id FROM res_partner
@@ -206,31 +194,36 @@ class MergePartnerAutomatic(osv.TransientModel):
)
SELECT id FROM cycle WHERE id = parent_id AND id = %s
"""
- cr.execute(query, (dst_partner.id,))
+ self._cr.execute(query, (dst_partner.id,))
+ # NOTE JEM : shouldn't we fetch the data ?
except psycopg2.Error:
# updating fails, most likely due to a violated unique constraint
# keeping record with nonexistent partner_id is useless, better delete it
query = 'DELETE FROM %(table)s WHERE %(column)s IN %%s' % query_dic
- cr.execute(query, (partner_ids,))
+ self._cr.execute(query, (tuple(src_partners.ids),))
- def _update_reference_fields(self, cr, uid, src_partners, dst_partner, context=None):
- _logger.debug('_update_reference_fields for dst_partner: %s for src_partners: %r', dst_partner.id, list(map(operator.attrgetter('id'), src_partners)))
+ @api.model
+ def _update_reference_fields(self, src_partners, dst_partner):
+ """ Update all reference fields from the src_partner to dst_partner.
+ :param src_partners : merge source res.partner recordset (does not include destination one)
+ :param dst_partner : record of destination res.partner
+ """
+ _logger.debug('_update_reference_fields for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids)
- def update_records(model, src, field_model='model', field_id='res_id', context=None):
- proxy = self.pool.get(model)
- if proxy is None:
+ def update_records(model, src, field_model='model', field_id='res_id'):
+ Model = self.env[model] if model in self.env else None
+ if Model is None:
return
- domain = [(field_model, '=', 'res.partner'), (field_id, '=', src.id)]
- ids = proxy.search(cr, openerp.SUPERUSER_ID, domain, context=context)
+ records = Model.sudo().search([(field_model, '=', 'res.partner'), (field_id, '=', src.id)])
try:
- with mute_logger('openerp.sql_db'), cr.savepoint():
- return proxy.write(cr, openerp.SUPERUSER_ID, ids, {field_id: dst_partner.id}, context=context)
+ with mute_logger('openerp.sql_db'), self._cr.savepoint():
+ return records.sudo().write({field_id: dst_partner.id})
except psycopg2.Error:
# updating fails, most likely due to a violated unique constraint
# keeping record with nonexistent partner_id is useless, better delete it
- return proxy.unlink(cr, openerp.SUPERUSER_ID, ids, context=context)
+ return records.sudo().unlink()
- update_records = functools.partial(update_records, context=context)
+ update_records = functools.partial(update_records)
for partner in src_partners:
update_records('calendar', src=partner, field_model='model_id.model')
@@ -240,102 +233,110 @@ class MergePartnerAutomatic(osv.TransientModel):
update_records('marketing.campaign.workitem', src=partner, field_model='object_id.model')
update_records('ir.model.data', src=partner)
- proxy = self.pool['ir.model.fields']
- domain = [('ttype', '=', 'reference')]
- record_ids = proxy.search(cr, openerp.SUPERUSER_ID, domain, context=context)
-
- for record in proxy.browse(cr, openerp.SUPERUSER_ID, record_ids, context=context):
+ records = self.env['ir.model.fields'].search([('ttype', '=', 'reference')])
+ for record in records.sudo():
try:
- proxy_model = self.pool[record.model]
- column = proxy_model._columns[record.name]
+ Model = self.env[record.model]
+ field = Model._fields[record.name]
except KeyError:
# unknown model or field => skip
continue
- if isinstance(column, fields.function):
+ if field.compute is not None:
continue
for partner in src_partners:
- domain = [
- (record.name, '=', 'res.partner,%d' % partner.id)
- ]
- model_ids = proxy_model.search(cr, openerp.SUPERUSER_ID, domain, context=context)
+ records_ref = Model.sudo().search([(record.name, '=', 'res.partner,%d' % partner.id)])
values = {
record.name: 'res.partner,%d' % dst_partner.id,
}
- proxy_model.write(cr, openerp.SUPERUSER_ID, model_ids, values, context=context)
+ records_ref.sudo().write(values)
- def _update_values(self, cr, uid, src_partners, dst_partner, context=None):
- _logger.debug('_update_values for dst_partner: %s for src_partners: %r', dst_partner.id, list(map(operator.attrgetter('id'), src_partners)))
+ @api.model
+ def _update_values(self, src_partners, dst_partner):
+ """ Update values of dst_partner with the ones from the src_partners.
+ :param src_partners : recordset of source res.partner
+ :param dst_partner : record of destination res.partner
+ """
+ _logger.debug('_update_values for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids)
+
+ model_fields = dst_partner._fields
- columns = dst_partner._columns
- def write_serializer(column, item):
- if isinstance(item, browse_record):
+ def write_serializer(item):
+ if isinstance(item, models.BaseModel):
return item.id
else:
return item
-
+ # get all fields that are not computed or x2many
values = dict()
- for column, field in columns.iteritems():
- if field._type not in ('many2many', 'one2many') and not isinstance(field, fields.function):
+ for column, field in model_fields.iteritems():
+ if field.type not in ('many2many', 'one2many') and field.compute is None:
for item in itertools.chain(src_partners, [dst_partner]):
if item[column]:
- values[column] = write_serializer(column, item[column])
-
+ values[column] = write_serializer(item[column])
+ # remove fields that can not be updated (id and parent_id)
values.pop('id', None)
parent_id = values.pop('parent_id', None)
dst_partner.write(values)
+ # try to update the parent_id
if parent_id and parent_id != dst_partner.id:
try:
dst_partner.write({'parent_id': parent_id})
except ValidationError:
_logger.info('Skip recursive partner hierarchies for parent_id %s of partner: %s', parent_id, dst_partner.id)
+ @api.model
@mute_logger('openerp.osv.expression', 'openerp.models')
- def _merge(self, cr, uid, partner_ids, dst_partner=None, context=None):
- proxy = self.pool.get('res.partner')
-
- partner_ids = proxy.exists(cr, uid, list(partner_ids), context=context)
+ def _merge(self, partner_ids, dst_partner=None):
+ """ private implementation of merge partner
+ :param partner_ids : ids of partner to merge
+ :param dst_partner : record of destination res.partner
+ """
+ Partner = self.env['res.partner']
+ partner_ids = Partner.browse(partner_ids).exists().ids
if len(partner_ids) < 2:
return
if len(partner_ids) > 3:
raise UserError(_("For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed."))
+ # check if the list of partners to merge contains child/parent relation
child_ids = set()
for partner_id in partner_ids:
- child_ids = child_ids.union(set(proxy.search(cr, uid, [('id', 'child_of', [partner_id])])) - set([partner_id]))
+ child_ids = child_ids.union(set(Partner.search([('id', 'child_of', [partner_id])]).ids) - set([partner_id]))
if set(partner_ids).intersection(child_ids):
raise UserError(_("You cannot merge a contact with one of his parent."))
- if openerp.SUPERUSER_ID != uid and len(set(partner.email for partner in proxy.browse(cr, uid, partner_ids, context=context))) > 1:
+ # check only admin can merge partners with different emails
+ if SUPERUSER_ID != self.env.uid and len(set(partner.email for partner in Partner.browse(partner_ids))) > 1:
raise UserError(_("All contacts must have the same email. Only the Administrator can merge contacts with different emails."))
+ # remove dst_partner from partners to merge
if dst_partner and dst_partner.id in partner_ids:
- src_partners = proxy.browse(cr, uid, [id for id in partner_ids if id != dst_partner.id], context=context)
+ src_partners = Partner.browse([pid for pid in partner_ids if pid != dst_partner.id])
else:
- ordered_partners = self._get_ordered_partner(cr, uid, partner_ids, context)
+ ordered_partners = self._get_ordered_partner(partner_ids)
dst_partner = ordered_partners[-1]
src_partners = ordered_partners[:-1]
_logger.info("dst_partner: %s", dst_partner.id)
- if openerp.SUPERUSER_ID != uid and self._model_is_installed(cr, uid, 'account.move.line', context=context) and \
- self.pool.get('account.move.line').search(cr, openerp.SUPERUSER_ID, [('partner_id', 'in', [partner.id for partner in src_partners])], context=context):
+ # FIXME : is it still required to make and exception for account.move.line since accounting v9.0 ?
+ if SUPERUSER_ID != self.env.uid and 'account.move.line' in self.env and self.env['account.move.line'].sudo().search([('partner_id', 'in', [partner.id for partner in src_partners])]):
raise UserError(_("Only the destination contact may be linked to existing Journal Items. Please ask the Administrator if you need to merge several contacts linked to existing Journal Items."))
- call_it = lambda function: function(cr, uid, src_partners, dst_partner,
- context=context)
-
+ # call sub methods to do the merge
+ call_it = lambda function: function(src_partners, dst_partner)
call_it(self._update_foreign_keys)
call_it(self._update_reference_fields)
call_it(self._update_values)
- _logger.info('(uid = %s) merged the partners %r with %s', uid, list(map(operator.attrgetter('id'), src_partners)), dst_partner.id)
- dst_partner.message_post(body='%s %s'%(_("Merged with the following partners:"), ", ".join('%s<%s>(ID %s)' % (p.name, p.email or 'n/a', p.id) for p in src_partners)))
+ _logger.info('(uid = %s) merged the partners %r with %s', self._uid, src_partners.ids, dst_partner.id)
+ dst_partner.message_post(body='%s %s' % (_("Merged with the following partners:"), ", ".join('%s <%s> (ID %s)' % (p.name, p.email or 'n/a', p.id) for p in src_partners)))
- for partner in src_partners:
- partner.unlink()
+ # delete source partner, since they are merged
+ src_partners.unlink()
+ #TODO JEM : remove me, since it is not used anymore
def clean_emails(self, cr, uid, context=None):
"""
Clean the email address of the partner, if there is an email field with
@@ -389,10 +390,17 @@ class MergePartnerAutomatic(osv.TransientModel):
raise
return True
- def close_cb(self, cr, uid, ids, context=None):
- return {'type': 'ir.actions.act_window_close'}
+ # ----------------------------------------
+ # Helpers
+ # ----------------------------------------
+ @api.model
def _generate_query(self, fields, maximum_group=100):
+ """ Build the SQL query on res.partner table to group them according to given criteria
+ :param fields : list of column names to group by the partners
+ :param maximum_group : limit of the query
+ """
+ # make the list of column to group by in sql query
sql_fields = []
for field in fields:
if field in ['email', 'name']:
@@ -401,17 +409,16 @@ class MergePartnerAutomatic(osv.TransientModel):
sql_fields.append("replace(%s, ' ', '')" % field)
else:
sql_fields.append(field)
-
group_fields = ', '.join(sql_fields)
+ # where clause : for given group by columns, only keep the 'not null' record
filters = []
for field in fields:
if field in ['email', 'name', 'vat']:
filters.append((field, 'IS NOT', 'NULL'))
+ criteria = ' AND '.join('%s %s %s' % (field, operator, value) for field, operator, value in filters)
- criteria = ' AND '.join('%s %s %s' % (field, operator, value)
- for field, operator, value in filters)
-
+ # build the query
text = [
"SELECT min(id), array_agg(id)",
"FROM res_partner",
@@ -433,55 +440,91 @@ class MergePartnerAutomatic(osv.TransientModel):
return ' '.join(text)
- def _compute_selected_groupby(self, this):
- group_by_str = 'group_by_'
- group_by_len = len(group_by_str)
-
- fields = [
- key[group_by_len:]
- for key in self._columns.keys()
- if key.startswith(group_by_str)
- ]
+ @api.model
+ def _compute_selected_groupby(self):
+ """ Returns the list of field names the partner can be grouped (as merge
+ criteria) according to the option checked on the wizard
+ """
+ groups = []
+ group_by_prefix = 'group_by_'
- groups = [
- field
- for field in fields
- if getattr(this, '%s%s' % (group_by_str, field), False)
- ]
+ for field_name in self._fields:
+ if field_name.startswith(group_by_prefix):
+ if getattr(self, field_name, False):
+ groups.append(field_name[len(group_by_prefix):])
if not groups:
raise UserError(_("You have to specify a filter for your selection"))
return groups
- def next_cb(self, cr, uid, ids, context=None):
+ @api.model
+ def _partner_use_in(self, aggr_ids, models):
+ """ Check if there is no occurence of this group of partner in the selected model
+ :param aggr_ids : stringified list of partner ids separated with a comma (sql array_agg)
+ :param models : dict mapping a model name with its foreign key with res_partner table
+ """
+ for model, field in models.iteritems():
+ proxy = self.env[model]
+ if proxy.search_count([(field, 'in', aggr_ids)]):
+ return True
+ return False
+
+ @api.model
+ def _get_ordered_partner(self, partner_ids):
+ """ Helper : returns a `res.partner` recordset ordered by create_date/active fields
+ :param partner_ids : list of partner ids to sort
"""
- Don't compute any thing
+ partners = self.env['res.partner'].browse(partner_ids)
+ ordered_partners = sorted(sorted(partners, key=operator.attrgetter('create_date'), reverse=True), key=operator.attrgetter('active'), reverse=True)
+ return self.env['res.partner'].browse([partner.id for partner in ordered_partners])
+
+ @api.multi
+ def _compute_models(self):
+ """ Compute the different models needed by the system if you want to exclude some partners. """
+ self.ensure_one()
+ model_mapping = {}
+ if self.exclude_contact:
+ model_mapping['res.users'] = 'partner_id'
+ if 'account.move.line' in self.env and self.exclude_journal_item:
+ model_mapping['account.move.line'] = 'partner_id'
+ return model_mapping
+
+ # ----------------------------------------
+ # Actions
+ # ----------------------------------------
+
+ @api.multi
+ def close_cb(self):
+ self.ensure_one()
+ return {'type': 'ir.actions.act_window_close'}
+
+ @api.multi
+ def next_cb(self):
+ """ Skip this wizard line. Don't compute any thing, and simply redirect to the new step."""
+ self.ensure_one()
+ if self.current_line_id:
+ self.current_line_id.unlink()
+ return self._next_screen()
+
+ #TODO JEM : rename with action_ prefix
+ @api.multi
+ def _next_screen(self):
+ """ return the action of the next screen ; this means the wizard is set to treat the
+ next wizard line. Each line is a subset of partner that can be merged together.
+ If no line left, the end screen will be displayed (but an action is still returned).
"""
- context = dict(context or {}, active_test=False)
- this = self.browse(cr, uid, ids[0], context=context)
- if this.current_line_id:
- this.current_line_id.unlink()
- return self._next_screen(cr, uid, this, context)
-
- def _get_ordered_partner(self, cr, uid, partner_ids, context=None):
- partners = self.pool.get('res.partner').browse(cr, uid, list(partner_ids), context=context)
- ordered_partners = sorted(sorted(partners,
- key=operator.attrgetter('create_date'), reverse=True),
- key=operator.attrgetter('active'), reverse=True)
- return ordered_partners
-
- def _next_screen(self, cr, uid, this, context=None):
- this.refresh()
+ self.ensure_one()
+ self.refresh() # TODO JEM : it is deprecated, so still usefull ?
values = {}
- if this.line_ids:
+ if self.line_ids:
# in this case, we try to find the next record.
- current_line = this.line_ids[0]
+ current_line = self.line_ids[0]
current_partner_ids = literal_eval(current_line.aggr_ids)
values.update({
'current_line_id': current_line.id,
'partner_ids': [(6, 0, current_partner_ids)],
- 'dst_partner_id': self._get_ordered_partner(cr, uid, current_partner_ids, context)[-1].id,
+ 'dst_partner_id': self._get_ordered_partner(current_partner_ids)[-1].id,
'state': 'selection',
})
else:
@@ -491,124 +534,88 @@ class MergePartnerAutomatic(osv.TransientModel):
'state': 'finished',
})
- this.write(values)
+ self.write(values)
return {
'type': 'ir.actions.act_window',
- 'res_model': this._name,
- 'res_id': this.id,
+ 'res_model': self._name,
+ 'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
- def _model_is_installed(self, cr, uid, model, context=None):
- proxy = self.pool.get('ir.model')
- domain = [('model', '=', model)]
- return proxy.search_count(cr, uid, domain, context=context) > 0
-
- def _partner_use_in(self, cr, uid, aggr_ids, models, context=None):
+ @api.multi
+ def _process_query(self, query):
+ """ Execute the select request and write the result in this wizard
+ :param query : the SQL query used to fill the wizard line
"""
- Check if there is no occurence of this group of partner in the selected
- model
- """
- for model, field in models.iteritems():
- proxy = self.pool.get(model)
- domain = [(field, 'in', aggr_ids)]
- if proxy.search_count(cr, uid, domain, context=context):
- return True
- return False
-
- def compute_models(self, cr, uid, ids, context=None):
- """
- Compute the different models needed by the system if you want to exclude
- some partners.
- """
- assert is_integer_list(ids)
-
- this = self.browse(cr, uid, ids[0], context=context)
-
- models = {}
- if this.exclude_contact:
- models['res.users'] = 'partner_id'
-
- if self._model_is_installed(cr, uid, 'account.move.line', context=context) and this.exclude_journal_item:
- models['account.move.line'] = 'partner_id'
+ self.ensure_one()
+ model_mapping = self._compute_models()
- return models
-
- def _process_query(self, cr, uid, ids, query, context=None):
- """
- Execute the select request and write the result in this wizard
- """
- proxy = self.pool.get('base.partner.merge.line')
- this = self.browse(cr, uid, ids[0], context=context)
- models = self.compute_models(cr, uid, ids, context=context)
- cr.execute(query)
+ # group partner query
+ self._cr.execute(query)
counter = 0
- for min_id, aggr_ids in cr.fetchall():
- if models and self._partner_use_in(cr, uid, aggr_ids, models, context=context):
+ for min_id, aggr_ids in self._cr.fetchall():
+ # exclude partner according to options
+ if model_mapping and self._partner_use_in(aggr_ids, model_mapping):
continue
- values = {
- 'wizard_id': this.id,
+ self.env['base.partner.merge.line'].create({
+ 'wizard_id': self.id,
'min_id': min_id,
'aggr_ids': aggr_ids,
- }
-
- proxy.create(cr, uid, values, context=context)
+ })
counter += 1
- values = {
+ self.write({
'state': 'selection',
'number_group': counter,
- }
-
- this.write(values)
+ })
_logger.info("counter: %s", counter)
- def start_process_cb(self, cr, uid, ids, context=None):
+ @api.multi
+ def start_process_cb(self):
+ """ Start the process 'Merge with Manual Check'. Fill the wizard according to the group_by and exclude
+ options, and redirect to the first step (treatment of first wizard line). After, for each subset of
+ partner to merge, the wizard will be actualized.
+ - Compute the selected groups (with duplication)
+ - If the user has selected the 'exclude_xxx' fields, avoid the partners
"""
- Start the process.
- * Compute the selected groups (with duplication)
- * If the user has selected the 'exclude_XXX' fields, avoid the partners.
+ self.ensure_one()
+ groups = self._compute_selected_groupby()
+ query = self._generate_query(groups, self.maximum_group)
+ self._process_query(query)
+ return self._next_screen()
+
+ @api.multi
+ def automatic_process_cb(self):
+ """ Start the process 'Merge Automatically'. This will fill the wizard with the same mechanism as 'Merge
+ with Manual Check', but instead of refreshing wizard with the current line, it will automatically process
+ all lines by merging partner grouped according to the checked options.
"""
- assert is_integer_list(ids)
-
- context = dict(context or {}, active_test=False)
- this = self.browse(cr, uid, ids[0], context=context)
- groups = self._compute_selected_groupby(this)
- query = self._generate_query(groups, this.maximum_group)
- self._process_query(cr, uid, ids, query, context=context)
+ self.ensure_one()
+ self.start_process_cb() # here we don't redirect to the next screen, since it is automatic process
+ self.refresh() # TODO JEM : deprecated, what to do ?
- return self._next_screen(cr, uid, this, context)
-
- def automatic_process_cb(self, cr, uid, ids, context=None):
- assert is_integer_list(ids)
- this = self.browse(cr, uid, ids[0], context=context)
- this.start_process_cb()
- this.refresh()
-
- for line in this.line_ids:
+ for line in self.line_ids:
partner_ids = literal_eval(line.aggr_ids)
- self._merge(cr, uid, partner_ids, context=context)
+ self._merge(partner_ids)
line.unlink()
- cr.commit()
+ self._cr.commit() # TODO JEM : explain why
- this.write({'state': 'finished'})
+ self.write({'state': 'finished'})
return {
'type': 'ir.actions.act_window',
- 'res_model': this._name,
- 'res_id': this.id,
+ 'res_model': self._name,
+ 'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
- def parent_migration_process_cb(self, cr, uid, ids, context=None):
- assert is_integer_list(ids)
-
- context = dict(context or {}, active_test=False)
- this = self.browse(cr, uid, ids[0], context=context)
+ @api.multi
+ def parent_migration_process_cb(self):
+ self.ensure_one()
query = """
SELECT
@@ -635,17 +642,17 @@ class MergePartnerAutomatic(osv.TransientModel):
min(p1.id)
"""
- self._process_query(cr, uid, ids, query, context=context)
+ self._process_query(query)
- for line in this.line_ids:
+ for line in self.line_ids:
partner_ids = literal_eval(line.aggr_ids)
- self._merge(cr, uid, partner_ids, context=context)
+ self._merge(partner_ids)
line.unlink()
- cr.commit()
+ self._cr.commit()
- this.write({'state': 'finished'})
+ self.write({'state': 'finished'})
- cr.execute("""
+ self._cr.execute("""
UPDATE
res_partner
SET
@@ -657,46 +664,24 @@ class MergePartnerAutomatic(osv.TransientModel):
return {
'type': 'ir.actions.act_window',
- 'res_model': this._name,
- 'res_id': this.id,
+ 'res_model': self._name,
+ 'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
- def update_all_process_cb(self, cr, uid, ids, context=None):
- assert is_integer_list(ids)
-
- # WITH RECURSIVE cycle(id, parent_id) AS (
- # SELECT id, parent_id FROM res_partner
- # UNION
- # SELECT cycle.id, res_partner.parent_id
- # FROM res_partner, cycle
- # WHERE res_partner.id = cycle.parent_id AND
- # cycle.id != cycle.parent_id
- # )
- # UPDATE res_partner
- # SET parent_id = NULL
- # WHERE id in (SELECT id FROM cycle WHERE id = parent_id);
-
- this = self.browse(cr, uid, ids[0], context=context)
-
- self.parent_migration_process_cb(cr, uid, ids, context=None)
-
- list_merge = [
- {'group_by_vat': True, 'group_by_email': True, 'group_by_name': True},
- # {'group_by_name': True, 'group_by_is_company': True, 'group_by_parent_id': True},
- # {'group_by_email': True, 'group_by_is_company': True, 'group_by_parent_id': True},
- # {'group_by_name': True, 'group_by_vat': True, 'group_by_is_company': True, 'exclude_journal_item': True},
- # {'group_by_email': True, 'group_by_vat': True, 'group_by_is_company': True, 'exclude_journal_item': True},
- # {'group_by_email': True, 'group_by_is_company': True, 'exclude_contact': True, 'exclude_journal_item': True},
- # {'group_by_name': True, 'group_by_is_company': True, 'exclude_contact': True, 'exclude_journal_item': True}
- ]
+ @api.multi
+ def update_all_process_cb(self):
+ self.ensure_one()
+ self.parent_migration_process_cb()
- for merge_value in list_merge:
- id = self.create(cr, uid, merge_value, context=context)
- self.automatic_process_cb(cr, uid, [id], context=context)
+ # NOTE JEM : seems louche to create a new wizard instead of reuse the current one with updated options.
+ # since it is like this from the initial commit of this wizard, I don't change it. yet ...
+ wizard = self.create({'group_by_vat': True, 'group_by_email': True, 'group_by_name': True})
+ wizard.automatic_process_cb()
- cr.execute("""
+ # NOTE JEM : no idea if this query is usefull
+ self._cr.execute("""
UPDATE
res_partner
SET
@@ -706,50 +691,35 @@ class MergePartnerAutomatic(osv.TransientModel):
is_company IS NOT NULL
""")
- # cr.execute("""
- # UPDATE
- # res_partner as p1
- # SET
- # is_company = NULL,
- # parent_id = (
- # SELECT p2.id
- # FROM res_partner as p2
- # WHERE p2.email = p1.email AND
- # p2.parent_id != p2.id
- # LIMIT 1
- # )
- # WHERE
- # p1.parent_id = p1.id
- # """)
-
- return self._next_screen(cr, uid, this, context)
-
- def merge_cb(self, cr, uid, ids, context=None):
- assert is_integer_list(ids)
-
- context = dict(context or {}, active_test=False)
- this = self.browse(cr, uid, ids[0], context=context)
-
- partner_ids = set(map(int, this.partner_ids))
- if not partner_ids:
- this.write({'state': 'finished'})
+ return self._next_screen()
+
+ @api.multi
+ def merge_cb(self):
+ """ Merge Contact button. Merge the selected partners, and redirect to
+ the end screen (since there is no other wizard line to process.
+ """
+ self.ensure_one()
+
+ if not self.partner_ids:
+ self.write({'state': 'finished'})
return {
'type': 'ir.actions.act_window',
- 'res_model': this._name,
- 'res_id': this.id,
+ 'res_model': self._name,
+ 'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
- self._merge(cr, uid, partner_ids, this.dst_partner_id, context=context)
+ self._merge(self.partner_ids.ids, self.dst_partner_id)
+
+ if self.current_line_id:
+ self.current_line_id.unlink()
- if this.current_line_id:
- this.current_line_id.unlink()
+ return self._next_screen()
- return self._next_screen(cr, uid, this, context)
+ # TODO JEM : remove me since it is not used ?
def auto_set_parent_id(self, cr, uid, ids, context=None):
- assert is_integer_list(ids)
# select partner who have one least invoice
partner_treated = ['@gmail.com']
diff --git a/addons/crm/wizard/base_partner_merge_views.xml b/addons/crm/wizard/base_partner_merge_views.xml
index f5cd82f9604..0189b348705 100644
--- a/addons/crm/wizard/base_partner_merge_views.xml
+++ b/addons/crm/wizard/base_partner_merge_views.xml
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
-<openerp>
- <data>
-
- <record model="ir.actions.act_window" id="action_partner_deduplicate">
+<odoo>
+ <record id="action_partner_deduplicate" model="ir.actions.act_window">
<field name="name">Deduplicate Contacts</field>
<field name="res_model">base.partner.merge.automatic.wizard</field>
<field name="view_type">form</field>
@@ -11,7 +9,7 @@
<field name="context">{'active_test': False}</field>
</record>
- <record model='ir.ui.view' id='base_partner_merge_automatic_wizard_form'>
+ <record id="base_partner_merge_automatic_wizard_form" model="ir.ui.view">
<field name='name'>base.partner.merge.automatic.wizard.form</field>
<field name='model'>base.partner.merge.automatic.wizard</field>
<field name='arch' type='xml'>
@@ -19,8 +17,7 @@
<sheet>
<group attrs="{'invisible': [('state', '!=', 'finished')]}" col="1">
<h2>There is no more contacts to merge for this request...</h2>
- <button name="%(action_partner_deduplicate)d" string="Deduplicate the other Contacts" class="oe_highlight"
- type="action"/>
+ <button name="%(action_partner_deduplicate)d" string="Deduplicate the other Contacts" class="oe_highlight" type="action"/>
</group>
<p class="oe_grey" attrs="{'invisible': [('state', '!=', ('option'))]}">
Select the list of fields used to search for
@@ -77,10 +74,9 @@
<button name='merge_cb' string='Merge Contacts'
class='oe_highlight'
type='object'
- attrs="{'invisible': [('state', 'in', ('option', 'finished' ))]}"
- />
+ attrs="{'invisible': [('state', 'in', ('option', 'finished' ))]}" />
<button name='next_cb' string='Skip these contacts'
- type='object' class='oe_link'
+ type='object'
attrs="{'invisible': [('state', '!=', 'selection')]}" />
<button name='start_process_cb'
string='Merge with Manual Check'
@@ -110,9 +106,13 @@
</field>
</record>
- <act_window id="action_partner_merge" res_model="base.partner.merge.automatic.wizard" src_model="res.partner"
- target="new" multi="True" key2="client_action_multi" view_mode="form" name="Merge Selected Contacts"/>
-
- </data>
-
-</openerp>
+ <act_window
+ id="action_partner_merge"
+ res_model="base.partner.merge.automatic.wizard"
+ src_model="res.partner"
+ target="new"
+ multi="True"
+ key2="client_action_multi"
+ view_mode="form"
+ name="Merge Selected Contacts"/>
+</odoo>
diff --git a/addons/crm/wizard/crm_activity_log.py b/addons/crm/wizard/crm_activity_log.py
index 43d3b294236..5cdffd6ecd9 100644
--- a/addons/crm/wizard/crm_activity_log.py
+++ b/addons/crm/wizard/crm_activity_log.py
@@ -3,26 +3,31 @@
from datetime import datetime, timedelta
-from openerp import api, fields, models, tools, _
+from odoo import api, fields, models, tools, _
class ActivityLog(models.TransientModel):
+
_name = "crm.activity.log"
_description = "Log an Activity"
_rec_name = 'title_action'
+ @api.model
+ def _default_lead_id(self):
+ if 'default_lead_id' in self._context:
+ return self._context['default_lead_id']
+ if self._context.get('active_model') == 'crm.lead':
+ return self._context.get('active_id')
+ return False
+
next_activity_id = fields.Many2one('crm.activity', 'Activity')
last_activity_id = fields.Many2one('crm.activity', 'Previous Activity', default=False)
recommended_activity_id = fields.Many2one("crm.activity", "Recommended Activities")
title_action = fields.Char('Summary')
note = fields.Html('Note')
date_action = fields.Date('Next Activity Date')
- lead_id = fields.Many2one(
- 'crm.lead', 'Lead', required=True,
- default=lambda self: self._context.get('default_lead_id',
- self._context.get('active_model') == 'crm.lead' and self._context.get('active_id') or False))
- team_id = fields.Many2one(
- 'crm.team', 'Sales Team')
+ lead_id = fields.Many2one('crm.lead', 'Lead', required=True, default=_default_lead_id)
+ team_id = fields.Many2one('crm.team', 'Sales Team')
date_deadline = fields.Date('Expected Closing')
planned_revenue = fields.Float('Expected Revenue')
diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py
index b08d8445b3d..a8c07299bb0 100644
--- a/addons/crm/wizard/crm_lead_to_opportunity.py
+++ b/addons/crm/wizard/crm_lead_to_opportunity.py
@@ -1,199 +1,177 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import fields, osv
-from openerp.tools.translate import _
-import re
-from openerp.exceptions import UserError
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+from odoo.tools.translate import _
+
+
+class Lead2OpportunityPartner(models.TransientModel):
-class crm_lead2opportunity_partner(osv.osv_memory):
_name = 'crm.lead2opportunity.partner'
_description = 'Lead To Opportunity Partner'
_inherit = 'crm.partner.binding'
- _columns = {
- 'name': fields.selection([
- ('convert', 'Convert to opportunity'),
- ('merge', 'Merge with existing opportunities')
- ], 'Conversion Action', required=True),
- 'opportunity_ids': fields.many2many('crm.lead', string='Opportunities'),
- 'user_id': fields.many2one('res.users', 'Salesperson', select=True),
- 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id', select=True),
- }
-
- def onchange_action(self, cr, uid, ids, action, context=None):
- return {'value': {'partner_id': False if action != 'exist' else self._find_matching_partner(cr, uid, context=context)}}
-
- def _get_duplicated_leads(self, cr, uid, partner_id, email, include_lost=False, context=None):
- """
- Search for opportunities that have the same partner and that arent done or cancelled
- """
- return self.pool.get('crm.lead')._get_duplicated_leads_by_emails(cr, uid, partner_id, email, include_lost=include_lost, context=context)
-
- def default_get(self, cr, uid, fields, context=None):
- """
- Default get for name, opportunity_ids.
- If there is an exisitng partner link to the lead, find all existing
- opportunities links with this partner to merge all information together
+ @api.model
+ def default_get(self, fields):
+ """ Default get for name, opportunity_ids.
+ If there is an exisitng partner link to the lead, find all existing
+ opportunities links with this partner to merge all information together
"""
- lead_obj = self.pool.get('crm.lead')
+ result = super(Lead2OpportunityPartner, self).default_get(fields)
+ if self._context.get('active_id'):
+ tomerge = [int(self._context['active_id'])]
- res = super(crm_lead2opportunity_partner, self).default_get(cr, uid, fields, context=context)
- if context.get('active_id'):
- tomerge = [int(context['active_id'])]
+ partner_id = result.get('partner_id')
+ lead = self.env['crm.lead'].browse(self._context['active_id'])
+ email = lead.partner_id.email if lead.partner_id else lead.email_from
- partner_id = res.get('partner_id')
- lead = lead_obj.browse(cr, uid, int(context['active_id']), context=context)
- email = lead.partner_id and lead.partner_id.email or lead.email_from
-
- tomerge.extend(self._get_duplicated_leads(cr, uid, partner_id, email, include_lost=True, context=context))
+ tomerge.extend(self._get_duplicated_leads(partner_id, email, include_lost=True).ids)
tomerge = list(set(tomerge))
- if 'action' in fields and not res.get('action'):
- res.update({'action' : partner_id and 'exist' or 'create'})
+ if 'action' in fields and not result.get('action'):
+ result.update({'action': 'exist' if partner_id else 'create'})
if 'partner_id' in fields:
- res.update({'partner_id' : partner_id})
+ result.update({'partner_id': partner_id})
if 'name' in fields:
- res.update({'name' : len(tomerge) >= 2 and 'merge' or 'convert'})
+ result.update({'name': 'merge' if len(tomerge) >= 2 else 'convert'})
if 'opportunity_ids' in fields and len(tomerge) >= 2:
- res.update({'opportunity_ids': tomerge})
+ result.update({'opportunity_ids': tomerge})
if lead.user_id:
- res.update({'user_id': lead.user_id.id})
+ result.update({'user_id': lead.user_id.id})
if lead.team_id:
- res.update({'team_id': lead.team_id.id})
+ result.update({'team_id': lead.team_id.id})
if not partner_id and not lead.contact_name:
- res.update({'action': 'nothing'})
- return res
+ result.update({'action': 'nothing'})
+ return result
+
+ name = fields.Selection([
+ ('convert', 'Convert to opportunity'),
+ ('merge', 'Merge with existing opportunities')
+ ], 'Conversion Action', required=True)
+ opportunity_ids = fields.Many2many('crm.lead', string='Opportunities')
+ user_id = fields.Many2one('res.users', 'Salesperson', index=True)
+ team_id = fields.Many2one('crm.team', 'Sales Team', oldname='section_id', index=True)
+
+ @api.onchange('action')
+ def onchange_action(self):
+ if self.action == 'exist':
+ self.partner_id = self._find_matching_partner()
+ else:
+ self.partner_id = False
- def on_change_user(self, cr, uid, ids, user_id, team_id, context=None):
+ @api.onchange('user_id')
+ def _onchange_user(self):
""" When changing the user, also set a team_id or restrict team id
- to the ones user_id is member of. """
- if user_id:
- if team_id:
- user_in_team = self.pool.get('crm.team').search(cr, uid, [('id', '=', team_id), '|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context, count=True)
+ to the ones user_id is member of.
+ """
+ if self.user_id:
+ if self.team_id:
+ user_in_team = self.env['crm.team'].search_count([('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id)])
else:
user_in_team = False
if not user_in_team:
- result = self.pool['crm.lead'].on_change_user(cr, uid, ids, user_id, context=context)
- team_id = result.get('value') and result['value'].get('team_id') and result['value']['team_id'] or False
- return {'value': {'team_id': team_id}}
-
- def view_init(self, cr, uid, fields, context=None):
- """
- Check some preconditions before the wizard executes.
- """
- if context is None:
- context = {}
- lead_obj = self.pool.get('crm.lead')
- for lead in lead_obj.browse(cr, uid, context.get('active_ids', []), context=context):
+ values = self.env['crm.lead']._onchange_user_values(self.user_id.id if self.user_id else False)
+ self.team_id = values.get('team_id', False)
+
+ @api.model
+ def _get_duplicated_leads(self, partner_id, email, include_lost=False):
+ """ Search for opportunities that have the same partner and that arent done or cancelled """
+ return self.env['crm.lead']._get_duplicated_leads_by_emails(partner_id, email, include_lost=include_lost)
+
+ # NOTE JEM : is it the good place to test this ?
+ @api.model
+ def view_init(self, fields):
+ """ Check some preconditions before the wizard executes. """
+ for lead in self.env['crm.lead'].browse(self._context.get('active_ids', [])):
if lead.probability == 100:
raise UserError(_("Closed/Dead leads cannot be converted into opportunities."))
return False
- def _convert_opportunity(self, cr, uid, ids, vals, context=None):
- if context is None:
- context = {}
- lead = self.pool.get('crm.lead')
+ @api.multi
+ def _convert_opportunity(self, vals):
+ self.ensure_one()
+
res = False
lead_ids = vals.get('lead_ids', [])
team_id = vals.get('team_id', False)
partner_id = vals.get('partner_id')
- data = self.browse(cr, uid, ids, context=context)[0]
- leads = lead.browse(cr, uid, lead_ids, context=context)
- for lead_id in leads:
- partner_id = self._create_partner(cr, uid, lead_id.id, data.action, partner_id or lead_id.partner_id.id, context=context)
- res = lead.convert_opportunity(cr, uid, [lead_id.id], partner_id, [], False, context=context)
+
+ for lead in self.env['crm.lead'].browse(lead_ids):
+ partner_id = self._create_partner(lead.id, self.action, partner_id or lead.partner_id.id)
+ res = lead.convert_opportunity(partner_id, [], False)
user_ids = vals.get('user_ids', False)
- if context.get('no_force_assignation'):
- leads_to_allocate = [lead_id.id for lead_id in leads if not lead_id.user_id]
+
+ if self._context.get('no_force_assignation'):
+ leads_to_allocate = self.env['crm.lead'].browse(lead_ids).filtered(lambda lead: not lead.user_id).ids
else:
leads_to_allocate = lead_ids
+
if user_ids:
- lead.allocate_salesman(cr, uid, leads_to_allocate, user_ids, team_id=team_id, context=context)
+ self.env['crm.lead'].browse(leads_to_allocate).allocate_salesman(user_ids, team_id=team_id)
return res
- def action_apply(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_apply(self):
+ """ Convert lead to opportunity or merge lead and opportunity and open
+ the freshly created opportunity view.
"""
- Convert lead to opportunity or merge lead and opportunity and open
- the freshly created opportunity view.
- """
- if context is None:
- context = {}
-
- lead_obj = self.pool['crm.lead']
- partner_obj = self.pool['res.partner']
+ self.ensure_one()
+ Lead = self.env['crm.lead']
- w = self.browse(cr, uid, ids, context=context)[0]
- opp_ids = [o.id for o in w.opportunity_ids]
- vals = {
- 'team_id': w.team_id.id,
+ opp_ids = self.opportunity_ids.ids
+ values = {
+ 'team_id': self.team_id.id,
}
- if w.partner_id:
- vals['partner_id'] = w.partner_id.id
- if w.name == 'merge':
- lead_id = lead_obj.merge_opportunity(cr, uid, opp_ids, context=context)
+
+ if self.partner_id:
+ values['partner_id'] = self.partner_id.id
+
+ if self.name == 'merge':
+ lead_id = self.env['crm.lead'].browse(opp_ids).merge_opportunity()
lead_ids = [lead_id]
- lead = lead_obj.read(cr, uid, lead_id, ['type', 'user_id'], context=context)
- if lead['type'] == "lead":
- context = dict(context, active_ids=lead_ids)
- vals.update({'lead_ids': lead_ids, 'user_ids': [w.user_id.id]})
- self._convert_opportunity(cr, uid, ids, vals, context=context)
- elif not context.get('no_force_assignation') or not lead['user_id']:
- vals.update({'user_id': w.user_id.id})
- lead_obj.write(cr, uid, lead_id, vals, context=context)
+ lead_data = Lead.browse(lead_id).read(['type', 'user_id'])[0]
+ if lead_data['type'] == "lead":
+ values.update({'lead_ids': lead_ids, 'user_ids': [self.user_id.id]})
+ self.with_context(active_ids=lead_ids)._convert_opportunity(values)
+ elif not self._context.get('no_force_assignation') or not lead_data['user_id']:
+ values.update({'user_id': self.user_id.id})
+ Lead.browse(lead_id).write(values)
else:
- lead_ids = context.get('active_ids', [])
- vals.update({'lead_ids': lead_ids, 'user_ids': [w.user_id.id]})
- self._convert_opportunity(cr, uid, ids, vals, context=context)
- for lead in lead_obj.browse(cr, uid, lead_ids, context=context):
+ lead_ids = self._context.get('active_ids', [])
+ values.update({'lead_ids': lead_ids, 'user_ids': [self.user_id.id]})
+ self._convert_opportunity(values)
+ for lead in Lead.browse(lead_ids):
if lead.partner_id and lead.partner_id.user_id != lead.user_id:
- partner_obj.write(cr, uid, [lead.partner_id.id], {'user_id': lead.user_id.id}, context=context)
+ self.env['res.partner'].browse(lead.partner_id.id).write({'user_id': lead.user_id.id})
- return self.pool.get('crm.lead').redirect_opportunity_view(cr, uid, lead_ids[0], context=context)
+ return self.env['crm.lead'].browse(list(lead_ids)[0]).redirect_opportunity_view()
- def _create_partner(self, cr, uid, lead_id, action, partner_id, context=None):
+ @api.model
+ def _create_partner(self, lead_id, action, partner_id):
+ """ Create partner based on action.
+ :return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
"""
- Create partner based on action.
- :return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
- """
- #TODO this method in only called by crm_lead2opportunity_partner
+ #TODO this method in only called by Lead2OpportunityPartner
#wizard and would probably diserve to be refactored or at least
#moved to a better place
- if context is None:
- context = {}
- lead = self.pool.get('crm.lead')
if action == 'each_exist_or_create':
- ctx = dict(context)
- ctx['active_id'] = lead_id
- partner_id = self._find_matching_partner(cr, uid, context=ctx)
+ partner_id = self.with_context(active_id=lead_id)._find_matching_partner()
action = 'create'
- res = lead.handle_partner_assignation(cr, uid, [lead_id], action, partner_id, context=context)
- return res.get(lead_id)
+ result = self.env['crm.lead'].browse(lead_id).handle_partner_assignation(action, partner_id)
+ return result.get(lead_id)
+
+
+class Lead2OpportunityMassConvert(models.TransientModel):
-class crm_lead2opportunity_mass_convert(osv.osv_memory):
_name = 'crm.lead2opportunity.partner.mass'
_description = 'Mass Lead To Opportunity Partner'
_inherit = 'crm.lead2opportunity.partner'
- _columns = {
- 'user_ids': fields.many2many('res.users', string='Salesmen'),
- 'team_id': fields.many2one('crm.team', 'Sales Team', select=True, oldname='section_id'),
- 'deduplicate': fields.boolean('Apply deduplication', help='Merge with existing leads/opportunities of each partner'),
- 'action': fields.selection([
- ('each_exist_or_create', 'Use existing partner or create'),
- ('nothing', 'Do not link to a customer')
- ], 'Related Customer', required=True),
- 'force_assignation': fields.boolean('Force assignation', help='If unchecked, this will leave the salesman of duplicated opportunities'),
- }
-
- _defaults = {
- 'deduplicate': True,
- }
-
- def default_get(self, cr, uid, fields, context=None):
- res = super(crm_lead2opportunity_mass_convert, self).default_get(cr, uid, fields, context)
- if 'partner_id' in fields:
- # avoid forcing the partner of the first lead as default
+ @api.model
+ def default_get(self, fields):
+ res = super(Lead2OpportunityMassConvert, self).default_get(fields)
+ if 'partner_id' in fields: # avoid forcing the partner of the first lead as default
res['partner_id'] = False
if 'action' in fields:
res['action'] = 'each_exist_or_create'
@@ -203,63 +181,71 @@ class crm_lead2opportunity_mass_convert(osv.osv_memory):
res['opportunity_ids'] = False
return res
- def on_change_action(self, cr, uid, ids, action, context=None):
- vals = {}
- if action != 'exist':
- vals = {'value': {'partner_id': False}}
- return vals
-
- def on_change_deduplicate(self, cr, uid, ids, deduplicate, context=None):
- if context is None:
- context = {}
- active_leads = self.pool['crm.lead'].browse(cr, uid, context['active_ids'], context=context)
+ user_ids = fields.Many2many('res.users', string='Salesmen')
+ team_id = fields.Many2one('crm.team', 'Sales Team', index=True, oldname='section_id')
+ deduplicate = fields.Boolean('Apply deduplication', default=True, help='Merge with existing leads/opportunities of each partner')
+ action = fields.Selection([
+ ('each_exist_or_create', 'Use existing partner or create'),
+ ('nothing', 'Do not link to a customer')
+ ], 'Related Customer', required=True)
+ force_assignation = fields.Boolean('Force assignation', help='If unchecked, this will leave the salesman of duplicated opportunities')
+
+ @api.onchange('action')
+ def _onchange_action(self):
+ if self.action != 'exist':
+ self.partner_id = False
+
+ @api.onchange('deduplicate')
+ def _onchange_deduplicate(self):
+ active_leads = self.env['crm.lead'].browse(self._context['active_ids'])
partner_ids = [(lead.partner_id.id, lead.partner_id and lead.partner_id.email or lead.email_from) for lead in active_leads]
partners_duplicated_leads = {}
for partner_id, email in partner_ids:
- duplicated_leads = self._get_duplicated_leads(cr, uid, partner_id, email)
+ duplicated_leads = self._get_duplicated_leads(partner_id, email)
if len(duplicated_leads) > 1:
partners_duplicated_leads.setdefault((partner_id, email), []).extend(duplicated_leads)
+
leads_with_duplicates = []
for lead in active_leads:
lead_tuple = (lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from)
if len(partners_duplicated_leads.get(lead_tuple, [])) > 1:
leads_with_duplicates.append(lead.id)
- return {'value': {'opportunity_ids': leads_with_duplicates}}
- def _convert_opportunity(self, cr, uid, ids, vals, context=None):
- """
- When "massively" (more than one at a time) converting leads to
- opportunities, check the salesteam_id and salesmen_ids and update
- the values before calling super.
+ self.opportunity_ids = self.env['crm.lead'].browse(leads_with_duplicates)
+
+ @api.multi
+ def _convert_opportunity(self, vals):
+ """ When "massively" (more than one at a time) converting leads to
+ opportunities, check the salesteam_id and salesmen_ids and update
+ the values before calling super.
"""
- if context is None:
- context = {}
- data = self.browse(cr, uid, ids, context=context)[0]
- salesteam_id = data.team_id and data.team_id.id or False
+ self.ensure_one()
+ salesteam_id = self.team_id.id if self.team_id else False
salesmen_ids = []
- if data.user_ids:
- salesmen_ids = [x.id for x in data.user_ids]
+ if self.user_ids:
+ salesmen_ids = self.user_ids.ids
vals.update({'user_ids': salesmen_ids, 'team_id': salesteam_id})
- return super(crm_lead2opportunity_mass_convert, self)._convert_opportunity(cr, uid, ids, vals, context=context)
+ return super(Lead2OpportunityMassConvert, self)._convert_opportunity(vals)
- def mass_convert(self, cr, uid, ids, context=None):
- data = self.browse(cr, uid, ids, context=context)[0]
- ctx = dict(context)
- if data.name == 'convert' and data.deduplicate:
+ @api.multi
+ def mass_convert(self):
+ self.ensure_one()
+ active_ids = []
+ if self.name == 'convert' and self.deduplicate:
merged_lead_ids = []
remaining_lead_ids = []
- lead_selected = context.get('active_ids', [])
+ lead_selected = self._context.get('active_ids', [])
for lead_id in lead_selected:
if lead_id not in merged_lead_ids:
- lead = self.pool['crm.lead'].browse(cr, uid, lead_id, context=context)
- duplicated_lead_ids = self._get_duplicated_leads(cr, uid, lead.partner_id.id, lead.partner_id and lead.partner_id.email or lead.email_from)
- if len(duplicated_lead_ids) > 1:
- lead_id = self.pool.get('crm.lead').merge_opportunity(cr, uid, duplicated_lead_ids, False, False, context=context)
- merged_lead_ids.extend(duplicated_lead_ids)
+ lead = self.env['crm.lead'].browse(lead_id)
+ duplicated_leads = self._get_duplicated_leads(lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from)
+ if len(duplicated_leads) > 1:
+ lead_id = duplicated_leads.merge_opportunity()
+ merged_lead_ids.extend(duplicated_leads.ids)
remaining_lead_ids.append(lead_id)
- active_ids = set(context.get('active_ids', []))
+ active_ids = set(self._context.get('active_ids', []))
active_ids = active_ids.difference(merged_lead_ids)
active_ids = active_ids.union(remaining_lead_ids)
- ctx['active_ids'] = list(active_ids)
- ctx['no_force_assignation'] = context.get('no_force_assignation', not data.force_assignation)
- return self.action_apply(cr, uid, ids, context=ctx)
+ self = self.with_context(active_ids=list(active_ids)) # only update active_ids when there are set
+ no_force_assignation = self._context.get('no_force_assignation', not self.force_assignation)
+ return self.with_context(no_force_assignation=no_force_assignation).action_apply()
diff --git a/addons/crm/wizard/crm_lead_to_opportunity_views.xml b/addons/crm/wizard/crm_lead_to_opportunity_views.xml
index 9d5d533db3d..201f2957636 100644
--- a/addons/crm/wizard/crm_lead_to_opportunity_views.xml
+++ b/addons/crm/wizard/crm_lead_to_opportunity_views.xml
@@ -1,6 +1,5 @@
<?xml version="1.0"?>
-<openerp>
- <data>
+<odoo>
<record id="view_crm_lead2opportunity_partner" model="ir.ui.view">
<field name="name">crm.lead2opportunity.partner.form</field>
@@ -11,7 +10,7 @@
<field name="name" widget="radio"/>
</group>
<group string="Assign this opportunity to">
- <field name="user_id" on_change="on_change_user(user_id, team_id, context)" domain="[('share', '=', False)]"/>
+ <field name="user_id" domain="[('share', '=', False)]"/>
<field name="team_id" widget="selection"/>
</group>
<group string="Opportunities" attrs="{'invisible': [('name', '!=', 'merge')]}">
@@ -31,7 +30,7 @@
</field>
</group>
<group name="action" attrs="{'invisible': [('name', '!=', 'convert')]}" string="Customers" col="1">
- <field name="action" nolabel="1" widget="radio" on_change="onchange_action(action, context)" />
+ <field name="action" nolabel="1" widget="radio"/>
<group col="2">
<field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}" attrs="{'required': [('action', '=', 'exist')], 'invisible':[('action','!=','exist')]}"/>
</group>
@@ -52,7 +51,7 @@
<separator string="Conversion Options"/>
<group>
<field name="name" class="oe_inline" widget="radio"/>
- <field name="deduplicate" class="oe_inline" on_change="on_change_deduplicate(deduplicate, context)"/>
+ <field name="deduplicate" class="oe_inline"/>
</group>
<group string="Assign these opportunities to">
<field name="team_id"/>
@@ -102,7 +101,8 @@
<field name="target">new</field>
</record>
- <act_window id="action_crm_send_mass_convert"
+ <act_window
+ id="action_crm_send_mass_convert"
multi="True"
key2="client_action_multi" name="Convert to opportunities"
res_model="crm.lead2opportunity.partner.mass" src_model="crm.lead"
@@ -112,5 +112,4 @@
groups="sales_team.group_sale_salesman"
/>
- </data>
-</openerp>
+</odoo>
diff --git a/addons/crm/wizard/crm_merge_opportunities.py b/addons/crm/wizard/crm_merge_opportunities.py
index 00f5c49de5b..e47274bcca7 100644
--- a/addons/crm/wizard/crm_merge_opportunities.py
+++ b/addons/crm/wizard/crm_merge_opportunities.py
@@ -1,77 +1,62 @@
+# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import fields, osv
-from openerp.tools.translate import _
-class crm_merge_opportunity(osv.osv_memory):
+from odoo import api, fields, models
+
+
+class MergeOpportunity(models.TransientModel):
"""
- Merge opportunities together.
- If we're talking about opportunities, it's just because it makes more sense
- to merge opps than leads, because the leads are more ephemeral objects.
- But since opportunities are leads, it's also possible to merge leads
- together (resulting in a new lead), or leads and opps together (resulting
- in a new opp).
+ Merge opportunities together.
+ If we're talking about opportunities, it's just because it makes more sense
+ to merge opps than leads, because the leads are more ephemeral objects.
+ But since opportunities are leads, it's also possible to merge leads
+ together (resulting in a new lead), or leads and opps together (resulting
+ in a new opp).
"""
_name = 'crm.merge.opportunity'
_description = 'Merge opportunities'
- _columns = {
- 'opportunity_ids': fields.many2many('crm.lead', rel='merge_opportunity_rel', id1='merge_id', id2='opportunity_id', string='Leads/Opportunities'),
- 'user_id': fields.many2one('res.users', 'Salesperson', select=True),
- 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id', select=True),
- }
- def action_merge(self, cr, uid, ids, context=None):
- context = dict(context or {})
+ @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'
+ """
+ record_ids = self._context.get('active_ids', False)
+ result = super(MergeOpportunity, self).default_get(fields)
- lead_obj = self.pool.get('crm.lead')
- wizard = self.browse(cr, uid, ids[0], context=context)
- opportunity2merge_ids = wizard.opportunity_ids
+ if record_ids:
+ if 'opportunity_ids' in fields:
+ opp_ids = self.env['crm.lead'].browse(record_ids).filtered(lambda opp: opp.probability < 100).ids
+ result.update({'opportunity_ids': opp_ids})
- #TODO: why is this passed through the context ?
- context['lead_ids'] = [opportunity2merge_ids[0].id]
+ return result
- merge_id = lead_obj.merge_opportunity(cr, uid, [x.id for x in opportunity2merge_ids], wizard.user_id.id, wizard.team_id.id, context=context)
+ opportunity_ids = fields.Many2many('crm.lead', 'merge_opportunity_rel', 'merge_id', 'opportunity_id', string='Leads/Opportunities')
+ user_id = fields.Many2one('res.users', 'Salesperson', index=True)
+ team_id = fields.Many2one('crm.team', 'Sales Team', oldname='section_id', index=True)
- # The newly created lead might be a lead or an opp: redirect toward the right view
- merge_result = lead_obj.browse(cr, uid, merge_id, context=context)
+ @api.multi
+ def action_merge(self):
+ self.ensure_one()
+ merge_opportunity_id = self.opportunity_ids.merge_opportunity(self.user_id.id, self.team_id.id)
- if merge_result.type == 'opportunity':
- return lead_obj.redirect_opportunity_view(cr, uid, merge_id, context=context)
+ # The newly created lead might be a lead or an opp: redirect toward the right view
+ merge_opportunity = self.env['crm.lead'].browse(merge_opportunity_id)
+ if merge_opportunity.type == 'opportunity':
+ return merge_opportunity.redirect_opportunity_view()
else:
- return lead_obj.redirect_lead_view(cr, uid, merge_id, context=context)
-
- def default_get(self, cr, uid, fields, context=None):
- """
- 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'
- """
- if context is None:
- context = {}
- record_ids = context.get('active_ids', False)
- res = super(crm_merge_opportunity, self).default_get(cr, uid, fields, context=context)
-
- if record_ids:
- opp_ids = []
- opps = self.pool.get('crm.lead').browse(cr, uid, record_ids, context=context)
- for opp in opps:
- if opp.probability < 100:
- opp_ids.append(opp.id)
- if 'opportunity_ids' in fields:
- res.update({'opportunity_ids': opp_ids})
-
- return res
+ return merge_opportunity.redirect_lead_view()
- def on_change_user(self, cr, uid, ids, user_id, team_id, context=None):
+ @api.onchange('user_id')
+ def _onchange_user(self):
""" When changing the user, also set a team_id or restrict team id
to the ones user_id is member of. """
- if user_id:
- if team_id:
- user_in_team = self.pool.get('crm.team').search(cr, uid, [('id', '=', team_id), '|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context, count=True)
- else:
- user_in_team = False
+ team_id = False
+ if self.user_id:
+ user_in_team = False
+ if self.team_id:
+ user_in_team = self.env['crm.team'].search_count([('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id)])
if not user_in_team:
- team_id = False
- team_ids = self.pool.get('crm.team').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
- if team_ids:
- team_id = team_ids[0]
- return {'value': {'team_id': team_id}}
+ team_id = self.env['crm.team'].search(['|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id)], limit=1)
+ self.team_id = team_id
diff --git a/addons/crm/wizard/crm_merge_opportunities_views.xml b/addons/crm/wizard/crm_merge_opportunities_views.xml
index 964372a6b64..c0f936cc7e1 100644
--- a/addons/crm/wizard/crm_merge_opportunities_views.xml
+++ b/addons/crm/wizard/crm_merge_opportunities_views.xml
@@ -1,15 +1,13 @@
<?xml version="1.0"?>
-<openerp>
- <data>
-
+<odoo>
<!-- Merge Opportunities -->
- <record model="ir.ui.view" id="merge_opportunity_form">
+ <record id="merge_opportunity_form" model="ir.ui.view">
<field name="name">crm.merge.opportunity.form</field>
<field name="model">crm.merge.opportunity</field>
<field name="arch" type="xml">
<form string="Merge Leads/Opportunities">
<group string="Assign opportunities to">
- <field name="user_id" class="oe_inline" on_change="on_change_user(user_id, team_id, context)"/>
+ <field name="user_id" class="oe_inline"/>
<field name="team_id" class="oe_inline"/>
</group>
<group string="Select Leads/Opportunities">
@@ -36,7 +34,7 @@
</record>
<!-- Merge leads/opportunities action -->
- <record model="ir.actions.act_window" id="merge_opportunity_act">
+ <record id="merge_opportunity_act" model="ir.actions.act_window">
<field name="name">Merge leads/opportunities</field>
<field name="res_model">crm.merge.opportunity</field>
<field name="view_type">form</field>
@@ -45,13 +43,16 @@
<field name="target">new</field>
</record>
- <act_window id="action_merge_opportunities"
+ <act_window
+ id="action_merge_opportunities"
+ name="Merge leads/opportunities"
multi="True"
- key2="client_action_multi" name="Merge leads/opportunities"
- res_model="crm.merge.opportunity" src_model="crm.lead"
- view_mode="form" target="new" view_type="form"
- groups="sales_team.group_sale_salesman"
- />
+ key2="client_action_multi"
+ res_model="crm.merge.opportunity"
+ src_model="crm.lead"
+ view_mode="form"
+ target="new"
+ view_type="form"
+ groups="sales_team.group_sale_salesman" />
- </data>
-</openerp>
+</odoo>
diff --git a/addons/crm/wizard/crm_partner_binding.py b/addons/crm/wizard/crm_partner_binding.py
index 8acba356956..0270a656200 100644
--- a/addons/crm/wizard/crm_partner_binding.py
+++ b/addons/crm/wizard/crm_partner_binding.py
@@ -1,77 +1,65 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import fields, osv
-from openerp.tools.translate import _
+from odoo import api, fields, models
+from odoo.tools.translate import _
-class crm_partner_binding(osv.osv_memory):
+
+class PartnerBinding(models.TransientModel):
"""
- Handle the partner binding or generation in any CRM wizard that requires
- such feature, like the lead2opportunity wizard, or the
- phonecall2opportunity wizard. Try to find a matching partner from the
- CRM model's information (name, email, phone number, etc) or create a new
- one on the fly.
- Use it like a mixin with the wizard of your choice.
+ Handle the partner binding or generation in any CRM wizard that requires
+ such feature, like the lead2opportunity wizard, or the
+ phonecall2opportunity wizard. Try to find a matching partner from the
+ CRM model's information (name, email, phone number, etc) or create a new
+ one on the fly.
+ Use it like a mixin with the wizard of your choice.
"""
+
_name = 'crm.partner.binding'
_description = 'Handle partner binding or generation in CRM wizards.'
- _columns = {
- 'action': fields.selection([
- ('exist', 'Link to an existing customer'),
- ('create', 'Create a new customer'),
- ('nothing', 'Do not link to a customer')
- ], 'Related Customer', required=True),
- 'partner_id': fields.many2one('res.partner', 'Customer'),
- }
-
- def _find_matching_partner(self, cr, uid, context=None):
- """
- Try to find a matching partner regarding the active model data, like
- the customer's name, email, phone number, etc.
-
- :return int partner_id if any, False otherwise
- """
- if context is None:
- context = {}
- partner_id = False
- partner_obj = self.pool.get('res.partner')
-
- # The active model has to be a lead
- if (context.get('active_model') == 'crm.lead') and context.get('active_id'):
- active_model = self.pool.get('crm.lead').browse(cr, uid, context.get('active_id'), context=context)
- # Find the best matching partner for the active model
- if (active_model):
- partner_obj = self.pool.get('res.partner')
-
- # A partner is set already
- if active_model.partner_id:
- partner_id = active_model.partner_id.id
- # Search through the existing partners based on the lead's email
- elif active_model.email_from:
- partner_ids = partner_obj.search(cr, uid, [('email', '=', active_model.email_from)], context=context)
- if partner_ids:
- partner_id = partner_ids[0]
- # Search through the existing partners based on the lead's partner or contact name
- elif active_model.partner_name:
- partner_ids = partner_obj.search(cr, uid, [('name', 'ilike', '%'+active_model.partner_name+'%')], context=context)
- if partner_ids:
- partner_id = partner_ids[0]
- elif active_model.contact_name:
- partner_ids = partner_obj.search(cr, uid, [
- ('name', 'ilike', '%'+active_model.contact_name+'%')], context=context)
- if partner_ids:
- partner_id = partner_ids[0]
-
- return partner_id
-
- def default_get(self, cr, uid, fields, context=None):
- res = super(crm_partner_binding, self).default_get(cr, uid, fields, context=context)
- partner_id = self._find_matching_partner(cr, uid, context=context)
+ @api.model
+ def default_get(self, fields):
+ res = super(PartnerBinding, self).default_get(fields)
+ partner_id = self._find_matching_partner()
if 'action' in fields and not res.get('action'):
res['action'] = partner_id and 'exist' or 'create'
if 'partner_id' in fields:
res['partner_id'] = partner_id
-
return res
+
+ action = fields.Selection([
+ ('exist', 'Link to an existing customer'),
+ ('create', 'Create a new customer'),
+ ('nothing', 'Do not link to a customer')
+ ], 'Related Customer', required=True)
+ partner_id = fields.Many2one('res.partner', 'Customer')
+
+ @api.model
+ def _find_matching_partner(self):
+ """ Try to find a matching partner regarding the active model data, like
+ the customer's name, email, phone number, etc.
+ :return int partner_id if any, False otherwise
+ """
+ # active model has to be a lead
+ lead = None
+ if self._context.get('active_model') == 'crm.lead' and self._context.get('active_id'):
+ lead = self.env['crm.lead'].browse(self._context.get('active_id'))
+
+ # find the best matching partner for the active model
+ partner_id = False
+ if lead:
+ Partner = self.env['res.partner']
+ if lead.partner_id: # a partner is set already
+ partner_id = lead.partner_id.id
+ elif lead.email_from: # search through the existing partners based on the lead's email
+ partner = Partner.search([('email', '=', lead.email_from)], limit=1)
+ partner_id = partner.id if partner else False
+ elif lead.partner_name: # search through the existing partners based on the lead's partner or contact name
+ partner = Partner.search([('name', 'ilike', '%' + lead.partner_name + '%')], limit=1)
+ partner_id = partner.id if partner else False
+ elif lead.contact_name:
+ partner = Partner.search([('name', 'ilike', '%' + lead.contact_name+'%')], limit=1)
+ partner_id = partner.id if partner else False
+ return partner_id
diff --git a/addons/website_crm_partner_assign/models/crm_partner_assign.py b/addons/website_crm_partner_assign/models/crm_partner_assign.py
index 46b84d916b8..26e8398ddf9 100644
--- a/addons/website_crm_partner_assign/models/crm_partner_assign.py
+++ b/addons/website_crm_partner_assign/models/crm_partner_assign.py
@@ -57,7 +57,7 @@ class res_partner(osv.osv):
_defaults = {
'partner_weight': lambda *args: 0
}
-
+
def onchange_grade_id(self, cr, uid, ids, grade_id, context=None):
res = {'value' :{'partner_weight':0}}
if grade_id:
@@ -80,9 +80,10 @@ class crm_lead(osv.osv):
string='Partner not interested'),
'date_assign': fields.date('Assignation Date', help="Last date this case was forwarded/assigned to a partner"),
}
- def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
+
+ def _merge_data(self, cr, uid, ids, fields, context=None):
fields += ['partner_latitude', 'partner_longitude', 'partner_assigned_id', 'date_assign']
- return super(crm_lead, self)._merge_data(cr, uid, ids, oldest, fields, context=context)
+ return super(crm_lead, self)._merge_data(cr, uid, ids, fields, context=context)
def onchange_assign_id(self, cr, uid, ids, partner_assigned_id, context=None):
"""This function updates the "assignation date" automatically, when manually assign a partner in the geo assign tab