Files
tryton/modules/purchase_request/purchase_request.py
T

545 lines
19 KiB
Python
Raw Normal View History

2016-03-25 17:01:00 +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
2016-03-25 17:01:00 +01:00
from decimal import Decimal
from functools import partial
2021-12-11 17:58:43 +01:00
from itertools import groupby
2016-03-25 17:01:00 +01:00
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
from trytond.model import Index, ModelSQL, ModelView, fields
2018-12-29 14:20:29 +01:00
from trytond.model.exceptions import AccessError
2020-04-10 18:53:44 +02:00
from trytond.modules.company.model import (
2021-12-11 17:58:43 +01:00
employee_field, reset_employee, set_employee)
2020-04-23 21:44:00 +02:00
from trytond.modules.product import round_price
2021-12-11 17:58:43 +01:00
from trytond.pool import Pool
2022-02-18 13:20:44 +01:00
from trytond.pyson import Bool, Eval, If
2021-12-11 17:58:43 +01:00
from trytond.tools import firstline, sortable_values
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
2020-04-10 18:53:44 +02:00
2016-03-25 17:01:00 +01:00
STATES = {
'readonly': Eval('state') != 'draft',
}
class PurchaseRequest(ModelSQL, ModelView):
'Purchase Request'
__name__ = 'purchase.request'
product = fields.Many2One(
'product.product', "Product", readonly=True,
domain=[
If((Eval('state') == 'draft')
& ~(Eval('quantity', 0) < 0),
('purchasable', '=', True),
()),
],
context={
'company': Eval('company', -1),
},
depends={'company'})
description = fields.Text('Description', readonly=True)
2020-12-25 17:14:57 +01:00
summary = fields.Function(fields.Char('Summary'), 'on_change_with_summary')
party = fields.Many2One(
'party.party', "Party", states=STATES,
context={
'company': Eval('company', -1),
},
depends={'company'})
2023-08-21 09:31:27 +02:00
quantity = fields.Float(
"Quantity", required=True, states=STATES, digits='unit')
unit = fields.Many2One(
'product.uom', "Unit", ondelete='RESTRICT',
domain=[
If(Bool(Eval('product_uom_category')),
('category', '=', Eval('product_uom_category')),
('category', '!=', -1)),
],
2016-07-08 10:47:48 +02:00
states={
'required': Bool(Eval('product')),
'readonly': STATES['readonly'],
})
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."),
'on_change_with_product_uom_category')
2016-03-25 17:01:00 +01:00
computed_quantity = fields.Float('Computed Quantity', readonly=True)
2023-08-21 09:31:27 +02:00
computed_unit = fields.Many2One(
'product.uom', "Computed Unit", readonly=True)
2016-03-25 17:01:00 +01:00
purchase_date = fields.Date('Best Purchase Date', readonly=True)
supply_date = fields.Date('Expected Supply Date', readonly=True)
2023-08-20 12:17:27 +02:00
default_uom = fields.Function(
fields.Many2One(
'product.uom', "Default UoM",
help="The default Unit of Measure."),
'on_change_with_default_uom')
stock_level = fields.Float(
"Stock at Supply Date", readonly=True, digits='default_uom')
2016-03-25 17:01:00 +01:00
warehouse = fields.Many2One(
'stock.location', "Warehouse",
states={
'required': Eval('warehouse_required', False),
},
domain=[('type', '=', 'warehouse')],
2016-03-25 17:01:00 +01:00
readonly=True)
warehouse_required = fields.Function(fields.Boolean('Warehouse Required'),
'get_warehouse_required')
purchase_line = fields.Many2One('purchase.line', 'Purchase Line',
readonly=True)
purchase = fields.Function(fields.Many2One('purchase.purchase',
'Purchase'), 'get_purchase', searcher='search_purchase')
company = fields.Many2One('company.company', 'Company', required=True,
2022-02-18 13:20:44 +01:00
readonly=True)
2017-06-13 09:47:29 +02:00
origin = fields.Reference('Origin', selection='get_origin', readonly=True)
2016-03-25 17:01:00 +01:00
exception_ignored = fields.Boolean('Ignored Exception')
2020-04-10 18:53:44 +02:00
purchased_by = employee_field(
"Purchased By", states=['purchased', 'done', 'cancelled', 'exception'])
state = fields.Selection([
2022-02-15 00:08:47 +01:00
('draft', "Draft"),
('purchased', "Purchased"),
('done', "Done"),
('cancelled', "Cancelled"),
('exception', "Exception"),
], "State", required=True, readonly=True, sort=False)
2016-03-25 17:01:00 +01:00
@classmethod
def __setup__(cls):
super(PurchaseRequest, cls).__setup__()
t = cls.__table__()
cls._sql_indexes.add(
Index(
t,
(t.state, Index.Equality()),
where=t.state.in_(['draft', 'purchased', 'exception'])))
2016-03-25 17:01:00 +01:00
cls._order[0] = ('id', 'DESC')
cls._buttons.update({
'handle_purchase_cancellation_exception': {
'invisible': Eval('state') != 'exception',
2018-02-19 11:40:21 +01:00
'depends': ['state'],
2016-03-25 17:01:00 +01:00
},
})
@classmethod
def __register__(cls, module_name):
request = cls.__table__()
2023-08-21 09:31:27 +02:00
table_h = cls.__table_handler__(module_name)
# Migration from 6.8: rename uom to unit
if (table_h.column_exist('uom')
and not table_h.column_exist('unit')):
table_h.column_rename('uom', 'unit')
# Migration from 6.8: rename uom to unit
if (table_h.column_exist('computed_uom')
and not table_h.column_exist('computed_unit')):
table_h.column_rename('computed_uom', 'computed_unit')
2016-03-25 17:01:00 +01:00
super(PurchaseRequest, cls).__register__(module_name)
cursor = Transaction().connection.cursor()
2017-06-13 09:47:29 +02:00
# Migration from 5.6: rename state cancel to cancelled
cursor.execute(*request.update(
[request.state], ['cancelled'],
where=request.state == 'cancel'))
2016-03-25 17:01:00 +01:00
def get_rec_name(self, name):
2019-11-26 22:20:24 +01:00
pool = Pool()
Lang = pool.get('ir.lang')
if self.product:
2019-11-26 22:20:24 +01:00
lang = Lang.get()
rec_name = (lang.format_number_symbol(
2023-08-21 09:31:27 +02:00
self.quantity, self.unit, digits=self.unit.digits)
+ ' %s' % self.product.name)
elif self.description:
rec_name = self.description.splitlines()[0]
2016-03-25 17:01:00 +01:00
else:
rec_name = str(self.id)
if self.warehouse:
2019-11-26 22:20:24 +01:00
return "%s @% s" % (rec_name, self.warehouse.name)
else:
return rec_name
2016-03-25 17:01:00 +01:00
@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]
if value is not None:
names = value.split('@', 1)
sub_domain = [('product.template.name', operator, names[0])]
if len(names) != 1 and names[1]:
sub_domain.append(('warehouse', operator, names[1]))
if bool_op == 'AND':
sub_domain.insert(0, 'OR')
domain.append(sub_domain)
domain.append(('description', *clause[1:]))
return domain
2016-03-25 17:01:00 +01:00
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_exception_ignored():
return False
def get_purchase(self, name):
if self.purchase_line:
return self.purchase_line.purchase.id
@classmethod
def search_purchase(cls, name, clause):
2017-09-27 18:37:31 +02:00
return [('purchase_line.' + clause[0],) + tuple(clause[1:])]
2016-03-25 17:01:00 +01:00
@property
def currency(self):
2022-04-11 21:24:28 +02:00
currency = self.company.currency
if self.party and self.party.supplier_currency:
currency = self.party.supplier_currency
return currency
2016-03-25 17:01:00 +01:00
@classmethod
def default_state(cls):
return 'draft'
def get_state(self):
2016-03-25 17:01:00 +01:00
if self.purchase_line:
if (self.purchase_line.purchase.state == 'cancelled'
2016-03-25 17:01:00 +01:00
and not self.exception_ignored):
return 'exception'
elif self.purchase_line.purchase.state == 'cancelled':
return 'cancelled'
2016-03-25 17:01:00 +01:00
elif self.purchase_line.purchase.state == 'done':
return 'done'
else:
return 'purchased'
return 'draft'
@classmethod
def update_state(cls, requests):
for request in requests:
state = request.get_state()
if state != request.state:
request.state = state
cls.save(requests)
2016-03-25 17:01:00 +01:00
2020-04-10 18:53:44 +02:00
@classmethod
@set_employee('purchased_by')
def set_purchased(cls, requests):
cls.update_state(requests)
@classmethod
@reset_employee('purchased_by')
def reset_purchased(cls, requests):
cls.update_state(requests)
2016-03-25 17:01:00 +01:00
def get_warehouse_required(self, name):
2016-07-08 10:47:48 +02:00
return self.product and self.product.type in ('goods', 'assets')
2016-03-25 17:01:00 +01:00
@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
2020-10-09 19:09:05 +02:00
@fields.depends('product')
def on_change_with_default_uom(self, name=None):
2023-03-26 09:49:00 +02:00
return self.product.default_uom if self.product else None
2020-10-09 19:09:05 +02:00
2020-12-25 17:14:57 +01:00
@fields.depends('description')
def on_change_with_summary(self, name=None):
return firstline(self.description or '')
2016-03-25 17:01:00 +01:00
@classmethod
def _get_origin(cls):
'Return the set of Model names for origin Reference'
return set()
@classmethod
def get_origin(cls):
pool = Pool()
IrModel = pool.get('ir.model')
2020-12-27 00:19:30 +01:00
get_name = IrModel.get_name
models = cls._get_origin()
return [(None, '')] + [(m, get_name(m)) for m in models]
2016-03-25 17:01:00 +01: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() + [
('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
2019-09-20 11:47:15 +02:00
]
2016-03-25 17:01:00 +01:00
@classmethod
def create(cls, vlist):
transaction = Transaction()
if transaction.user and transaction.check_access:
raise AccessError(
gettext('purchase_request.msg_request_no_create'))
2016-03-25 17:01:00 +01:00
return super(PurchaseRequest, cls).create(vlist)
@classmethod
def delete(cls, requests):
2018-12-29 14:20:29 +01:00
for request in requests:
if request.purchase_line:
raise AccessError(
gettext('purchase_request.msg_request_delete_purchased',
request=request.rec_name))
2016-03-25 17:01:00 +01:00
super(PurchaseRequest, cls).delete(requests)
2021-04-05 16:24:13 +02:00
@classmethod
def copy(cls, requests, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('purchased_by')
return super().copy(requests, default=default)
@classmethod
def find_best_product_supplier(cls, product, date, **pattern):
"Return the best product supplier to request product at date"
pool = Pool()
Date = pool.get('ir.date')
with Transaction().set_context(context=product._context):
today = Date.today()
for product_supplier in product.product_suppliers_used(**pattern):
2021-02-12 23:58:33 +01:00
if date is None:
return product_supplier
supply_date = product_supplier.compute_supply_date(date=today)
timedelta = date - supply_date
if timedelta >= datetime.timedelta(0):
return product_supplier
@classmethod
def find_best_supplier(cls, product, date, **pattern):
'''
Return the best supplier and purchase_date for the product.
'''
pool = Pool()
Date = pool.get('ir.date')
product_supplier = cls.find_best_product_supplier(
product, date, **pattern)
if product_supplier:
supplier = product_supplier.party
purchase_date = product_supplier.compute_purchase_date(date)
else:
supplier = None
with Transaction().set_context(context=product._context):
purchase_date = Date.today()
return supplier, purchase_date
2016-03-25 17:01:00 +01:00
@classmethod
@ModelView.button_action(
'purchase_request.wizard_purchase_cancellation_handle_exception')
def handle_purchase_cancellation_exception(cls, purchases):
pass
class CreatePurchaseAskParty(ModelView):
'Create Purchase Ask Party'
__name__ = 'purchase.request.create_purchase.ask_party'
product = fields.Many2One('product.product', 'Product', readonly=True)
2016-07-08 10:47:48 +02:00
description = fields.Text('Description', readonly=True)
2016-03-25 17:01:00 +01:00
company = fields.Many2One('company.company', 'Company', readonly=True)
party = fields.Many2One('party.party', 'Supplier', required=True)
class CreatePurchase(Wizard):
'Create Purchase'
__name__ = 'purchase.request.create_purchase'
start = StateTransition()
ask_party = StateView('purchase.request.create_purchase.ask_party',
'purchase_request.purchase_request_create_purchase_ask_party', [
Button('Cancel', 'end', 'tryton-cancel'),
2018-08-19 20:02:58 +02:00
Button('Continue', 'start', 'tryton-forward', default=True),
2016-03-25 17:01:00 +01:00
])
def default_ask_party(self, fields):
for request in self.records:
2016-03-25 17:01:00 +01:00
if request.purchase_line:
continue
if not request.party:
2016-07-08 10:47:48 +02:00
break
2016-03-25 17:01:00 +01:00
return {
2016-07-08 10:47:48 +02:00
'product': request.product.id if request.product else None,
'description': request.description,
2016-03-25 17:01:00 +01:00
'company': request.company.id,
}
2019-06-04 14:44:33 +02:00
@classmethod
def _group_purchase_key(cls, requests, request):
2016-03-25 17:01:00 +01:00
'''
The key to group lines by purchases
A list of key-value as tuples of the purchase
'''
return (
('company', request.company),
('party', request.party),
('payment_term', request.party.supplier_payment_term),
('warehouse', request.warehouse),
('currency', request.currency),
('invoice_address', request.party.address_get(type='invoice')),
)
def _group_purchase_line_key(self, request):
'''
The key to group requests by lines
A list of key-value as tuples of the purchase line
'''
return (
('product', request.product),
2016-07-08 10:47:48 +02:00
('description', request.description),
2023-08-21 09:31:27 +02:00
('unit', request.unit),
)
2016-03-25 17:01:00 +01:00
def transition_start(self):
pool = Pool()
Request = pool.get('purchase.request')
Purchase = pool.get('purchase.purchase')
2016-12-27 17:50:05 +01:00
Line = pool.get('purchase.line')
2016-03-25 17:01:00 +01:00
Date = pool.get('ir.date')
requests = self.records
2016-03-25 17:01:00 +01:00
2016-07-08 10:47:48 +02:00
if (getattr(self.ask_party, 'party', None)
2016-03-25 17:01:00 +01:00
and getattr(self.ask_party, 'company', None)):
def compare_string(first, second):
return (first or '') == (second or '')
2017-03-13 20:43:02 +01:00
def to_write(request):
return (not request.purchase_line
and not request.party
and request.product == self.ask_party.product
and compare_string(
request.description, self.ask_party.description))
2018-06-26 19:11:51 +02:00
reqs = list(filter(to_write, requests))
2016-03-25 17:01:00 +01:00
if reqs:
Request.write(reqs, {
'party': self.ask_party.party.id,
})
self.ask_party.product = None
2016-07-08 10:47:48 +02:00
self.ask_party.description = None
2016-03-25 17:01:00 +01:00
self.ask_party.party = None
self.ask_party.company = None
2017-03-13 20:43:02 +01:00
def to_ask_party(request):
return not request.purchase_line and not request.party
2018-06-26 19:11:51 +02:00
reqs = filter(to_ask_party, requests)
2017-03-13 20:43:02 +01:00
if any(reqs):
2016-03-25 17:01:00 +01:00
return 'ask_party'
requests = [r for r in requests if not r.purchase_line]
def _sort_keyfunc(requests, request):
return (
self._group_purchase_key(requests, request)
+ self._group_purchase_line_key(request))
sort_keyfunc = partial(_sort_keyfunc, requests)
2016-03-25 17:01:00 +01:00
keyfunc = partial(self._group_purchase_key, requests)
requests = sorted(requests, key=sortable_values(sort_keyfunc))
2016-03-25 17:01:00 +01:00
2016-12-27 17:50:05 +01:00
purchases = []
lines = []
2016-03-25 17:01:00 +01:00
for key, grouped_requests in groupby(requests, key=keyfunc):
grouped_requests = list(grouped_requests)
key = dict(key)
with Transaction().set_context(company=key['company']):
today = Date.today()
2016-03-25 17:01:00 +01:00
try:
purchase_date = min(r.purchase_date
for r in grouped_requests
if r.purchase_date)
except ValueError:
purchase_date = today
if purchase_date < today:
purchase_date = today
purchase = Purchase(purchase_date=purchase_date)
for f, v in key.items():
2016-03-25 17:01:00 +01:00
setattr(purchase, f, v)
2016-12-27 17:50:05 +01:00
purchases.append(purchase)
for line_key, line_requests in groupby(
grouped_requests, key=self._group_purchase_line_key):
line_requests = list(line_requests)
line = self.compute_purchase_line(
line_key, line_requests, purchase)
2016-03-25 17:01:00 +01:00
line.purchase = purchase
2016-12-27 17:50:05 +01:00
line.requests = line_requests
lines.append(line)
Purchase.save(purchases)
Line.save(lines)
2020-04-10 18:53:44 +02:00
Request.set_purchased(requests)
2016-03-25 17:01:00 +01:00
return 'end'
@classmethod
def compute_purchase_line(cls, key, requests, purchase):
2016-03-25 17:01:00 +01:00
pool = Pool()
Line = pool.get('purchase.line')
line = Line()
for f, v in key:
setattr(line, f, v)
2017-11-10 10:50:11 +01:00
line.purchase = purchase
line.on_change_product()
line.quantity = cls.compute_quantity(requests, line, purchase)
if line.unit:
line.quantity = line.unit.ceil(line.quantity)
# Set again in case on_change's changed them
for f, v in key:
setattr(line, f, v)
line.on_change_quantity()
if line.unit_price is None:
line.unit_price = round_price(Decimal(0))
2016-03-25 17:01:00 +01:00
return line
@classmethod
def compute_quantity(cls, requests, line, purchase):
pool = Pool()
Uom = pool.get('product.uom')
unit = line.unit
compute_qty = Uom.compute_qty
return sum(
2023-08-21 09:31:27 +02:00
compute_qty(r.unit, r.quantity, unit, round=False)
for r in requests)
def end(self):
return 'reload'
2016-03-25 17:01:00 +01:00
class HandlePurchaseCancellationException(Wizard):
'Handle Purchase Cancellation Exception'
__name__ = 'purchase.request.handle.purchase.cancellation'
start = StateView('purchase.request.handle.purchase.cancellation.start',
'purchase_request.handle_purchase_cancellation_start', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Reset to draft', 'reset', 'tryton-clear'),
Button('Cancel Request', 'cancel_request', 'tryton-delete',
default=True),
])
reset = StateTransition()
cancel_request = StateTransition()
def transition_reset(self):
for request in self.records:
request.purchase_line = None
self.model.reset_purchased(self.records)
2016-03-25 17:01:00 +01:00
return 'end'
def transition_cancel_request(self):
for request in self.records:
request.exception_ignored = True
self.model.update_state(self.records)
2016-03-25 17:01:00 +01:00
return 'end'
class HandlePurchaseCancellationExceptionStart(ModelView):
'Handle Purchase Cancellation Exception - Start'
__name__ = 'purchase.request.handle.purchase.cancellation.start'