[REF] crm: quickly reorganize crm_lead file
In this commit we perform some small code move in order to better understand the huge crm_lead.py file. Notably * add some code sections headers (actions, PLS, opp/leads merge); * move crm.lead.tag and crm.lost.reason in their own file; * reorganize fields by main category because it is a mess; * remove some unnecessary help messages on fields; * merge two _auto_init methods definitions; No functional change occur with this commit as it contains only some code move. It prepares some further technical cleaning in CRM application, notably onchange -> compute move. LINKS Related to Task ID 2092799 (not really spec, spotted while working on it) PR odoo/odoo#42015 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
from . import res_users
|
||||
from . import calendar
|
||||
from . import crm_lead
|
||||
from . import crm_lead_tag
|
||||
from . import crm_lost_reason
|
||||
from . import crm_stage
|
||||
from . import crm_team
|
||||
from . import res_config_settings
|
||||
|
||||
+144
-150
@@ -53,16 +53,14 @@ class Lead(models.Model):
|
||||
_name = "crm.lead"
|
||||
_description = "Lead/Opportunity"
|
||||
_order = "priority desc, id desc"
|
||||
_inherit = ['mail.thread.cc', 'mail.thread.blacklist', 'mail.activity.mixin',
|
||||
'utm.mixin', 'format.address.mixin', 'phone.validation.mixin']
|
||||
_inherit = ['mail.thread.cc',
|
||||
'mail.thread.blacklist',
|
||||
'mail.activity.mixin',
|
||||
'utm.mixin',
|
||||
'format.address.mixin',
|
||||
'phone.validation.mixin']
|
||||
_primary_email = 'email_from'
|
||||
|
||||
def _auto_init(self):
|
||||
res = super(Lead, self)._auto_init()
|
||||
tools.create_index(self._cr, 'crm_lead_create_date_team_id_idx',
|
||||
self._table, ['create_date', 'team_id'])
|
||||
return res
|
||||
|
||||
def _default_team_id(self, user_id):
|
||||
domain = [('use_leads', '=', True)] if self._context.get('default_type') == "lead" or self.type == 'lead' else [('use_opportunities', '=', True)]
|
||||
return self.env['crm.team']._get_default_team_id(user_id=user_id, domain=domain)
|
||||
@@ -71,75 +69,86 @@ class Lead(models.Model):
|
||||
team = self._default_team_id(user_id=self.env.uid)
|
||||
return self._stage_find(team_id=team.id, domain=[('fold', '=', False)]).id
|
||||
|
||||
# Description
|
||||
name = fields.Char('Opportunity', required=True, index=True)
|
||||
partner_id = fields.Many2one('res.partner', string='Customer', tracking=10, index=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Linked partner (optional). Usually created when converting the lead. You can find a partner by its Name, TIN, Email or Internal Reference.")
|
||||
active = fields.Boolean('Active', default=True, tracking=True)
|
||||
date_action_last = fields.Datetime('Last Action', readonly=True)
|
||||
email_from = fields.Char('Email', help="Email address of the contact", tracking=40, index=True)
|
||||
website = fields.Char('Website', index=True, help="Website of the contact")
|
||||
team_id = fields.Many2one('crm.team', string='Sales Team', default=lambda self: self._default_team_id(self.env.uid),
|
||||
index=True, tracking=True, 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='Kanban State', compute='_compute_kanban_state')
|
||||
user_id = fields.Many2one('res.users', string='Salesperson', index=True, tracking=True, default=lambda self: self.env.user)
|
||||
user_email = fields.Char('User Email', related='user_id.email', readonly=True)
|
||||
user_login = fields.Char('User Login', related='user_id.login', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', index=True, default=lambda self: self.env.company.id)
|
||||
referred = fields.Char('Referred By')
|
||||
description = fields.Text('Notes')
|
||||
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', tracking=30)
|
||||
partner_name = fields.Char("Company Name", tracking=20, index=True, help='The name of the future partner company that will be created while converting the lead into opportunity')
|
||||
type = fields.Selection([('lead', 'Lead'), ('opportunity', 'Opportunity')], index=True, required=True, tracking=15,
|
||||
active = fields.Boolean('Active', default=True, tracking=True)
|
||||
type = fields.Selection([
|
||||
('lead', 'Lead'), ('opportunity', 'Opportunity')],
|
||||
index=True, required=True, tracking=15,
|
||||
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='Priority', index=True, default=crm_stage.AVAILABLE_PRIORITIES[0][0])
|
||||
date_closed = fields.Datetime('Closed Date', readonly=True, copy=False)
|
||||
|
||||
stage_id = fields.Many2one('crm.stage', string='Stage', ondelete='restrict', tracking=True, index=True, copy=False,
|
||||
priority = fields.Selection(
|
||||
crm_stage.AVAILABLE_PRIORITIES, string='Priority', index=True,
|
||||
default=crm_stage.AVAILABLE_PRIORITIES[0][0])
|
||||
team_id = fields.Many2one(
|
||||
'crm.team', string='Sales Team', index=True, tracking=True,
|
||||
default=lambda self: self._default_team_id(self.env.uid))
|
||||
stage_id = fields.Many2one(
|
||||
'crm.stage', string='Stage', ondelete='restrict',
|
||||
copy=False, group_expand='_read_group_stage_ids', index=True, tracking=True,
|
||||
domain="['|', ('team_id', '=', False), ('team_id', '=', team_id)]",
|
||||
group_expand='_read_group_stage_ids', default=lambda self: self._default_stage_id())
|
||||
user_id = fields.Many2one('res.users', string='Salesperson', index=True, tracking=True, default=lambda self: self.env.user)
|
||||
referred = fields.Char('Referred By')
|
||||
|
||||
default=lambda self: self._default_stage_id())
|
||||
kanban_state = fields.Selection([
|
||||
('grey', 'No next activity planned'), ('red', 'Next activity late'), ('green', 'Next activity is planned')],
|
||||
string='Kanban State', compute='_compute_kanban_state')
|
||||
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")
|
||||
color = fields.Integer('Color Index', default=0)
|
||||
# Opportunity specific
|
||||
planned_revenue = fields.Monetary('Expected Revenue', currency_field='company_currency', tracking=True)
|
||||
expected_revenue = fields.Monetary('Prorated Revenue', currency_field='company_currency', store=True, compute="_compute_expected_revenue")
|
||||
company_currency = fields.Many2one("res.currency", string='Currency', related='company_id.currency_id', readonly=True)
|
||||
# Dates
|
||||
date_closed = fields.Datetime('Closed Date', readonly=True, copy=False)
|
||||
date_action_last = fields.Datetime('Last Action', readonly=True)
|
||||
date_open = fields.Datetime('Assignation Date', readonly=True, default=fields.Datetime.now)
|
||||
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)
|
||||
day_open = fields.Float('Days to Assign', compute='_compute_day_open', store=True)
|
||||
day_close = fields.Float('Days to Close', compute='_compute_day_close', store=True)
|
||||
date_last_stage_update = fields.Datetime('Last Stage Update', index=True, default=fields.Datetime.now)
|
||||
date_conversion = fields.Datetime('Conversion Date', readonly=True)
|
||||
|
||||
# Probability - Only used for type opportunity
|
||||
probability = fields.Float('Probability', group_operator="avg", copy=False)
|
||||
automated_probability = fields.Float('Automated Probability', readonly=True)
|
||||
is_automated_probability = fields.Boolean('Is automated probability?', compute="_compute_is_automated_probability")
|
||||
date_deadline = fields.Date('Expected Closing', help="Estimate of the date on which the opportunity will be won.")
|
||||
# Customer / contact
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', string='Customer', tracking=10, index=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
help="Linked partner (optional). Usually created when converting the lead. You can find a partner by its Name, TIN, Email or Internal Reference.")
|
||||
partner_address_email = fields.Char('Partner Contact Email', related='partner_id.email', readonly=True)
|
||||
partner_is_blacklisted = fields.Boolean('Partner is blacklisted', related='partner_id.is_blacklisted', readonly=True)
|
||||
contact_name = fields.Char('Contact Name', tracking=30)
|
||||
partner_name = fields.Char("Company Name", tracking=20, index=True, help='The name of the future partner company that will be created while converting the lead into opportunity')
|
||||
function = fields.Char('Job Position')
|
||||
title = fields.Many2one('res.partner.title', string='Title')
|
||||
email_from = fields.Char('Email', index=True, tracking=40)
|
||||
phone = fields.Char('Phone', tracking=50)
|
||||
mobile = fields.Char('Mobile')
|
||||
phone_mobile_search = fields.Char('Phone/Mobile', store=False, search='_search_phone_mobile_search')
|
||||
phone_state = fields.Selection([
|
||||
('correct', 'Correct'),
|
||||
('incorrect', 'Incorrect')], string='Phone Quality', compute="_compute_phone_state", store=True)
|
||||
email_state = fields.Selection([
|
||||
('correct', 'Correct'),
|
||||
('incorrect', 'Incorrect')], string='Email Quality', compute="_compute_email_state", store=True)
|
||||
|
||||
# Only used for type opportunity
|
||||
planned_revenue = fields.Monetary('Expected Revenue', currency_field='company_currency', tracking=True)
|
||||
expected_revenue = fields.Monetary('Prorated Revenue', currency_field='company_currency', store=True, compute="_compute_expected_revenue")
|
||||
date_deadline = fields.Date('Expected Closing', help="Estimate of the date on which the opportunity will be won.")
|
||||
color = fields.Integer('Color Index', default=0)
|
||||
partner_address_email = fields.Char('Partner Contact Email', related='partner_id.email', readonly=True)
|
||||
partner_is_blacklisted = fields.Boolean('Partner is blacklisted', related='partner_id.is_blacklisted', 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
|
||||
website = fields.Char('Website', index=True, help="Website of the contact")
|
||||
lang_id = fields.Many2one('res.lang', string='Language')
|
||||
# Address fields
|
||||
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', domain="[('country_id', '=?', country_id)]")
|
||||
country_id = fields.Many2one('res.country', string='Country')
|
||||
lang_id = fields.Many2one('res.lang', string='Language', help="Language of the lead.")
|
||||
phone = fields.Char('Phone', tracking=50)
|
||||
mobile = fields.Char('Mobile')
|
||||
phone_mobile_search = fields.Char('Phone/Mobile', store=False, search='_search_phone_mobile_search')
|
||||
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.company.id)
|
||||
# Probability (Opportunity only)
|
||||
probability = fields.Float('Probability', group_operator="avg", copy=False)
|
||||
automated_probability = fields.Float('Automated Probability', readonly=True)
|
||||
is_automated_probability = fields.Boolean('Is automated probability?', compute="_compute_is_automated_probability")
|
||||
# External records
|
||||
meeting_count = fields.Integer('# Meetings', compute='_compute_meeting_count')
|
||||
lost_reason = fields.Many2one('crm.lost.reason', string='Lost Reason', index=True, tracking=True)
|
||||
|
||||
@@ -147,28 +156,6 @@ class Lead(models.Model):
|
||||
('check_probability', 'check(probability >= 0 and probability <= 100)', 'The probability of closing the deal should be between 0% and 100%!')
|
||||
]
|
||||
|
||||
def _auto_init(self):
|
||||
res = super(Lead, self)._auto_init()
|
||||
tools.create_index(self._cr, 'crm_lead_user_id_team_id_type_index',
|
||||
self._table, ['user_id', 'team_id', 'type'])
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _read_group_stage_ids(self, stages, domain, order):
|
||||
# retrieve team_id from the context and write the domain
|
||||
# - ('id', 'in', stages.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 = self._context.get('default_team_id')
|
||||
if team_id:
|
||||
search_domain = ['|', ('id', 'in', stages.ids), '|', ('team_id', '=', False), ('team_id', '=', team_id)]
|
||||
else:
|
||||
search_domain = ['|', ('id', 'in', stages.ids), ('team_id', '=', False)]
|
||||
|
||||
# perform search
|
||||
stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID)
|
||||
return stages.browse(stage_ids)
|
||||
|
||||
def _compute_kanban_state(self):
|
||||
today = date.today()
|
||||
for lead in self:
|
||||
@@ -387,8 +374,6 @@ class Lead(models.Model):
|
||||
def _onchange_lang_id(self):
|
||||
self._onchange_compute_probability(optional_field_name='lang_id')
|
||||
|
||||
# Phone Validation
|
||||
# ----------------
|
||||
@api.onchange('phone', 'country_id', 'company_id')
|
||||
def _onchange_phone_validation(self):
|
||||
if self.phone:
|
||||
@@ -399,9 +384,17 @@ class Lead(models.Model):
|
||||
if self.mobile:
|
||||
self.mobile = self.phone_format(self.mobile)
|
||||
|
||||
# ----------------------------------------
|
||||
# ORM override (CRUD, fields_view_get, ...)
|
||||
# ----------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
# ORM
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _auto_init(self):
|
||||
res = super(Lead, self)._auto_init()
|
||||
tools.create_index(self._cr, 'crm_lead_user_id_team_id_type_index',
|
||||
self._table, ['user_id', 'team_id', 'type'])
|
||||
tools.create_index(self._cr, 'crm_lead_create_date_team_id_idx',
|
||||
self._table, ['create_date', 'team_id'])
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
@@ -496,9 +489,49 @@ class Lead(models.Model):
|
||||
res['arch'] = self._fields_view_get_address(res['arch'])
|
||||
return res
|
||||
|
||||
# ----------------------------------------
|
||||
# Actions Methods
|
||||
# ----------------------------------------
|
||||
@api.model
|
||||
def _read_group_stage_ids(self, stages, domain, order):
|
||||
# retrieve team_id from the context and write the domain
|
||||
# - ('id', 'in', stages.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 = self._context.get('default_team_id')
|
||||
if team_id:
|
||||
search_domain = ['|', ('id', 'in', stages.ids), '|', ('team_id', '=', False), ('team_id', '=', team_id)]
|
||||
else:
|
||||
search_domain = ['|', ('id', 'in', stages.ids), ('team_id', '=', False)]
|
||||
|
||||
# perform search
|
||||
stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID)
|
||||
return stages.browse(stage_ids)
|
||||
|
||||
def _stage_find(self, team_id=False, 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 recordset
|
||||
"""
|
||||
# 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 self:
|
||||
if lead.team_id:
|
||||
team_ids.add(lead.team_id.id)
|
||||
# generate the domain
|
||||
if team_ids:
|
||||
search_domain = ['|', ('team_id', '=', False), ('team_id', 'in', list(team_ids))]
|
||||
else:
|
||||
search_domain = [('team_id', '=', False)]
|
||||
# AND with the domain in parameter
|
||||
if domain:
|
||||
search_domain += list(domain)
|
||||
# perform search, return the first found
|
||||
return self.env['crm.stage'].search(search_domain, order=order, limit=1)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ACTIONS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def toggle_active(self):
|
||||
super(Lead, self).toggle_active()
|
||||
@@ -605,33 +638,24 @@ class Lead(models.Model):
|
||||
}
|
||||
return action
|
||||
|
||||
# ----------------------------------------
|
||||
# Business Methods
|
||||
# ----------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
# BUSINESS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _stage_find(self, team_id=False, 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 recordset
|
||||
"""
|
||||
# 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 self:
|
||||
if lead.team_id:
|
||||
team_ids.add(lead.team_id.id)
|
||||
# generate the domain
|
||||
if team_ids:
|
||||
search_domain = ['|', ('team_id', '=', False), ('team_id', 'in', list(team_ids))]
|
||||
def log_meeting(self, meeting_subject, meeting_date, duration):
|
||||
if not duration:
|
||||
duration = _('unknown')
|
||||
else:
|
||||
search_domain = [('team_id', '=', False)]
|
||||
# AND with the domain in parameter
|
||||
if domain:
|
||||
search_domain += list(domain)
|
||||
# perform search, return the first found
|
||||
return self.env['crm.stage'].search(search_domain, order=order, limit=1)
|
||||
duration = str(duration)
|
||||
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 hours") % (html_time, meeting_subject, duration)
|
||||
return self.message_post(body=message)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MERGE LEADS / OPPS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _merge_get_result_type(self):
|
||||
""" Define the type of the result of the merge. If at least one of the
|
||||
@@ -1046,20 +1070,10 @@ class Lead(models.Model):
|
||||
sub_title = _('or send an email to %s') % (email_link)
|
||||
return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (help_title, sub_title)
|
||||
|
||||
def log_meeting(self, meeting_subject, meeting_date, duration):
|
||||
if not duration:
|
||||
duration = _('unknown')
|
||||
else:
|
||||
duration = str(duration)
|
||||
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 hours") % (html_time, meeting_subject, duration)
|
||||
return self.message_post(body=message)
|
||||
# ------------------------------------------------------------
|
||||
# MAILING
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------
|
||||
# Mail Gateway
|
||||
# ----------------------------------------
|
||||
def _creation_subtype(self):
|
||||
return self.env.ref('crm.mt_lead_create')
|
||||
|
||||
@@ -1192,9 +1206,10 @@ class Lead(models.Model):
|
||||
'template': '/crm/static/xls/crm_lead.xls'
|
||||
}]
|
||||
|
||||
# ----------------------------------------
|
||||
# Predictive Lead Scoring
|
||||
# ----------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
# PLS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _pls_get_naive_bayes_probabilities(self, batch_mode=False):
|
||||
"""
|
||||
In machine learning, naive Bayes classifiers (NBC) are a family of simple "probabilistic classifiers" based on
|
||||
@@ -1587,24 +1602,3 @@ class Lead(models.Model):
|
||||
return 0, 0, 0
|
||||
stage_result = team_results['stage_id'][str(first_stage_id.id)]
|
||||
return stage_result['won'], stage_result['lost'], stage_result['won'] + stage_result['lost']
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
|
||||
_name = "crm.lead.tag"
|
||||
_description = "Lead Tag"
|
||||
|
||||
name = fields.Char('Tag Name', required=True, translate=True)
|
||||
color = fields.Integer('Color Index')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
|
||||
|
||||
class LostReason(models.Model):
|
||||
_name = "crm.lost.reason"
|
||||
_description = 'Opp. Lost Reason'
|
||||
|
||||
name = fields.Char('Description', required=True, translate=True)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
|
||||
_name = "crm.lead.tag"
|
||||
_description = "Lead Tag"
|
||||
|
||||
name = fields.Char('Tag Name', required=True, translate=True)
|
||||
color = fields.Integer('Color Index')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class LostReason(models.Model):
|
||||
_name = "crm.lost.reason"
|
||||
_description = 'Opp. Lost Reason'
|
||||
|
||||
name = fields.Char('Description', required=True, translate=True)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
Reference in New Issue
Block a user