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
##############################################################################
22
from osv import osv, fields
23
from datetime import datetime
24
from tools.translate import _
25
from mx.DateTime import *
33
class procurement_order(osv.osv):
34
_name = 'procurement.order'
35
_inherit = 'procurement.order'
37
def run_automatic_cycle(self, cr, uid, use_new_cursor=False, context={}):
39
Create procurement on fixed date
42
cr = pooler.get_db(use_new_cursor).cursor()
44
request_obj = self.pool.get('res.request')
45
cycle_obj = self.pool.get('stock.warehouse.order.cycle')
46
proc_obj = self.pool.get('procurement.order')
47
product_obj = self.pool.get('product.product')
48
freq_obj = self.pool.get('stock.frequence')
50
start_date = datetime.now()
52
cycle_ids = cycle_obj.search(cr, uid, [('next_date', '<=', start_date.strftime('%Y-%m-%d'))])
58
# Cache for product/location
62
# We start with only category Automatic Supply
63
for cycle in cycle_obj.browse(cr, uid, cycle_ids):
64
# We define the replenish location
66
if not cycle.location_id or not cycle.location_id.id:
67
location_id = cycle.warehouse_id.lot_input_id.id
69
location_id = cycle.location_id.id
71
d_values = {'leadtime': cycle.leadtime,
72
'coverage': cycle.order_coverage,
73
'safety_time': cycle.safety_stock_time,
74
'safety': cycle.safety_stock,
75
'past_consumption': cycle.past_consumption,
76
'reviewed_consumption': cycle.reviewed_consumption,
77
'manual_consumption': cycle.manual_consumption,}
79
if not cycle.product_id:
81
for p in cycle.product_ids:
82
not_products.append(p.id)
84
product_ids = product_obj.search(cr, uid, [('categ_id', 'child_of', cycle.category_id.id), ('id', 'not in', not_products)])
86
for product in product_obj.browse(cr, uid, product_ids):
87
proc_id = self.create_proc_cycle(cr, uid, cycle, product.id, location_id, d_values, cache=cache)
91
created_proc.append(proc_id)
93
proc_id = self.create_proc_cycle(cr, uid, cycle, cycle.product_id.id, location_id, d_values, cache=cache)
96
created_proc.append(proc_id)
98
if cycle.frequence_id:
99
freq_obj.write(cr, uid, cycle.frequence_id.id, {'last_run': start_date.strftime('%Y-%m-%d')})
102
for proc in proc_obj.browse(cr, uid, created_proc):
103
if proc.state == 'exception':
104
report.append('PROC %d: from stock - %3.2f %-5s - %s' % \
105
(proc.id, proc.product_qty, proc.product_uom.name,
106
proc.product_id.name,))
109
end_date = datetime.now()
111
summary = '''Here is the procurement scheduling report for Order Cycle
115
Total Procurements processed: %d
116
Procurements with exceptions: %d
117
\n'''% (start_date, end_date, len(created_proc), report_except)
118
summary += '\n'.join(report)
119
req_id = request_obj.create(cr, uid,
120
{'name': "Procurement Processing Report.",
126
request_obj.request_send(cr, uid, [req_id])
134
def create_proc_cycle(self, cr, uid, cycle, product_id, location_id, d_values={}, cache={}, context={}):
136
Creates a procurement order for a product and a location
138
proc_obj = self.pool.get('procurement.order')
139
cycle_obj = self.pool.get('stock.warehouse.order.cycle')
140
product_obj = self.pool.get('product.product')
141
wf_service = netsvc.LocalService("workflow")
145
if isinstance(product_id, (int, long)):
146
product_id = [product_id]
148
product = product_obj.browse(cr, uid, product_id[0])
150
# Enter the stock location in cache to know which products has been already replenish for this location
151
if not cache.get(location_id, False):
152
cache.update({location_id: []})
154
# If a rule already exist for the category of the product or for the product
155
# itself for the same location, we don't create a procurement order
156
#cycle_ids = cycle_obj.search(cr, uid, [('category_id', '=', product.categ_id.id), ('product_id', '=', False), ('location_id', '=', location_id), ('id', '!=', cycle.id)])
157
#cycle2_ids = cycle_obj.search(cr, uid, [('product_id', '=', product.id), ('location_id', '=', location_id), ('id', '!=', cycle.id)])
159
# cr.execute('''SELECT order_cycle_id
160
# FROM order_cycle_product_rel
161
# WHERE order_cycle_id in %s
162
# AND product_id = %s''', (tuple(cycle_ids), product.id))
163
# res = cr.fetchall()
165
# cycle_ids.remove(r[0])
166
#if cycle2_ids or cycle_ids:
170
if product.id not in cache.get(location_id):
171
newdate = datetime.today()
172
quantity_to_order = self._compute_quantity(cr, uid, cycle, product.id, location_id, d_values)
174
if quantity_to_order <= 0:
177
proc_id = proc_obj.create(cr, uid, {
178
'name': _('Automatic Supply: %s') % (cycle.name,),
179
'origin': cycle.name,
180
'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
181
'product_id': product.id,
182
'product_qty': quantity_to_order,
183
'product_uom': product.uom_id.id,
184
'location_id': location_id,
185
'procure_method': 'make_to_order',
187
wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
188
wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_check', cr)
189
context.update({'button': 'scheduler'})
190
cycle_obj.write(cr, uid, [cycle.id], {'procurement_id': proc_id}, context=context)
193
cache.get(location_id).append(product.id)
197
def _compute_quantity(self, cr, uid, cycle_id, product_id, location_id, d_values={}, context={}):
199
Compute the quantity of product to order like thid :
200
[Delivery lead time (from supplier tab of the product or by default or manually overwritten) x Monthly Consumption]
201
+ Order coverage (number of months : 3 by default, manually overwritten) x Monthly consumption
202
- Projected available quantity
204
product_obj = self.pool.get('product.product')
205
supplier_info_obj = self.pool.get('product.supplierinfo')
206
location_obj = self.pool.get('stock.location')
207
cycle_obj = self.pool.get('stock.warehouse.order.cycle')
209
product = product_obj.browse(cr, uid, product_id)
210
location = location_obj.browse(cr, uid, location_id)
213
# Get the delivery lead time
214
delivery_leadtime = product.procure_delay and product.procure_delay/30.0 or 1
215
if 'leadtime' in d_values and d_values.get('leadtime', 0.00) != 0.00:
216
delivery_leadtime = d_values.get('leadtime')
219
for supplier_info in product.seller_ids:
220
if sequence and supplier_info.sequence < sequence:
221
sequence = supplier_info.sequence
222
delivery_leadtime = supplier_info.delay/30.0
224
sequence = supplier_info.sequence
225
delivery_leadtime = supplier_info.delay/30.0
227
# Get the monthly consumption
228
monthly_consumption = 1.0
230
if cycle_id.product_id and cycle_id.product_id.id and d_values.get('manual_consumption', 0.00) != 0.00:
231
monthly_consumption = d_values.get('manual_consumption')
232
elif 'reviewed_consumption' in d_values and d_values.get('reviewed_consumption'):
233
monthly_consumption = product.reviewed_consumption
235
monthly_consumption = product.monthly_consumption
237
# Get the order coverage
238
order_coverage = d_values.get('coverage', 3)
240
# Get the projected available quantity
241
available_qty = self.get_available(cr, uid, product_id, location_id, monthly_consumption, d_values)
243
return (delivery_leadtime * monthly_consumption) + (order_coverage * monthly_consumption) - available_qty
246
def get_available(self, cr, uid, product_id, location_id, monthly_consumption, d_values={}, context={}):
248
Compute the projected available quantity like this :
249
Available stock (real stock - picked reservation)
250
+ Quantity on order ("in pipe")
251
- Safety stock [blank by default but can be overwritten for a product category or at product level]
252
- Safety time [= X (= 0 by default) month x Monthly consumption (validated consumption by default or
253
manually overwritten for a product or at product level)]
256
product_obj = self.pool.get('product.product')
257
location_obj = self.pool.get('stock.location')
258
move_obj = self.pool.get('stock.move')
260
context.update({'location': location_id,
261
'compute_child': True,
262
'from_date': time.strftime('%Y-%m-%d')})
264
product = product_obj.browse(cr, uid, product_id, context=context)
266
''' Set this part of algorithm as comments because this algorithm seems to be equal to virtual stock
268
To do validate by Magali
270
Picked reservation will be developed on future sprint
273
# Get the available stock
275
# real_stock = product_obj.get_product_available(cr, uid, [product_id], context={'states': ['done'],
277
# 'location': location_id,
278
# 'compute_child': True,
279
# 'from_date': time.strftime('%Y-%m-%d')})
280
# # Get the picked reservation
281
# ## TODO: To confirm by Magali
282
# picked_reservation = 0.00
284
# for location in location_obj.search(cr, uid, [('location_id', 'child_of', [location_id])]):
285
# for move_id in move_obj.search(cr, uid, [('product_id', '=', product_id), ('location_dest_id', '=', location),
286
# ('state', '!=', 'draft'), ('move_dest_id', '!=', False)]):
287
# move_ids.append(move_id)
289
# for move in move_obj.browse(cr, uid, move_ids):
290
# picked_reservation += move.product_qty
292
# available_stock = real_stock.get(product_id) - picked_reservation
294
# # Get the quantity on order
295
# ## TODO : To confirm by Magali
296
# quantity_on_order = 0.00
298
# for location in location_obj.search(cr, uid, [('location_id', 'child_of', [location_id])]):
299
# for move_id in move_obj.search(cr, uid, [('product_id', '=', product_id), ('location_dest_id', '=', location)]):
300
# move_ids.append(move_id)
302
# for move in move_obj.browse(cr, uid, move_ids):
303
# quantity_on_order += move.product_qty
305
# Get the safety stock
306
safety_stock = d_values.get('safety', 0)
308
# Get the safety time
309
safety_time = d_values.get('safety_time', 0)
311
# Get the expiry quantity
312
# Set as comment because expiry quantity will be developed in a future sprint
313
# expiry_quantity = self.get_expiry_qty(cr, uid, product_id, location_id, monthly_consumption, d_values)
314
expiry_quantity = 0.00
316
# Set this part of algorithm as comments because this algorithm seems to be equal to virtual stock
317
# return available_stock + quantity_on_order - safety_stock - (safety_time * monthly_consumption) - expiry_quantity
319
return product.virtual_available - safety_stock - (safety_time * monthly_consumption) - expiry_quantity
322
def get_expiry_qty(self, cr, uid, product_id, location_id, monthly_consumption, d_values={}, context={}):
324
Compute the expiry quantities
326
INFO : This method is not use on Sprint1 because the algorithm is
329
product_obj = self.pool.get('product.product')
330
stock_obj = self.pool.get('stock.location')
331
batch_obj = self.pool.get('stock.production.lot')
332
move_obj = self.pool.get('stock.move')
336
location_ids = stock_obj.search(cr, uid, [('location_id', 'child_of', location_id)])
337
available_stock = 0.00
339
# Get all batches for this product
340
batch_ids = batch_obj.search(cr, uid, [('product_id', '=', product_id)], offset=0, limit=None, order='life_date')
341
if len(batch_ids) == 1:
342
# Search all moves with this batch number
343
for location in location_ids:
344
context.update({'location_id': location})
345
available_stock += batch_obj.browse(cr, uid, batch_ids, context=context)[0].stock_available
346
expiry_date = batch_obj.browse(cr, uid, batch_ids)[0].life_date or time.strftime('%Y-%m-%d')
347
nb_month = self.get_diff_date(expiry_date)
348
res = available_stock - (nb_month * monthly_consumption)
350
# Get the stock available for the product
351
for location in location_ids:
352
context.update({'location_id': location})
353
for batch in batch_obj.browse(cr, uid, batch_ids, context=context):
354
available_stock += batch.stock_available
359
for batch in batch_obj.browse(cr, uid, batch_ids):
360
nb_month = self.get_diff_date(batch.life_date)
361
if (nb_month - sum_nb_month) > 0:
362
tmp_qty = (nb_month - sum_nb_month) * monthly_consumption
363
res += available_stock - (last_nb_month * monthly_consumption) - tmp_qty
369
def get_diff_date(self, date):
371
Returns the number of month between the date in parameter and today
373
date = Parser.DateFromString(date)
376
# The batch is expired
377
if date.year < today.year or (date.year == today.year and date.month < today.month):
380
# The batch expires this month
381
if date.year == today.year and date.month == today.month:
384
# The batch expires in one month
385
if date.year == today.year and date.month == today.month+1 and date.day >= today.day:
388
# Compute the number of months
390
nb_month += (date.year - today.year) * 12
391
nb_month += date.month - today.month
392
if date.day < today.day:
399
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: