Files
tryton/purchase_request.py
T
2008-10-01 12:26:50 +02:00

516 lines
21 KiB
Python

#This file is part of Tryton. The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms.
from trytond.osv import fields, OSV
from trytond.wizard import Wizard, WizardOSV
import datetime
# TODO ensure that the link p_request p_line is never inconsistent
# (uom, qty, product, ...).
class PurchaseRequest(OSV):
'Purchase Request'
_name = 'stock.purchase_request'
_description = __doc__
product = fields.Many2One(
'product.product', 'Product', required=True, select=True, readonly=True)
party = fields.Many2One('relationship.party', 'Party', select=True)
quantity = fields.Float('Quantity', required=True)
uom = fields.Many2One('product.uom', 'UOM', required=True, select=True)
purchase_date = fields.Date('Best Purchase Date', readonly=True)
supply_date = fields.Date('Expected Supply Date', readonly=True)
stock_level = fields.Float('Stock at Supply Date', readonly=True)
warehouse = fields.Many2One(
'stock.location', "Warehouse", required=True,
domain="[('type', '=', 'warehouse')]", readonly=True)
purchase_line = fields.Many2One(
'purchase.line', 'Purchase Line',readonly=True)
company = fields.Many2One(
'company.company', 'Company', required=True, readonly=True)
def default_company(self, cursor, user, context=None):
company_obj = self.pool.get('company.company')
if context is None:
context = {}
if context.get('company'):
return company_obj.name_get(cursor, user, context['company'],
context=context)[0]
return False
def generate_requests(self, cursor, user, context=None):
"""
For each product compute the purchase request that must be
create today to meet product outputs.
"""
order_point_obj = self.pool.get('stock.order_point')
purchase_request_obj = self.pool.get('stock.purchase_request')
product_obj = self.pool.get('product.product')
location_obj = self.pool.get('stock.location')
user_obj = self.pool.get('res.user')
company = user_obj.browse(cursor, user, user, context=context).company
# fetch warehouses:
warehouse_ids = location_obj.search(
cursor, user, [('type','=','warehouse')], context=context)
# fetch order points
order_point_ids = order_point_obj.search(
cursor, user, [], context=context)
# index them by product
product2ops = {}
for order_point in order_point_obj.browse(
cursor, user, order_point_ids, context=context):
product2ops[
(order_point.location.id, order_point.product.id)
] = order_point
# fetch stockable products
product_ids = product_obj.search(
cursor, user, [('type', '=', 'stockable')], context=context)
#aggregate product by minimum supply date
date2products = {}
for product in product_obj.browse(cursor, user, product_ids,
context=context):
min_date, max_date = self.get_supply_dates(
cursor, user, product, context=context)
date2products.setdefault(min_date, []).append((product, max_date))
# compute requests
local_context = context and context.copy() or {}
new_requests = []
for min_date in date2products:
product_ids = [x[0].id for x in date2products[min_date]]
local_context.update(
{'stock_date_end': min_date or datetime.date.max})
pbl = product_obj.products_by_location(
cursor, user, warehouse_ids, product_ids, with_childs=True,
skip_zero=False, context=local_context)
for product, max_date in date2products[min_date]:
for warehouse_id in warehouse_ids:
qty = pbl.pop((warehouse_id, product.id))
order_point = product2ops.get((warehouse_id, product.id))
# Search for shortage between min-max
shortage_date, product_quantity = self.get_shortage(
cursor, user, warehouse_id, product.id, min_date,
max_date, min_date_qty=qty, order_point=order_point,
context=context)
if shortage_date == None or product_quantity == None:
continue
# generate request values
request_val = self.compute_request(
cursor, user, product, warehouse_id, shortage_date,
product_quantity, company, order_point, context=context)
new_requests.append(request_val)
self.create_requests(cursor, user, new_requests, context=context)
return {}
def create_requests(self, cursor, user, new_requests, context=None):
"""
Compare new_requests with already existing request and avoid
to re-create existing requests.
"""
# delete purchase request without a purchase line
uom_obj = self.pool.get('product.uom')
request_obj = self.pool.get('stock.purchase_request')
product_supplier_obj = self.pool.get('purchase.product_supplier')
req_ids = request_obj.search(
cursor, user, ['OR',[('purchase_line', '=', False)],
[('purchase_line.purchase.state', '=', 'cancel')]],
context=context)
request_obj.delete(cursor, user, req_ids, context=context)
req_ids = request_obj.search(cursor, user, [], context=context)
requests = request_obj.browse(cursor, user, req_ids, context=context)
# Fetch delivery_times for each (product,supplier)
sup_delivery_time = {}
for request in requests:
product, supplier = None, None
if request.purchase_line:
product = request.purchase_line.product.id
supplier = request.purchase_line.purchase.party.id
else:
product = request.product.id
supplier = request.party and request.party.id
if not supplier:
continue
sup_delivery_time[product, supplier] = None
prod_sup_ids = product_supplier_obj.search(
cursor, user,
['OR', ] + [
['AND', ('product', '=', x[0]), ('party', '=', x[1])] \
for x in sup_delivery_time.iterkeys()
],
context=context)
for prod_sup in product_supplier_obj.browse(cursor, user, prod_sup_ids,
context=context):
sup_delivery_time[prod_sup.product.id, prod_sup.party.id] = \
prod_sup.delivery_time
# Fetch data from existing requests
existing_req = {}
for request in requests:
if request.purchase_line:
product = request.purchase_line.product
warehouse = request.purchase_line.purchase.warehouse
purchase_date = request.purchase_line.purchase.purchase_date
qty = line.quantity
uom = line.unit
supplier = line.purchase.party
else:
product = request.product
warehouse = request.warehouse
purchase_date = request.purchase_date
qty = request.quantity
uom = request.uom
supplier = request.party
delivery_time = sup_delivery_time.get((product.id, supplier.id))
if delivery_time:
supply_date = purchase_date + \
datetime.timedelta(delivery_time)
else:
supply_date = datetime.date.max
existing_req.setdefault((product.id, warehouse.id), []).append(
{'supply_date': supply_date, 'quantity': qty, 'uom': uom})
for i in existing_req.itervalues():
i.sort
# Update new requests to take existing requests into account
new_requests.sort(lambda r,s: cmp(r['supply_date'],s['supply_date']))
for new_req in new_requests:
for old_req in existing_req.get((new_req['product'].id,
new_req['warehouse']), []):
if old_req['supply_date'] <= new_req['supply_date']:
quantity = uom_obj.compute_qty(
cursor, user, old_req['uom'], old_req['quantity'],
new_req['uom'], context=context)
new_req['quantity'] = max(0.0, new_req['quantity'] - quantity)
old_req['quantity'] = uom_obj.compute_qty(
cursor, user, new_req['uom'],
max(0.0, quantity - new_req['quantity']),
old_req['uom'], context=context)
else:
break
for new_req in new_requests:
if new_req['supply_date'] == datetime.date.max:
new_req['supply_date'] = None
if new_req['quantity'] > 0.0:
new_req.update({'product': new_req['product'].id,
'party': new_req['party'] and new_req['party'].id,
'uom': new_req['uom'].id,
'company': new_req['company'].id
})
request_obj.create(cursor, user, new_req, context=context)
def get_supply_dates(self, cursor, user, product, context=None):
"""
Return for the given product min and max values for the
earliest supply dates across all available suppliers.
"""
min_date = None
max_date = None
today = datetime.date.today()
product_supplier_obj = self.pool.get('purchase.product_supplier')
for product_supplier in product.product_suppliers:
supply_date = product_supplier_obj.compute_supply_date(
cursor, user, product_supplier, date=today, context=context)
if (not min_date) or supply_date < min_date:
min_date = supply_date
if (not max_date) or supply_date > max_date:
max_date = supply_date
if not min_date:
min_date = datetime.date.max
max_date = datetime.date.max
return (min_date, max_date)
def compute_request(self, cursor, user, product, location_id, shortage_date,
product_quantity, company, order_point=None,
context=None):
"""
Return the value of the purchase request which will answer to
the needed quantity at the given date. I.e: the latest
purchase date, the expected supply date and the prefered
supplier.
"""
uom_obj = self.pool.get('product.uom')
supplier = None
seq = None
on_time = False
today = datetime.date.today()
max_quantity = order_point and order_point.max_quantity or 0.0
for product_supplier in product.product_suppliers:
supply_date = today + datetime.timedelta(product_supplier.delivery_time)
sup_on_time = supply_date < shortage_date
if not supplier:
supplier = product_supplier.party
on_time = sup_on_time
seq = product_supplier.sequence
continue
if (sup_on_time, product_supplier.sequence) < (on_time, seq):
supplier = product_supplier.party
on_time = sup_on_time
seq = product_supplier.sequence
if supplier and product_supplier.delivery_time:
purchase_date = \
shortage_date - datetime.timedelta(product_supplier.delivery_time)
else:
purchase_date = today
quantity = uom_obj.compute_qty(cursor, user, product.default_uom,
max_quantity - product_quantity, product.purchase_uom,
context=context)
return {'product': product,
'party': supplier and supplier or None,
'quantity': quantity,
'uom': product.purchase_uom,
'purchase_date': purchase_date,
'supply_date': shortage_date,
'stock_level': product_quantity,
'company': company,
'warehouse': location_id,
}
def get_shortage(self, cursor, user, location_id, product_id, min_date,
max_date, min_date_qty, order_point=None,context=None):
"""
Compute stock quantities between the two given dates. If given
the order point, one date will be lacking in products this
date is returned alongside the stock level at this date.
If no dates are given, the stock is computed for an infinire
date and the shortage date is today.
"""
product_obj = self.pool.get('product.product')
min_quantity = order_point and order_point.min_quantity or 0.0
if not min_date:
if min_date_qty < min_quantity:
return (
datetime.date.today(), min_date_qty)
else:
return (None, None)
if not max_date: max_date = min_date
current_date = min_date
current_stock = min_date_qty
while current_date <= max_date:
if current_stock < min_quantity:
return current_date, current_stock
local_context = context and context.copy() or {}
local_context['stock_date_start'] = current_date
local_context['stock_date_end'] = current_date
res = product_obj.products_by_location(
cursor, user, [location_id],
[product_id], with_childs=True, skip_zero=False,
context=context)
for qty in res.itervalues():
current_stock += qty
if current_date == datetime.date.max:
break
current_date += datetime.timedelta(1)
return (None, None)
PurchaseRequest()
class CreatePurchaseAsk(WizardOSV):
_name = 'stock.purchase_request.create_purchase.ask'
party = fields.Many2One('relationship.party', 'Supplier', readonly=True)
company = fields.Many2One('company.company', 'Company', readonly=True)
payment_term = fields.Many2One(
'account.invoice.payment_term', 'Payment Term', required=True)
CreatePurchaseAsk()
class CreatePurchase(Wizard):
'Create Purchase'
_name = 'stock.purchase_request.create_purchase'
states = {
'init': {
'result': {
'type': 'action',
'action': '_compute_purchase',
'state': 'choice',
},
},
'choice': {
'result': {
'type': 'choice',
'next_state': '_check_payment_term',
},
},
'ask_user': {
'actions': ['_set_default'],
'result': {
'type': 'form',
'object': 'stock.purchase_request.create_purchase.ask',
'state': [
('end', 'Cancel', 'tryton-cancel'),
('choice', 'Continue', 'tryton-ok', True),
],
},
},
'create': {
'result': {
'type': 'action',
'action': '_create_purchase',
'state': 'end',
},
},
}
def _set_default(self, cursor, user, data, context=None):
if not data.get('party_wo_pt'):
return {}
party, company = data['party_wo_pt'].pop()
return {'party': party,'company': company}
def _check_payment_term(self, cursor, user, data, context=None):
party_obj = self.pool.get('relationship.party')
if 'purchases' not in data:
return 'end'
form = data['form']
if form.get('payment_term') and form.get('party') and \
form.get('company'):
for key, val in data['purchases'].iteritems():
if (key[0], key[1]) == (form['party'],
form['company']):
val['payment_term'] = form['payment_term']
local_context = context and context.copy() or {}
local_context['company'] = form['company']
party_obj.write(
cursor, user, form['party'],
{'supplier_payment_term': form['payment_term']}, context=local_context)
if data.get('party_wo_pt'):
return 'ask_user'
return 'create'
def _compute_purchase(self, cursor, user, data, context=None):
request_obj = self.pool.get('stock.purchase_request')
requests = request_obj.browse(cursor, user, data['ids'], context=context)
purchases = {}
for request in requests:
if (not request.party) or request.purchase_line:
continue
key = (request.party.id, request.company.id, request.warehouse.id)
if key not in purchases:
purchase = {
'company': request.company.id,
'party': request.party.id,
'purchase_date': request.purchase_date or datetime.date.today(),
'payment_term': request.party.supplier_payment_term and \
request.party.supplier_payment_term.id or None,
'warehouse': request.warehouse.id,
'currency': request.company.currency.id,
'lines': [],
}
purchases[key] = purchase
else:
purchase = purchases[key]
purchase['lines'].append({
'product': request.product.id,
'unit': request.uom.id,
'quantity': request.quantity,
'request': request.id,
})
if request.purchase_date:
if not purchase['purchase_date']:
purchase['purchase_date'] = request.purchase_date
else:
purchase['purchase_date'] = min(purchase['purchase_date'],
request.purchase_date)
data['purchases'] = purchases
data['party_wo_pt'] = set(
k[:2] for k in purchases if not purchases[k]['payment_term'])
return {}
def _create_purchase(self, cursor, user, data, context=None):
request_obj = self.pool.get('stock.purchase_request')
purchase_obj = self.pool.get('purchase.purchase')
product_obj = self.pool.get('product.product')
party_obj = self.pool.get('relationship.party')
line_obj = self.pool.get('purchase.line')
created_ids = []
party_ids = []
product_ids = []
# collect product names
for purchase in data['purchases'].itervalues():
party_ids.append(purchase['party'])
for line in purchase['lines']:
product_ids.append(line['product'])
id2product = dict((p.id, p) for p in product_obj.browse(
cursor, user, product_ids, context=context))
id2party = dict((p.id, p) for p in party_obj.browse(
cursor, user, party_ids, context=context))
# create purchases, lines and update requests
for purchase in data['purchases'].itervalues():
party = id2party[purchase['party']]
purchase_lines = purchase.pop('lines')
purchase_id = purchase_obj.create(
cursor, user, purchase, context=context)
created_ids.append(purchase_id)
for line in purchase_lines:
product = id2product[line['product']]
request_id = line.pop('request')
line_values = self.compute_purchase_line(
cursor, user, line, purchase_id, purchase, product,
party, context=context)
line_id = line_obj.create(cursor, user, line, context=context)
request_obj.write(
cursor, user, request_id, {'purchase_line': line_id},
context=context)
return {}
def compute_purchase_line(self, cursor, user, line, purchase_id, purchase,
product, party, context=None):
party_obj = self.pool.get('relationship.party')
product_obj = self.pool.get('product.product')
line['purchase'] = purchase_id
line['description'] = product.name
local_context = context.copy()
local_context['uom'] = line['unit']
local_context['supplier'] = purchase['party']
local_context['currency'] = purchase['currency']
product_price = product_obj.get_purchase_price(
cursor, user, [line['product']], line['quantity'],
context=local_context)[line['product']]
line['unit_price'] = product_price
taxes = []
for tax in product.supplier_taxes:
if 'supplier_' + tax.group.code in party_obj._columns \
and party['supplier_' + tax.group.code]:
taxes.append(
party['supplier_' + tax.group.code].id)
continue
taxes.append(tax.id)
line['taxes'] = [('add', taxes)]
return line
CreatePurchase()