mirror of
https://bitbucket.org/presik/trytonpsk-crm.git
synced 2023-12-14 05:22:56 +01:00
1362 lines
50 KiB
Python
1362 lines
50 KiB
Python
# 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"
|
|
# import datetime
|
|
from datetime import datetime, date
|
|
|
|
from sql import Literal, Null
|
|
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
|
|
|
|
from trytond.modules.company import CompanyReport
|
|
|
|
# Manage errors
|
|
# from trytond.exceptions import UserError
|
|
from exceptions import IncompletePartyValidation, ChangeStateWarning
|
|
from trytond.exceptions import UserWarning
|
|
|
|
from trytond.ir.attachment import AttachmentCopyMixin
|
|
from trytond.ir.note import NoteCopyMixin
|
|
from trytond.modules.company.model import employee_field, set_employee
|
|
|
|
# class PartyEvaluationConcept(ModelSQL, ModelView):
|
|
# ''' Model to create concepts to Party Evaluation '''
|
|
# 'Party Evaluation Concept'
|
|
# __name__ = 'crm.party_evaluation_concept'
|
|
# name = fields.Char('Concept Name', required=True)
|
|
#
|
|
# class PartyEvaluation(ModelSQL, ModelView):
|
|
# ''' Model to make analisys of economic behavior of party'''
|
|
# 'Party Evaluation'
|
|
# __name__ = 'crm.party_evaluation'
|
|
# concept = fields.Many2One('crm.party_evaluation_concept', 'Concept', required=True )
|
|
# opportunity = fields.Many2One('crm.opportunity', 'Opportunity', required=True )
|
|
# date = fields.Date('Date', required=True)
|
|
# observation = fields.Text('Observation')
|
|
# approved = fields.Boolean('Approved')
|
|
|
|
|
|
class Opportunity(
|
|
Workflow, ModelSQL, ModelView,
|
|
AttachmentCopyMixin, NoteCopyMixin):
|
|
'CRM Opportunity'
|
|
__name__ = "crm.opportunity"
|
|
_history = True
|
|
_rec_name = 'number'
|
|
|
|
_states_start = {
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review'])
|
|
}
|
|
_depends_start = ['state']
|
|
_states_stop = {
|
|
'readonly': Eval('state').in_(
|
|
['cancelled']),
|
|
}
|
|
# 'quote_revision', 'accepted', 'quote_approbation', 'customer_approbation', 'lost',
|
|
_depends_stop = ['state']
|
|
|
|
number = fields.Char('Number', readonly=True, required=True, select=True)
|
|
reference = fields.Char('Reference', select=True,
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review'])
|
|
})
|
|
|
|
kind_opportunity = fields.Many2One('crm.opportunity_kind_concept', 'Opportunity Kind Concept', select=True,
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review'])
|
|
})
|
|
|
|
# reference = fields.Selection([
|
|
# ('physical', "Physical"),
|
|
# ('electronic', "Electronic")], "Reference", select=True, sort=False,
|
|
# states={
|
|
# 'readonly': ~Eval('state').in_(['prospecting', 'analysis'])
|
|
# })
|
|
|
|
party = fields.Many2One(
|
|
'party.party', "Party", select=True,
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review']),
|
|
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
|
|
},
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['state', 'company'])
|
|
contact = fields.Many2One(
|
|
'party.contact_mechanism', "Contact",
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
search_context={
|
|
'related_party': Eval('party'),
|
|
},
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review']),
|
|
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
|
|
},
|
|
depends=['party', 'company'])
|
|
address = fields.Many2One('party.address', 'Address',
|
|
domain=[('party', '=', Eval('party'))],
|
|
select=True, depends=['party', 'state'],
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review']),
|
|
# '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',
|
|
'Currency'), 'get_currency')
|
|
currency_digits = fields.Function(fields.Integer('Currency Digits'),
|
|
'get_currency_digits')
|
|
amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)),
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review']),
|
|
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
|
|
}, depends=_depends_stop +
|
|
['currency_digits'],
|
|
help='Estimated revenue amount.')
|
|
time_ammount = fields.Selection([
|
|
('monthly', "Monthly"),
|
|
('bimonthly', "Bimonthly"),
|
|
('biannual', "Biannual"),
|
|
('annual', "Annual")], "Time Ammount", select=True, sort=False,
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review'])
|
|
})
|
|
|
|
payment_term = fields.Many2One('account.invoice.payment_term',
|
|
'Payment Term', states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review'])
|
|
},
|
|
depends=['state'])
|
|
employee = fields.Many2One('company.employee', 'Employee',
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review'])
|
|
},
|
|
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',
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review']),
|
|
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
|
|
})
|
|
comment = fields.Text('Comment', states=_states_stop,
|
|
depends=_depends_stop)
|
|
lines = fields.One2Many('crm.opportunity.line', 'opportunity', 'Lines',
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review']),
|
|
# '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),
|
|
],
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review'])
|
|
},
|
|
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')
|
|
contracts = fields.One2Many('sale.contract', 'origin', 'Contracts')
|
|
|
|
converted_by = employee_field(
|
|
"Converted By", states=['accepted', 'lost', 'cancelled'])
|
|
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)
|
|
|
|
type = fields.Selection([
|
|
('sale', 'Sale'),
|
|
('contract', 'Contract'),
|
|
], "Type", required=True, select=True,
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review'])
|
|
})
|
|
|
|
source = fields.Selection([
|
|
('website', 'Website'),
|
|
('phone', 'Phone'),
|
|
('email', 'Email'),
|
|
('whatsapp', 'WhatsApp'),
|
|
('salesman', 'Salesman'),
|
|
('referred', 'Referred'),
|
|
], "Source", required=True, select=True,
|
|
states={
|
|
'readonly': ~Eval('state').in_(['prospecting', 'quote_revision', 'review'])
|
|
})
|
|
|
|
total = fields.Function(fields.Float('Total'),
|
|
'get_total_opportunity')
|
|
total_without_tax = fields.Function(fields.Float('Total'),
|
|
'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'])
|
|
# }
|
|
|
|
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_(['analysis', 'review', 'quote_revision', 'cancelled', 'lost'])
|
|
}, depends=['state'])
|
|
|
|
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')
|
|
traceability = fields.One2Many('crm.opportunity_traceability', 'opportunity', 'Opportunity Traceability', readonly=False)
|
|
# del _states_start, _depends_start
|
|
# del _states_stop, _depends_stop
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Opportunity, cls).__setup__()
|
|
cls._order.insert(0, ('start_date', 'DESC'))
|
|
cls._transitions |= set((
|
|
('prospecting', 'analysis'),
|
|
('analysis', 'prospecting'),
|
|
('analysis', 'quotation'),
|
|
('analysis', 'review'),
|
|
('analysis', 'cancelled'),
|
|
('review', 'quotation'),
|
|
('review', 'cancelled'),
|
|
('review', 'analysis'),
|
|
# ('review', 'lost'),
|
|
('quotation', 'analysis'),
|
|
('quotation', 'quote_approbation'),
|
|
('quote_approbation', 'cancelled'),
|
|
('quote_approbation', 'quotation'),
|
|
('quote_approbation', 'quote_revision'),
|
|
('quote_approbation', 'customer_approbation'),
|
|
('quote_revision', 'quote_approbation'),
|
|
('quote_revision', 'cancelled'),
|
|
('customer_approbation', 'quote_revision'),
|
|
('customer_approbation', 'accepted'),
|
|
('customer_approbation', 'lost'),
|
|
# ('quote_approbation', 'quotation'),
|
|
# ('accepted', 'quote_approbation'),
|
|
# ('accepted', 'cancelled'),
|
|
('accepted', 'customer_approbation'),
|
|
('cancelled', 'prospecting'),
|
|
('lost', 'quote_approbation'),
|
|
('lost', 'cancelled'),
|
|
))
|
|
cls._buttons.update({
|
|
'prospecting': {
|
|
'invisible': ~Eval('state').in_(
|
|
['analysis','cancelled']),
|
|
'icon': If(Eval('state').in_(['analysis','cancelled']),
|
|
'tryton-back', 'tryton-forward'),
|
|
},
|
|
'analysis': {
|
|
'invisible': ~Eval('state').in_(['prospecting', 'review', 'quotation']),
|
|
'icon': If(Eval('state').in_(['review','quotation']),
|
|
'tryton-back', 'tryton-forward'),
|
|
},
|
|
'quotation': {
|
|
'invisible': ~Eval('state').in_(['analysis', 'review', 'quote_approbation']),
|
|
'icon': If(Eval('state').in_(['quote_approbation']),
|
|
'tryton-back', 'tryton-forward'),
|
|
},
|
|
'review': {
|
|
'invisible': ~Eval('state').in_(['analysis']),
|
|
},
|
|
'customer_approbation': {
|
|
'invisible': ~Eval('state').in_(['quote_approbation','lost']),
|
|
'icon': If(Eval('state').in_(['lost']),
|
|
'tryton-back', 'tryton-forward'),
|
|
},
|
|
'quote_approbation': {
|
|
'invisible': ~Eval('state').in_(['quotation', 'quote_revision','lost']),
|
|
'icon': If(Eval('state').in_([ 'quote_revision']),
|
|
'tryton-back', 'tryton-forward'),
|
|
},
|
|
'quote_revision': {
|
|
'invisible': ~Eval('state').in_(['quote_approbation', 'customer_approbation']),
|
|
'icon': If(Eval('state').in_([ 'customer_approbation']),
|
|
'tryton-back', 'tryton-forward'),
|
|
},
|
|
'customer_approbation': {
|
|
'invisible': ~Eval('state').in_(['quote_approbation','accepted']),
|
|
'icon': If(Eval('state').in_(['quote_approbation']),
|
|
'tryton-forward', 'tryton-back'),
|
|
},
|
|
'accepted': {
|
|
'invisible': ~Eval('state').in_(['customer_approbation']),
|
|
'icon': If(Eval('state').in_(['customer_approbation']),
|
|
'tryton-create', 'tryton-back'),
|
|
},
|
|
'lost': {
|
|
'invisible': ~Eval('state').in_(['customer_approbation']),
|
|
},
|
|
'cancelled': {
|
|
'invisible': ~Eval('state').in_(['analysis', 'quote_revision','review','lost']),
|
|
},
|
|
})
|
|
|
|
@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
|
|
|
|
# @fields.depends('party')
|
|
def get_is_approved(self,name=None):
|
|
res = True
|
|
if self.party_validations:
|
|
for validation in self.party_validations:
|
|
print(validation.response,'response')
|
|
if (validation.response !='approved'):
|
|
res = False
|
|
print(res,'res')
|
|
return res
|
|
|
|
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'prospecting'
|
|
|
|
@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')
|
|
|
|
@staticmethod
|
|
def default_employee():
|
|
return Transaction().context.get('employee')
|
|
|
|
@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()
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
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()
|
|
|
|
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)
|
|
default.setdefault('converted_by')
|
|
return super(CrmOpportunity, 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()
|
|
|
|
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 {
|
|
'description':self.description,
|
|
'party':self.party,
|
|
'salesman':self.employee,
|
|
'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',
|
|
}
|
|
|
|
|
|
def _get_sale_opportunity(self):
|
|
'''
|
|
Return sale for an opportunity
|
|
'''
|
|
Sale = Pool().get('sale.sale')
|
|
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,
|
|
salesman=self.employee,
|
|
invoice_address=self.address,
|
|
shipment_address=self.address,
|
|
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
|
|
'''
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
|
|
value = {
|
|
'opportunity':self.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Creación de venta para la oportunidad'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
sale = self._get_sale_opportunity()
|
|
sale_lines = []
|
|
for line in self.lines:
|
|
sale_lines.append(line.get_sale_line(sale))
|
|
sale.lines = sale_lines
|
|
return sale
|
|
|
|
def create_contract(self):
|
|
'''
|
|
Create a contract for the opportunity and return the contract
|
|
'''
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
|
|
value = {
|
|
'opportunity':self.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Creación de contrato para la oportunidad'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
contract = self._get_contract_opportunity()
|
|
# Contract = Pool().get('sale.contract')
|
|
# contract = opportunity.create_contract()
|
|
# Contract.save([contract])
|
|
contract_lines = []
|
|
for line in self.lines:
|
|
contract_lines.append(line.get_contract_line())
|
|
contract['product_lines'] = [('create',contract_lines)]
|
|
return contract
|
|
|
|
@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
|
|
@Workflow.transition('prospecting')
|
|
def prospecting(cls, records):
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
for record in records:
|
|
value = {
|
|
'opportunity':record.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Cambio de estado a Prospecto'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('analysis')
|
|
def analysis(cls, records):
|
|
cls.check_party(records)
|
|
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
for record in records:
|
|
value = {
|
|
'opportunity':record.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Cambio de estado a Análisis'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
|
|
@classmethod
|
|
def check_party(cls,records):
|
|
ValidationTemplate = Pool().get('crm.validation_template')
|
|
ValidationLine = Pool().get('crm.opportunity.validation')
|
|
def get_lines(template,validation_lines):
|
|
lines = []
|
|
for line in template.lines:
|
|
print(line.id, 'validation')
|
|
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]
|
|
if record.is_prospect:
|
|
template, = ValidationTemplate.search([
|
|
('type','=','prospect'),
|
|
])
|
|
lines = get_lines(template, party_validations)
|
|
|
|
else:
|
|
template, = ValidationTemplate.search([
|
|
('type','=','client'),
|
|
])
|
|
lines = get_lines(template,party_validations)
|
|
if lines:
|
|
cls.write([record],{'party_validations':[('add', lines)]})
|
|
# record.party_validations = [('create',lines)]
|
|
# record.party_validations = ('add', lines)
|
|
# record.save()
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('review')
|
|
def review(cls, records):
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
for record in records:
|
|
value = {
|
|
'opportunity':record.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Cambio de estado a Reconsideración'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('quote_revision')
|
|
def quote_revision(cls, records):
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
for record in records:
|
|
value = {
|
|
'opportunity':record.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Cambio de estado a Revisión'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('quote_approbation')
|
|
def quote_approbation(cls, records):
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
for record in records:
|
|
value = {
|
|
'opportunity':record.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Cambio de estado a Aprobación Interna'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('customer_approbation')
|
|
def customer_approbation(cls, records):
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
for record in records:
|
|
value = {
|
|
'opportunity':record.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Cambio de estado a Aprobacion del Cliente'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
|
|
# @classmethod
|
|
# @ModelView.button
|
|
# @Workflow.transition('quote_approbation')
|
|
# def quote_approbation(cls, records):
|
|
# pass
|
|
|
|
@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)
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
for record in records:
|
|
value = {
|
|
'opportunity':record.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Cambio de estado a Cotización'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
|
|
@property
|
|
def is_forecast(self):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
today = Date.today()
|
|
return self.end_date or datetime.date.max > today
|
|
|
|
@classmethod
|
|
@Workflow.transition('accepted')
|
|
def accepted(cls, records):
|
|
cls.procces_opportunity(records)
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
history = []
|
|
for record in records:
|
|
value = {
|
|
'opportunity':record.id,
|
|
'validated_by':Transaction().user,
|
|
'create_date':date.today(),
|
|
'action':'Cambio de estado a Aceptado'
|
|
}
|
|
history.append(value)
|
|
history = traceability.create(history)
|
|
|
|
@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.")
|
|
|
|
@classmethod
|
|
def procces_opportunity(cls, records):
|
|
for opportunity in records:
|
|
if opportunity.type == 'contract':
|
|
Contract = Pool().get('sale.contract')
|
|
contract = opportunity.create_contract()
|
|
Contract.create([contract])
|
|
|
|
else:
|
|
pool = Pool()
|
|
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')
|
|
# if o.is_forecast
|
|
cls.write([o for o in records], {
|
|
'end_date': Date.today(),
|
|
'state': 'lost',
|
|
})
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
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)
|
|
history = traceability.create(history)
|
|
|
|
@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(),
|
|
})
|
|
traceability = Pool().get('crm.opportunity_traceability')
|
|
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)
|
|
history = traceability.create(history)
|
|
|
|
@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)
|
|
|
|
@fields.depends('lines')
|
|
def get_total_opportunity(self, name):
|
|
self.total = 0
|
|
for line in self.lines:
|
|
self.total += line.total_line_with_tax
|
|
return self.total
|
|
|
|
@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
|
|
|
|
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 OpportunityKindConcept(ModelSQL, ModelView):
|
|
''' Opportunity Kind Concept '''
|
|
'Opportunity Kind Concept'
|
|
__name__ = 'crm.opportunity_kind_concept'
|
|
name = fields.Char('Concept Name', required=True)
|
|
|
|
|
|
class OpportunityCancelledReason(ModelSQL, ModelView):
|
|
''' Opportunity Cancelled Reason '''
|
|
'Opportunity Cancelled Reason'
|
|
__name__ = 'crm.opportunity_cancelled_reason'
|
|
name = fields.Char('Reason', required=True)
|
|
|
|
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')
|
|
|
|
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')
|
|
|
|
|
|
class CrmOpportunityLine(sequence_ordered(), ModelSQL, ModelView):
|
|
'CRM Opportunity Line'
|
|
__name__ = "crm.opportunity.line"
|
|
_history = True
|
|
_states = {
|
|
'readonly': Eval('opportunity_state').in_(
|
|
['quote_revision', 'quote_approbation', 'customer_approbation', 'review', 'accepted', 'lost', 'cancelled']),
|
|
}
|
|
_depends = ['opportunity_state']
|
|
|
|
opportunity = fields.Many2One('crm.opportunity', 'Opportunity',
|
|
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,
|
|
domain=[('salable', '=', True)], states=_states, depends=_depends)
|
|
quantity = fields.Float('Quantity', required=True,
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
states=_states, depends=['unit_digits'] + _depends)
|
|
unit = fields.Many2One('product.uom', 'Unit', required=True,
|
|
states=_states, depends=_depends)
|
|
|
|
unit_digits = fields.Function(fields.Integer('Unit Digits'),
|
|
'on_change_with_unit_digits')
|
|
|
|
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')
|
|
|
|
description = fields.Function(fields.Text('Description'),
|
|
'on_change_with_description')
|
|
|
|
unit_price = fields.Numeric('Unit Price', digits=(16, 2))
|
|
|
|
total_line_with_tax = fields.Function(fields.Integer('Total Line'),
|
|
'get_total_line_with_tax')
|
|
|
|
del _states, _depends
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('opportunity')
|
|
|
|
@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
|
|
|
|
@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
|
|
|
|
@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
|
|
|
|
@fields.depends('product')
|
|
def on_change_with_description(self, name=None):
|
|
if self.product:
|
|
return self.product.description
|
|
|
|
# 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,
|
|
)
|
|
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
|
|
|
|
class OpportunityReport(CompanyReport):
|
|
__name__ = 'crm.opportunity'
|
|
|
|
class OpportunityOnlyReport(CompanyReport):
|
|
__name__ = 'crm.opportunity_only'
|
|
print("Hola")
|
|
|
|
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
|