~unifield-team/unifield-wm/us-826

« back to all changes in this revision

Viewing changes to procurement_cycle/scheduler.py

  • Committer: jf
  • Date: 2011-03-23 13:23:55 UTC
  • Revision ID: jf@tempo4-20110323132355-agyf1soy7m5ewatr
Initial Import

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
##############################################################################
3
 
#
4
 
#    OpenERP, Open Source Management Solution
5
 
#    Copyright (C) 2011 TeMPO Consulting, MSF 
6
 
#
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.
11
 
#
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.
16
 
#
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/>.
19
 
#
20
 
##############################################################################
21
 
 
22
 
from osv import osv, fields
23
 
from datetime import datetime
24
 
from tools.translate import _
25
 
from mx.DateTime import *
26
 
 
27
 
import time
28
 
import pooler
29
 
import netsvc
30
 
 
31
 
 
32
 
 
33
 
class procurement_order(osv.osv):
34
 
    _name = 'procurement.order'
35
 
    _inherit = 'procurement.order'
36
 
    
37
 
    def run_automatic_cycle(self, cr, uid, use_new_cursor=False, context={}):
38
 
        '''
39
 
        Create procurement on fixed date
40
 
        '''
41
 
        if use_new_cursor:
42
 
            cr = pooler.get_db(use_new_cursor).cursor()
43
 
            
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')
49
 
 
50
 
        start_date = datetime.now()
51
 
        
52
 
        cycle_ids = cycle_obj.search(cr, uid, [('next_date', '<=', start_date.strftime('%Y-%m-%d'))])
53
 
        
54
 
        created_proc = []
55
 
        report = []
56
 
        report_except = 0
57
 
        
58
 
        # Cache for product/location
59
 
        cache = {}
60
 
 
61
 
        
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
65
 
            location_id = False
66
 
            if not cycle.location_id or not cycle.location_id.id:
67
 
                location_id = cycle.warehouse_id.lot_input_id.id
68
 
            else:
69
 
                location_id = cycle.location_id.id
70
 
                
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,}
78
 
 
79
 
            if not cycle.product_id:
80
 
                not_products = []
81
 
                for p in cycle.product_ids:
82
 
                    not_products.append(p.id)
83
 
 
84
 
                product_ids = product_obj.search(cr, uid, [('categ_id', 'child_of', cycle.category_id.id), ('id', 'not in', not_products)])
85
 
                
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)
88
 
                    
89
 
 
90
 
                    if proc_id:
91
 
                        created_proc.append(proc_id)
92
 
            else:
93
 
                proc_id = self.create_proc_cycle(cr, uid, cycle, cycle.product_id.id, location_id, d_values, cache=cache)
94
 
                
95
 
                if proc_id:
96
 
                    created_proc.append(proc_id)
97
 
        
98
 
            if cycle.frequence_id:
99
 
                freq_obj.write(cr, uid, cycle.frequence_id.id, {'last_run': start_date.strftime('%Y-%m-%d')})
100
 
 
101
 
                    
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,))
107
 
                report_except += 1
108
 
                
109
 
        end_date = datetime.now()
110
 
                
111
 
        summary = '''Here is the procurement scheduling report for Order Cycle
112
 
 
113
 
        Start Time: %s
114
 
        End Time: %s
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.",
121
 
                 'act_from': uid,
122
 
                 'act_to': uid,
123
 
                 'body': summary,
124
 
                })
125
 
        if req_id:
126
 
            request_obj.request_send(cr, uid, [req_id])
127
 
        
128
 
        if use_new_cursor:
129
 
            cr.commit()
130
 
            cr.close()
131
 
            
132
 
        return {}
133
 
    
134
 
    def create_proc_cycle(self, cr, uid, cycle, product_id, location_id, d_values={}, cache={}, context={}):
135
 
        '''
136
 
        Creates a procurement order for a product and a location
137
 
        '''
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")
142
 
        report = []
143
 
        proc_id = False
144
 
        
145
 
        if isinstance(product_id, (int, long)):
146
 
            product_id = [product_id]
147
 
        
148
 
        product = product_obj.browse(cr, uid, product_id[0])
149
 
        
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: []})
153
 
        
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)])
158
 
        #if cycle_ids:
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()
164
 
        #    for r in res:
165
 
        #        cycle_ids.remove(r[0])
166
 
        #if cycle2_ids or cycle_ids:
167
 
        #    return False
168
 
        
169
 
            
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)
173
 
                
174
 
            if quantity_to_order <= 0:
175
 
                return False
176
 
            else:
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',
186
 
                })
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)
191
 
            
192
 
            # Fill the cache
193
 
            cache.get(location_id).append(product.id)
194
 
        
195
 
        return proc_id
196
 
    
197
 
    def _compute_quantity(self, cr, uid, cycle_id, product_id, location_id, d_values={}, context={}):
198
 
        '''
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
203
 
        '''
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')
208
 
        
209
 
        product = product_obj.browse(cr, uid, product_id)
210
 
        location = location_obj.browse(cr, uid, location_id)
211
 
 
212
 
        
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')
217
 
        else:
218
 
            sequence = False
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
223
 
                elif not sequence:
224
 
                    sequence = supplier_info.sequence
225
 
                    delivery_leadtime = supplier_info.delay/30.0
226
 
                
227
 
        # Get the monthly consumption
228
 
        monthly_consumption = 1.0
229
 
        
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
234
 
        else:
235
 
            monthly_consumption = product.monthly_consumption
236
 
            
237
 
        # Get the order coverage
238
 
        order_coverage = d_values.get('coverage', 3)
239
 
        
240
 
        # Get the projected available quantity
241
 
        available_qty = self.get_available(cr, uid, product_id, location_id, monthly_consumption, d_values)
242
 
        
243
 
        return (delivery_leadtime * monthly_consumption) + (order_coverage * monthly_consumption) - available_qty
244
 
        
245
 
        
246
 
    def get_available(self, cr, uid, product_id, location_id, monthly_consumption, d_values={}, context={}):
247
 
        '''
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)]
254
 
            - Expiry quantities.
255
 
        '''
256
 
        product_obj = self.pool.get('product.product')
257
 
        location_obj = self.pool.get('stock.location')
258
 
        move_obj = self.pool.get('stock.move')
259
 
        
260
 
        context.update({'location': location_id,
261
 
                        'compute_child': True, 
262
 
                        'from_date': time.strftime('%Y-%m-%d')})
263
 
        
264
 
        product = product_obj.browse(cr, uid, product_id, context=context)
265
 
        
266
 
        ''' Set this part of algorithm as comments because this algorithm seems to be equal to virtual stock
267
 
        
268
 
            To do validate by Magali
269
 
            
270
 
            Picked reservation will be developed on future sprint
271
 
        ''' 
272
 
        
273
 
        # Get the available stock
274
 
        # Get the real stock
275
 
#        real_stock = product_obj.get_product_available(cr, uid, [product_id], context={'states': ['done'],
276
 
#                                                                                       'what': 'in', 
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
283
 
#        move_ids = []
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)
288
 
#            
289
 
#        for move in move_obj.browse(cr, uid, move_ids):
290
 
#            picked_reservation += move.product_qty
291
 
#        
292
 
#        available_stock = real_stock.get(product_id) - picked_reservation
293
 
#        
294
 
#        # Get the quantity on order
295
 
#        ## TODO : To confirm by Magali
296
 
#        quantity_on_order = 0.00
297
 
#        move_ids = []
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)
301
 
#            
302
 
#        for move in move_obj.browse(cr, uid, move_ids):
303
 
#            quantity_on_order += move.product_qty
304
 
            
305
 
        # Get the safety stock
306
 
        safety_stock = d_values.get('safety', 0)
307
 
        
308
 
        # Get the safety time
309
 
        safety_time = d_values.get('safety_time', 0)
310
 
        
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
315
 
        
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
318
 
 
319
 
        return product.virtual_available - safety_stock - (safety_time * monthly_consumption) - expiry_quantity
320
 
     
321
 
     
322
 
    def get_expiry_qty(self, cr, uid, product_id, location_id, monthly_consumption, d_values={}, context={}):
323
 
        '''
324
 
        Compute the expiry quantities
325
 
        
326
 
        INFO : This method is not use on Sprint1 because the algorithm is
327
 
        not determined
328
 
        '''
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')
333
 
        
334
 
        res = 0.00
335
 
        
336
 
        location_ids = stock_obj.search(cr, uid, [('location_id', 'child_of', location_id)])
337
 
        available_stock = 0.00
338
 
        
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)
349
 
        else:
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
355
 
                    
356
 
            last_nb_month = 0
357
 
            sum_nb_month = 0
358
 
            res = 0
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
364
 
                else:
365
 
                    break 
366
 
            
367
 
        return res
368
 
    
369
 
    def get_diff_date(self, date):
370
 
        '''
371
 
        Returns the number of month between the date in parameter and today
372
 
        '''
373
 
        date = Parser.DateFromString(date)
374
 
        today = today()
375
 
        
376
 
        # The batch is expired
377
 
        if date.year < today.year or (date.year == today.year and date.month < today.month):
378
 
            return 0 
379
 
        
380
 
        # The batch expires this month
381
 
        if date.year == today.year and date.month == today.month:
382
 
            return 0
383
 
        
384
 
        # The batch expires in one month
385
 
        if date.year == today.year and date.month == today.month+1 and date.day >= today.day:
386
 
            return 0
387
 
        
388
 
        # Compute the number of months
389
 
        nb_month = 0
390
 
        nb_month += (date.year - today.year) * 12
391
 
        nb_month += date.month - today.month
392
 
        if date.day < today.day:
393
 
            nb_month -= 1
394
 
            
395
 
        return nb_month
396
 
        
397
 
procurement_order()
398
 
 
399
 
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: