Files
tryton/modules/purchase_request_quotation/purchase.py
T

662 lines
23 KiB
Python
Raw Normal View History

2018-03-23 22:28:41 +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.
import datetime
from functools import wraps
from itertools import groupby
2021-12-11 17:58:43 +01:00
2018-03-23 22:28:41 +01:00
from sql.conditionals import Case
from sql.functions import CharLength
2018-03-23 22:28:41 +01:00
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
from trytond.model import Index, ModelSQL, ModelView, Workflow, fields
2018-03-23 22:28:41 +01:00
from trytond.modules.company import CompanyReport
2021-12-11 17:58:43 +01:00
from trytond.modules.currency.fields import Monetary
2018-03-23 22:28:41 +01:00
from trytond.modules.product import price_digits
from trytond.pool import Pool, PoolMeta
2021-12-11 17:58:43 +01:00
from trytond.pyson import Bool, Eval, Id, If
from trytond.tools import sortable_values
2018-03-23 22:28:41 +01:00
from trytond.transaction import Transaction
2021-12-11 17:58:43 +01:00
from trytond.wizard import Button, StateTransition, StateView, Wizard
2021-08-21 09:09:56 +02:00
2018-12-29 14:20:29 +01:00
from .exceptions import PreviousQuotation
2018-03-23 22:28:41 +01:00
def process_request(func):
@wraps(func)
def wrapper(cls, quotations):
pool = Pool()
Request = pool.get('purchase.request')
func(cls, quotations)
requests = [l.request for q in quotations for l in q.lines]
Request.update_state(requests)
return wrapper
2018-06-26 19:11:51 +02:00
class Configuration(metaclass=PoolMeta):
2018-03-23 22:28:41 +01:00
__name__ = 'purchase.configuration'
purchase_request_quotation_sequence = fields.MultiValue(fields.Many2One(
'ir.sequence', 'Purchase Request Quotation Sequence',
required=True,
domain=[
('company', 'in',
[Eval('context', {}).get('company', -1), None]),
2021-03-11 23:00:10 +01:00
('sequence_type', '=',
Id('purchase_request_quotation',
'sequence_type_purchase_request_quotation')),
2018-03-23 22:28:41 +01:00
]))
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field == 'purchase_request_quotation_sequence':
return pool.get('purchase.configuration.sequence')
return super(Configuration, cls).multivalue_model(field)
@classmethod
def default_purchase_request_quotation_sequence(cls, **pattern):
return cls.multivalue_model('purchase_request_quotation_sequence'
).default_purchase_request_quotation_sequence()
2018-06-26 19:11:51 +02:00
class ConfigurationSequence(metaclass=PoolMeta):
2018-03-23 22:28:41 +01:00
__name__ = 'purchase.configuration.sequence'
purchase_request_quotation_sequence = fields.Many2One(
2021-03-11 23:00:10 +01:00
'ir.sequence', 'Purchase Request Quotation Sequence',
required=True,
domain=[
('company', 'in',
[Eval('context', {}).get('company', -1), None]),
('sequence_type', '=',
Id('purchase_request_quotation',
'sequence_type_purchase_request_quotation')),
])
2018-03-23 22:28:41 +01:00
@classmethod
def default_purchase_request_quotation_sequence(cls):
pool = Pool()
ModelData = pool.get('ir.model.data')
try:
return ModelData.get_id(
'purchase_request_quotation',
'sequence_purchase_request_quotation')
except KeyError:
return None
class Quotation(Workflow, ModelSQL, ModelView):
"Purchase Request For Quotation"
__name__ = 'purchase.request.quotation'
_rec_name = 'number'
number = fields.Char('Number', readonly=True,
states={
'required': ~Eval('state').in_(['draft', 'cancelled'])
},
help="The unique identifier of the quotation.")
revision = fields.Integer('Revision', readonly=True,
2018-04-14 17:52:05 +02:00
help="Number incremented each time the quotation is sent.")
reference = fields.Char(
"Reference",
2018-03-23 22:28:41 +01:00
help="The reference used by the supplier.")
2021-04-20 09:18:58 +02:00
company = fields.Many2One(
'company.company', "Company", required=True,
2021-04-20 09:18:58 +02:00
states={
'readonly': Eval('state') != 'draft',
})
2018-03-23 22:28:41 +01:00
warehouse = fields.Many2One('stock.location', 'Warehouse',
domain=[('type', '=', 'warehouse')])
supplier = fields.Many2One(
'party.party', "Supplier", required=True,
2018-03-23 22:28:41 +01:00
states={
'readonly': Eval('lines', [0]) & Eval('supplier'),
},
context={
'company': Eval('company', -1),
},
depends={'company'})
2018-03-23 22:28:41 +01:00
supplier_address = fields.Many2One('party.address', 'Supplier Address',
domain=[
('party', '=', Eval('supplier')),
])
2018-03-23 22:28:41 +01:00
lines = fields.One2Many('purchase.request.quotation.line', 'quotation',
'Lines', states={
'readonly': Eval('state') != 'draft',
})
2018-03-23 22:28:41 +01:00
state = fields.Selection([
('draft', 'Draft'),
('sent', 'Sent'),
('received', 'Received'),
2022-02-15 00:08:47 +01:00
('rejected', 'Rejected'),
2018-03-23 22:28:41 +01:00
('cancelled', 'Cancelled'),
2022-02-15 00:08:47 +01:00
], "State", readonly=True, required=True, sort=False)
2018-03-23 22:28:41 +01:00
@classmethod
def __setup__(cls):
cls.number.search_unaccented = False
cls.reference.search_unaccented = False
2018-03-23 22:28:41 +01:00
super(Quotation, cls).__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(t, (t.reference, Index.Similarity())),
Index(
t, (t.state, Index.Equality()),
where=t.state.in_(['draft', 'sent'])),
})
2018-03-23 22:28:41 +01:00
cls._transitions |= set((
('draft', 'cancelled'),
('cancelled', 'draft'),
('draft', 'sent'),
('sent', 'rejected'),
('sent', 'received'),
('sent', 'draft'),
('received', 'rejected'),
('rejected', 'received'),
))
cls._buttons.update({
'cancel': {
'invisible': Eval('state') != 'draft',
},
'draft': {
'invisible': ~Eval('state').in_(['cancelled', 'sent']),
'icon': If(Eval('state') == 'cancelled',
2018-08-19 20:02:58 +02:00
'tryton-undo',
'tryton-back'),
2018-03-23 22:28:41 +01:00
},
'send': {
'invisible': ((Eval('state') != 'draft')
| ~Eval('lines', [])),
'readonly': ~Eval('lines', []),
},
'receive': {
'invisible': ~Eval('state').in_(['sent', 'rejected']),
},
'reject': {
'invisible': ~Eval('state').in_(['sent', 'received']),
},
})
@classmethod
def order_number(cls, tables):
table, _ = tables[None]
return [CharLength(table.number), table.number]
2018-03-23 22:28:41 +01:00
@classmethod
def default_company(cls):
return Transaction().context.get('company')
@classmethod
def default_state(cls):
return 'draft'
@classmethod
def default_revision(cls):
return 1
@classmethod
def default_warehouse(cls):
Location = Pool().get('stock.location')
return Location.get_default_warehouse()
2018-03-23 22:28:41 +01:00
@classmethod
def set_number(cls, quotations):
pool = Pool()
Config = pool.get('purchase.configuration')
config = Config(1)
for quotation in quotations:
if quotation.number:
quotation.revision += 1
else:
2021-04-03 14:27:10 +02:00
quotation.number = config.get_multivalue(
'purchase_request_quotation_sequence',
company=quotation.company.id).get()
2018-03-23 22:28:41 +01:00
cls.save(quotations)
@fields.depends('supplier')
def on_change_supplier(self):
self.supplier_address = None
if self.supplier:
self.supplier_address = self.supplier.address_get()
@classmethod
def copy(cls, groups, default=None):
if default is None:
default = {}
else:
default = default.copy()
2018-09-01 01:02:04 +02:00
default.setdefault('number', None)
default.setdefault('revision', cls.default_revision())
2018-03-23 22:28:41 +01:00
return super(Quotation, cls).copy(groups, default=default)
@property
def delivery_full_address(self):
if self.warehouse and self.warehouse.address:
return self.warehouse.address.full_address
return ''
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, quotations):
pass
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, quotations):
pass
@classmethod
@ModelView.button
@Workflow.transition('sent')
def send(cls, quotations):
cls.set_number(quotations)
@classmethod
@ModelView.button
@process_request
@Workflow.transition('received')
def receive(cls, quotations):
pass
@classmethod
@ModelView.button
@process_request
@Workflow.transition('rejected')
def reject(cls, quotations):
pass
class QuotationLine(ModelSQL, ModelView):
"Purchase Request For Quotation Line"
__name__ = 'purchase.request.quotation.line'
supplier = fields.Function(fields.Many2One('party.party', 'Supplier'),
'get_supplier')
supply_date = fields.Date('Supply Date',
help="When it should be delivered.")
product = fields.Function(fields.Many2One('product.product', 'Product'),
'get_product', searcher='search_product')
description = fields.Text('Description',
states={
'required': ~Eval('product')
})
2021-08-30 00:27:13 +02:00
quantity = fields.Float("Quantity", digits='unit', required=True)
2018-03-23 22:28:41 +01:00
unit = fields.Many2One(
'product.uom', 'Unit', ondelete='RESTRICT',
states={
'required': Bool(Eval('product')),
},
domain=[
If(Bool(Eval('product_uom_category')),
('category', '=', Eval('product_uom_category')),
('category', '!=', -1)),
])
2018-03-23 22:28:41 +01:00
product_uom_category = fields.Function(
2023-08-20 12:17:27 +02:00
fields.Many2One(
'product.uom.category', "Product UoM Category",
help="The category of Unit of Measure for the product."),
2018-03-23 22:28:41 +01:00
'on_change_with_product_uom_category')
2021-08-21 09:09:56 +02:00
unit_price = Monetary(
"Unit Price", currency='currency', digits=price_digits)
2018-03-23 22:28:41 +01:00
currency = fields.Many2One('currency.currency', 'Currency',
states={
'required': Bool(Eval('unit_price')),
})
request = fields.Many2One(
'purchase.request', "Request", ondelete='CASCADE', required=True,
2018-03-23 22:28:41 +01:00
domain=[
If(Eval('quotation_state') == 'draft',
('state', 'in', ['draft', 'quotation', 'received']), (), ),
],
states={
'readonly': Eval('quotation_state') != 'draft'
},
help="The request which this line belongs to.")
quotation = fields.Many2One('purchase.request.quotation', 'Quotation',
ondelete='CASCADE', required=True,
domain=[
('supplier', '=', Eval('supplier')),
])
2018-03-23 22:28:41 +01:00
quotation_state = fields.Function(fields.Selection(
2019-09-28 23:59:09 +02:00
'get_quotation_state', 'Quotation State'),
2018-03-23 22:28:41 +01:00
'on_change_with_quotation_state', searcher='search_quotation_state')
2021-03-07 11:13:53 +01:00
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('quotation')
2018-03-23 22:28:41 +01:00
@staticmethod
def order_quotation_state(tables):
pool = Pool()
Quotation = pool.get('purchase.request.quotation')
table, _ = tables[None]
if 'quotation' not in tables:
quotation = Quotation.__table__()
tables['quotation'] = {
None: (quotation, table.quotation == quotation.id),
}
else:
quotation, _ = tables['quotation'][None]
2018-03-23 22:28:41 +01:00
return [Case((quotation.state == 'received', 0), else_=1),
quotation.state]
def get_supplier(self, name):
if self.quotation and self.quotation.supplier:
return self.quotation.supplier.id
2019-10-16 21:21:34 +02:00
@fields.depends('request',
'_parent_request.product', '_parent_request.description',
2023-08-21 09:31:27 +02:00
'_parent_request.quantity', '_parent_request.unit',
'_parent_request.company', '_parent_request.supply_date')
2018-03-23 22:28:41 +01:00
def on_change_request(self):
if self.request:
self.product = self.request.product
self.description = self.request.description
self.quantity = self.request.quantity
2023-08-21 09:31:27 +02:00
self.unit = self.request.unit
2019-10-16 21:21:26 +02:00
if self.request.company:
self.currency = self.request.company.currency
2018-03-23 22:28:41 +01:00
self.supply_date = self.request.supply_date or datetime.date.max
@fields.depends('product')
def on_change_with_product_uom_category(self, name=None):
2023-03-26 09:49:00 +02:00
return self.product.default_uom_category if self.product else None
2018-03-23 22:28:41 +01:00
@classmethod
def get_quotation_state(cls):
pool = Pool()
Quotation = pool.get('purchase.request.quotation')
return (Quotation.fields_get(
['state'])['state']['selection'])
@fields.depends('quotation', '_parent_quotation.state')
def on_change_with_quotation_state(self, name=None):
pool = Pool()
Quotation = pool.get('purchase.request.quotation')
if self.quotation:
return self.quotation.state
return Quotation.default_state()
@classmethod
def search_quotation_state(cls, name, clause):
return [('quotation.state',) + tuple(clause[1:])]
def get_rec_name(self, name):
return '%s - %s' % (self.quotation.rec_name, self.supplier.rec_name)
@classmethod
def search_rec_name(cls, name, clause):
domain = []
_, operator, value = clause
if value is not None:
names = clause[2].split(' - ', 1)
domain.append(('quotation', operator, names[0]))
if len(names) != 1 and names[1]:
domain.append(('supplier', operator, names[1]))
if operator.startswith('!') or operator.startswith('not'):
domain.insert(0, 'OR')
elif not operator.startswith('!') and not operator.startswith('not'):
domain.append(('id', '<', 0))
return domain
2018-03-23 22:28:41 +01:00
@classmethod
def delete(cls, quotationlines):
pool = Pool()
Request = pool.get('purchase.request')
requests = [l.request for l in quotationlines]
super(QuotationLine, cls).delete(quotationlines)
Request.update_state(requests)
def get_product(self, name):
if self.request and self.request.product:
return self.request.product.id
@classmethod
def search_product(cls, name, clause):
return [('request.' + clause[0],) + tuple(clause[1:])]
class PurchaseRequestQuotationReport(CompanyReport):
__name__ = 'purchase.request.quotation'
@classmethod
def execute(cls, ids, data):
with Transaction().set_context(address_with_party=True):
return super(
PurchaseRequestQuotationReport, cls).execute(ids, data)
2018-05-31 14:03:31 +02:00
@classmethod
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
2018-05-31 14:03:31 +02:00
pool = Pool()
Date = pool.get('ir.date')
2021-02-21 19:19:26 +01:00
context = super().get_context(records, header, data)
company = header.get('company')
with Transaction().set_context(
company=company.id if company else None):
context['today'] = Date.today()
2018-05-31 14:03:31 +02:00
return context
2018-03-23 22:28:41 +01:00
class CreatePurchaseRequestQuotationAskSuppliers(ModelView):
'Create Purchase Request Quotation Ask Suppliers'
__name__ = 'purchase.request.quotation.create.ask_suppliers'
suppliers = fields.Many2Many('party.party', None, None, 'Suppliers',
required=True)
class CreatePurchaseRequestQuotationSucceed(ModelView):
'Create Purchase Request Quotation Succeed'
__name__ = 'purchase.request.quotation.create.succeed'
number_quotations = fields.Integer('Number of Created Quotations',
readonly=True)
class CreatePurchaseRequestQuotation(Wizard):
'Create Purchase Request Quotation'
__name__ = 'purchase.request.quotation.create'
start = StateTransition()
ask_suppliers = StateView(
'purchase.request.quotation.create.ask_suppliers',
'purchase_request_quotation.'
'purchase_request_quotation_create_ask_suppliers', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Process', 'create_quotations', 'tryton-ok', default=True),
])
create_quotations = StateTransition()
succeed = StateView(
'purchase.request.quotation.create.succeed',
'purchase_request_quotation.'
'purchase_request_quotation_create_succeed', [
Button('Close', 'end', 'tryton-close', True),
])
def transition_start(self):
pool = Pool()
2018-12-29 14:20:29 +01:00
Warning = pool.get('res.user.warning')
2018-03-23 22:28:41 +01:00
reqs = [r for r in self.records if r.state in {'draft', 'quotation'}]
2018-03-23 22:28:41 +01:00
if reqs:
for r in reqs:
if r.state == 'quotation':
2018-12-29 14:20:29 +01:00
if Warning.check(str(r)):
raise PreviousQuotation(str(r),
gettext('purchase_request_quotation'
'.msg_previous_quotation',
request=r.rec_name))
2018-03-23 22:28:41 +01:00
return 'ask_suppliers'
return 'end'
def default_ask_suppliers(self, fields):
reqs = [
r for r in self.records
2018-06-26 19:11:51 +02:00
if r.party and r.state in ['draft', 'quotation']]
2018-03-23 22:28:41 +01:00
return {
'suppliers': [r.party.id for r in reqs],
}
def default_succeed(self, fields):
return {
'number_quotations': self.succeed.number_quotations,
}
def filter_request(self, request, supplier):
return request
def _group_request_key(self, request):
return (('company', request.company),)
def transition_create_quotations(self):
pool = Pool()
Quotation = pool.get('purchase.request.quotation')
QuotationLine = pool.get('purchase.request.quotation.line')
quotations = []
lines = []
2021-04-26 13:06:26 +02:00
requests = [
r for r in self.records if r.state in ['draft', 'quotation']]
2018-03-23 22:28:41 +01:00
for supplier in self.ask_suppliers.suppliers:
2021-04-26 13:06:26 +02:00
sub_requests = [
r for r in requests if self.filter_request(r, supplier)]
sub_requests = sorted(
sub_requests, key=sortable_values(self._group_request_key))
for key, grouped_requests in groupby(
sub_requests, key=self._group_request_key):
quotation = self.get_quotation(supplier, key)
for request in grouped_requests:
line = self.get_quotation_line(request, quotation)
2018-03-23 22:28:41 +01:00
line.quotation = quotation
lines.append(line)
quotations.append(quotation)
2018-03-23 22:28:41 +01:00
QuotationLine.save(lines)
Quotation.save(quotations)
2021-04-26 13:06:26 +02:00
self.model.update_state(requests)
2018-03-23 22:28:41 +01:00
self.succeed.number_quotations = len(quotations)
return 'succeed'
def get_quotation(self, supplier, key):
pool = Pool()
Quotation = pool.get('purchase.request.quotation')
quotation = Quotation()
quotation.supplier = supplier
quotation.supplier_address = supplier.address_get()
for f, v in key:
setattr(quotation, f, v)
return quotation
def get_quotation_line(self, request, quotation):
2018-03-23 22:28:41 +01:00
pool = Pool()
QuotationLine = pool.get('purchase.request.quotation.line')
quotation_line = QuotationLine()
quotation_line.request = request
quotation_line.description = request.description
quotation_line.quantity = request.quantity
2023-08-21 09:31:27 +02:00
quotation_line.unit = request.unit
2018-03-23 22:28:41 +01:00
quotation_line.currency = request.currency
quotation_line.supply_date = request.supply_date or datetime.date.max
return quotation_line
2018-06-26 19:11:51 +02:00
class PurchaseRequest(metaclass=PoolMeta):
2018-03-23 22:28:41 +01:00
__name__ = 'purchase.request'
quotation_lines = fields.One2Many(
'purchase.request.quotation.line', 'request', 'Quotation Lines',
)
quotation_lines_active = fields.One2Many(
'purchase.request.quotation.line', 'request', 'Active Quotation Lines',
filter=[('quotation.state', 'in', ['draft', 'sent', 'received'])],
order=[('quotation_state', 'ASC'), ('unit_price', 'ASC')])
best_quotation_line = fields.Function(fields.Many2One(
'purchase.request.quotation.line', 'Best Quotation Line'),
'get_best_quotation')
preferred_quotation_line = fields.Many2One(
'purchase.request.quotation.line', 'Preferred Quotation Line',
domain=[
('quotation_state', '=', 'received'),
('request', '=', Eval('id'))
],
help="The quotation that will be chosen to create the purchase\n"
"otherwise first ordered received quotation line will be selected.")
@property
def currency(self):
currency = super(PurchaseRequest, self).currency
if self.best_quotation_line:
return self.best_quotation_line.currency
return currency
def get_best_quotation(self, name):
if self.preferred_quotation_line:
return self.preferred_quotation_line
else:
for line in self.quotation_lines_active:
if line.quotation_state == 'received':
return line
return None
@classmethod
def __setup__(cls):
super(PurchaseRequest, cls).__setup__()
selection = [('quotation', 'Quotation'), ('received', 'Received')]
for s in selection:
if s not in cls.state.selection:
cls.state.selection.append(s)
def get_state(self):
state = super(PurchaseRequest, self).get_state()
if state == 'draft' and self.quotation_lines:
state = 'quotation'
if any(l.quotation_state == 'received'
for l in self.quotation_lines):
state = 'received'
return state
class CreatePurchase(Wizard):
'Create Purchase'
__name__ = 'purchase.request.create_purchase'
init = StateTransition()
@classmethod
def __setup__(cls):
super(CreatePurchase, cls).__setup__()
def transition_start(self):
to_save = []
reqs = [r for r in self.records
2020-03-01 12:33:53 +01:00
if not r.purchase_line and r.quotation_lines]
2018-03-23 22:28:41 +01:00
to_save = []
for req in reqs:
if req.best_quotation_line:
to_save.append(self.apply_quotation(req))
if to_save:
self.model.save(to_save)
2018-03-23 22:28:41 +01:00
state = super(CreatePurchase, self).transition_start()
return state
def apply_quotation(self, request):
request.party = request.best_quotation_line.supplier.id
request.description = request.best_quotation_line.description
request.quantity = request.best_quotation_line.quantity
if not request.preferred_quotation_line:
request.preferred_quotation_line = request.best_quotation_line
return request
@classmethod
def compute_purchase_line(cls, key, requests, purchase):
line = super(CreatePurchase, cls).compute_purchase_line(key,
requests, purchase)
try:
line.unit_price = min(req.best_quotation_line.unit_price
for req in requests if req.best_quotation_line)
except ValueError:
pass
return line