trytonpsk-crm/opportunity.py

1124 lines
39 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"
from datetime import datetime, date
from sql import Literal
from sql.aggregate import Max, Count, Sum
from sql.conditionals import Case, Coalesce
from sql.functions import Extract
from decimal import Decimal
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
from trytond.exceptions import UserError
from trytond.ir.attachment import AttachmentCopyMixin
from trytond.ir.note import NoteCopyMixin
from trytond.modules.account.tax import TaxableMixin
class LeadOrigin(ModelSQL, ModelView):
'CRM Lead Origin'
__name__ = 'crm.lead_origin'
name = fields.Char("Lead Origin")
class Prospect(ModelSQL, ModelView):
'CRM Prospect'
__name__ = 'crm.prospect'
name = fields.Char("Name", required=True)
phone = fields.Char('Contact Phone')
email = fields.Char('Email')
agent = fields.Many2One('commission.agent', 'Agent')
# class Person(ModelSQL, ModelView):
# 'Person'
# __name__ = 'crm.person'
# opportunity = fields.Many2One('crm.opportunity', 'Opportunity',
# required=True, readonly=True)
# name = fields.Char("Name", required=True)
# phone = fields.Char('Contact Phone')
# email = fields.Char('Email')
# id_number = fields.Char('Id Number')
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 = {
'readonly': ~Eval('state').in_(['opportunity', 'converted', 'won', 'lost'])
}
_depends_start = ['state']
_states_stop = {
'readonly': Eval('state').in_(
['cancelled']),
}
_depends_stop = ['state']
number = fields.Char('Number', readonly=True, required=True)
prospect = fields.Many2One('crm.prospect', 'Prospect', required=True,
states=_states_opp)
party_contact = fields.Char('Party Contact', readonly=True)
contact_phone = fields.Char('Contact Phone', states=_states_opp)
contact_email = fields.Char('Email')
reference = fields.Char('Reference', states=_states_opp)
party = fields.Many2One(
'party.party', "Party",
states={
'readonly': Eval('state').in_(['cancelled', 'converted', 'won', 'lost']),
},
context={
'company': Eval('company', -1),
},
depends=['state', 'company'])
party_category = fields.Many2One('party.category', 'Party Category')
contact = fields.Many2One(
'party.contact_mechanism', "Contact",
domain=[('party', '=', Eval('party'))],
context={
'company': Eval('company', -1),
},
search_context={
'related_party': Eval('party'),
},
states={
'readonly': Eval('state').in_(['won', 'lost']),
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
},
depends=['party', 'company'])
address = fields.Many2One('party.address', 'Address',
domain=[('party', '=', Eval('party'))], depends=['party', 'state'],
states={
'readonly': Eval('state').in_(['won', 'lost']),
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
})
company = fields.Many2One('company.company', 'Company', required=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_(['opportunity', 'converted', 'won', 'lost']),
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
}, depends=_depends_stop +
['currency_digits'],
help='Estimated revenue amount.')
payment_term = fields.Many2One('account.invoice.payment_term',
'Payment Term')
agent = fields.Many2One('commission.agent', 'Agent',
states={
'readonly': Eval('state').in_(['won', 'lost'])
},
depends=['state', 'company'],
domain=[('company', '=', Eval('company'))])
start_date = fields.Date('Start Date', required=True,
states=_states_start, depends=_depends_start)
end_date = fields.Date('End Date', states=_states_stop,
depends=_depends_stop)
description = fields.Char('Description',
states={
'readonly': Eval('state').in_(['won', 'lost']),
'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_(['won', 'lost']),
# 'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
})
activities = fields.One2Many('crm.activity', 'opportunity', 'Activities',
states={
'readonly': Eval('state').in_(['won', 'lost']),
})
conversion_probability = fields.Float('Conversion Probability',
digits=(1, 4), required=True,
domain=[
('conversion_probability', '>=', 0),
('conversion_probability', '<=', 1),
],
states={
'readonly': Eval('state').in_(['won', 'lost'])
},
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')
city = fields.Many2One('party.city_code', 'City')
# Must to add from sale_contract
# contracts = fields.One2Many('sale.contract', 'origin', 'Contracts')
state = fields.Selection([
('lead', 'Lead'),
('opportunity', 'Opportunity'),
('converted', 'Converted'),
('won', 'Won'),
('cancelled', 'Cancelled'),
('lost', 'Lost'),
], "State", required=True, readonly=True)
type = fields.Selection([
('sale', 'Sale'),
('contract', 'Contract'),
], "Type", required=True,
states={
'readonly': Eval('state').in_(['won', 'lost'])
})
lead_origin = fields.Many2One('crm.lead_origin', 'Lead Origin')
total_amount = fields.Function(
fields.Numeric('Amount Total', digits=(16, 2)), 'get_total_amount')
# total_without_tax = fields.Function(fields.Numeric('Total', digits=(16, 2)),
# 'get_total_without_tax_opportunity')
conditions = fields.Many2Many('crm.opportunity-sale.condition',
'opportunity', 'condition', 'Commercial Conditions')
cancelled_reason = fields.Many2One(
'crm.opportunity_cancelled_reason', 'Cancelled Reason Concept',
states={
'invisible': ~Eval('state').in_(['won'])
}, depends=['state'])
# persons = fields.One2Many('crm.person', 'opportunity', 'Persons',
# states={
# 'readonly': Eval('state').in_(['won', 'lost']),
# })
@classmethod
def __setup__(cls):
super(Opportunity, cls).__setup__()
cls._order.insert(0, ('start_date', 'DESC'))
cls._transitions |= set((
('lead', 'opportunity'),
('opportunity', 'converted'),
('opportunity', 'cancelled'),
('cancelled', 'opportunity'),
('cancelled', 'converted'),
('converted', 'cancelled'),
('converted', 'opportunity'),
('converted', 'won'),
('converted', 'lost'),
('won', 'converted'),
('lost', 'converted'),
))
cls._buttons.update({
'opportunity': {
'invisible': Eval('state').in_(
['won', 'lost', 'opportunity','cancelled']),
},
'converted': {
'invisible': Eval('state').in_(['lead', 'convert', 'cancelled']),
},
'won': {
'invisible': ~Eval('state').in_(['converted']),
},
'lost': {
'invisible': ~Eval('state').in_(['converted']),
},
'cancelled': {
'invisible': Eval('state').in_(['lead', 'won', 'lost', 'cancelled']),
},
})
# @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():
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()
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('contracts', None)
default.setdefault('converted_by')
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()
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
'''
sale = self._get_sale_opportunity()
sale_lines = []
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")
def create_contract(self):
contract = self._get_contract_opportunity()
# Contract = Pool().get('sale.contract')
# contract = opportunity.create_contract()
# Contract.save([contract])
contract_lines = []
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")
@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('opportunity')
def opportunity(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('converted')
def converted(cls, records):
cls.procces_opportunity(records)
@classmethod
@ModelView.button
@Workflow.transition('won')
def won(cls, records):
pass
@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)
# history = []
# for record in records:
# value = {
# 'opportunity':record.id,
# 'create_date':date.today(),
# 'action':'Cambio de estado a Cotización'
# }
# history.append(value)
@classmethod
def send_email(cls, opportunity_id, name='opportunity_email'):
pool = Pool()
config = pool.get('crm.configuration')(1)
oppo = cls(opportunity_id)
Template = pool.get('email.template')
# Activity = pool.get('email.activity')
email = oppo.contact_email
if oppo.party and oppo.party.email:
email = oppo.party.email
if not email:
print("Correo no enviado no encontre un correo valido...!")
return
template = getattr(config, name)
if not template:
return
template.subject = f'{template.subject} No. {oppo.number}'
if email:
Template.send(template, oppo, email, attach=True)
# Activity.create([{
# 'template': template.id,
# 'origin': str(oppo),
# 'status': 'sended',
# }])
else:
raise UserError(gettext('hotel.msg_missing_party_email'))
@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
# 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):
pool = Pool()
for opportunity in records:
if opportunity.type == 'contract':
Contract = pool.get('sale.contract')
contract = opportunity.create_contract()
Contract.create([contract])
else:
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')
cls.write([o for o in records], {
'end_date': Date.today(),
'state': 'lost',
})
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):
Date = Pool().get('ir.date')
cls.write([o for o in records if o.cancelled_reason], {
'end_date': Date.today(),
})
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)
def get_total_amount(self, name):
return sum(line.amount for line in self.lines)
# @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 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)
# 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 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')
# notes = fields.Text('Notes')
# done_by = fields.Many2One('res.user', 'Done By')
# state = fields.Selection([
# ('draft', "Draft"),
# ('done', "Done"),
# ], "State", required=False, readonly=True)
#
# @staticmethod
# def default_state():
# return 'draft'
class CrmOpportunityLine(sequence_ordered(), ModelSQL, ModelView):
'CRM Opportunity Line'
__name__ = "crm.opportunity.line"
_history = True
_states = {
'readonly': Eval('opportunity_state').in_([
'won',
'lost',
'cancelled']),
}
_depends = ['opportunity_state']
billing_frecuency = fields.Selection([
('one_payment', "One Payment"),
('monthly', "Monthly"),
('bimonthly', "Bimonthly"),
('biannual', "Biannual"),
('annual', "Annual")], "Billing Frecuency",
states={
'readonly': Eval('state').in_(['won', 'lost'])
})
opportunity = fields.Many2One('crm.opportunity', 'Opportunity',
ondelete='CASCADE', required=True, depends=_depends,
states={
'readonly': _states['readonly'] & Bool(Eval('opportunity')),
})
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')
taxes_amount = fields.Function(
fields.Numeric('Tax Line'), 'get_taxes_amount')
amount = fields.Function(
fields.Numeric('Amount', digits=(16, 2)), 'get_amount')
description = fields.Text('Description')
unit_price = fields.Numeric('Unit Price', digits=(16, 2))
start_invoice_date = fields.Date('Start Invoice Date')
amount_taxed = fields.Function(
fields.Numeric('Amount Taxed', digits=(16, 2)), 'get_amount_taxed')
payment_term = fields.Many2One(
'account.invoice.payment_term', 'Payment Term')
availability = fields.Char('Availability')
del _states, _depends
# @classmethod
# def __register__(cls, module_name):
# super().__register__(module_name)
# table_h = cls.__table_handler__(module_name)
# table_h.drop_column('total_line')
@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
def _get_taxes(self, product, unit_price, quantity, currency=None):
# Return tax amount for: product, unit_price, quantity
Tax = Pool().get('account.tax')
res = 0
_taxes = list(product.customer_taxes_used)
list_taxes = Tax.compute(_taxes, unit_price, quantity)
for tax in list_taxes:
taxline = TaxableMixin._compute_tax_line(**tax)
res += taxline['amount']
return res
@fields.depends('product')
def get_taxes_amount(self, name=None):
res = 0
if all(self.product, self.unit_price, self.quantity):
res = self._get_taxes(self.product, self.unit_price, self.quantity)
return res
@fields.depends('product')
def get_amount(self, name=None):
res = 0
if self.unit_price and self.quantity:
res = float(self.unit_price) * float(self.quantity)
res = self.opportunity.currency.round(Decimal(res))
return res
@fields.depends('product')
def get_amount_taxed(self, name=None):
res = 0
if self.amount:
res = float(self.amount) + self.tax
return res
@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', '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,
)
# if self.opportunity.employee:
# sale_line.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
class OpportunityReport(CompanyReport):
__name__ = 'crm.opportunity'
class OpportunityOnlyReport(CompanyReport):
__name__ = 'crm.opportunity_only'
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
class OpportunitySaleConditions(ModelSQL, ModelView):
"Opportunity Sale Contitions"
__name__ = 'crm.opportunity-sale.condition'
opportunity = fields.Many2One('crm.opportunity', 'Opportunity',
ondelete='CASCADE')
condition = fields.Many2One('sale.condition', 'Condition',
ondelete='RESTRICT')