Files
tryton/opportunity.py
T

793 lines
28 KiB
Python
Raw Normal View History

2014-12-02 12:55:56 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2010-07-05 23:16:33 +02:00
"Sales extension for managing leads and opportunities"
import datetime
2015-11-10 13:07:11 +01:00
from sql import Literal, Null
from sql.aggregate import Max, Count, Sum
2017-01-18 09:57:00 +01:00
from sql.conditionals import Case, Coalesce
2013-09-06 22:34:41 +02:00
from sql.functions import Extract
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
2016-09-13 14:09:46 +02:00
from trytond.model import ModelView, ModelSQL, Workflow, fields, \
sequence_ordered
2018-12-29 14:20:29 +01:00
from trytond.model.exceptions import AccessError
2016-06-30 18:53:13 +02:00
from trytond.pyson import Eval, In, If, Get, Bool
2010-09-07 20:43:03 +02:00
from trytond.transaction import Transaction
2011-07-03 13:19:11 +02:00
from trytond.pool import Pool
2010-07-05 23:16:33 +02:00
from trytond.ir.attachment import AttachmentCopyMixin
from trytond.ir.note import NoteCopyMixin
2020-04-10 18:53:44 +02:00
from trytond.modules.company.model import employee_field, set_employee
2010-09-07 20:43:03 +02:00
class SaleOpportunity(
Workflow, ModelSQL, ModelView,
AttachmentCopyMixin, NoteCopyMixin):
2010-07-05 23:16:33 +02:00
'Sale Opportunity'
2012-09-24 22:18:14 +02:00
__name__ = "sale.opportunity"
2010-07-05 23:16:33 +02:00
_history = True
_rec_name = 'number'
_states_start = {
'readonly': Eval('state') != 'lead',
}
_depends_start = ['state']
_states_stop = {
'readonly': Eval('state').in_(
['converted', 'won', 'lost', 'cancelled']),
}
_depends_stop = ['state']
number = fields.Char('Number', readonly=True, required=True, select=True)
reference = fields.Char('Reference', select=True)
party = fields.Many2One(
'party.party', "Party", select=True,
2014-01-26 14:29:33 +01:00
states={
2013-10-05 12:17:22 +02:00
'readonly': Eval('state').in_(['converted', 'lost', 'cancelled']),
'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
},
context={
'company': Eval('company', -1),
},
depends=['state', 'company'])
2020-03-28 14:25:57 +01:00
contact = fields.Many2One(
'party.contact_mechanism', "Contact",
context={
'company': Eval('company', -1),
},
2020-03-28 14:25:57 +01:00
search_context={
'related_party': Eval('party'),
},
depends=['party', 'company'])
2010-07-05 23:16:33 +02:00
address = fields.Many2One('party.address', 'Address',
2011-08-04 16:57:40 +02:00
domain=[('party', '=', Eval('party'))],
2012-03-05 10:53:07 +01:00
select=True, depends=['party', 'state'],
states=_states_stop)
2010-07-05 23:16:33 +02:00
company = fields.Many2One('company.company', 'Company', required=True,
select=True, states=_states_stop, domain=[
2011-08-04 16:57:40 +02:00
('id', If(In('company', Eval('context', {})), '=', '!='),
Get(Eval('context', {}), 'company', 0)),
], depends=_depends_stop)
2010-07-05 23:16:33 +02:00
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=_states_stop, depends=_depends_stop + ['currency_digits'],
2019-09-30 09:39:42 +02:00
help='Estimated revenue amount.')
2010-07-05 23:16:33 +02:00
payment_term = fields.Many2One('account.invoice.payment_term',
2011-08-04 16:57:40 +02:00
'Payment Term', states={
'readonly': In(Eval('state'),
['converted', 'lost', 'cancelled']),
},
depends=['state'])
2017-01-18 09:57:00 +01:00
employee = fields.Many2One('company.employee', 'Employee',
states={
'readonly': _states_stop['readonly'],
2017-01-18 09:57:00 +01:00
'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
},
depends=['state', 'company'],
domain=[('company', '=', Eval('company'))])
2012-03-05 10:53:07 +01:00
start_date = fields.Date('Start Date', required=True, select=True,
states=_states_start, depends=_depends_start)
2015-03-10 23:53:36 +01:00
end_date = fields.Date('End Date', select=True,
states=_states_stop, depends=_depends_stop)
2019-03-22 11:12:06 +01:00
description = fields.Char('Description',
states=_states_stop, depends=_depends_stop)
comment = fields.Text('Comment', states=_states_stop,
depends=_depends_stop)
2010-07-05 23:16:33 +02:00
lines = fields.One2Many('sale.opportunity.line', 'opportunity', 'Lines',
states=_states_stop, depends=_depends_stop)
conversion_probability = fields.Float('Conversion Probability',
digits=(1, 4), required=True,
domain=[
('conversion_probability', '>=', 0),
('conversion_probability', '<=', 1),
],
2015-03-02 23:13:04 +01:00
states={
'readonly': ~Eval('state').in_(
['opportunity', 'lead', 'converted']),
},
2019-09-30 09:39:42 +02:00
depends=['state'], help="Percentage between 0 and 100.")
2010-07-05 23:16:33 +02:00
lost_reason = fields.Text('Reason for loss', states={
2011-08-21 11:20:52 +02:00
'invisible': Eval('state') != 'lost',
}, depends=['state'])
2015-03-02 23:13:04 +01:00
sales = fields.One2Many('sale.sale', 'origin', 'Sales')
2010-07-05 23:16:33 +02:00
2020-04-10 18:53:44 +02:00
converted_by = employee_field(
"Converted By", states=['converted', 'won', 'lost', 'cancelled'])
state = fields.Selection([
('lead', "Lead"),
('opportunity', "Opportunity"),
('converted', "Converted"),
('won', "Won"),
('cancelled', "Cancelled"),
('lost', "Lost"),
], "State", required=True, select=True,
sort=False, readonly=True)
del _states_start, _depends_start
del _states_stop, _depends_stop
2013-07-16 16:44:41 +02:00
@classmethod
def __register__(cls, module_name):
2015-03-02 23:13:04 +01:00
pool = Pool()
Sale = pool.get('sale.sale')
transaction = Transaction()
cursor = transaction.connection.cursor()
update = transaction.connection.cursor()
2013-09-06 22:34:41 +02:00
sql_table = cls.__table__()
2015-03-02 23:13:04 +01:00
sale = Sale.__table__()
2013-09-06 22:34:41 +02:00
2018-07-24 22:01:08 +02:00
table = cls.__table_handler__(module_name)
number_exists = table.column_exist('number')
# Migration from 3.8: rename reference into number
if table.column_exist('reference') and not number_exists:
table.column_rename('reference', 'number')
number_exists = True
2013-07-16 16:44:41 +02:00
super(SaleOpportunity, cls).__register__(module_name)
2018-07-24 22:01:08 +02:00
table = cls.__table_handler__(module_name)
2013-07-16 16:44:41 +02:00
2015-03-02 23:13:04 +01:00
# Migration from 3.4: replace sale by origin
if table.column_exist('sale'):
cursor.execute(*sql_table.select(
sql_table.id, sql_table.sale,
where=sql_table.sale != Null))
for id_, sale_id in cursor:
update.execute(*sale.update(
2015-03-02 23:13:04 +01:00
columns=[sale.origin],
2015-07-09 15:33:59 +02:00
values=['%s,%s' % (cls.__name__, id_)],
2015-03-02 23:13:04 +01:00
where=sale.id == sale_id))
2018-03-15 00:46:40 +01:00
table.drop_column('sale')
2015-03-02 23:13:04 +01:00
# Migration from 4.0: change probability into conversion probability
if table.column_exist('probability'):
cursor.execute(*sql_table.update(
[sql_table.conversion_probability],
[sql_table.probability / 100.0]))
table.drop_constraint('check_percentage')
table.drop_column('probability')
2017-01-18 09:57:00 +01:00
# Migration from 4.2: make employee not required
table.not_null_action('employee', action='remove')
2019-03-22 11:12:06 +01:00
# Migration from 5.0: drop required on description
table.not_null_action('description', action='remove')
2012-09-24 22:18:14 +02:00
@classmethod
def __setup__(cls):
super(SaleOpportunity, cls).__setup__()
cls._order.insert(0, ('start_date', 'DESC'))
cls._transitions |= set((
2012-03-25 00:18:14 +01:00
('lead', 'opportunity'),
('lead', 'lost'),
('lead', 'cancelled'),
2015-03-10 23:57:35 +01:00
('lead', 'converted'),
2012-03-25 00:18:14 +01:00
('opportunity', 'converted'),
('opportunity', 'lead'),
('opportunity', 'lost'),
('opportunity', 'cancelled'),
2015-03-02 23:13:04 +01:00
('converted', 'won'),
('converted', 'lost'),
('won', 'converted'),
('lost', 'converted'),
2012-03-25 00:18:14 +01:00
('lost', 'lead'),
('cancelled', 'lead'),
))
2012-09-24 22:18:14 +02:00
cls._buttons.update({
2012-03-25 00:18:14 +01:00
'lead': {
'invisible': ~Eval('state').in_(
['cancelled', 'lost', 'opportunity']),
'icon': If(Eval('state').in_(['cancelled', 'lost']),
2018-08-19 20:02:58 +02:00
'tryton-undo', 'tryton-back'),
2018-02-19 11:40:49 +01:00
'depends': ['state'],
2012-03-25 00:18:14 +01:00
},
'opportunity': {
'invisible': ~Eval('state').in_(['lead']),
2018-02-19 11:40:49 +01:00
'depends': ['state'],
2012-03-25 00:18:14 +01:00
},
'convert': {
'invisible': ~Eval('state').in_(['opportunity']),
2018-02-19 11:40:49 +01:00
'depends': ['state'],
2012-03-25 00:18:14 +01:00
},
'lost': {
'invisible': ~Eval('state').in_(['lead', 'opportunity']),
2018-02-19 11:40:49 +01:00
'depends': ['state'],
2012-03-25 00:18:14 +01:00
},
'cancel': {
'invisible': ~Eval('state').in_(['lead', 'opportunity']),
2018-02-19 11:40:49 +01:00
'depends': ['state'],
2012-03-25 00:18:14 +01:00
},
})
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@staticmethod
def default_state():
2010-07-05 23:16:33 +02:00
return 'lead'
2012-09-24 22:18:14 +02:00
@staticmethod
def default_start_date():
Date = Pool().get('ir.date')
return Date.today()
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@staticmethod
def default_conversion_probability():
return 0.5
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@staticmethod
def default_company():
2012-03-27 10:43:59 +02:00
return Transaction().context.get('company')
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@staticmethod
def default_employee():
2016-09-22 12:25:16 +02:00
return Transaction().context.get('employee')
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@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
2010-07-05 23:16:33 +02:00
2019-09-20 11:47:15 +02:00
@classmethod
def view_attributes(cls):
2021-04-09 10:52:03 +02:00
return super().view_attributes() + [
2019-09-20 11:47:15 +02:00
('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
]
@classmethod
def get_resources_to_copy(cls, name):
return {
'sale.sale',
}
2013-07-16 16:44:41 +02:00
@classmethod
def create(cls, vlist):
pool = Pool()
Config = pool.get('sale.configuration')
config = Config(1)
2013-07-16 16:44:41 +02:00
vlist = [x.copy() for x in vlist]
2021-04-03 14:27:10 +02:00
default_company = cls.default_company()
2013-07-16 16:44:41 +02:00
for vals in vlist:
if vals.get('number') is None:
2021-04-03 14:27:10 +02:00
vals['number'] = config.get_multivalue(
'sale_opportunity_sequence',
company=vals.get('company', default_company)).get()
2013-07-16 16:44:41 +02:00
return super(SaleOpportunity, cls).create(vlist)
@classmethod
def copy(cls, opportunities, default=None):
if default is None:
default = {}
2018-09-01 01:02:04 +02:00
else:
default = default.copy()
default.setdefault('number', None)
2015-03-02 23:13:04 +01:00
default.setdefault('sales', None)
2021-04-05 16:24:13 +02:00
default.setdefault('converted_by')
2013-07-16 16:44:41 +02:00
return super(SaleOpportunity, cls).copy(opportunities, default=default)
2012-09-24 22:18:14 +02:00
def get_currency(self, name):
return self.company.currency.id
def get_currency_digits(self, name):
return self.company.currency.digits
2014-01-26 14:29:33 +01:00
@fields.depends('company')
2012-09-24 22:18:14 +02:00
def on_change_company(self):
if self.company:
2014-10-29 20:40:17 +01:00
self.currency = self.company.currency
self.currency_digits = self.company.currency.digits
2010-07-05 23:16:33 +02:00
2014-01-26 14:29:33 +01:00
@fields.depends('party')
2012-09-24 22:18:14 +02:00
def on_change_party(self):
2014-10-29 20:40:17 +01:00
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()
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
def _get_sale_opportunity(self):
2010-07-05 23:16:33 +02:00
'''
2012-09-24 22:18:14 +02:00
Return sale for an opportunity
2010-07-05 23:16:33 +02:00
'''
2012-09-24 22:18:14 +02:00
Sale = Pool().get('sale.sale')
2014-09-17 21:45:52 +02:00
return Sale(
description=self.description,
party=self.party,
2020-03-28 14:25:57 +01:00
contact=self.contact,
2014-09-17 21:45:52 +02:00
payment_term=self.payment_term,
company=self.company,
invoice_address=self.address,
shipment_address=self.address,
currency=self.company.currency,
comment=self.comment,
sale_date=None,
2015-03-02 23:13:04 +01:00
origin=self,
warehouse=Sale.default_warehouse(),
2014-09-17 21:45:52 +02:00
)
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
def create_sale(self):
2010-07-05 23:16:33 +02:00
'''
2012-09-24 22:18:14 +02:00
Create a sale for the opportunity and return the sale
2010-07-05 23:16:33 +02:00
'''
2012-09-24 22:18:14 +02:00
sale = self._get_sale_opportunity()
2015-03-02 23:13:04 +01:00
sale_lines = []
for line in self.lines:
sale_lines.append(line.get_sale_line(sale))
sale.lines = sale_lines
2012-09-24 22:18:14 +02:00
return sale
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@classmethod
def delete(cls, opportunities):
# Cancel before delete
2012-09-24 22:18:14 +02:00
cls.cancel(opportunities)
for opportunity in opportunities:
2013-07-16 11:17:30 +02:00
if opportunity.state != 'cancelled':
2018-12-29 14:20:29 +01:00
raise AccessError(
gettext('sale_opportunity.msg_opportunity_delete_cancel',
opportunity=opportunity.rec_name))
2012-09-24 22:18:14 +02:00
super(SaleOpportunity, cls).delete(opportunities)
2012-09-24 22:18:14 +02:00
@classmethod
2012-03-25 00:18:14 +01:00
@ModelView.button
@Workflow.transition('lead')
2012-09-24 22:18:14 +02:00
def lead(cls, opportunities):
2012-03-25 00:18:14 +01:00
pass
2012-09-24 22:18:14 +02:00
@classmethod
2012-03-25 00:18:14 +01:00
@ModelView.button
@Workflow.transition('opportunity')
2012-09-24 22:18:14 +02:00
def opportunity(cls, opportunities):
2012-03-25 00:18:14 +01:00
pass
2012-09-24 22:18:14 +02:00
@classmethod
2012-03-25 00:18:14 +01:00
@ModelView.button
@Workflow.transition('converted')
2020-04-10 18:53:44 +02:00
@set_employee('converted_by')
2012-09-24 22:18:14 +02:00
def convert(cls, opportunities):
2015-03-02 23:13:04 +01:00
pool = Pool()
Sale = pool.get('sale.sale')
sales = [o.create_sale() for o in opportunities if not o.sales]
Sale.save(sales)
for sale in sales:
sale.origin.copy_resources_to(sale)
2015-03-02 23:13:04 +01:00
2015-03-10 23:53:36 +01:00
@property
def is_forecast(self):
pool = Pool()
Date = pool.get('ir.date')
today = Date.today()
return self.end_date or datetime.date.max > today
2015-03-02 23:13:04 +01:00
@classmethod
@Workflow.transition('won')
def won(cls, opportunities):
pool = Pool()
Date = pool.get('ir.date')
2018-06-26 19:11:53 +02:00
cls.write([o for o in opportunities if o.is_forecast], {
2012-09-24 22:18:14 +02:00
'end_date': Date.today(),
2015-06-28 22:59:02 +02:00
'state': 'won',
})
2012-09-24 22:18:14 +02:00
@classmethod
2012-03-25 00:18:14 +01:00
@ModelView.button
@Workflow.transition('lost')
2012-09-24 22:18:14 +02:00
def lost(cls, opportunities):
Date = Pool().get('ir.date')
2018-06-26 19:11:53 +02:00
cls.write([o for o in opportunities if o.is_forecast], {
2012-09-24 22:18:14 +02:00
'end_date': Date.today(),
2015-06-28 22:59:02 +02:00
'state': 'lost',
})
2012-09-24 22:18:14 +02:00
@classmethod
2012-03-25 00:18:14 +01:00
@ModelView.button
@Workflow.transition('cancelled')
2012-09-24 22:18:14 +02:00
def cancel(cls, opportunities):
Date = Pool().get('ir.date')
2018-06-26 19:11:53 +02:00
cls.write([o for o in opportunities if o.is_forecast], {
2012-09-24 22:18:14 +02:00
'end_date': Date.today(),
})
2015-03-02 23:13:04 +01:00
@staticmethod
def _sale_won_states():
return ['confirmed', 'processing', 'done']
@staticmethod
def _sale_lost_states():
2020-06-06 10:04:28 +01:00
return ['cancelled']
2015-03-02 23:13:04 +01:00
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:
2015-06-28 22:59:02 +02:00
sale_amount = opportunity.sale_amount
if opportunity.amount != sale_amount:
opportunity.amount = sale_amount
2015-03-02 23:13:04 +01:00
if opportunity.is_won():
won.append(opportunity)
elif opportunity.is_lost():
lost.append(opportunity)
2015-03-10 23:57:35 +01:00
elif (opportunity.state != 'converted'
2015-03-02 23:13:04 +01:00
and opportunity.sales):
converted.append(opportunity)
cls.save(opportunities)
if won:
cls.won(won)
if lost:
cls.lost(lost)
if converted:
cls.convert(converted)
2010-07-05 23:16:33 +02:00
2016-09-13 14:09:46 +02:00
class SaleOpportunityLine(sequence_ordered(), ModelSQL, ModelView):
2010-07-05 23:16:33 +02:00
'Sale Opportunity Line'
2012-09-24 22:18:14 +02:00
__name__ = "sale.opportunity.line"
2010-07-05 23:16:33 +02:00
_history = True
2016-06-30 18:53:13 +02:00
_states = {
'readonly': Eval('opportunity_state').in_(
['converted', 'won', 'lost', 'cancelled']),
}
_depends = ['opportunity_state']
opportunity = fields.Many2One('sale.opportunity', 'Opportunity',
ondelete='CASCADE', select=True, required=True,
2016-06-30 18:53:13 +02:00
states={
'readonly': _states['readonly'] & Bool(Eval('opportunity')),
},
depends=_depends)
opportunity_state = fields.Function(
fields.Selection('get_opportunity_states', "Opportunity State"),
2016-06-30 18:53:13 +02:00
'on_change_with_opportunity_state')
2010-07-05 23:16:33 +02:00
product = fields.Many2One('product.product', 'Product', required=True,
2016-06-30 18:53:13 +02:00
domain=[('salable', '=', True)], states=_states, depends=_depends)
2010-07-05 23:16:33 +02:00
quantity = fields.Float('Quantity', required=True,
2016-06-30 18:53:13 +02:00
digits=(16, Eval('unit_digits', 2)),
states=_states, depends=['unit_digits'] + _depends)
unit = fields.Many2One('product.uom', 'Unit', required=True,
states=_states, depends=_depends)
2014-01-26 14:29:33 +01:00
unit_digits = fields.Function(fields.Integer('Unit Digits'),
'on_change_with_unit_digits')
2010-07-05 23:16:33 +02:00
2016-06-30 18:53:13 +02:00
del _states, _depends
2021-03-07 11:13:53 +01:00
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('opportunity')
@classmethod
def get_opportunity_states(cls):
pool = Pool()
Opportunity = pool.get('sale.opportunity')
return Opportunity.fields_get(['state'])['state']['selection']
2016-06-30 18:53:13 +02:00
@fields.depends('opportunity', '_parent_opportunity.state')
def on_change_with_opportunity_state(self, name=None):
if self.opportunity:
return self.opportunity.state
2014-01-26 14:29:33 +01:00
@fields.depends('unit')
2012-09-24 22:18:14 +02:00
def on_change_with_unit_digits(self, name=None):
if self.unit:
return self.unit.digits
2010-07-05 23:16:33 +02:00
return 2
2014-01-26 14:29:33 +01:00
@fields.depends('product', 'unit')
2012-09-24 22:18:14 +02:00
def on_change_product(self):
if not self.product:
2014-10-29 20:40:17 +01:00
return
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
category = self.product.sale_uom.category
if not self.unit or self.unit.category != category:
2014-10-29 20:40:17 +01:00
self.unit = self.product.sale_uom
self.unit_digits = self.product.sale_uom.digits
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
def get_sale_line(self, sale):
2010-07-05 23:16:33 +02:00
'''
2012-09-24 22:18:14 +02:00
Return sale line for opportunity line
2010-07-05 23:16:33 +02:00
'''
2012-09-24 22:18:14 +02:00
SaleLine = Pool().get('sale.line')
2014-09-17 21:45:52 +02:00
sale_line = SaleLine(
type='line',
product=self.product,
sale=sale,
description=None,
)
2015-03-02 23:13:04 +01:00
sale_line.on_change_product()
self._set_sale_line_quantity(sale_line)
sale_line.on_change_quantity()
2012-09-24 22:18:14 +02:00
return sale_line
2010-07-05 23:16:33 +02:00
def _set_sale_line_quantity(self, sale_line):
sale_line.quantity = self.quantity
sale_line.unit = self.unit
2017-07-26 23:43:26 +02:00
def get_rec_name(self, name):
2019-11-26 22:20:24 +01:00
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))
2017-07-26 23:43:26 +02:00
@classmethod
def search_rec_name(cls, name, clause):
2017-09-27 18:37:32 +02:00
return [('product.rec_name',) + tuple(clause[1:])]
2017-07-26 23:43:26 +02:00
2010-07-05 23:16:33 +02:00
2015-03-02 23:13:04 +01:00
class SaleOpportunityReportMixin:
2019-09-07 23:45:08 +02:00
__slots__ = ()
2010-07-05 23:16:33 +02:00
number = fields.Integer('Number')
converted = fields.Integer('Converted')
conversion_rate = fields.Function(fields.Float('Conversion Rate',
2017-01-26 09:34:35 +01:00
digits=(1, 4)), 'get_conversion_rate')
2015-03-02 23:13:04 +01:00
won = fields.Integer('Won')
2017-01-26 09:34:35 +01:00
winning_rate = fields.Function(fields.Float('Winning Rate', digits=(1, 4)),
2015-03-02 23:13:04 +01:00
'get_winning_rate')
2010-07-05 23:16:33 +02:00
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(
2017-01-26 09:34:35 +01:00
'Conversion Amount Rate', digits=(1, 4)), 'get_conversion_amount_rate')
2015-03-02 23:13:04 +01:00
won_amount = fields.Numeric('Won Amount',
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'])
winning_amount_rate = fields.Function(fields.Float(
2017-01-26 09:34:35 +01:00
'Winning Amount Rate', digits=(1, 4)), 'get_winning_amount_rate')
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@staticmethod
def _converted_state():
2015-03-02 23:13:04 +01:00
return ['converted', 'won']
@staticmethod
def _won_state():
return ['won']
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@staticmethod
def _lost_state():
2010-07-05 23:16:33 +02:00
return ['lost']
2012-09-24 22:18:14 +02:00
def get_conversion_rate(self, name):
if self.number:
2017-01-26 09:34:35 +01:00
digits = getattr(self.__class__, name).digits[1]
return round(float(self.converted) / self.number, digits)
2012-09-24 22:18:14 +02:00
else:
return 0.0
2010-07-05 23:16:33 +02:00
2015-03-02 23:13:04 +01:00
def get_winning_rate(self, name):
if self.number:
2017-01-26 09:34:35 +01:00
digits = getattr(self.__class__, name).digits[1]
return round(float(self.won) / self.number, digits)
2015-03-02 23:13:04 +01:00
else:
return 0.0
2012-09-24 22:18:14 +02:00
def get_currency(self, name):
return self.company.currency.id
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
def get_currency_digits(self, name):
return self.company.currency.digits
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
def get_conversion_amount_rate(self, name):
if self.amount:
2017-01-26 09:34:35 +01:00
digits = getattr(self.__class__, name).digits[1]
return round(
float(self.converted_amount) / float(self.amount), digits)
2012-09-24 22:18:14 +02:00
else:
return 0.0
2010-07-05 23:16:33 +02:00
2015-03-02 23:13:04 +01:00
def get_winning_amount_rate(self, name):
if self.amount:
2017-01-26 09:34:35 +01:00
digits = getattr(self.__class__, name).digits[1]
return round(float(self.won_amount) / float(self.amount), digits)
2015-03-02 23:13:04 +01:00
else:
return 0.0
@classmethod
def table_query(cls):
Opportunity = Pool().get('sale.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'),
2020-03-01 12:33:53 +01:00
Sum(Case(
(opportunity.state.in_(cls._converted_state()),
2015-03-02 23:13:04 +01:00
Literal(1)), else_=Literal(0))).as_('converted'),
2020-03-01 12:33:53 +01:00
Sum(Case(
(opportunity.state.in_(cls._won_state()),
2015-03-02 23:13:04 +01:00
Literal(1)), else_=Literal(0))).as_('won'),
2020-03-01 12:33:53 +01:00
Sum(Case(
(opportunity.state.in_(cls._lost_state()),
2015-03-02 23:13:04 +01:00
Literal(1)), else_=Literal(0))).as_('lost'),
Sum(opportunity.amount).as_('amount'),
2020-03-01 12:33:53 +01:00
Sum(Case(
(opportunity.state.in_(cls._converted_state()),
2015-03-02 23:13:04 +01:00
opportunity.amount),
else_=Literal(0))).as_('converted_amount'),
2020-03-01 12:33:53 +01:00
Sum(Case(
(opportunity.state.in_(cls._won_state()),
2015-03-02 23:13:04 +01:00
opportunity.amount),
else_=Literal(0))).as_('won_amount'))
class SaleOpportunityEmployee(SaleOpportunityReportMixin, ModelSQL, ModelView):
'Sale Opportunity per Employee'
__name__ = 'sale.opportunity_employee'
employee = fields.Many2One('company.employee', 'Employee')
@classmethod
def table_query(cls):
query = super(SaleOpportunityEmployee, cls).table_query()
opportunity, = query.from_
query.columns += (
2017-01-18 09:57:00 +01:00
Coalesce(opportunity.employee, 0).as_('id'),
2015-03-02 23:13:04 +01:00
opportunity.employee,
)
where = Literal(True)
if Transaction().context.get('start_date'):
2020-03-01 12:33:53 +01:00
where &= (opportunity.start_date
>= Transaction().context['start_date'])
2015-03-02 23:13:04 +01:00
if Transaction().context.get('end_date'):
2020-03-01 12:33:53 +01:00
where &= (opportunity.start_date
<= Transaction().context['end_date'])
2015-03-02 23:13:04 +01:00
query.where = where
query.group_by = (opportunity.employee, opportunity.company)
return query
2010-07-05 23:16:33 +02:00
class SaleOpportunityEmployeeContext(ModelView):
'Sale Opportunity per Employee Context'
__name__ = 'sale.opportunity_employee.context'
2010-07-05 23:16:33 +02:00
start_date = fields.Date('Start Date')
end_date = fields.Date('End Date')
2015-03-02 23:13:04 +01:00
class SaleOpportunityMonthly(SaleOpportunityReportMixin, ModelSQL, ModelView):
2010-07-05 23:16:33 +02:00
'Sale Opportunity per Month'
2012-09-24 22:18:14 +02:00
__name__ = 'sale.opportunity_monthly'
2010-07-05 23:16:33 +02:00
year = fields.Char('Year')
month = fields.Many2One('ir.calendar.month', "Month")
2010-07-05 23:16:33 +02:00
year_month = fields.Function(fields.Char('Year-Month'),
'get_year_month')
2012-09-24 22:18:14 +02:00
@classmethod
def __setup__(cls):
super(SaleOpportunityMonthly, cls).__setup__()
cls._order.insert(0, ('year', 'DESC'))
cls._order.insert(1, ('month.index', 'DESC'))
2010-07-05 23:16:33 +02:00
2015-03-02 23:13:04 +01:00
def get_year_month(self, name):
return '%s-%s' % (self.year, self.month.index)
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@classmethod
def table_query(cls):
pool = Pool()
Month = pool.get('ir.calendar.month')
month = Month.__table__()
2015-03-02 23:13:04 +01:00
query = super(SaleOpportunityMonthly, cls).table_query()
opportunity, = query.from_
2013-09-06 22:34:41 +02:00
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)
2015-03-02 23:13:04 +01:00
query.columns += (
2013-09-06 22:34:41 +02:00
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)
2015-03-02 23:13:04 +01:00
return query
2010-07-05 23:16:33 +02:00
2015-03-02 23:13:04 +01:00
class SaleOpportunityEmployeeMonthly(
SaleOpportunityReportMixin, ModelSQL, ModelView):
2010-07-05 23:16:33 +02:00
'Sale Opportunity per Employee per Month'
2012-09-24 22:18:14 +02:00
__name__ = 'sale.opportunity_employee_monthly'
2010-07-05 23:16:33 +02:00
year = fields.Char('Year')
month = fields.Many2One('ir.calendar.month', "Month")
2010-07-05 23:16:33 +02:00
employee = fields.Many2One('company.employee', 'Employee')
2012-09-24 22:18:14 +02:00
@classmethod
def __setup__(cls):
super(SaleOpportunityEmployeeMonthly, cls).__setup__()
cls._order.insert(0, ('year', 'DESC'))
cls._order.insert(1, ('month.index', 'DESC'))
2012-09-24 22:18:14 +02:00
cls._order.insert(2, ('employee', 'ASC'))
2010-07-05 23:16:33 +02:00
2012-09-24 22:18:14 +02:00
@classmethod
def table_query(cls):
pool = Pool()
Month = pool.get('ir.calendar.month')
month = Month.__table__()
2015-03-02 23:13:04 +01:00
query = super(SaleOpportunityEmployeeMonthly, cls).table_query()
opportunity, = query.from_
2013-09-06 22:34:41 +02:00
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)
2015-03-02 23:13:04 +01:00
query.columns += (
2013-09-06 22:34:41 +02:00
Max(Extract('MONTH', opportunity.start_date)
+ Extract('YEAR', opportunity.start_date) * 100
2017-01-18 09:57:00 +01:00
+ Coalesce(opportunity.employee, 0) * 1000000
2013-09-06 22:34:41 +02:00
).cast(type_id).as_('id'),
year_column,
month.id.as_('month'),
2013-09-06 22:34:41 +02:00
opportunity.employee,
2015-03-02 23:13:04 +01:00
)
query.group_by = (year_column, month.id,
2015-03-02 23:13:04 +01:00
opportunity.employee, opportunity.company)
return query