1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# OpenERP, Open Source Management Solution
5
# Copyright (C) 2011 TeMPO Consulting, MSF
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Affero General Public License as
9
# published by the Free Software Foundation, either version 3 of the
10
# License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Affero General Public License for more details.
17
# You should have received a copy of the GNU Affero General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
##############################################################################
23
from datetime import datetime
24
from tools.translate import _
25
from mx.DateTime import RelativeDate
26
from mx.DateTime import now
27
from mx.DateTime import Parser
35
class procurement_order(osv.osv):
36
_name = 'procurement.order'
37
_inherit = 'procurement.order'
39
def run_automatic_cycle(self, cr, uid, use_new_cursor=False, batch_id=False, context=None):
41
Create procurement on fixed date
44
cr = pooler.get_db(use_new_cursor).cursor()
46
request_obj = self.pool.get('res.request')
47
cycle_obj = self.pool.get('stock.warehouse.order.cycle')
48
proc_obj = self.pool.get('procurement.order')
49
freq_obj = self.pool.get('stock.frequence')
51
start_date = time.strftime('%Y-%m-%d %H:%M:%S')
53
cycle_ids = cycle_obj.search(cr, uid, [('next_date', '<=', datetime.now())])
60
# We start with only category Automatic Supply
61
for cycle in cycle_obj.browse(cr, uid, cycle_ids):
62
# We define the replenish location
64
if not cycle.location_id or not cycle.location_id.id:
65
location_id = cycle.warehouse_id.lot_input_id.id
67
location_id = cycle.location_id.id
69
d_values = {'leadtime': cycle.leadtime,
70
'coverage': cycle.order_coverage,
71
'safety_time': cycle.safety_stock_time,
72
'consumption_period_from': cycle.consumption_period_from,
73
'consumption_period_to': cycle.consumption_period_to,
74
'past_consumption': cycle.past_consumption,
75
'reviewed_consumption': cycle.reviewed_consumption,
76
'manual_consumption': cycle.manual_consumption,}
79
ran_proc.append(cycle.id)
80
for line in cycle.product_ids:
81
# Update the safety stock according to the safety stock defined in the line
82
d_values.update({'safety_stock': line.safety_stock})
83
proc_id = self.create_proc_cycle(cr, uid, cycle, line.product_id.id, location_id, d_values)
86
created_proc.append(proc_id)
88
if cycle.frequence_id:
89
freq_obj.write(cr, uid, cycle.frequence_id.id, {'last_run': datetime.now()})
91
created_doc = '''################################
92
Created documents : \n'''
94
for proc in proc_obj.browse(cr, uid, created_proc):
95
if proc.state == 'exception':
96
report.append('PROC %d: from stock - %3.2f %-5s - %s' % \
97
(proc.id, proc.product_qty, proc.product_uom.name,
98
proc.product_id.name,))
100
elif proc.purchase_id:
101
created_doc += " * %s => %s \n" % (proc.name, proc.purchase_id.name)
103
end_date = time.strftime('%Y-%m-%d %H:%M:%S')
105
summary = '''Here is the procurement scheduling report for Order Cycle
109
Total Rules processed: %d
110
Procurements with exceptions: %d
111
\n %s \n Exceptions: \n'''% (start_date, end_date, len(ran_proc), report_except, len(created_proc) > 0 and created_doc or '')
112
summary += '\n'.join(report)
114
self.pool.get('procurement.batch.cron').write(cr, uid, batch_id, {'last_run_on': time.strftime('%Y-%m-%d %H:%M:%S')})
115
old_request = request_obj.search(cr, uid, [('batch_id', '=', batch_id), ('name', '=', 'Procurement Processing Report (Order cycle).')])
116
request_obj.write(cr, uid, old_request, {'batch_id': False})
118
request_obj.create(cr, uid,
119
{'name': "Procurement Processing Report (Order cycle).",
122
'batch_id': batch_id,
125
# UF-952 : Requests should be in consistent state
127
# request_obj.request_send(cr, uid, [req_id])
135
def create_proc_cycle(self, cr, uid, cycle, product_id, location_id, d_values=None, context=None):
137
Creates a procurement order for a product and a location
139
proc_obj = self.pool.get('procurement.order')
140
cycle_obj = self.pool.get('stock.warehouse.order.cycle')
141
product_obj = self.pool.get('product.product')
142
wf_service = netsvc.LocalService("workflow")
150
if isinstance(product_id, (int, long)):
151
product_id = [product_id]
153
if d_values.get('past_consumption', False):
154
# If the AMC should be used, compute the period of calculation
155
if not d_values.get('consumption_period_from', False):
156
order_coverage = d_values.get('coverage', 3)
157
d_values.update({'consumption_period_from': (now() + RelativeDate(day=1, months=-round(order_coverage, 1)+1)).strftime('%Y-%m-%d')})
158
if not d_values.get('consumption_period_to', False):
159
d_values.update({'consumption_period_to': (now() + RelativeDate(days=-1, day=1, months=1)).strftime('%Y-%m-%d')})
160
context.update({'from_date': d_values.get('consumption_period_from'), 'to_date': d_values.get('consumption_period_to')})
162
product = product_obj.browse(cr, uid, product_id[0], context=context)
164
newdate = datetime.today()
165
quantity_to_order = self._compute_quantity(cr, uid, cycle, product, location_id, d_values, context=context)
167
# Create a procurement only if the quantity to order is more than 0.00
168
if quantity_to_order <= 0.00:
171
proc_id = proc_obj.create(cr, uid, {
172
'name': _('Procurement cycle: %s') % (cycle.name,),
173
'origin': cycle.name,
174
'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
175
'product_id': product.id,
176
'product_qty': quantity_to_order,
177
'product_uom': product.uom_id.id,
178
'location_id': location_id,
179
'procure_method': 'make_to_order',
181
# Confirm the procurement order
182
wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
183
wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_check', cr)
184
context.update({'button': 'scheduler'})
185
cycle_obj.write(cr, uid, [cycle.id], {'procurement_id': proc_id}, context=context)
189
def _compute_quantity(self, cr, uid, cycle_id, product, location_id, d_values=None, context=None):
191
Compute the quantity of product to order like thid :
192
[Delivery lead time (from supplier tab of the product or by default or manually overwritten) x Monthly Consumption]
193
+ Order coverage (number of months : 3 by default, manually overwritten) x Monthly consumption
194
- Projected available quantity
199
# Get the delivery lead time of the product if the leadtime is not defined in rule and no supplier found in product form
200
delivery_leadtime = product.procure_delay and round(int(product.procure_delay)/30.0, 2) or 1
201
# Get the leadtime of the rule if defined
202
if 'leadtime' in d_values and d_values.get('leadtime', 0.00) != 0.00:
203
delivery_leadtime = d_values.get('leadtime')
204
elif product.seller_ids:
205
# Get the supplier lead time if supplier is defined
206
# The seller delay is defined in days, so divide it by 30.0 to have a LT in months
207
delivery_leadtime = product.seller_delay and round(int(product.seller_delay)/30.0, 2) or 1
209
# Get the monthly consumption
210
monthly_consumption = 0.00
212
if 'reviewed_consumption' in d_values and d_values.get('reviewed_consumption'):
213
monthly_consumption = product.reviewed_consumption
214
elif 'past_consumption' in d_values and d_values.get('past_consumption'):
215
monthly_consumption = product.product_amc
217
monthly_consumption = d_values.get('manual_consumption', 0.00)
219
# Get the order coverage
220
order_coverage = d_values.get('coverage', 0.00)
222
# Get the projected available quantity
223
available_qty = self.get_available(cr, uid, product.id, location_id, monthly_consumption, d_values)
225
qty_to_order = (delivery_leadtime * monthly_consumption) + (order_coverage * monthly_consumption) - available_qty
227
return round(self.pool.get('product.uom')._compute_qty(cr, uid, product.uom_id.id, qty_to_order, product.uom_id.id), 2)
230
def get_available(self, cr, uid, product_id, location_id, monthly_consumption, d_values=None, context=None):
232
Compute the projected available quantity like this :
233
Available stock (real stock - picked reservation)
234
+ Quantity on order ("in pipe")
235
- Safety stock [blank by default but can be overwritten for a product category or at product level]
236
- Safety time [= X (= 0 by default) month x Monthly consumption (validated consumption by default or
237
manually overwritten for a product or at product level)]
245
product_obj = self.pool.get('product.product')
247
context.update({'location': location_id,
248
'compute_child': True, })
250
product = product_obj.browse(cr, uid, product_id, context=context)
252
''' Set this part of algorithm as comment because this algorithm seems to be equal to virtual stock
254
To do validate by Magali
256
Picked reservation will be developed on future sprint
259
# Get the available stock
261
picked_resa = product_obj.get_product_available(cr, uid, [product_id], context={'states': ['assigned'],
263
'location': location_id,
264
'compute_child': True,})
266
available_stock = product.qty_available + picked_resa.get(product.id)
268
quantity_on_order = product_obj.get_product_available(cr, uid, [product_id], context={'states': ['confirmed'],
270
'location': location_id,
271
'compute_child': True,})
273
# Get the safety stock
274
safety_stock = d_values.get('safety_stock', 0)
276
# Get the safety time
277
safety_time = d_values.get('safety_time', 0)
279
# Get the expiry quantity
280
expiry_quantity = product_obj.get_expiry_qty(cr, uid, product_id, location_id, monthly_consumption, d_values, context=context)
281
expiry_quantity = expiry_quantity and expiry_quantity or 0.00
284
# Set this part of algorithm as comments because this algorithm seems to be equal to virtual stock
285
# return product.virtual_available - safety_stock - (safety_time * monthly_consumption) - expiry_quantity
286
return available_stock + quantity_on_order.get(product.id) - safety_stock - (safety_time * monthly_consumption) - expiry_quantity
288
def get_diff_date(self, date):
290
Returns the number of month between the date in parameter and today
292
date = Parser.DateFromString(date)
293
today = datetime.today()
295
# The batch is expired
296
if date.year < today.year or (date.year == today.year and date.month < today.month):
299
# The batch expires this month
300
if date.year == today.year and date.month == today.month:
303
# The batch expires in one month
304
if date.year == today.year and date.month == today.month+1 and date.day >= today.day:
307
# Compute the number of months
309
nb_month += (date.year - today.year) * 12
310
nb_month += date.month - today.month
311
if date.day < today.day:
318
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: