trytonpsk-crm/opportunity.py

1160 lines
41 KiB
Python
Raw Normal View History

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
"Sales extension for managing leads and opportunities"
from datetime import datetime, date
2023-06-12 14:48:50 +02:00
from sql import Literal
from sql.aggregate import Max, Count, Sum
from sql.conditionals import Case, Coalesce
from sql.functions import Extract
from trytond.i18n import gettext
from trytond.model import ModelView, ModelSQL, Workflow, fields, \
sequence_ordered
from trytond.model.exceptions import AccessError
from trytond.pyson import Eval, In, If, Get, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool
2022-02-19 15:57:13 +01:00
from trytond.modules.company import CompanyReport
2022-09-22 22:30:16 +02:00
from trytond.exceptions import UserError
from trytond.ir.attachment import AttachmentCopyMixin
from trytond.ir.note import NoteCopyMixin
2023-06-12 14:48:50 +02:00
# from .exceptions import IncompletePartyValidation, ChangeStateWarning
2023-05-19 05:51:13 +02:00
# from trytond.modules.company.model import employee_field, set_employee
2023-06-12 14:48:50 +02:00
class LeadOrigin(ModelSQL, ModelView):
'CRM Lead Origin'
__name__ = 'crm.lead_origin'
name = fields.Char("Lead Origin")
class Opportunity(
Workflow, ModelSQL, ModelView,
AttachmentCopyMixin, NoteCopyMixin):
'CRM Opportunity'
__name__ = "crm.opportunity"
_history = True
_rec_name = 'number'
_states_opp = {
'readonly': Eval('state').in_(['converted', 'won', 'cancelled', 'lost'])
}
_states_start = {
2022-09-22 22:30:16 +02:00
'readonly': ~Eval('state').in_(['opportunity', 'converted', 'won', 'lost'])
}
_depends_start = ['state']
_states_stop = {
'readonly': Eval('state').in_(
2022-03-03 23:39:05 +01:00
['cancelled']),
}
_depends_stop = ['state']
number = fields.Char('Number', readonly=True, required=True)
party_contact = fields.Char('Party Contact', states=_states_opp)
reference = fields.Char('Reference', states=_states_opp)
party = fields.Many2One(
'party.party', "Party",
states={
2022-09-22 22:30:16 +02:00
'readonly': Eval('state').in_(['cancelled', 'converted', 'won', 'lost']),
},
context={
'company': Eval('company', -1),
},
depends=['state', 'company'])
contact = fields.Many2One(
'party.contact_mechanism', "Contact",
2023-05-19 05:51:13 +02:00
domain=[('party', '=', Eval('party'))],
context={
'company': Eval('company', -1),
},
search_context={
'related_party': Eval('party'),
},
2022-03-03 23:39:05 +01:00
states={
2022-09-22 22:30:16 +02:00
'readonly': Eval('state').in_(['won', 'lost']),
2022-03-03 23:39:05 +01:00
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
},
depends=['party', 'company'])
address = fields.Many2One('party.address', 'Address',
2022-06-24 16:22:06 +02:00
domain=[('party', '=', Eval('party'))],
select=True, depends=['party', 'state'],
states={
2022-09-22 22:30:16 +02:00
'readonly': Eval('state').in_(['won', 'lost']),
2022-06-24 16:22:06 +02:00
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
})
company = fields.Many2One('company.company', 'Company', required=True,
select=True,
states={
'readonly': _states_stop['readonly'] | Eval('party', True),
},
domain=[
('id', If(In('company', Eval('context', {})), '=', '!='),
Get(Eval('context', {}), 'company', 0)),
],
depends=_depends_stop)
currency = fields.Function(fields.Many2One('currency.currency',
2023-05-19 05:51:13 +02:00
'Currency'), 'get_currency')
currency_digits = fields.Function(fields.Integer('Currency Digits'),
2023-05-19 05:51:13 +02:00
'get_currency_digits')
amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)),
2022-03-03 23:39:05 +01:00
states={
'readonly': ~Eval('state').in_(['opportunity', 'converted', 'won', 'lost']),
2022-03-03 23:39:05 +01:00
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
}, depends=_depends_stop +
['currency_digits'],
help='Estimated revenue amount.')
payment_term = fields.Many2One('account.invoice.payment_term',
2022-09-22 22:30:16 +02:00
'Payment Term')
2023-05-19 05:51:13 +02:00
agent = fields.Many2One('commission.agent', 'Agent',
2022-03-03 23:39:05 +01:00
states={
2022-09-22 22:30:16 +02:00
'readonly': Eval('state').in_(['won', 'lost'])
2022-03-03 23:39:05 +01:00
},
depends=['state', 'company'],
domain=[('company', '=', Eval('company'))])
start_date = fields.Date('Start Date', required=True, select=True,
states=_states_start, depends=_depends_start)
end_date = fields.Date('End Date', select=True,
states=_states_stop, depends=_depends_stop)
description = fields.Char('Description',
2023-05-19 05:51:13 +02:00
states={
'readonly': Eval('state').in_(['won', 'lost']),
2023-05-19 14:52:41 +02:00
'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
2023-05-19 05:51:13 +02:00
})
2022-06-24 16:22:06 +02:00
comment = fields.Text('Comment', states=_states_stop, depends=_depends_stop)
lines = fields.One2Many('crm.opportunity.line', 'opportunity', 'Lines',
2023-05-19 05:51:13 +02:00
states={
'readonly': Eval('state').in_(['won', 'lost']),
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
})
follow_ups = fields.One2Many('crm.opportunity.follow_up',
'opportunity', 'Follow-Ups',
states={
'readonly': Eval('state').in_(['won', 'lost']),
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
})
conversion_probability = fields.Float('Conversion Probability',
digits=(1, 4), required=True,
domain=[
('conversion_probability', '>=', 0),
('conversion_probability', '<=', 1),
],
2022-03-03 23:39:05 +01:00
states={
2022-09-22 22:30:16 +02:00
'readonly': Eval('state').in_(['won', 'lost'])
2022-03-03 23:39:05 +01:00
},
depends=['state'], help="Percentage between 0 and 100.")
lost_reason = fields.Text('Reason for loss', states={
'invisible': Eval('state') != 'lost',
}, depends=['state'])
sales = fields.One2Many('sale.sale', 'origin', 'Sales')
2023-06-15 17:22:35 +02:00
2023-06-15 17:22:24 +02:00
# Must to add from sale_contract
# contracts = fields.One2Many('sale.contract', 'origin', 'Contracts')
2023-05-19 05:51:13 +02:00
# converted_by = employee_field(
# "Converted By", states=['converted', 'won', 'lost'])
2022-09-22 22:30:16 +02:00
# state = fields.Selection([
# ('prospecting', "Prospecting"),
# ('analysis', "Analysis"),
# ('quotation', "Quotation"),
# ('quote_approbation', "Quote Approbation"),
# ('quote_revision', "Quote Revision"),
# ('review', "Review"),
# ('customer_approbation', "Customer Approbation"),
# ('accepted', "Accepted"),
# ('cancelled', "Cancelled"),
# ('lost', "Lost"),
# ], "State", required=True, select=True,
# sort=False, readonly=True)
state = fields.Selection([
2022-09-22 22:30:16 +02:00
('lead', 'Lead'),
('opportunity', 'Opportunity'),
('converted', 'Converted'),
('won', 'Won'),
('cancelled', 'Cancelled'),
('lost', 'Lost'),
], "State", required=True, select=True,
sort=False, readonly=True)
type = fields.Selection([
('sale', 'Sale'),
('contract', 'Contract'),
2023-05-19 05:51:13 +02:00
], "Type", required=True, select=True,
states={
'readonly': Eval('state').in_(['won', 'lost'])
})
2023-07-11 00:52:24 +02:00
lead_origin = fields.Many2One('crm.lead_origin', 'Lead Origin')
2023-05-19 05:51:13 +02:00
total = fields.Function(fields.Float('Total'), 'get_total_opportunity')
2022-03-10 00:04:50 +01:00
total_without_tax = fields.Function(fields.Float('Total'),
2023-05-19 05:51:13 +02:00
'get_total_without_tax_opportunity')
# cancelled_reason = fields.Selection([
# ('',''),
# ('Sin acuerdo', "No se llego a un acuerdo"),
# ('cliente_bloqueado', "No rentable para la empresa")], "Cancelled Reason", required=False,
# states={
# 'invisible': ~Eval('state').in_(['analysis', 'review', 'quote_revision', 'cancelled', 'lost'])
# }, depends=['state'])
# states={
# 'readonly': ~Eval('state').in_(['prospecting', 'analysis'])
# }
2023-05-19 05:51:13 +02:00
cancelled_reason = fields.Many2One(
'crm.opportunity_cancelled_reason', 'Cancelled Reason Concept',
select=True,
states={
#'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review']),
'invisible': ~Eval('state').in_(['won'])
}, depends=['state'])
2022-06-24 16:44:18 +02:00
party_validations = fields.One2Many('crm.opportunity.validation',
'opportunity', 'Party Validations')
is_prospect = fields.Function(fields.Boolean('Is Prospect'), 'get_is_prospect')
is_approved = fields.Function(fields.Boolean('Is Approved'), 'get_is_approved')
@classmethod
def __setup__(cls):
super(Opportunity, cls).__setup__()
cls._order.insert(0, ('start_date', 'DESC'))
cls._transitions |= set((
2022-09-22 22:30:16 +02:00
('lead', 'opportunity'),
('opportunity', 'converted'),
('opportunity', 'cancelled'),
('cancelled', 'opportunity'),
2023-05-19 05:51:13 +02:00
('cancelled', 'converted'),
2022-09-22 22:30:16 +02:00
('converted', 'cancelled'),
('converted', 'opportunity'),
('converted', 'won'),
('converted', 'lost'),
('won', 'converted'),
('lost', 'converted'),
))
cls._buttons.update({
2022-09-22 22:30:16 +02:00
'opportunity': {
'invisible': Eval('state').in_(
['won', 'lost', 'opportunity','cancelled']),
},
2022-09-22 22:30:16 +02:00
'converted': {
'invisible': Eval('state').in_(['lead', 'convert', 'cancelled']),
},
2022-09-22 22:30:16 +02:00
'won': {
'invisible': ~Eval('state').in_(['converted']),
},
'lost': {
2022-09-22 22:30:16 +02:00
'invisible': ~Eval('state').in_(['converted']),
},
2022-09-22 22:30:16 +02:00
'cancelled': {
'invisible': Eval('state').in_(['lead', 'won', 'lost', 'cancelled']),
},
})
2022-09-22 22:30:16 +02:00
# @fields.depends('party')
# def get_is_prospect(self, name=None):
# res = True
# if self.party:
# Invoice = Pool().get('account.invoice')
# invoices = Invoice.search([
# ('party','=',self.party.id),
# ('type','=','out'),
# ('state','!=','draft')
# ])
# if len(invoices) > 0:
# res = False
# return res
# def get_is_approved(self, name=None):
# res = True
# if self.party_validations:
# for validation in self.party_validations:
# if (validation.response !='approved'):
# res = False
# return res
@staticmethod
def default_state():
2022-09-22 22:30:16 +02:00
return 'lead'
@staticmethod
def default_start_date():
Date = Pool().get('ir.date')
return Date.today()
@staticmethod
def default_conversion_probability():
return 0.5
@staticmethod
def default_company():
return Transaction().context.get('company')
@classmethod
def default_payment_term(cls):
PaymentTerm = Pool().get('account.invoice.payment_term')
payment_terms = PaymentTerm.search(cls.payment_term.domain)
if len(payment_terms) == 1:
return payment_terms[0].id
@classmethod
def view_attributes(cls):
return super().view_attributes() + [
('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
]
@classmethod
def get_resources_to_copy(cls, name):
return {
'sale.sale',
}
@classmethod
def create(cls, vlist):
pool = Pool()
2022-03-09 00:19:24 +01:00
history = []
Config = pool.get('crm.configuration')
config = Config().get_configuration()
vlist = [x.copy() for x in vlist]
default_company = cls.default_company()
for vals in vlist:
if vals.get('number') is None and config:
vals['number'] = config.opportunity_sequence.get()
2022-03-09 00:19:24 +01:00
opportunity = super(Opportunity, cls).create(vlist)
# print(opportunity,'oportunidad')
# value = {
# 'opportunity':opportunity[id],
# 'validated_by':Transaction().user,
# 'create_date':date.today(),
# 'action':'Creación de la oportunidad'
# }
# history.append(value)
# history = traceability.create(history)
return opportunity
@classmethod
def copy(cls, opportunities, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('number', None)
default.setdefault('sales', None)
2023-05-19 05:51:13 +02:00
default.setdefault('contracts', None)
default.setdefault('converted_by')
2023-07-11 00:52:24 +02:00
return super(Opportunity, cls).copy(opportunities, default=default)
def get_currency(self, name):
return self.company.currency.id
def get_currency_digits(self, name):
return self.company.currency.digits
@fields.depends('company')
def on_change_company(self):
if self.company:
self.currency = self.company.currency
self.currency_digits = self.company.currency.digits
@fields.depends('party')
def on_change_party(self):
if self.party and self.party.customer_payment_term:
self.payment_term = self.party.customer_payment_term
else:
self.payment_term = self.default_payment_term()
2022-01-29 00:07:24 +01:00
def _get_contract_opportunity(self):
'''
Return contract for an opportunity
'''
# Contract = Pool().get('sale.contract')
# print(Contract)
Date = Pool().get('ir.date')
today = Date.today()
return {
2023-07-11 00:52:24 +02:00
'description': self.description,
'party': self.party,
2023-05-19 05:51:13 +02:00
# 'salesman':self.employee,
2023-07-11 00:52:24 +02:00
'payment_term': self.payment_term,
'company': self.company,
'currency': self.company.currency,
'comment': self.comment,
'reference': self.reference,
'contract_date': today,
'origin': self,
'state': 'draft',
}
2022-01-29 00:07:24 +01:00
def _get_sale_opportunity(self):
'''
Return sale for an opportunity
'''
Sale = Pool().get('sale.sale')
2022-03-05 00:02:28 +01:00
Date = Pool().get('ir.date')
today = Date.today()
return Sale(
description=self.description,
party=self.party,
contact=self.contact,
payment_term=self.payment_term,
company=self.company,
2023-05-19 05:51:13 +02:00
# salesman=self.employee,
invoice_address=self.address,
shipment_address=self.address,
2022-03-05 00:02:28 +01:00
reference=self.reference,
start_contract=today,
currency=self.company.currency,
comment=self.comment,
sale_date=None,
origin=self,
warehouse=Sale.default_warehouse(),
)
def create_sale(self):
'''
Create a sale for the opportunity and return the sale
'''
sale = self._get_sale_opportunity()
sale_lines = []
2022-09-22 22:30:16 +02:00
if self.lines:
for line in self.lines:
sale_lines.append(line.get_sale_line(sale))
sale.lines = sale_lines
return sale
else:
raise UserError("No hay lineas de producto")
2022-01-29 00:07:24 +01:00
def create_contract(self):
contract = self._get_contract_opportunity()
# Contract = Pool().get('sale.contract')
# contract = opportunity.create_contract()
# Contract.save([contract])
contract_lines = []
2022-09-22 22:30:16 +02:00
if self.lines:
for line in self.lines:
contract_lines.append(line.get_contract_line())
contract['product_lines'] = [('create',contract_lines)]
return contract
else:
raise UserError("No hay lineas de producto")
2022-01-29 00:07:24 +01:00
@classmethod
def delete(cls, opportunities):
# Cancel before delete
cls.cancel(opportunities)
for opportunity in opportunities:
if opportunity.state != 'cancelled':
raise AccessError(
gettext('crm.msg_opportunity_delete_cancel',
opportunity=opportunity.rec_name))
super(Opportunity, cls).delete(opportunities)
@classmethod
@ModelView.button
2022-09-22 22:30:16 +02:00
@Workflow.transition('opportunity')
def opportunity(cls, records):
print('entro pero se murio')
@classmethod
@ModelView.button
2022-09-22 22:30:16 +02:00
@Workflow.transition('converted')
def converted(cls, records):
cls.procces_opportunity(records)
# @classmethod
# def check_party(cls, records):
# pool = Pool()
# ValidationTemplate = pool.get('crm.validation_template')
# ValidationLine = pool.get('crm.opportunity.validation')
# def get_lines(template, validation_lines):
# lines = []
# for line in template.lines:
# if line.id not in validation_lines:
# value = {
# 'opportunity': record.id,
# 'party': record.party.id,
# 'sequence': line.sequence,
# 'line_ask': line.ask,
# 'template': line.id
# }
# lines.append(value)
# lines = ValidationLine.create(lines)
# return lines
# for record in records:
# lines = None
# party_validations = [
# v.template.id for v in record.party_validations
# ]
# _type = 'client'
# if record.is_prospect:
# _type = 'prospect'
# templates = ValidationTemplate.search([
# ('type', '=', _type),
# ])
# if not templates:
# continue
# lines = get_lines(templates[0], party_validations)
# if lines:
# cls.write([record], {'party_validations': [('add', lines)]})
@classmethod
@ModelView.button
2022-09-22 22:30:16 +02:00
@Workflow.transition('won')
def won(cls, records):
pass
2022-02-02 22:20:44 +01:00
@classmethod
@ModelView.button
@Workflow.transition('quote_approbation')
def quote_approbation(cls, records):
2022-09-22 22:30:16 +02:00
pass
2023-05-19 05:51:13 +02:00
# @classmethod
# @ModelView.button
# @Workflow.transition('quotation')
# @set_employee('converted_by')
# def quotation(cls, records):
# # Analyze if party validation is true in all lines
# cls.get_party_validation(records)
# # cls.procces_opportunity(records)
# history = []
# for record in records:
# value = {
# 'opportunity':record.id,
# 'create_date':date.today(),
# 'action':'Cambio de estado a Cotización'
# }
# history.append(value)
@property
def is_forecast(self):
pool = Pool()
Date = pool.get('ir.date')
today = Date.today()
return self.end_date or datetime.date.max > today
2022-09-22 22:30:16 +02:00
# @classmethod
# def get_party_validation(cls, records):
# for opportunity in records:
# if opportunity.is_approved:
# Warning = Pool().get('res.user.warning')
# # warning_name = Warning.format('mywarning', [self])
# warning_name = 'quotation'
# if Warning.check(warning_name ):
# raise ChangeStateWarning(warning_name, gettext('crm.msg_change_state_to_quotation'))
# else:
# raise IncompletePartyValidation(gettext('crm.title_msg_incomplete_party_validation'),
# gettext('crm.msg_incomplete_party_validation'))
# # raise UserError("No es posible pasar al estado de Cotización", "Todos los conceptos del anállisis del tercero no estan validados.")
2022-01-29 00:07:24 +01:00
@classmethod
def procces_opportunity(cls, records):
2022-06-24 17:35:04 +02:00
pool = Pool()
2022-01-31 21:25:27 +01:00
for opportunity in records:
2022-01-29 00:07:24 +01:00
if opportunity.type == 'contract':
2022-06-24 17:35:04 +02:00
Contract = pool.get('sale.contract')
contract = opportunity.create_contract()
Contract.create([contract])
2022-01-29 00:07:24 +01:00
else:
2022-03-05 00:02:28 +01:00
Sale = pool.get('sale.sale')
sales = [o.create_sale() for o in records if not o.sales]
Sale.save(sales)
for sale in sales:
sale.origin.copy_resources_to(sale)
# Sale = Pool().get('sale.sale')
# sale = opportunity.create_sale()
# Sale.save(sale)
@classmethod
@ModelView.button
@Workflow.transition('lost')
def lost(cls, records):
Date = Pool().get('ir.date')
2022-03-10 22:40:13 +01:00
# if o.is_forecast
cls.write([o for o in records], {
'end_date': Date.today(),
'state': 'lost',
})
2022-03-09 00:19:24 +01:00
history = []
for record in records:
value = {
'opportunity':record.id,
'validated_by':Transaction().user,
'create_date':date.today(),
'action':'Cambio de estado a Perdido'
}
history.append(value)
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancelled(cls, records):
# pass
Date = Pool().get('ir.date')
cls.write([o for o in records if o.cancelled_reason], {
'end_date': Date.today(),
})
2022-03-09 00:19:24 +01:00
history = []
for record in records:
value = {
'opportunity':record.id,
'validated_by':Transaction().user,
'create_date':date.today(),
'action':'Cambio de estado a Anulado'
}
history.append(value)
@staticmethod
def _sale_won_states():
return ['confirmed', 'processing', 'done']
@staticmethod
def _sale_lost_states():
return ['cancelled']
def is_won(self):
sale_won_states = self._sale_won_states()
sale_lost_states = self._sale_lost_states()
end_states = sale_won_states + sale_lost_states
return (self.sales
and all(s.state in end_states for s in self.sales)
and any(s.state in sale_won_states for s in self.sales))
def is_lost(self):
sale_lost_states = self._sale_lost_states()
return (self.sales
and all(s.state in sale_lost_states for s in self.sales))
@property
def sale_amount(self):
pool = Pool()
Currency = pool.get('currency.currency')
if not self.sales:
return
sale_lost_states = self._sale_lost_states()
amount = 0
for sale in self.sales:
if sale.state not in sale_lost_states:
amount += Currency.compute(sale.currency, sale.untaxed_amount,
self.currency)
return amount
@classmethod
def process(cls, opportunities):
won = []
lost = []
converted = []
for opportunity in opportunities:
sale_amount = opportunity.sale_amount
if opportunity.amount != sale_amount:
opportunity.amount = sale_amount
if opportunity.is_won():
won.append(opportunity)
elif opportunity.is_lost():
lost.append(opportunity)
elif (opportunity.state != 'converted'
and opportunity.sales):
converted.append(opportunity)
cls.save(opportunities)
if won:
cls.won(won)
if lost:
cls.lost(lost)
if converted:
cls.convert(converted)
2022-03-10 00:04:50 +01:00
@fields.depends('lines')
def get_total_opportunity(self, name):
2022-03-10 00:04:50 +01:00
self.total = 0
for line in self.lines:
2022-03-10 00:04:50 +01:00
self.total += line.total_line_with_tax
return self.total
2022-03-10 00:04:50 +01:00
@fields.depends('lines')
def get_total_without_tax_opportunity(self, name):
self.total = 0
for line in self.lines:
self.total += line.total_line
return self.total
2022-09-22 22:30:16 +02:00
# class PartyValidationOpportunity(ModelSQL, ModelView):
# ''' Model to make analisys of economic behavior of party in a opportunity'''
# 'Party Validation Opportunity'
# __name__ = 'crm.party_validation_opportunity'
# # concept = fields.Many2One('crm.party_evaluation_concept', 'Concept', required=True )
# opportunity = fields.Many2One('crm.opportunity', 'Opportunity', required=True )
# template = fields.Many2One('crm.validation_template', 'Template', required=True )
# date = fields.Date('Date')
# observation = fields.Text('Observation')
# class OpportunityKind(ModelSQL, ModelView):
# ''' Opportunity Kind Concept '''
# 'Opportunity Kind Concept'
# __name__ = 'crm.opportunity_kind_concept'
# name = fields.Char('Concept Name', required=True)
class OpportunityCancellReason(ModelSQL, ModelView):
''' Opportunity Cancell Reason '''
'Opportunity Cancelled Reason'
__name__ = 'crm.opportunity_cancelled_reason'
name = fields.Char('Reason', required=True)
2022-09-22 22:30:16 +02:00
# class OpportunityKind(ModelSQL, ModelView):
# ''' Opportunity Kind'''
# __name__ = 'crm.opportunity.kind'
# concept = fields.Many2One('crm.opportunity_kind_concept', 'Concept Opportunity Kind', required=True )
# opportunity = fields.Many2One('crm.opportunity', 'Opportunity')
2022-09-22 22:30:16 +02:00
# class OpportunityTraceability(ModelSQL, ModelView):
# ''' Model to save traceability of the opportunity'''
# 'Opportunity Traceability'
# __name__ = 'crm.opportunity_traceability'
# action = fields.Char('Action')
# opportunity = fields.Many2One('crm.opportunity', 'Opportunity', required=True )
# observation = fields.Text('Observation')
# validated_by = fields.Many2One('res.user', 'User')
2023-05-19 05:51:13 +02:00
class CrmOpportunityFollowUp(sequence_ordered(), ModelSQL, ModelView):
'CRM Opportunity FollowUp'
__name__ = "crm.opportunity.follow_up"
opportunity = fields.Many2One('crm.opportunity', 'Opportunity', required=True )
follow_date = fields.Date('Follow Date')
action = fields.Char('Action')
2023-05-19 14:52:41 +02:00
notes = fields.Text('Notes')
2023-05-19 05:51:13 +02:00
done_by = fields.Many2One('res.user', 'Done By')
2023-07-11 00:12:03 +02:00
state = fields.Selection([
('draft', "Draft"),
('done', "Done"),
], "State", required=False, readonly=True)
@staticmethod
def default_state():
return 'draft'
2023-05-19 05:51:13 +02:00
class CrmOpportunityLine(sequence_ordered(), ModelSQL, ModelView):
'CRM Opportunity Line'
__name__ = "crm.opportunity.line"
_history = True
_states = {
2023-07-11 00:12:03 +02:00
'readonly': Eval('opportunity_state').in_([
'quote_revision',
'quote_approbation',
'customer_approbation',
'review',
'accepted',
'lost',
'cancelled']),
}
_depends = ['opportunity_state']
# time_ammount = fields.Selection([
billing_frecuency = fields.Selection([
('one_payment', "One Payment"),
2023-05-19 05:51:13 +02:00
('monthly', "Monthly"),
('bimonthly', "Bimonthly"),
('biannual', "Biannual"),
('annual', "Annual")], "Billing Frecuency",
2023-05-19 05:51:13 +02:00
states={
'readonly': Eval('state').in_(['won', 'lost'])
})
opportunity = fields.Many2One('crm.opportunity', 'Opportunity',
2023-05-19 05:51:13 +02:00
ondelete='CASCADE', select=True, required=True,
states={
'readonly': _states['readonly'] & Bool(Eval('opportunity')),
},
depends=_depends)
opportunity_state = fields.Function(
fields.Selection('get_opportunity_states', "Opportunity State"),
'on_change_with_opportunity_state')
product = fields.Many2One('product.product', 'Product', required=True,
2023-05-19 05:51:13 +02:00
domain=[('salable', '=', True)], states=_states, depends=_depends)
quantity = fields.Float('Quantity', required=True,
2023-05-19 05:51:13 +02:00
digits=(16, Eval('unit_digits', 2)),
states=_states, depends=['unit_digits'] + _depends)
unit = fields.Many2One('product.uom', 'Unit', required=True,
2023-05-19 05:51:13 +02:00
states=_states, depends=_depends)
unit_digits = fields.Function(fields.Integer('Unit Digits'),
'on_change_with_unit_digits')
2023-05-19 05:51:13 +02:00
tax = fields.Function(fields.Float('Tax Line'), 'get_tax_line')
total_line = fields.Function(fields.Integer('Total Line'), 'get_total_line')
base_tax = fields.Function(fields.Float('Base Tax'), 'get_base_tax')
2023-07-11 00:12:03 +02:00
description = fields.Text('Description')
unit_price = fields.Numeric('Unit Price', digits=(16, 2))
2023-07-11 00:12:03 +02:00
start_invoice_date = fields.Date('Start Invoice Date')
2022-03-15 23:28:45 +01:00
total_line_with_tax = fields.Function(fields.Integer('Total Line'),
2023-05-19 05:51:13 +02:00
'get_total_line_with_tax')
2023-07-11 00:12:03 +02:00
payment_term = fields.Many2One('account.invoice.payment_term',
'Payment Term')
2022-03-10 00:04:50 +01:00
del _states, _depends
@staticmethod
def default_billing_frecuency():
return 'monthly'
@classmethod
def get_opportunity_states(cls):
pool = Pool()
Opportunity = pool.get('crm.opportunity')
return Opportunity.fields_get(['state'])['state']['selection']
@fields.depends('opportunity', '_parent_opportunity.state')
def on_change_with_opportunity_state(self, name=None):
if self.opportunity:
return self.opportunity.state
2022-03-10 00:04:50 +01:00
@fields.depends('product')
def get_base_tax(self, name=None):
if self.unit_price:
self.base_tax = (float(self.total_line) * 0.1)
else:
self.base_tax = 0
return self.base_tax
@fields.depends('product')
def get_tax_line(self, name=None):
if self.unit_price:
self.tax = (self.base_tax * 0.19)
else:
self.tax = 0
return self.tax
@fields.depends('product')
def get_total_line(self, name=None):
if self.unit_price:
self.total_line = float(self.unit_price) * float(self.quantity)
else:
self.total_line = 0
return self.total_line
2022-03-10 00:04:50 +01:00
@fields.depends('product')
def get_total_line_with_tax(self, name=None):
if self.unit_price:
self.total_line = float(self.total_line) + self.tax
else:
self.total_line = 0
return self.total_line
@fields.depends('unit')
def on_change_with_unit_digits(self, name=None):
if self.unit:
return self.unit.digits
return 2
@fields.depends('product')
def on_change_with_unit_price(self, name=None):
if self.product:
return self.product.list_price
2023-07-11 00:12:03 +02:00
# Product = Pool().get('product.product')
# sale_line = SaleLine(
# type='line',
# product=self.product,
# sale=sale,
# description=None,
# )
# return sale_line
@fields.depends('product', 'unit')
def on_change_product(self):
if not self.product:
return
category = self.product.sale_uom.category
if not self.unit or self.unit.category != category:
self.unit = self.product.sale_uom
self.unit_digits = self.product.sale_uom.digits
def get_sale_line(self, sale):
'''
Return sale line for opportunity line
'''
SaleLine = Pool().get('sale.line')
sale_line = SaleLine(
type='line',
product=self.product,
sale=sale,
description=None,
2022-09-22 22:30:16 +02:00
operation_center= self.opportunity.employee.operation_center,
)
sale_line.on_change_product()
self._set_sale_line_quantity(sale_line)
sale_line.on_change_quantity()
return sale_line
def get_contract_line(self):
'''
Return contract line for opportunity line
'''
# ContractLine = Pool().get('sale.contract.product_line')
contract_line = {
'type': 'line',
# 'contract': contract,
'product': self.product,
'description': self.product.name,
'unit_price': self.unit_price,
# quantity = self.quantity,
}
# print(contract, "Contract in contract line")
# print(contract_line, "contract line")
return contract_line
def _set_sale_line_quantity(self, sale_line):
sale_line.quantity = self.quantity
sale_line.unit = self.unit
def get_rec_name(self, name):
pool = Pool()
Lang = pool.get('ir.lang')
lang = Lang.get()
return (lang.format(
'%.*f', (self.unit.digits, self.quantity or 0))
+ '%s %s @ %s' % (
self.unit.symbol, self.product.rec_name,
self.opportunity.rec_name))
@classmethod
def search_rec_name(cls, name, clause):
return [('product.rec_name',) + tuple(clause[1:])]
class SaleOpportunityReportMixin:
__slots__ = ()
number = fields.Integer('Number')
converted = fields.Integer('Converted')
conversion_rate = fields.Function(fields.Float('Conversion Rate',
digits=(1, 4)), 'get_conversion_rate')
won = fields.Integer('Won')
winning_rate = fields.Function(fields.Float('Winning Rate', digits=(1, 4)),
'get_winning_rate')
lost = fields.Integer('Lost')
company = fields.Many2One('company.company', 'Company')
currency = fields.Function(fields.Many2One('currency.currency',
'Currency'), 'get_currency')
currency_digits = fields.Function(fields.Integer('Currency Digits'),
'get_currency_digits')
amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
converted_amount = fields.Numeric('Converted Amount',
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
conversion_amount_rate = fields.Function(fields.Float(
'Conversion Amount Rate', digits=(1, 4)), 'get_conversion_amount_rate')
won_amount = fields.Numeric('Won Amount',
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
winning_amount_rate = fields.Function(fields.Float(
'Winning Amount Rate', digits=(1, 4)), 'get_winning_amount_rate')
@staticmethod
def _converted_state():
return ['converted', 'won']
@staticmethod
def _won_state():
return ['won']
@staticmethod
def _lost_state():
return ['lost']
def get_conversion_rate(self, name):
if self.number:
digits = getattr(self.__class__, name).digits[1]
return round(float(self.converted) / self.number, digits)
else:
return 0.0
def get_winning_rate(self, name):
if self.number:
digits = getattr(self.__class__, name).digits[1]
return round(float(self.won) / self.number, digits)
else:
return 0.0
def get_currency(self, name):
return self.company.currency.id
def get_currency_digits(self, name):
return self.company.currency.digits
def get_conversion_amount_rate(self, name):
if self.amount:
digits = getattr(self.__class__, name).digits[1]
return round(
float(self.converted_amount) / float(self.amount), digits)
else:
return 0.0
def get_winning_amount_rate(self, name):
if self.amount:
digits = getattr(self.__class__, name).digits[1]
return round(float(self.won_amount) / float(self.amount), digits)
else:
return 0.0
@classmethod
def table_query(cls):
Opportunity = Pool().get('crm.opportunity')
opportunity = Opportunity.__table__()
return opportunity.select(
Max(opportunity.create_uid).as_('create_uid'),
Max(opportunity.create_date).as_('create_date'),
Max(opportunity.write_uid).as_('write_uid'),
Max(opportunity.write_date).as_('write_date'),
opportunity.company,
Count(Literal(1)).as_('number'),
Sum(Case(
(opportunity.state.in_(cls._converted_state()),
Literal(1)), else_=Literal(0))).as_('converted'),
Sum(Case(
(opportunity.state.in_(cls._won_state()),
Literal(1)), else_=Literal(0))).as_('won'),
Sum(Case(
(opportunity.state.in_(cls._lost_state()),
Literal(1)), else_=Literal(0))).as_('lost'),
Sum(opportunity.amount).as_('amount'),
Sum(Case(
(opportunity.state.in_(cls._converted_state()),
opportunity.amount),
else_=Literal(0))).as_('converted_amount'),
Sum(Case(
(opportunity.state.in_(cls._won_state()),
opportunity.amount),
else_=Literal(0))).as_('won_amount'))
class SaleOpportunityEmployee(SaleOpportunityReportMixin, ModelSQL, ModelView):
'Sale Opportunity per Employee'
__name__ = 'crm.opportunity_employee'
employee = fields.Many2One('company.employee', 'Employee')
@classmethod
def table_query(cls):
query = super(SaleOpportunityEmployee, cls).table_query()
opportunity, = query.from_
query.columns += (
Coalesce(opportunity.employee, 0).as_('id'),
opportunity.employee,
)
where = Literal(True)
if Transaction().context.get('start_date'):
where &= (opportunity.start_date
>= Transaction().context['start_date'])
if Transaction().context.get('end_date'):
where &= (opportunity.start_date
<= Transaction().context['end_date'])
query.where = where
query.group_by = (opportunity.employee, opportunity.company)
return query
class SaleOpportunityEmployeeContext(ModelView):
'Sale Opportunity per Employee Context'
__name__ = 'crm.opportunity_employee.context'
start_date = fields.Date('Start Date')
end_date = fields.Date('End Date')
class SaleOpportunityMonthly(SaleOpportunityReportMixin, ModelSQL, ModelView):
'Sale Opportunity per Month'
__name__ = 'crm.opportunity_monthly'
year = fields.Char('Year')
month = fields.Many2One('ir.calendar.month', "Month")
year_month = fields.Function(fields.Char('Year-Month'),
'get_year_month')
@classmethod
def __setup__(cls):
super(SaleOpportunityMonthly, cls).__setup__()
cls._order.insert(0, ('year', 'DESC'))
cls._order.insert(1, ('month.index', 'DESC'))
def get_year_month(self, name):
return '%s-%s' % (self.year, self.month.index)
@classmethod
def table_query(cls):
pool = Pool()
Month = pool.get('ir.calendar.month')
month = Month.__table__()
query = super(SaleOpportunityMonthly, cls).table_query()
opportunity, = query.from_
type_id = cls.id.sql_type().base
type_year = cls.year.sql_type().base
year_column = Extract('YEAR', opportunity.start_date
).cast(type_year).as_('year')
month_index = Extract('MONTH', opportunity.start_date)
query.from_ = opportunity.join(
month, condition=month_index == month.index)
query.columns += (
Max(Extract('MONTH', opportunity.start_date)
+ Extract('YEAR', opportunity.start_date) * 100
).cast(type_id).as_('id'),
year_column,
month.id.as_('month'))
query.group_by = (year_column, month.id, opportunity.company)
return query
2022-02-19 15:57:13 +01:00
class OpportunityReport(CompanyReport):
__name__ = 'crm.opportunity'
2022-03-03 23:39:05 +01:00
class OpportunityOnlyReport(CompanyReport):
__name__ = 'crm.opportunity_only'
2022-06-24 16:22:06 +02:00
2022-03-03 23:39:05 +01:00
class OpportunityWithoutTaxReport(CompanyReport):
__name__ = 'crm.opportunity_without_tax'
class OpportunityLargeReport(CompanyReport):
__name__ = 'crm.opportunity_large_format'
class SaleOpportunityEmployeeMonthly(
SaleOpportunityReportMixin, ModelSQL, ModelView):
'Sale Opportunity per Employee per Month'
__name__ = 'crm.opportunity_employee_monthly'
year = fields.Char('Year')
month = fields.Many2One('ir.calendar.month', "Month")
employee = fields.Many2One('company.employee', 'Employee')
@classmethod
def __setup__(cls):
super(SaleOpportunityEmployeeMonthly, cls).__setup__()
cls._order.insert(0, ('year', 'DESC'))
cls._order.insert(1, ('month.index', 'DESC'))
cls._order.insert(2, ('employee', 'ASC'))
@classmethod
def table_query(cls):
pool = Pool()
Month = pool.get('ir.calendar.month')
month = Month.__table__()
query = super(SaleOpportunityEmployeeMonthly, cls).table_query()
opportunity, = query.from_
type_id = cls.id.sql_type().base
type_year = cls.year.sql_type().base
year_column = Extract('YEAR', opportunity.start_date
).cast(type_year).as_('year')
month_index = Extract('MONTH', opportunity.start_date)
query.from_ = opportunity.join(
month, condition=month_index == month.index)
query.columns += (
Max(Extract('MONTH', opportunity.start_date)
+ Extract('YEAR', opportunity.start_date) * 100
+ Coalesce(opportunity.employee, 0) * 1000000
).cast(type_id).as_('id'),
year_column,
month.id.as_('month'),
opportunity.employee,
)
query.group_by = (year_column, month.id,
opportunity.employee, opportunity.company)
return query