199 lines
6.5 KiB
Python
199 lines
6.5 KiB
Python
from trytond.model import fields
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval, If
|
|
from trytond.modules.product import price_digits
|
|
from trytond.exceptions import UserError
|
|
from trytond.i18n import gettext
|
|
|
|
STATES = [
|
|
('quotation', 'Quotation'),
|
|
('confirmed', 'Confirmed'),
|
|
('rejected', 'Rejected'),
|
|
('cancel', 'Canceled'),
|
|
]
|
|
|
|
READONLY_STATE = {
|
|
'readonly': (Eval('state') != 'draft'),
|
|
}
|
|
|
|
|
|
class Design(metaclass=PoolMeta):
|
|
__name__ = 'configurator.design'
|
|
opportunity = fields.Many2One('sale.opportunity', 'Opportunity',
|
|
domain=[If(Eval('party'), ('party', '=', Eval('party', -1)), ())],
|
|
states=READONLY_STATE, depends=['party', 'state'])
|
|
|
|
@fields.depends('party', 'opportunity', '_parent_opportunity.party')
|
|
def on_change_opportunity(self):
|
|
self.party = self.opportunity and self.opportunity.party
|
|
|
|
@classmethod
|
|
def copy(cls, designs, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('opportunity', None)
|
|
return super(Design, cls).copy(designs, default=default)
|
|
|
|
@classmethod
|
|
def process(cls, designs):
|
|
super().process(designs)
|
|
for design in designs:
|
|
if not design.product:
|
|
continue
|
|
quotes = [line for line in design.prices if line.state == 'confirmed']
|
|
if not quotes:
|
|
continue
|
|
quote = quotes[0]
|
|
template = design.product.template
|
|
template.list_price = quote.manual_list_price
|
|
template.save()
|
|
|
|
@classmethod
|
|
def validate(cls, designs):
|
|
for design in designs:
|
|
design.check_quotation_confirmed()
|
|
|
|
def check_quotation_confirmed(self):
|
|
confirmed = [x for x in self.prices if x.state == 'confirmed']
|
|
if confirmed and len(confirmed) > 1:
|
|
raise UserError(gettext(
|
|
'product_dynamic_configurator_sale_opportunity.msg_only_one_quotation_confirmed_allowed'))
|
|
|
|
|
|
class QuotationLine(metaclass=PoolMeta):
|
|
__name__ = "configurator.quotation.line"
|
|
|
|
state = fields.Selection(STATES, 'State', required=True,
|
|
states={'readonly': Eval('design_state') != 'draft'},
|
|
depends=['design_state'])
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'quotation'
|
|
|
|
@classmethod
|
|
def copy(cls, lines, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
|
|
default.setdefault('state', 'quotation')
|
|
return super().copy(lines, default=default)
|
|
|
|
|
|
class SaleOpportunity(metaclass=PoolMeta):
|
|
__name__ = "sale.opportunity"
|
|
|
|
design = fields.One2Many('configurator.design', 'opportunity', 'Design')
|
|
|
|
def get_rec_name(self, name):
|
|
return "%s (%s) %s" % (self.number, self.reference, self.description)
|
|
|
|
def get_quoted_lines(self, design, state=None):
|
|
res = []
|
|
for d in design:
|
|
res += [line for line in d.prices if line.state in state]
|
|
return res
|
|
|
|
def get_design_sale_line(self, sale, quote_line):
|
|
'''
|
|
Return sale line for opportunity line
|
|
'''
|
|
SaleLine = Pool().get('sale.line')
|
|
Uom = Pool().get('product.uom')
|
|
|
|
sale_line = SaleLine(
|
|
type='line',
|
|
product=quote_line.design.product,
|
|
sale=sale,
|
|
description=None,
|
|
)
|
|
sale_line.on_change_product()
|
|
sale_line.unit = quote_line.design.sale_uom
|
|
quantity = Uom.compute_qty(quote_line.design.quotation_uom,
|
|
quote_line.quantity, sale_line.unit, round=True)
|
|
sale_line.quantity = quantity
|
|
unit_price = quote_line.manual_list_price or quote_line.unit_price
|
|
unit_price = Uom.compute_price(sale_line.product.default_uom,
|
|
unit_price, sale_line.unit)
|
|
sale_line.unit_price = round(unit_price, price_digits[1])
|
|
return sale_line
|
|
|
|
def create_sale(self):
|
|
sale = super().create_sale()
|
|
sale_lines = list(sale.lines) if sale.lines else []
|
|
confirmed_lines = self.get_quoted_lines(self.design,
|
|
'confirmed')
|
|
|
|
for line in confirmed_lines:
|
|
sale_lines.insert(0, self.get_design_sale_line(sale, line))
|
|
|
|
sale.lines = sale_lines
|
|
return sale
|
|
|
|
@classmethod
|
|
def convert(cls, opportunities):
|
|
Design = Pool().get('configurator.design')
|
|
QuoteLine = Pool().get('configurator.quotation.line')
|
|
|
|
designs = []
|
|
rejected_lines = []
|
|
for opportunity in opportunities:
|
|
if not opportunity.design:
|
|
continue
|
|
designs += opportunity.design
|
|
confirmed = opportunity.get_quoted_lines(opportunity.design,
|
|
'confirmed')
|
|
if not confirmed:
|
|
raise UserError(gettext(
|
|
'product_dynamic_configurator_sale_opportunity.msg_no_quotation_confirmed'))
|
|
|
|
rejected_lines += opportunity.get_quoted_lines(opportunity.design,
|
|
'quotation')
|
|
|
|
QuoteLine.write(rejected_lines, {'state': 'rejected'})
|
|
Design.process(designs)
|
|
super().convert(opportunities)
|
|
|
|
@classmethod
|
|
def lost(cls, opportunities):
|
|
super().lost(opportunities)
|
|
Design = Pool().get('configurator.design')
|
|
QuoteLine = Pool().get('configurator.quotation.line')
|
|
designs = []
|
|
rejected_lines = []
|
|
for opportunity in opportunities:
|
|
designs += opportunity.design
|
|
rejected_lines += opportunity.get_quoted_lines(opportunity.design,
|
|
STATES)
|
|
|
|
QuoteLine.write(rejected_lines, {'state': 'rejected'})
|
|
Design.cancel(designs)
|
|
|
|
@classmethod
|
|
def cancel(cls, opportunities):
|
|
super().cancel(opportunities)
|
|
Design = Pool().get('configurator.design')
|
|
QuoteLine = Pool().get('configurator.quotation.line')
|
|
designs = []
|
|
rejected_lines = []
|
|
for opportunity in opportunities:
|
|
designs += opportunity.design
|
|
rejected_lines += opportunity.get_quoted_lines(opportunity.design,
|
|
STATES)
|
|
|
|
QuoteLine.write(rejected_lines, {'state': 'cancel'})
|
|
Design.cancel(designs)
|
|
|
|
@classmethod
|
|
def copy(cls, opportunities, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('design', None)
|
|
return super(SaleOpportunity, cls).copy(opportunities, default=default)
|