Files
odoo_source/addons/stock_calendar/procurement.py
T
2014-12-31 15:52:13 +01:00

326 lines
18 KiB
Python

from datetime import datetime
from dateutil.relativedelta import relativedelta
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
from openerp import SUPERUSER_ID
from psycopg2 import OperationalError
from openerp.tools import float_compare, float_round
import pytz
import openerp
class procurement_order(osv.osv):
_inherit = 'procurement.order'
_columns = {
'next_delivery_date': fields.datetime('Next Delivery Date',
help="The date of the next delivery for this procurement group, when this group is on the purchase calendar of the orderpoint"),
'next_purchase_date': fields.datetime('Next Purchase Date',
help="The date the next purchase order should be sent to the supplier"),
}
def assign_group_date(self, cr, uid, ids, context=None):
orderpoint_obj = self.pool.get("stock.warehouse.orderpoint")
for procurement in self.browse(cr, uid, ids, context=context):
ops = orderpoint_obj.search(cr, uid, [('location_id', '=', procurement.location_id.id),
('product_id', '=', procurement.product_id.id)], context=context)
if ops and ops[0]:
orderpoint = orderpoint_obj.browse(cr, uid, ops[0], context=context)
date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
purchase_date, delivery_date = self._get_previous_dates(cr, uid, orderpoint, date_planned, context=context)
if purchase_date and delivery_date:
self.write(cr, uid, {'next_delivery_date': self._convert_to_UTC(cr, uid, delivery_date, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'next_purchase_date': self._convert_to_UTC(cr, uid, purchase_date, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT),}, context=context)
def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
"""Return the datetime value to use as Order Date (``date_order``) for the
Purchase Order created to satisfy the given procurement.
:param browse_record procurement: the procurement for which a PO will be created.
:param browse_report company: the company to which the new PO will belong to.
:param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
:rtype: datetime
:return: the desired Order Date for the PO
"""
if procurement.next_purchase_date:
return datetime.strptime(procurement.next_purchase_date, DEFAULT_SERVER_DATETIME_FORMAT)
return super(procurement_order, self)._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None):
"""Return the datetime value to use as Schedule Date (``date_planned``) for the
Purchase Order Lines created to satisfy the given procurement.
:param browse_record procurement: the procurement for which a PO will be created.
:param browse_report company: the company to which the new PO will belong to.
:rtype: datetime
:return: the desired Schedule Date for the PO lines
"""
if procurement.next_delivery_date:
return datetime.strptime(procurement.next_delivery_date, DEFAULT_SERVER_DATETIME_FORMAT)
return super(procurement_order, self)._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
def _prepare_orderpoint_procurement(self, cr, uid, orderpoint, product_qty, date=False, purchase_date = False, group=False, context=None):
return {
'name': orderpoint.name,
'date_planned': date or self._get_orderpoint_date_planned(cr, uid, orderpoint, datetime.today(), context=context),
'next_delivery_date': date,
'next_purchase_date': purchase_date,
'product_id': orderpoint.product_id.id,
'product_qty': product_qty,
'company_id': orderpoint.company_id.id,
'product_uom': orderpoint.product_uom.id,
'location_id': orderpoint.location_id.id,
'origin': orderpoint.name,
'warehouse_id': orderpoint.warehouse_id.id,
'orderpoint_id': orderpoint.id,
'group_id': group or orderpoint.group_id.id,
}
def _get_previous_dates(self, cr, uid, orderpoint, start_date = False, context=None):
"""
Date should be given in utc
"""
calendar_obj = self.pool.get('resource.calendar')
att_obj = self.pool.get('resource.calendar.attendance')
context = context or {}
context['no_round_hours'] = True
# Date should be converted to the correct timezone
start_date = self._convert_to_tz(cr, uid, start_date, context=context)
# First check if the orderpoint has a Calendar as it should be delivered at this calendar date
purchase_date = False
delivery_date = start_date
if orderpoint.calendar_id and orderpoint.calendar_id.attendance_ids:
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, -1, start_date, compute_leaves=True, context=context)
if res and res[0][0] < start_date:
group_to_find = res[0][2] and att_obj.browse(cr, uid, res[0][2], context=context).group_id.id or False
delivery_date = res[0][0]
found_date = delivery_date
if orderpoint.purchase_calendar_id and orderpoint.purchase_calendar_id.attendance_ids:
while not purchase_date:
found_date = found_date + relativedelta(days=-1) # won't allow to deliver within the day
res = calendar_obj._schedule_days(cr, uid, orderpoint.purchase_calendar_id.id, -1, found_date, compute_leaves=True, context=context)
for re in res:
group = re[2] and att_obj.browse(cr, uid, re[2], context=context).group_id.id or False
found_date = re[0]
if not purchase_date and (group_to_find and group_to_find == group or (not group_to_find)):
purchase_date = re[0]
else:
delivery_date = start_date or datetime.utcnow()
return purchase_date, delivery_date
def _get_next_dates(self, cr, uid, orderpoint, new_date=False, group=False, context=None):
calendar_obj = self.pool.get('resource.calendar')
att_obj = self.pool.get('resource.calendar.attendance')
context = context or {}
context['no_round_hours'] = True
if not new_date:
new_date = self._convert_to_tz(cr, uid, datetime.utcnow(), context=context)
now_date = self._convert_to_tz(cr, uid, datetime.utcnow(), context=context)
# Search first calendar day (without group)
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context)
att_group = res and res[0][2] and att_obj.browse(cr, uid, res[0][2], context=context).group_id.id or False
#If hours are smaller than the current date, search a day further
if res and res[0][0] < now_date:
new_date = res[0][1] + relativedelta(days=1)
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context)
for re in res:
att_group = False
if re[2]:
att_group = att_obj.browse(cr, uid, re[2], context=context).group_id.id
if att_group == group:
break
# If you can find an entry and you have a group to match, but it does not, search further until you find one that corresponds
number = 0
while res and group and att_group != group and number < 100:
number += 1
new_date = res[0][1] + relativedelta(days=1)
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context)
att_group = False
for re in res:
if re[2]:
att_group = att_obj.browse(cr, uid, re[2], context=context).group_id.id
if att_group == group:
break
#number as safety pall for endless loops
if number >= 100:
res = False
# If you found a solution(first date), you need the second date until the next delivery because you need to deliver
# everything needed until the second date on the first date
if res:
date1 = res[0][1]
new_date = res[0][1] + relativedelta(days=1)
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context)
if res:
return (date1, res[0][1])
return (False, False)
def _convert_to_tz(self, cr, uid, date, context=None):
if not context or not context.get('tz'):
return date
utc_date = pytz.UTC.localize(date)
timezone = pytz.timezone(context['tz'])
return utc_date.astimezone(timezone)
def _convert_to_UTC(self, cr, uid, date, context=None):
"""
The date should be timezone aware
"""
if not context or not context.get('tz'):
return date
return date.astimezone(pytz.UTC)
def _get_group(self, cr, uid, orderpoint, context=None):
"""
Will return the groups and the end dates of the intervals of the purchase calendar
that need to be executed now.
If a purchase calendar is defined, it should give the
:return [(date, group)]
"""
#Check if orderpoint has last execution date and calculate if we need to calculate again already
calendar_obj = self.pool.get("resource.calendar")
att_obj = self.pool.get("resource.calendar.attendance")
group = False
context = context or {}
context['no_round_hours'] = True
date = False
now_date = self._convert_to_tz(cr, uid, datetime.utcnow(), context=context)
res_intervals = []
if orderpoint.purchase_calendar_id and orderpoint.purchase_calendar_id.attendance_ids:
if orderpoint.last_execution_date:
new_date = datetime.strptime(orderpoint.last_execution_date, DEFAULT_SERVER_DATETIME_FORMAT)
else:
new_date = datetime.utcnow()
# Convert to timezone of user
new_date = self._convert_to_tz(cr, uid, new_date, context=context)
intervals = calendar_obj._schedule_days(cr, uid, orderpoint.purchase_calendar_id.id, 1, new_date, compute_leaves=True, context=context)
for interval in intervals:
# If last execution date, interval should start after it in order not to execute the same orderpoint twice
# TODO: Make the interval a little bigger
if (orderpoint.last_execution_date and (interval[0] > new_date and interval[0] < now_date)) or (not orderpoint.last_execution_date and interval[0] < now_date and interval[1] > now_date):
group = att_obj.browse(cr, uid, interval[2], context=context).group_id.id
date = interval[1]
res_intervals += [(date, group), ]
else:
return [(now_date, None)]
return res_intervals
def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id=False, context=None):
'''
Create procurement based on Orderpoint
:param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement.
This is appropriate for batch jobs only.
'''
if context is None:
context = {}
if use_new_cursor:
cr = openerp.registry(cr.dbname).cursor()
orderpoint_obj = self.pool.get('stock.warehouse.orderpoint')
procurement_obj = self.pool.get('procurement.order')
product_obj = self.pool.get('product.product')
dom = company_id and [('company_id', '=', company_id)] or []
orderpoint_ids = orderpoint_obj.search(cr, uid, dom, order="location_id, purchase_calendar_id, calendar_id")
prev_ids = []
tot_procs = []
while orderpoint_ids:
ids = orderpoint_ids[:1000]
del orderpoint_ids[:1000]
dates_dict = {}
product_dict = {}
ops_dict = {}
ops = orderpoint_obj.browse(cr, uid, ids, context=context)
#Calculate groups that can be executed together
for op in ops:
key = (op.location_id.id, op.purchase_calendar_id.id, op.calendar_id.id)
res_groups=[]
if not dates_dict.get(key):
date_groups = self._get_group(cr, uid, op, context=context)
for date, group in date_groups:
if op.calendar_id and op.calendar_id.attendance_ids:
date1, date2 = self._get_next_dates(cr, uid, op, date, group, context=context)
res_groups += [(group, date1, date2, date)] #date1/date2 as deliveries and date as purchase confirmation date
else:
res_groups += [(group, date, False, date)]
dates_dict[key] = res_groups
product_dict[key] = [op.product_id]
ops_dict[key] = [op]
else:
product_dict[key] += [op.product_id]
ops_dict[key] += [op]
for key in product_dict.keys():
for res_group in dates_dict[key]:
ctx = context.copy()
ctx.update({'location': ops_dict[key][0].location_id.id})
if res_group[2]:
ctx.update({'to_date': res_group[2].strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
prod_qty = product_obj._product_available(cr, uid, [x.id for x in product_dict[key]],
context=ctx)
group = res_group[0]
date = res_group[1]
subtract_qty = orderpoint_obj.subtract_procurements_from_orderpoints(cr, uid, [x.id for x in ops_dict[key]], context=context)
first_op = True
ndelivery = date and self._convert_to_UTC(cr, uid, date, context=context) or False
npurchase = res_group[3] and self._convert_to_UTC(cr, uid, res_group[3], context=context) or False
for op in ops_dict[key]:
try:
prods = prod_qty[op.product_id.id]['virtual_available']
if prods is None:
continue
if float_compare(prods, op.product_min_qty, precision_rounding=op.product_uom.rounding) < 0:
qty = max(op.product_min_qty, op.product_max_qty) - prods
reste = op.qty_multiple > 0 and qty % op.qty_multiple or 0.0
if float_compare(reste, 0.0, precision_rounding=op.product_uom.rounding) > 0:
qty += op.qty_multiple - reste
if float_compare(qty, 0.0, precision_rounding=op.product_uom.rounding) <= 0:
continue
qty -= subtract_qty[op.id]
qty_rounded = float_round(qty, precision_rounding=op.product_uom.rounding)
if qty_rounded > 0:
proc_id = procurement_obj.create(cr, uid,
self._prepare_orderpoint_procurement(cr, uid, op, qty_rounded, date=ndelivery, purchase_date=npurchase, group=group, context=context),
context=context)
tot_procs.append(proc_id)
orderpoint_obj.write(cr, uid, [op.id], {'last_execution_date': datetime.utcnow().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
if use_new_cursor:
cr.commit()
except OperationalError:
if use_new_cursor:
orderpoint_ids.append(op.id)
cr.rollback()
continue
else:
raise
try:
tot_procs.reverse()
self.run(cr, uid, tot_procs, context=context)
tot_procs = []
if use_new_cursor:
cr.commit()
except OperationalError:
if use_new_cursor:
cr.rollback()
continue
else:
raise
if use_new_cursor:
cr.commit()
if prev_ids == ids:
break
else:
prev_ids = ids
if use_new_cursor:
cr.commit()
cr.close()
return {}