first commit
This commit is contained in:
674
modules/sale_opportunity/opportunity.py
Normal file
674
modules/sale_opportunity/opportunity.py
Normal file
@@ -0,0 +1,674 @@
|
||||
# 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 itertools import groupby
|
||||
|
||||
from sql.functions import CharLength
|
||||
|
||||
from trytond.i18n import gettext
|
||||
from trytond.ir.attachment import AttachmentCopyMixin
|
||||
from trytond.ir.note import NoteCopyMixin
|
||||
from trytond.model import (
|
||||
ChatMixin, Index, ModelSQL, ModelView, Workflow, fields, sequence_ordered)
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.modules.company.model import employee_field, set_employee
|
||||
from trytond.modules.currency.fields import Monetary
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Bool, Eval, Get, If, In
|
||||
from trytond.tools import firstline
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class SaleOpportunity(
|
||||
Workflow, ModelSQL, ModelView,
|
||||
AttachmentCopyMixin, NoteCopyMixin, ChatMixin):
|
||||
__name__ = "sale.opportunity"
|
||||
_history = True
|
||||
_rec_name = 'number'
|
||||
|
||||
_states_start = {
|
||||
'readonly': Eval('state') != 'lead',
|
||||
}
|
||||
_states_stop = {
|
||||
'readonly': Eval('state').in_(
|
||||
['converted', 'won', 'lost', 'cancelled']),
|
||||
}
|
||||
|
||||
number = fields.Char("Number", readonly=True, required=True)
|
||||
reference = fields.Char("Reference")
|
||||
party = fields.Many2One(
|
||||
'party.party', "Party",
|
||||
states={
|
||||
'readonly': Eval('state').in_(['converted', 'lost', 'cancelled']),
|
||||
'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
|
||||
},
|
||||
context={
|
||||
'company': Eval('company', -1),
|
||||
},
|
||||
depends={'company'})
|
||||
contact = fields.Many2One(
|
||||
'party.contact_mechanism', "Contact",
|
||||
context={
|
||||
'company': Eval('company', -1),
|
||||
},
|
||||
search_context={
|
||||
'related_party': Eval('party'),
|
||||
},
|
||||
depends=['party', 'company'])
|
||||
address = fields.Many2One(
|
||||
'party.address', "Address", states=_states_stop,
|
||||
domain=[('party', '=', Eval('party', -1))],
|
||||
help="The default address for the invoice and shipment.\n"
|
||||
"Leave empty to use the default values.")
|
||||
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)),
|
||||
])
|
||||
currency = fields.Many2One(
|
||||
'currency.currency', "Currency", required=True, states=_states_start)
|
||||
amount = Monetary(
|
||||
"Amount", currency='currency', digits='currency',
|
||||
states=_states_stop,
|
||||
help='Estimated revenue amount.')
|
||||
payment_term = fields.Many2One(
|
||||
'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT',
|
||||
states={
|
||||
'readonly': In(Eval('state'),
|
||||
['converted', 'lost', 'cancelled']),
|
||||
})
|
||||
employee = fields.Many2One('company.employee', 'Employee',
|
||||
states={
|
||||
'readonly': _states_stop['readonly'],
|
||||
'required': ~Eval('state').in_(['lead', 'lost', 'cancelled']),
|
||||
},
|
||||
domain=[('company', '=', Eval('company', -1))])
|
||||
start_date = fields.Date("Start Date", required=True, states=_states_start)
|
||||
end_date = fields.Date("End Date", states=_states_stop)
|
||||
description = fields.Char('Description', states=_states_stop)
|
||||
comment = fields.Text('Comment', states=_states_stop)
|
||||
lines = fields.One2Many('sale.opportunity.line', 'opportunity', 'Lines',
|
||||
states=_states_stop)
|
||||
conversion_probability = fields.Float('Conversion Probability',
|
||||
digits=(1, 4), required=True,
|
||||
domain=[
|
||||
('conversion_probability', '>=', 0),
|
||||
('conversion_probability', '<=', 1),
|
||||
],
|
||||
states={
|
||||
'readonly': ~Eval('state').in_(
|
||||
['opportunity', 'lead', 'converted']),
|
||||
},
|
||||
help="Percentage between 0 and 100.")
|
||||
lost_reason = fields.Text('Reason for loss', states={
|
||||
'invisible': Eval('state') != 'lost',
|
||||
})
|
||||
sales = fields.One2Many('sale.sale', 'origin', 'Sales')
|
||||
|
||||
converted_by = employee_field(
|
||||
"Converted By", states=['converted', 'won', 'lost', 'cancelled'])
|
||||
state = fields.Selection([
|
||||
('lead', "Lead"),
|
||||
('opportunity', "Opportunity"),
|
||||
('converted', "Converted"),
|
||||
('won', "Won"),
|
||||
('lost', "Lost"),
|
||||
('cancelled', "Cancelled"),
|
||||
], "State", required=True, sort=False, readonly=True)
|
||||
|
||||
del _states_start
|
||||
del _states_stop
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module_name):
|
||||
pool = Pool()
|
||||
Company = pool.get('company.company')
|
||||
transaction = Transaction()
|
||||
cursor = transaction.connection.cursor()
|
||||
sql_table = cls.__table__()
|
||||
company = Company.__table__()
|
||||
|
||||
table = cls.__table_handler__(module_name)
|
||||
currency_exists = table.column_exist('currency')
|
||||
|
||||
super().__register__(module_name)
|
||||
|
||||
# Migration from 6.4: store currency
|
||||
if not currency_exists:
|
||||
value = company.select(
|
||||
company.currency,
|
||||
where=(sql_table.company == company.id))
|
||||
cursor.execute(*sql_table.update(
|
||||
[sql_table.currency],
|
||||
[value]))
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
cls.number.search_unaccented = False
|
||||
cls.reference.search_unaccented = False
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_indexes.update({
|
||||
Index(t, (t.reference, Index.Similarity())),
|
||||
Index(t, (t.party, Index.Range())),
|
||||
Index(
|
||||
t,
|
||||
(t.start_date, Index.Range(order='DESC')),
|
||||
(t.end_date, Index.Range(order='DESC'))),
|
||||
Index(
|
||||
t, (t.state, Index.Equality(cardinality='low')),
|
||||
where=t.state.in_(['lead', 'opportunity'])),
|
||||
})
|
||||
cls._order.insert(0, ('start_date', 'DESC'))
|
||||
cls._transitions |= set((
|
||||
('lead', 'opportunity'),
|
||||
('lead', 'lost'),
|
||||
('lead', 'cancelled'),
|
||||
('lead', 'converted'),
|
||||
('opportunity', 'converted'),
|
||||
('opportunity', 'lead'),
|
||||
('opportunity', 'lost'),
|
||||
('opportunity', 'cancelled'),
|
||||
('converted', 'won'),
|
||||
('converted', 'lost'),
|
||||
('won', 'converted'),
|
||||
('lost', 'converted'),
|
||||
('lost', 'lead'),
|
||||
('cancelled', 'lead'),
|
||||
))
|
||||
cls._buttons.update({
|
||||
'lead': {
|
||||
'invisible': ~Eval('state').in_(
|
||||
['cancelled', 'lost', 'opportunity']),
|
||||
'icon': If(Eval('state').in_(['cancelled', 'lost']),
|
||||
'tryton-undo', 'tryton-back'),
|
||||
'depends': ['state'],
|
||||
},
|
||||
'opportunity': {
|
||||
'pre_validate': [
|
||||
If(~Eval('party'),
|
||||
('party', '!=', None),
|
||||
()),
|
||||
If(~Eval('employee'),
|
||||
('employee', '!=', None),
|
||||
()),
|
||||
],
|
||||
'invisible': ~Eval('state').in_(['lead']),
|
||||
'depends': ['state'],
|
||||
},
|
||||
'convert': {
|
||||
'invisible': ~Eval('state').in_(['opportunity']),
|
||||
'depends': ['state'],
|
||||
},
|
||||
'lost': {
|
||||
'invisible': ~Eval('state').in_(['lead', 'opportunity']),
|
||||
'depends': ['state'],
|
||||
},
|
||||
'cancel': {
|
||||
'invisible': ~Eval('state').in_(['lead', 'opportunity']),
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def order_number(cls, tables):
|
||||
table, _ = tables[None]
|
||||
return [CharLength(table.number), table.number]
|
||||
|
||||
@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')
|
||||
|
||||
@fields.depends('amount', 'company')
|
||||
def on_change_company(self):
|
||||
self.payment_term = self.default_payment_term(
|
||||
company=self.company.id if self.company else None)
|
||||
if not self.amount:
|
||||
self.currency = self.default_currency(
|
||||
company=self.company.id if self.company else None)
|
||||
|
||||
@classmethod
|
||||
def default_currency(cls, **pattern):
|
||||
pool = Pool()
|
||||
Company = pool.get('company.company')
|
||||
company = pattern.get('company')
|
||||
if not company:
|
||||
company = cls.default_company()
|
||||
if company is not None and company >= 0:
|
||||
return Company(company).currency.id
|
||||
|
||||
@staticmethod
|
||||
def default_employee():
|
||||
return Transaction().context.get('employee')
|
||||
|
||||
@classmethod
|
||||
def default_payment_term(cls, **pattern):
|
||||
pool = Pool()
|
||||
Configuration = pool.get('account.configuration')
|
||||
config = Configuration(1)
|
||||
payment_term = config.get_multivalue(
|
||||
'default_customer_payment_term', **pattern)
|
||||
return payment_term.id if payment_term else None
|
||||
|
||||
def get_rec_name(self, name):
|
||||
items = [self.number]
|
||||
if self.reference:
|
||||
items.append(f'[{self.reference}]')
|
||||
return ' '.join(items)
|
||||
|
||||
@classmethod
|
||||
def search_rec_name(cls, name, clause):
|
||||
_, operator, value = clause
|
||||
if operator.startswith('!') or operator.startswith('not '):
|
||||
bool_op = 'AND'
|
||||
else:
|
||||
bool_op = 'OR'
|
||||
domain = [bool_op,
|
||||
('number', operator, value),
|
||||
('reference', operator, value),
|
||||
]
|
||||
return domain
|
||||
|
||||
@classmethod
|
||||
def view_attributes(cls):
|
||||
return super().view_attributes() + [
|
||||
('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
|
||||
]
|
||||
|
||||
def chat_language(self, audience='internal'):
|
||||
language = super().chat_language(audience=audience)
|
||||
if audience == 'public':
|
||||
language = self.party.lang.code if self.party.lang else None
|
||||
return language
|
||||
|
||||
@classmethod
|
||||
def get_resources_to_copy(cls, name):
|
||||
return {
|
||||
'sale.sale',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def preprocess_values(cls, mode, values):
|
||||
pool = Pool()
|
||||
Configuration = pool.get('sale.configuration')
|
||||
values = super().preprocess_values(mode, values)
|
||||
if mode == 'create' and not values.get('number'):
|
||||
company_id = values.get('company', cls.default_company())
|
||||
if company_id is not None:
|
||||
configuration = Configuration(1)
|
||||
if sequence := configuration.get_multivalue(
|
||||
'sale_opportunity_sequence', company=company_id):
|
||||
values['number'] = sequence.get()
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def check_modification(
|
||||
cls, mode, opportunities, values=None, external=False):
|
||||
super().check_modification(
|
||||
mode, opportunities, values=values, external=external)
|
||||
if mode == 'delete':
|
||||
for opportunity in opportunities:
|
||||
if opportunity.state not in {'cancelled', 'draft'}:
|
||||
raise AccessError(gettext(
|
||||
'sale_opportunity.msg_opportunity_delete_cancel',
|
||||
opportunity=opportunity.rec_name))
|
||||
|
||||
@classmethod
|
||||
def copy(cls, opportunities, default=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
else:
|
||||
default = default.copy()
|
||||
default.setdefault('number', None)
|
||||
default.setdefault('reference')
|
||||
default.setdefault('sales', None)
|
||||
default.setdefault('converted_by')
|
||||
return super().copy(opportunities, default=default)
|
||||
|
||||
@fields.depends('party', 'amount', 'company')
|
||||
def on_change_party(self):
|
||||
self.payment_term = self.default_payment_term(
|
||||
company=self.company.id if self.company else None)
|
||||
if self.party:
|
||||
if self.party.customer_payment_term:
|
||||
self.payment_term = self.party.customer_payment_term
|
||||
if not self.amount:
|
||||
if self.party.customer_currency:
|
||||
self.currency = self.party.customer_currency
|
||||
|
||||
def _get_sale_opportunity(self):
|
||||
'''
|
||||
Return sale for an opportunity
|
||||
'''
|
||||
pool = Pool()
|
||||
Sale = pool.get('sale.sale')
|
||||
sale = Sale(
|
||||
description=self.description,
|
||||
party=self.party,
|
||||
contact=self.contact,
|
||||
company=self.company,
|
||||
comment=self.comment,
|
||||
sale_date=None,
|
||||
origin=self,
|
||||
warehouse=Sale.default_warehouse(),
|
||||
)
|
||||
sale.on_change_party()
|
||||
if self.address:
|
||||
sale.invoice_address = sale.shipment_address = self.address
|
||||
if self.payment_term:
|
||||
sale.payment_term = self.payment_term
|
||||
sale.currency = self.currency
|
||||
return sale
|
||||
|
||||
def create_sale(self):
|
||||
'''
|
||||
Create a sale for the opportunity and return the sale
|
||||
'''
|
||||
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
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('lead')
|
||||
def lead(cls, opportunities):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('opportunity')
|
||||
def opportunity(cls, opportunities):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button_action('sale.act_sale_form')
|
||||
@Workflow.transition('converted')
|
||||
@set_employee('converted_by')
|
||||
def convert(cls, opportunities):
|
||||
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)
|
||||
return {
|
||||
'res_id': [s.id for s in sales],
|
||||
}
|
||||
|
||||
@property
|
||||
def is_forecast(self):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
with Transaction().set_context(company=self.company.id):
|
||||
today = Date.today()
|
||||
return self.end_date or datetime.date.max > today
|
||||
|
||||
@classmethod
|
||||
@Workflow.transition('won')
|
||||
def won(cls, opportunities):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
for company, c_opportunities in groupby(
|
||||
opportunities, key=lambda o: o.company):
|
||||
with Transaction().set_context(company=company.id):
|
||||
today = Date.today()
|
||||
cls.write([o for o in c_opportunities if o.is_forecast], {
|
||||
'end_date': today,
|
||||
'state': 'won',
|
||||
})
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('lost')
|
||||
def lost(cls, opportunities):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
for company, c_opportunities in groupby(
|
||||
opportunities, key=lambda o: o.company):
|
||||
with Transaction().set_context(company=company.id):
|
||||
today = Date.today()
|
||||
cls.write([o for o in c_opportunities if o.is_forecast], {
|
||||
'end_date': today,
|
||||
'state': 'lost',
|
||||
})
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('cancelled')
|
||||
def cancel(cls, opportunities):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
for company, c_opportunities in groupby(
|
||||
opportunities, key=lambda o: o.company):
|
||||
with Transaction().set_context(company=company.id):
|
||||
today = Date.today()
|
||||
cls.write([o for o in c_opportunities if o.is_forecast], {
|
||||
'end_date': today,
|
||||
'state': 'cancelled',
|
||||
})
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
class SaleOpportunityLine(sequence_ordered(), ModelSQL, ModelView):
|
||||
__name__ = "sale.opportunity.line"
|
||||
_history = True
|
||||
_states = {
|
||||
'readonly': Eval('opportunity_state').in_(
|
||||
['converted', 'won', 'lost', 'cancelled']),
|
||||
}
|
||||
|
||||
opportunity = fields.Many2One(
|
||||
'sale.opportunity', "Opportunity", ondelete='CASCADE', required=True,
|
||||
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",
|
||||
domain=[
|
||||
If(Eval('opportunity_state').in_(['lead', 'opportunity'])
|
||||
& ~(Eval('quantity', 0) < 0),
|
||||
('salable', '=', True),
|
||||
()),
|
||||
],
|
||||
states=_states,
|
||||
context={
|
||||
'company': Eval('company', None),
|
||||
},
|
||||
depends=['company'])
|
||||
product_uom_category = fields.Function(
|
||||
fields.Many2One(
|
||||
'product.uom.category', "Product UoM Category"),
|
||||
'on_change_with_product_uom_category')
|
||||
quantity = fields.Float(
|
||||
"Quantity", digits='unit', required=True, states=_states)
|
||||
unit = fields.Many2One(
|
||||
'product.uom', "Unit",
|
||||
domain=[
|
||||
If(Eval('product_uom_category'),
|
||||
('category', '=', Eval('product_uom_category', -1)),
|
||||
('category', '=', -1)),
|
||||
],
|
||||
states={
|
||||
'required': Bool(Eval('product')),
|
||||
'readonly': _states['readonly'],
|
||||
})
|
||||
description = fields.Text("Description", states=_states)
|
||||
summary = fields.Function(
|
||||
fields.Char("Summary"), 'on_change_with_summary',
|
||||
searcher='search_summary')
|
||||
note = fields.Text("Note")
|
||||
|
||||
company = fields.Function(
|
||||
fields.Many2One('company.company', "Company"),
|
||||
'on_change_with_company')
|
||||
|
||||
del _states
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.__access__.add('opportunity')
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module):
|
||||
table_h = cls.__table_handler__(module)
|
||||
super().__register__(module)
|
||||
# Migration from 7.0: remove required on product and unit
|
||||
table_h.not_null_action('product', 'remove')
|
||||
table_h.not_null_action('unit', 'remove')
|
||||
|
||||
@classmethod
|
||||
def get_opportunity_states(cls):
|
||||
pool = Pool()
|
||||
Opportunity = pool.get('sale.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', '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
|
||||
|
||||
@fields.depends('product')
|
||||
def on_change_with_product_uom_category(self, name=None):
|
||||
return self.product.default_uom_category if self.product else None
|
||||
|
||||
@fields.depends('description')
|
||||
def on_change_with_summary(self, name=None):
|
||||
return firstline(self.description or '')
|
||||
|
||||
@classmethod
|
||||
def search_summary(cls, name, clause):
|
||||
return [('description', *clause[1:])]
|
||||
|
||||
@fields.depends('opportunity', '_parent_opportunity.company')
|
||||
def on_change_with_company(self, name=None):
|
||||
return self.opportunity.company if self.opportunity else None
|
||||
|
||||
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=self.description,
|
||||
)
|
||||
sale_line.on_change_product()
|
||||
self._set_sale_line_quantity(sale_line)
|
||||
sale_line.on_change_quantity()
|
||||
return sale_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()
|
||||
if self.product:
|
||||
return (lang.format_number_symbol(
|
||||
self.quantity or 0, self.unit, digits=self.unit.digits)
|
||||
+ ' %s @ %s' % (
|
||||
self.product.rec_name, self.opportunity.rec_name))
|
||||
else:
|
||||
return self.opportunity.rec_name
|
||||
|
||||
@classmethod
|
||||
def search_rec_name(cls, name, clause):
|
||||
return [('product.rec_name',) + tuple(clause[1:])]
|
||||
Reference in New Issue
Block a user