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

« back to all changes in this revision

Viewing changes to procurement_cycle/scheduler.py

UF-73: [MERGE] Merge with unifield-wm branch

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
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
28
 
 
29
 
import time
30
 
import pooler
31
 
import netsvc
32
 
 
33
 
 
34
 
 
35
 
class procurement_order(osv.osv):
36
 
    _name = 'procurement.order'
37
 
    _inherit = 'procurement.order'
38
 
    
39
 
    def run_automatic_cycle(self, cr, uid, use_new_cursor=False, batch_id=False, context=None):
40
 
        '''
41
 
        Create procurement on fixed date
42
 
        '''
43
 
        if use_new_cursor:
44
 
            cr = pooler.get_db(use_new_cursor).cursor()
45
 
            
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')
50
 
 
51
 
        start_date = time.strftime('%Y-%m-%d %H:%M:%S')
52
 
        
53
 
        cycle_ids = cycle_obj.search(cr, uid, [('next_date', '<=', datetime.now())])
54
 
        
55
 
        created_proc = []
56
 
        report = []
57
 
        report_except = 0
58
 
        ran_proc = []
59
 
        
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
63
 
            location_id = False
64
 
            if not cycle.location_id or not cycle.location_id.id:
65
 
                location_id = cycle.warehouse_id.lot_input_id.id
66
 
            else:
67
 
                location_id = cycle.location_id.id
68
 
                
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,}
77
 
 
78
 
            if cycle.product_ids:
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)
84
 
 
85
 
                    if proc_id:
86
 
                        created_proc.append(proc_id)
87
 
            
88
 
            if cycle.frequence_id:
89
 
                freq_obj.write(cr, uid, cycle.frequence_id.id, {'last_run': datetime.now()})
90
 
 
91
 
        created_doc = '''################################
92
 
Created documents : \n'''
93
 
                    
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,))
99
 
                report_except += 1
100
 
            elif proc.purchase_id:
101
 
                created_doc += "    * %s => %s \n" % (proc.name, proc.purchase_id.name)
102
 
                
103
 
        end_date = time.strftime('%Y-%m-%d %H:%M:%S')
104
 
                
105
 
        summary = '''Here is the procurement scheduling report for Order Cycle
106
 
 
107
 
        Start Time: %s
108
 
        End Time: %s
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)
113
 
        if batch_id:
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})
117
 
        
118
 
        request_obj.create(cr, uid,
119
 
                {'name': "Procurement Processing Report (Order cycle).",
120
 
                 'act_from': uid,
121
 
                 'act_to': uid,
122
 
                 'batch_id': batch_id,
123
 
                 'body': summary,
124
 
                })
125
 
        # UF-952 : Requests should be in consistent state
126
 
#        if req_id:
127
 
#            request_obj.request_send(cr, uid, [req_id])
128
 
        
129
 
        if use_new_cursor:
130
 
            cr.commit()
131
 
            cr.close()
132
 
            
133
 
        return {}
134
 
    
135
 
    def create_proc_cycle(self, cr, uid, cycle, product_id, location_id, d_values=None, context=None):
136
 
        '''
137
 
        Creates a procurement order for a product and a location
138
 
        '''
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")
143
 
        proc_id = False
144
 
       
145
 
        if context is None:
146
 
            context = {}
147
 
        if d_values is None:
148
 
            d_values = {}
149
 
 
150
 
        if isinstance(product_id, (int, long)):
151
 
            product_id = [product_id]
152
 
            
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')})
161
 
        
162
 
        product = product_obj.browse(cr, uid, product_id[0], context=context)
163
 
            
164
 
        newdate = datetime.today()
165
 
        quantity_to_order = self._compute_quantity(cr, uid, cycle, product, location_id, d_values, context=context)
166
 
            
167
 
        # Create a procurement only if the quantity to order is more than 0.00
168
 
        if quantity_to_order <= 0.00:
169
 
            return False
170
 
        else:
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',
180
 
            })
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)
186
 
        
187
 
        return proc_id
188
 
    
189
 
    def _compute_quantity(self, cr, uid, cycle_id, product, location_id, d_values=None, context=None):
190
 
        '''
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
195
 
        '''
196
 
        if d_values is None:
197
 
            d_values = {}
198
 
        
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
208
 
                
209
 
        # Get the monthly consumption
210
 
        monthly_consumption = 0.00
211
 
        
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
216
 
        else:
217
 
            monthly_consumption = d_values.get('manual_consumption', 0.00)
218
 
            
219
 
        # Get the order coverage
220
 
        order_coverage = d_values.get('coverage', 0.00)
221
 
        
222
 
        # Get the projected available quantity
223
 
        available_qty = self.get_available(cr, uid, product.id, location_id, monthly_consumption, d_values)
224
 
        
225
 
        qty_to_order = (delivery_leadtime * monthly_consumption) + (order_coverage * monthly_consumption) - available_qty
226
 
        
227
 
        return round(self.pool.get('product.uom')._compute_qty(cr, uid, product.uom_id.id, qty_to_order, product.uom_id.id), 2)
228
 
        
229
 
        
230
 
    def get_available(self, cr, uid, product_id, location_id, monthly_consumption, d_values=None, context=None):
231
 
        '''
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)]
238
 
            - Expiry quantities.
239
 
        '''
240
 
        if context is None:
241
 
            context = {}
242
 
        if d_values is None:
243
 
            d_values = {}
244
 
            
245
 
        product_obj = self.pool.get('product.product')
246
 
        
247
 
        context.update({'location': location_id,
248
 
                        'compute_child': True, })
249
 
        
250
 
        product = product_obj.browse(cr, uid, product_id, context=context)
251
 
 
252
 
        ''' Set this part of algorithm as comment because this algorithm seems to be equal to virtual stock
253
 
        
254
 
            To do validate by Magali
255
 
            
256
 
            Picked reservation will be developed on future sprint
257
 
        ''' 
258
 
        
259
 
        # Get the available stock
260
 
        # Get the real stock
261
 
        picked_resa = product_obj.get_product_available(cr, uid, [product_id], context={'states': ['assigned'],
262
 
                                                                                       'what': ('in, out'), 
263
 
                                                                                       'location': location_id,
264
 
                                                                                       'compute_child': True,})
265
 
            
266
 
        available_stock = product.qty_available + picked_resa.get(product.id)
267
 
            
268
 
        quantity_on_order = product_obj.get_product_available(cr, uid, [product_id], context={'states': ['confirmed'],
269
 
                                                                                              'what': ('in, out'), 
270
 
                                                                                              'location': location_id,
271
 
                                                                                              'compute_child': True,})
272
 
           
273
 
        # Get the safety stock
274
 
        safety_stock = d_values.get('safety_stock', 0)
275
 
        
276
 
        # Get the safety time
277
 
        safety_time = d_values.get('safety_time', 0)
278
 
        
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
282
 
 
283
 
        
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
287
 
    
288
 
    def get_diff_date(self, date):
289
 
        '''
290
 
        Returns the number of month between the date in parameter and today
291
 
        '''
292
 
        date = Parser.DateFromString(date)
293
 
        today = datetime.today()
294
 
        
295
 
        # The batch is expired
296
 
        if date.year < today.year or (date.year == today.year and date.month < today.month):
297
 
            return 0 
298
 
        
299
 
        # The batch expires this month
300
 
        if date.year == today.year and date.month == today.month:
301
 
            return 0
302
 
        
303
 
        # The batch expires in one month
304
 
        if date.year == today.year and date.month == today.month+1 and date.day >= today.day:
305
 
            return 0
306
 
        
307
 
        # Compute the number of months
308
 
        nb_month = 0
309
 
        nb_month += (date.year - today.year) * 12
310
 
        nb_month += date.month - today.month
311
 
        if date.day < today.day:
312
 
            nb_month -= 1
313
 
            
314
 
        return nb_month
315
 
        
316
 
procurement_order()
317
 
 
318
 
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: