diff --git a/purchase_request.py b/purchase_request.py index 3c0a300b1c..2287f07ca8 100644 --- a/purchase_request.py +++ b/purchase_request.py @@ -35,6 +35,286 @@ class PurchaseRequest(OSV): 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, [('purchase_line', '=', False)], 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. + """ + 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 + + return {'product': product, + 'party': supplier and supplier or None, + 'quantity': max_quantity - product_quantity, + 'uom': product.default_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() diff --git a/purchase_request.xml b/purchase_request.xml index 6676aaa4ef..80bdd964b5 100644 --- a/purchase_request.xml +++ b/purchase_request.xml @@ -109,5 +109,25 @@ User/Current Company + + + user_cron_purchase_request + User Purchase Request + + + + + Generate Purchase Requests + + + 1 + days + -1 + + stock.purchase_request + generate_requests + 5 + + diff --git a/supply_schedule.py b/supply_schedule.py deleted file mode 100644 index bb7e983d91..0000000000 --- a/supply_schedule.py +++ /dev/null @@ -1,298 +0,0 @@ -from trytond.wizard import Wizard, WizardOSV -import datetime -# * Generate for X day in advance ? -# * Provide a way to choose how mutch day before the shortage the suplly -# must be planned - -class GeneratePurchaseRequest(Wizard): - 'Generate Purchase Requests' - _name = 'stock.generate_purchase_request' - states = { - 'init': { - 'result': { - 'type': 'action', - 'action': 'generate_requests', - 'state': 'end', - }, - }, - } - - - def generate_requests(self, cursor, user, data, 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.copy() - 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, [('purchase_line', '=', False)], 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. - """ - 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 - - return {'product': product, - 'party': supplier and supplier or None, - 'quantity': max_quantity - product_quantity, - 'uom': product.default_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.copy() - 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) - -GeneratePurchaseRequest() diff --git a/supply_schedule.xml b/supply_schedule.xml deleted file mode 100644 index f25f6d8b2a..0000000000 --- a/supply_schedule.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Generate Purchase Requests - stock.generate_purchase_request - - - -