Launchpad automatic translations update.
[odoo/odoo.git] / addons / stock_planning / stock_planning.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution   
5 #    Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 from osv import fields,osv
24 import pooler
25 from tools import config
26 import time
27 import netsvc
28 import math
29 import mx.DateTime
30 from mx.DateTime import RelativeDateTime, now, DateTime, localtime
31 from tools.translate import _
32
33 def rounding(fl, round_value):
34     if not round_value:
35         return fl
36     return round(fl / round_value) * round_value
37
38
39 # Object creating periods quickly
40 # changed that stop_date is created with hour 23:59:00 when it was 00:00:00 stop date was excluded from period
41 class stock_period_createlines(osv.osv_memory):
42     _name = "stock.period.createlines"
43
44     def _get_new_period_start(self,cr,uid,context={}):
45         cr.execute("select max(date_stop) from stock_period")
46         result=cr.fetchone()
47         last_date = result and result[0] or False
48         if last_date:
49             period_start = mx.DateTime.strptime(last_date,"%Y-%m-%d %H:%M:%S")+ RelativeDateTime(days=1)
50             period_start = period_start - RelativeDateTime(hours=period_start.hour, minutes=period_start.minute, seconds=period_start.second)
51         else:
52             period_start = mx.DateTime.today()
53         return period_start.strftime('%Y-%m-%d')
54    
55
56     _columns = {
57         'name': fields.char('Period Name', size=64),
58         'date_start': fields.date('Start Date', required=True),
59         'date_stop': fields.date('End Date', required=True),
60         'period_ids': fields.one2many('stock.period', 'planning_id', 'Periods'),
61     }
62     _defaults={
63         'date_start':_get_new_period_start,
64     }
65     
66     def create_period_weekly(self,cr, uid, ids, context={}):
67         res=self.create_period(cr, uid, ids, context, 6, 'Weekly')
68         return {
69                 'view_type': 'form',
70                 "view_mode": 'tree',
71                 'res_model': 'stock.period',
72                 'type': 'ir.actions.act_window',                
73             }
74     
75     def create_period_monthly(self,cr, uid, ids, context={},interval=1):
76         for p in self.browse(cr, uid, ids, context):
77             dt = p.date_start
78             ds = mx.DateTime.strptime(p.date_start, '%Y-%m-%d')
79             while ds.strftime('%Y-%m-%d')<p.date_stop:
80                 de = ds + RelativeDateTime(months=interval, minutes=-1)
81                 self.pool.get('stock.period').create(cr, uid, {
82                     'name': ds.strftime('%Y/%m'),
83                     'date_start': ds.strftime('%Y-%m-%d'),
84                     'date_stop': de.strftime('%Y-%m-%d %H:%M:%S'),                    
85                 })
86                 ds = ds + RelativeDateTime(months=interval)
87         return {
88                 'view_type': 'form',
89                 "view_mode": 'tree',
90                 'res_model': 'stock.period',
91                 'type': 'ir.actions.act_window',                
92             }
93
94     def create_period(self,cr, uid, ids, context={}, interval=0, name='Daily'):
95         for p in self.browse(cr, uid, ids, context):
96             dt = p.date_start
97             ds = mx.DateTime.strptime(p.date_start, '%Y-%m-%d')            
98             while ds.strftime('%Y-%m-%d')<p.date_stop:
99                 de = ds + RelativeDateTime(days=interval, minutes =-1)
100                 if name=='Daily':
101                     new_name=de.strftime('%Y-%m-%d')
102                 if name=="Weekly":
103                     new_name=de.strftime('%Y, week %W')
104                 self.pool.get('stock.period').create(cr, uid, {
105                     'name': new_name,
106                     'date_start': ds.strftime('%Y-%m-%d'),
107                     'date_stop': de.strftime('%Y-%m-%d %H:%M:%S'),
108                 })
109                 ds = ds + RelativeDateTime(days=interval) + 1
110         return {
111                 'view_type': 'form',
112                 "view_mode": 'tree',
113                 'res_model': 'stock.period',
114                 'type': 'ir.actions.act_window',                
115             }
116 stock_period_createlines()
117
118 # Periods have no company_id field as they can be shared across similar companies.
119 # If somone thinks different it can be improved.
120 class stock_period(osv.osv):
121     _name = "stock.period"
122     _columns = {
123         'name': fields.char('Period Name', size=64),
124         'date_start': fields.datetime('Start Date', required=True),
125         'date_stop': fields.datetime('End Date', required=True),        
126         'state' : fields.selection([('draft','Draft'),('open','Open'),('close','Close')],'State')
127     }
128     _defaults = {
129         'state' : lambda * a : 'draft'
130     }
131 stock_period()
132
133 # Creates forecasts records for products from selected Product Category for selected 'Warehouse - Period'
134 # Object added by contributor in ver 1.1
135 class stock_sale_forecast_createlines(osv.osv_memory):
136     _name = "stock.sale.forecast.createlines"
137
138 # FIXME Add some period sugestion like below
139
140 #    def _get_latest_period(self,cr,uid,context={}):
141 #        cr.execute("select max(date_stop) from stock_period")
142 #        result=cr.fetchone()        
143 #        return result and result[0] or False
144    
145
146     _columns = {
147         'company_id': fields.many2one('res.company', 'Company', required=True, select=1),
148         'warehouse_id1': fields.many2one('stock.warehouse' , 'Warehouse', required=True, \
149                                 help='Warehouse which forecasts will concern. '\
150                                    'If during stock planning you will need sales forecast for all warehouses choose any warehouse now.'),
151         'period_id1': fields.many2one('stock.period' , 'Period', required=True, help = 'Period which forecasts will concern.' ),
152         'product_categ_id1': fields.many2one('product.category' , 'Product Category', required=True, \
153                                 help ='Product Category of products which created forecasts will concern.'),
154         'copy_forecast' : fields.boolean('Copy Last Forecast', help="Copy quantities from last Stock and Sale Forecast."),
155     }
156
157     _defaults = { 
158         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.sale.forecast.createlines', context=c),
159     }
160
161     def create_forecast(self,cr, uid, ids, context={}):
162         product_obj = self.pool.get('product.product')
163         forecast_obj=self.pool.get('stock.sale.forecast')
164         for f in self.browse(cr, uid, ids, context):
165             prod_categ_obj=self.pool.get('product.category')
166             template_obj=self.pool.get('product.template')
167             categ_ids =  f.product_categ_id1.id and [f.product_categ_id1.id] or []
168             prod_categ_ids=prod_categ_obj.search(cr,uid,[('parent_id','child_of',categ_ids)]) 
169             templates_ids = template_obj.search(cr,uid,[('categ_id','in',prod_categ_ids)]) 
170             products_ids = product_obj.search(cr,uid,[('product_tmpl_id','in',templates_ids)])
171             if len(products_ids)==0:
172                 raise osv.except_osv(_('Error !'), _('No products in selected category !'))
173             copy = f.copy_forecast
174             for p in product_obj.browse(cr, uid, products_ids,{}):
175                 if len(forecast_obj.search(cr, uid, [('product_id','=',p.id) , \
176                                                        ('period_id','=',f.period_id1.id), \
177                                                        ('user_id','=',uid), \
178                                                        ('warehouse_id','=',f.warehouse_id1.id)]))== 0:
179                     forecast_qty = 0.0
180 # Not sure if it is expected quantity for this feature (copying forecast from previous period)
181 # because it can take incidental forecast of this warehouse, this product and this user (creating, writing or validating forecast).
182 # It takes only one forecast line (no sum). If user creates only one forecast per period it will be OK. If not I have no idea how to count it.
183                     
184                     prod_uom = False
185                     if copy:
186                         cr.execute("SELECT period.date_stop, forecast.product_qty, forecast.product_uom \
187                                     FROM stock_sale_forecast AS forecast \
188                                     LEFT JOIN stock_period AS period \
189                                     ON forecast.period_id = period.id \
190                                     WHERE (forecast.user_id = %s OR forecast.create_uid = %s OR forecast.write_uid = %s) \
191                                         AND forecast.warehouse_id = %s AND forecast.product_id = %s \
192                                         AND period.date_stop < %s \
193                                     ORDER BY period.date_stop DESC",
194                                     (uid, uid, uid, f.warehouse_id1.id, p.id, f.period_id1.date_stop) )
195                         ret=cr.fetchone()
196                         if ret:
197                             forecast_qty = ret[1]        
198                             prod_uom = ret[2]
199                     prod_uom = prod_uom or p.uom_id.id
200                     prod_uos_categ = False
201                     if p.uos_id:
202                         prod_uos_categ = p.uos_id.category_id.id
203                     forecast_obj.create(cr, uid, {
204                         'company_id'  : f.warehouse_id1.company_id.id,
205                         'period_id': f.period_id1.id,
206                         'warehouse_id': f.warehouse_id1.id,
207                         'product_id': p.id,
208                         'product_qty' : forecast_qty,
209                         'product_amt' : 0.0,
210                         'product_uom' : prod_uom,
211                         'active_uom' : prod_uom,
212                         'product_uom_categ' : p.uom_id.category_id.id,
213                         'product_uos_categ' : prod_uos_categ,
214                      })
215         return {
216                 'view_type': 'form',
217                 "view_mode": 'tree',
218                 'res_model': 'stock.sale.forecast',
219                 'type': 'ir.actions.act_window',                
220             }
221 stock_sale_forecast_createlines()
222
223
224 # Stock and Sales Forecast object. Previously stock_planning_sale_prevision.
225 # A lot of changes in 1.1
226 class stock_sale_forecast(osv.osv):
227     _name = "stock.sale.forecast"
228     
229     _columns = {
230         'company_id': fields.many2one('res.company', 'Company', required=True),
231         'create_uid':  fields.many2one('res.users', 'Responsible'),
232         'name' : fields.char('Name', size=64, readonly=True, states={'draft':[('readonly',False)]}),
233         'user_id': fields.many2one('res.users' , 'Created/Validated by',readonly=True, \
234                                         help='Shows who created this forecast, or who validated.'),
235         'warehouse_id': fields.many2one('stock.warehouse' , 'Warehouse', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
236                                         help='Shows which warehouse this forecast concerns. '\
237                                          'If during stock planning you will need sales forecast for all warehouses choose any warehouse now.'),
238         'period_id': fields.many2one('stock.period' , 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
239                                         help = 'Shows which period this forecast concerns.'),
240         'product_id': fields.many2one('product.product' , 'Product', readonly=True, required=True, states={'draft':[('readonly',False)]}, \
241                                         help = 'Shows which product this forecast concerns.'),
242         'product_qty' : fields.float('Product Quantity', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
243                                         help= 'Forecasted quantity.'),
244         'product_amt' : fields.float('Product Amount', readonly=True, states={'draft':[('readonly',False)]}, \
245                                         help='Forecast value which will be converted to Product Quantity according to prices.'),
246         'product_uom_categ' : fields.many2one('product.uom.categ', 'Product UoM Category'),  # Invisible field for product_uom domain
247         'product_uom' : fields.many2one('product.uom', 'Product UoM', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
248                         help = "Unit of Measure used to show the quanities of stock calculation." \
249                         "You can use units form default category or from second category (UoS category)."),
250         'product_uos_categ' : fields.many2one('product.uom.categ', 'Product UoS Category'), # Invisible field for product_uos domain
251 # Field used in onchange_uom to check what uom was before change and recalculate quantities acording to old uom (active_uom) and new uom.
252         'active_uom' :fields.many2one('product.uom',  string = "Active UoM"),  
253         'state' : fields.selection([('draft','Draft'),('validated','Validated')],'State',readonly=True),
254         'analyzed_period1_id' : fields.many2one('stock.period' , 'Period1', readonly=True, states={'draft':[('readonly',False)]},),
255         'analyzed_period2_id' : fields.many2one('stock.period' , 'Period2', readonly=True, states={'draft':[('readonly',False)]},),
256         'analyzed_period3_id' : fields.many2one('stock.period' , 'Period3', readonly=True, states={'draft':[('readonly',False)]},),
257         'analyzed_period4_id' : fields.many2one('stock.period' , 'Period4', readonly=True, states={'draft':[('readonly',False)]},),
258         'analyzed_period5_id' : fields.many2one('stock.period' , 'Period5', readonly=True, states={'draft':[('readonly',False)]},),
259         'analyzed_user_id' : fields.many2one('res.users' , 'This User', required=False, readonly=True, states={'draft':[('readonly',False)]},),
260         'analyzed_dept_id' : fields.many2one('hr.department' , 'This Department', required=False, \
261                                     readonly=True, states={'draft':[('readonly',False)]},),
262         'analyzed_warehouse_id' : fields.many2one('stock.warehouse' , 'This Warehouse', required=False, \
263                                     readonly=True, states={'draft':[('readonly',False)]}),
264         'analyze_company' : fields.boolean('Per Company', readonly=True, states={'draft':[('readonly',False)]}, \
265                                     help = "Check this box to see the sales for whole company."),
266         'analyzed_period1_per_user' : fields.float('This User Period1', readonly=True),
267         'analyzed_period2_per_user' : fields.float('This User Period2', readonly=True),
268         'analyzed_period3_per_user' : fields.float('This User Period3', readonly=True),
269         'analyzed_period4_per_user' : fields.float('This User Period4', readonly=True),
270         'analyzed_period5_per_user' : fields.float('This User Period5', readonly=True),
271         'analyzed_period1_per_dept' : fields.float('This Dept Period1', readonly=True),
272         'analyzed_period2_per_dept' : fields.float('This Dept Period2', readonly=True),
273         'analyzed_period3_per_dept' : fields.float('This Dept Period3', readonly=True),
274         'analyzed_period4_per_dept' : fields.float('This Dept Period4', readonly=True),
275         'analyzed_period5_per_dept' : fields.float('This Dept Period5', readonly=True),
276         'analyzed_period1_per_warehouse' : fields.float('This Warehouse Period1', readonly=True),
277         'analyzed_period2_per_warehouse' : fields.float('This Warehouse Period2', readonly=True),
278         'analyzed_period3_per_warehouse' : fields.float('This Warehouse Period3', readonly=True),
279         'analyzed_period4_per_warehouse' : fields.float('This Warehouse Period4', readonly=True),
280         'analyzed_period5_per_warehouse' : fields.float('This Warehouse Period5', readonly=True),
281         'analyzed_period1_per_company' : fields.float('This Copmany Period1', readonly=True),
282         'analyzed_period2_per_company' : fields.float('This Company Period2', readonly=True),
283         'analyzed_period3_per_company' : fields.float('This Company Period3', readonly=True),
284         'analyzed_period4_per_company' : fields.float('This Company Period4', readonly=True),
285         'analyzed_period5_per_company' : fields.float('This Company Period5', readonly=True),
286     }
287     _defaults = {
288         'user_id': lambda obj, cr, uid, context: uid,
289         'state': lambda *args: 'draft',
290         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.sale.forecast', context=c),
291     }
292
293     def action_validate(self, cr, uid, ids, *args):
294         self.write(cr, uid, ids, {'state':'validated','user_id':uid})
295         return True
296     
297     def unlink(self, cr, uid, ids, context={}):
298         forecasts = self.read(cr, uid, ids, ['state'])
299         unlink_ids = []
300         for t in forecasts:
301             if t['state'] in ('draft'):
302                 unlink_ids.append(t['id'])
303             else:
304                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Validated Sale Forecasts !'))
305         osv.osv.unlink(self, cr, uid, unlink_ids,context=context)
306         return True
307
308     def onchange_company(self, cr, uid, ids, company_id):
309         result = {}
310         if company_id:
311             result['warehouse_id'] = False
312             result['analyzed_user_id'] = False
313             result['analyzed_dept_id'] = False
314             result['analyzed_warehouse_id'] = False
315         return {'value': result}
316
317     def product_id_change(self, cr, uid, ids, product_id):
318         ret={}
319         if product_id:
320             product_rec =  self.pool.get('product.product').browse(cr, uid, product_id)
321             ret['product_uom'] = product_rec.uom_id.id
322             ret['product_uom_categ'] = product_rec.uom_id.category_id.id
323             ret['product_uos_categ'] = product_rec.uos_id and product_rec.uos_id.category_id.id or False
324             ret['active_uom'] = product_rec.uom_id.id
325         else:
326             ret['product_uom'] = False
327             ret['product_uom_categ'] = False
328             ret['product_uos_categ'] = False
329         res = {'value': ret}
330         return res
331
332     def onchange_uom(self, cr, uid, ids, product_uom=False, product_qty=0.0, active_uom=False ):
333         ret={}
334         val1 = self.browse(cr, uid, ids)
335         val = val1[0]
336         coeff_uom2def = self._to_default_uom_factor(cr, uid, val, active_uom, {})
337         coeff_def2uom, round_value = self._from_default_uom_factor( cr, uid, val, product_uom, {})
338         coeff = coeff_uom2def * coeff_def2uom
339         ret['product_qty'] = rounding(coeff * product_qty, round_value)
340         ret['active_uom'] = product_uom
341         return {'value': ret}
342
343     def product_amt_change(self, cr, uid, ids, product_amt = 0.0, product_uom=False):
344         ret={}
345         if product_amt:
346             coeff_def2uom = 1
347             val1 = self.browse(cr, uid, ids)
348             val = val1[0]
349             if (product_uom != val.product_id.uom_id.id):
350                 coeff_def2uom, rounding = self._from_default_uom_factor( cr, uid, val, product_uom, {})
351             ret['product_qty'] = rounding(coeff_def2uom * product_amt/(val.product_id.product_tmpl_id.list_price), round_value)
352         res = {'value': ret}
353         return res
354
355     def _to_default_uom_factor(self, cr, uid, val, uom_id, context):
356         uom_obj = self.pool.get('product.uom')
357         uom = uom_obj.browse(cr, uid, uom_id, context=context)
358         coef =  uom.factor
359         if uom.category_id.id <> val.product_id.uom_id.category_id.id:
360             coef = coef / val.product_id.uos_coeff
361         return val.product_id.uom_id.factor / coef
362
363     def _from_default_uom_factor(self, cr, uid, val, uom_id, context):
364         uom_obj = self.pool.get('product.uom')
365         uom = uom_obj.browse(cr, uid, uom_id, context=context)
366         res = uom.factor
367         if uom.category_id.id <> val.product_id.uom_id.category_id.id:
368             res = res / val.product_id.uos_coeff
369         return res / val.product_id.uom_id.factor, uom.rounding
370
371     def _sales_per_users(self, cr, uid, so, so_line, company, users):
372         cr.execute("SELECT sum(sol.product_uom_qty) FROM sale_order_line AS sol LEFT JOIN sale_order AS s ON (s.id = sol.order_id) " \
373                    "WHERE (sol.id IN (%s)) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN (%s)) AND (s.company_id=%s) " \
374                     "AND (s.user_id IN (%s)) " %(so_line, so, company, users))
375         ret = cr.fetchone()[0] or 0.0
376         return ret
377
378     def _sales_per_warehouse(self, cr, uid, so, so_line, company, shops):        
379         cr.execute("SELECT sum(sol.product_uom_qty) FROM sale_order_line AS sol LEFT JOIN sale_order AS s ON (s.id = sol.order_id) " \
380                    "WHERE (sol.id IN (%s)) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN (%s))AND (s.company_id=%s) " \
381                     "AND (s.shop_id IN (%s))" %(so_line, so, company, shops))
382         ret = cr.fetchone()[0] or 0.0
383         return ret
384
385     def _sales_per_company(self, cr, uid, so, so_line, company, ):
386         cr.execute("SELECT sum(sol.product_uom_qty) FROM sale_order_line AS sol LEFT JOIN sale_order AS s ON (s.id = sol.order_id) " \
387                    "WHERE (sol.id IN (%s)) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN (%s)) AND (s.company_id=%s)"%(so_line, so, company))
388         ret = cr.fetchone()[0] or 0.0
389         return ret
390     
391     def calculate_sales_history(self, cr, uid, ids, context, *args):
392         sales=[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],]
393         for obj in  self.browse(cr, uid, ids):
394             periods =obj.analyzed_period1_id, obj.analyzed_period2_id, obj.analyzed_period3_id, obj.analyzed_period4_id, obj.analyzed_period5_id
395             so_obj = self.pool.get('sale.order')
396             so_line_obj= self.pool.get('sale.order.line')
397             so_line_product_ids = so_line_obj.search(cr, uid, [('product_id','=', obj.product_id.id)], context = context)
398             if so_line_product_ids:
399                 so_line_product_set = ','.join(map(str,so_line_product_ids))
400                 if obj.analyzed_warehouse_id:
401                     shops = self.pool.get('sale.shop').search(cr, uid,[('warehouse_id','=', obj.analyzed_warehouse_id.id)], context = context)
402                     shops_set = ','.join(map(str,shops))
403                 else:
404                     shops = False
405                 if obj.analyzed_dept_id:
406                     dept_obj = self.pool.get('hr.department')
407                     dept_id =  obj.analyzed_dept_id.id and [obj.analyzed_dept_id.id] or []
408                     dept_ids = dept_obj.search(cr,uid,[('parent_id','child_of',dept_id)])
409                     dept_ids_set = ','.join(map(str,dept_ids))
410                     cr.execute("SELECT user_id FROM hr_department_user_rel WHERE (department_id IN (%s))" %(dept_ids_set))
411                     dept_users = [x for x, in cr.fetchall()]
412                     dept_users_set =  ','.join(map(str,dept_users))
413                 else:
414                     dept_users = False
415                 factor, round_value = self._from_default_uom_factor(cr, uid, obj, obj.product_uom.id, context=context)
416                 for i, period in enumerate(periods):
417                     if period:
418                         so_period_ids = so_obj.search(cr, uid, [('date_order','>=',period.date_start), ('date_order','<=',period.date_stop)], context = context)
419                         if so_period_ids:
420                             so_period_set = ','.join(map(str,so_period_ids))
421                             if obj.analyzed_user_id:
422                                 user_set = str(obj.analyzed_user_id.id)
423                                 sales[i][0] =self._sales_per_users(cr, uid, so_period_set, so_line_product_set, obj.company_id.id, user_set)
424                                 sales[i][0] *=factor
425                             if dept_users:
426                                 sales[i][1]=  self._sales_per_users(cr, uid, so_period_set,  so_line_product_set, obj.company_id.id, dept_users_set)
427                                 sales[i][1]*=factor
428                             if shops:
429                                 sales[i][2]= self._sales_per_warehouse(cr, uid, so_period_set,  so_line_product_set, obj.company_id.id, shops_set)
430                                 sales[i][2]*=factor
431                             if obj.analyze_company:
432                                 sales[i][3]= self._sales_per_company(cr, uid, so_period_set, so_line_product_set, obj.company_id.id, )
433                                 sales[i][3]*=factor
434         self.write(cr, uid, ids, {
435             'analyzed_period1_per_user':sales[0][0],
436             'analyzed_period2_per_user':sales[1][0],
437             'analyzed_period3_per_user':sales[2][0],
438             'analyzed_period4_per_user':sales[3][0],
439             'analyzed_period5_per_user':sales[4][0],
440             'analyzed_period1_per_dept':sales[0][1],
441             'analyzed_period2_per_dept':sales[1][1],
442             'analyzed_period3_per_dept':sales[2][1],
443             'analyzed_period4_per_dept':sales[3][1],
444             'analyzed_period5_per_dept':sales[4][1],
445             'analyzed_period1_per_warehouse':sales[0][2],
446             'analyzed_period2_per_warehouse':sales[1][2],
447             'analyzed_period3_per_warehouse':sales[2][2],
448             'analyzed_period4_per_warehouse':sales[3][2],
449             'analyzed_period5_per_warehouse':sales[4][2],
450             'analyzed_period1_per_company':sales[0][3],
451             'analyzed_period2_per_company':sales[1][3],
452             'analyzed_period3_per_company':sales[2][3],
453             'analyzed_period4_per_company':sales[3][3],
454             'analyzed_period5_per_company':sales[4][3],
455         })
456         return True
457
458
459 stock_sale_forecast()
460
461 # Creates stock planning records for products from selected Product Category for selected 'Warehouse - Period' 
462 # Object added by contributor in ver 1.1 
463 class stock_planning_createlines(osv.osv_memory):
464     _name = "stock.planning.createlines"
465
466     def onchange_company(self, cr, uid, ids, company_id):
467         result = {}
468         if company_id:
469             result['warehouse_id2'] = False
470         return {'value': result}
471
472     _columns = {
473         'company_id': fields.many2one('res.company', 'Company', required = True),
474         'period_id2': fields.many2one('stock.period' , 'Period', required=True, help = 'Period which planning will concern.'),
475         'warehouse_id2': fields.many2one('stock.warehouse' , 'Warehouse', required=True, help = 'Warehouse which planning will concern.'),
476         'product_categ_id2': fields.many2one('product.category' , 'Product Category', \
477                         help = 'Planning will be created for products from Product Category selected by this field. '\
478                                'This field is ignored when you check \"All Forecasted Product\" box.' ),
479         'forecasted_products': fields.boolean('All Products with Forecast', \
480                      help = "Check this box to create planning for all products having any forecast for selected Warehouse and Period. "\
481                             "Product Category field will be ignored."),
482     }
483
484     _defaults = {
485         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.planning', context=c),
486     }
487
488     def create_planning(self,cr, uid, ids, context={}):
489         product_obj = self.pool.get('product.product')
490         planning_obj=self.pool.get('stock.planning')
491
492         for f in self.browse(cr, uid, ids, context=context):
493             if f.forecasted_products:
494                 cr.execute("SELECT product_id \
495                                 FROM stock_sale_forecast \
496                                 WHERE (period_id = %s) AND (warehouse_id = %s)", (f.period_id2.id, f.warehouse_id2.id))
497                 products_id1 = [x for x, in cr.fetchall()]
498             else:
499                 prod_categ_obj=self.pool.get('product.category')
500                 template_obj=self.pool.get('product.template')
501                 categ_ids =  f.product_categ_id2.id and [f.product_categ_id2.id] or []
502                 prod_categ_ids=prod_categ_obj.search(cr,uid,[('parent_id','child_of',categ_ids)]) 
503                 templates_ids = template_obj.search(cr,uid,[('categ_id','in',prod_categ_ids)]) 
504                 products_id1 = product_obj.search(cr,uid,[('product_tmpl_id','in',templates_ids)])
505             if len(products_id1)==0:
506                 raise osv.except_osv(_('Error !'), _('No forecasts for selected period or no products in selected category !'))
507
508             for p in product_obj.browse(cr, uid, products_id1,context=context):
509                 if len(planning_obj.search(cr, uid, [('product_id','=',p.id),
510                                                       ('period_id','=',f.period_id2.id),
511                                                       ('warehouse_id','=',f.warehouse_id2.id)]))== 0:
512                     cr.execute("SELECT period.date_stop, planning.product_uom, planning.planned_outgoing, planning.to_procure, \
513                                     planning.stock_only, planning.procure_to_stock, planning.confirmed_forecasts_only, \
514                                     planning.supply_warehouse_id, planning.stock_supply_location \
515                                 FROM stock_planning AS planning \
516                                 LEFT JOIN stock_period AS period \
517                                 ON planning.period_id = period.id \
518                                 WHERE (planning.create_uid = %s OR planning.write_uid = %s) \
519                                      AND planning.warehouse_id = %s AND planning.product_id = %s \
520                                      AND period.date_stop < %s \
521                                 ORDER BY period.date_stop DESC",
522                                     (uid, uid, f.warehouse_id2.id, p.id, f.period_id2.date_stop) )
523                     ret=cr.fetchone()
524 #                        forecast_qty = ret and ret[0] or 0.0
525                     if ret:
526 #                            raise osv.except_osv(_('Error !'), _('ret is %s %s %s %s %s %s')%(ret[0],ret[2],ret[3],ret[4],ret[5],ret[6],))
527                         prod_uom = ret[1]
528                         planned_out = ret[2]        
529                         to_procure = ret[3]
530                         stock_only = ret[4]
531                         procure_to_stock = ret[5]
532                         confirmed_forecasts_only = ret[6]
533                         supply_warehouse_id = ret[7]
534                         stock_supply_location = ret[8]
535                     else:
536                         prod_uom = p.uom_id.id
537                         planned_out = False        
538                         to_procure = False
539                         stock_only = False
540                         procure_to_stock = False
541                         confirmed_forecasts_only = False
542                         supply_warehouse_id = False
543                         stock_supply_location = False
544                     prod_uos_categ = False
545                     if p.uos_id:
546                         prod_uos_categ = p.uos_id.category_id.id
547                     planning_obj.create(cr, uid, {
548                         'company_id' : f.warehouse_id2.company_id.id,
549                         'period_id': f.period_id2.id,
550                         'warehouse_id' : f.warehouse_id2.id,
551                         'product_id': p.id,
552                         'product_uom' : prod_uom,
553                         'product_uom_categ' : p.uom_id.category_id.id,
554                         'product_uos_categ' : prod_uos_categ,
555                         'active_uom' : prod_uom,
556                         'planned_outgoing': planned_out,       
557                         'to_procure': to_procure,
558                         'stock_only': stock_only,
559                         'procure_to_stock': procure_to_stock,
560                         'confirmed_forecasts_only': confirmed_forecasts_only,
561                         'supply_warehouse_id': supply_warehouse_id,
562                         'stock_supply_location': stock_supply_location,
563
564                     })
565         return {
566                 'view_type': 'form',
567                 "view_mode": 'tree',
568                 'res_model': 'stock.planning',
569                 'type': 'ir.actions.act_window',                
570             }
571 stock_planning_createlines()
572
573
574 # The main Stock Planning object
575 # A lot of changes by contributor in ver 1.1
576 class stock_planning(osv.osv):
577     _name = "stock.planning"
578     
579     def _get_in_out(self, cr, uid, val, date_start, date_stop, direction, done, context):
580 #        res = {}
581 #        if not context:
582 #            context = {}
583         mapping = {'in': {
584                         'field': "incoming_qty",
585                         'adapter': lambda x: x,
586                   },
587                   'out': {
588                         'field': "outgoing_qty",
589                         'adapter': lambda x: -x,
590                   },
591         }
592         context['from_date'] = date_start
593         context['to_date'] = date_stop
594         locations = [val.warehouse_id.lot_stock_id.id,]
595         if not val.stock_only:
596             locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
597         context['location'] = locations
598         context['compute_child'] = True
599         product_obj =  self.pool.get('product.product')
600         prod_id = val.product_id.id
601         if done:
602             c1=context.copy()
603             c1.update({ 'states':('done',), 'what':(direction,) })
604             prod_ids = [prod_id]
605             st = product_obj.get_product_available(cr,uid, prod_ids, context=c1)
606             res = mapping[direction]['adapter'](st.get(prod_id,0.0))
607         else:
608             product=product_obj.read(cr, uid, prod_id,[], context)
609             product_qty = product[mapping[direction]['field']]
610             res = mapping[direction]['adapter'](product_qty)
611 #            res[val.id] = product_obj['incoming_qty']
612         return res
613
614     def _get_outgoing_before(self, cr, uid, val, date_start, date_stop, context):
615         cr.execute("SELECT sum(planning.planned_outgoing), planning.product_uom \
616                     FROM stock_planning AS planning \
617                     LEFT JOIN stock_period AS period \
618                     ON (planning.period_id = period.id) \
619                     WHERE (period.date_stop >= %s) AND (period.date_stop <= %s) \
620                         AND (planning.product_id = %s) AND (planning.company_id = %s) \
621                     GROUP BY planning.product_uom", \
622                         (date_start, date_stop, val.product_id.id, val.company_id.id,))
623         planning_qtys = cr.fetchall()
624         res = self._to_planning_uom(cr, uid, val, planning_qtys, context)
625         return res
626
627     def _to_planning_uom(self, cr, uid, val, qtys, context):
628         res_qty = 0
629         if qtys:
630             uom_obj = self.pool.get('product.uom')
631             for qty, prod_uom in qtys:
632                 coef = self._to_default_uom_factor(cr, uid, val, prod_uom, context=context)
633                 res_coef, round_value = self._from_default_uom_factor(cr, uid, val, val.product_uom.id, context=context)
634                 coef = coef * res_coef
635                 res_qty += rounding(qty * coef, round_value)
636         return res_qty
637         
638
639     def _get_forecast(self, cr, uid, ids, field_names, arg, context):
640         res = {}
641         for val in self.browse(cr, uid, ids):
642             res[val.id]={}
643             valid_part = val.confirmed_forecasts_only and " AND state = 'validated'" or ""
644             cr.execute('SELECT sum(product_qty), product_uom  \
645                         FROM stock_sale_forecast \
646                         WHERE product_id = %s AND period_id = %s AND company_id = %s '+valid_part+ \
647                        'GROUP BY product_uom', \
648                             (val.product_id.id,val.period_id.id, val.company_id.id))
649             company_qtys = cr.fetchall()
650             res[val.id]['company_forecast'] = self._to_planning_uom(cr, uid, val, company_qtys, context)
651
652             cr.execute('SELECT sum(product_qty), product_uom \
653                         FROM stock_sale_forecast \
654                         WHERE product_id = %s and period_id = %s AND warehouse_id = %s ' + valid_part + \
655                        'GROUP BY product_uom', \
656                         (val.product_id.id,val.period_id.id, val.warehouse_id.id))
657             warehouse_qtys = cr.fetchall()
658             res[val.id]['warehouse_forecast'] = self._to_planning_uom(cr, uid, val, warehouse_qtys, context)
659             res[val.id]['warehouse_forecast'] = rounding(res[val.id]['warehouse_forecast'],  val.product_id.uom_id.rounding)
660         return res
661
662     def _get_stock_start(self, cr, uid, val, date, context):
663         context['from_date'] = None
664         context['to_date'] = date
665         locations = [val.warehouse_id.lot_stock_id.id,]
666         if not val.stock_only:
667             locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
668         context['location'] = locations
669         context['compute_child'] = True
670         product_obj =  self.pool.get('product.product').read(cr, uid,val.product_id.id,[], context)
671         res = product_obj['qty_available']     # value for stock_start
672         return res
673     
674     def _get_past_future(self, cr, uid, ids, field_names, arg, context):
675         res = {}
676         for val in self.browse(cr, uid, ids, context=context):
677             if val.period_id.date_stop < time.strftime('%Y-%m-%d'):
678                 res[val.id] = 'Past'
679             else:
680                 res[val.id] = 'Future'
681         return res
682
683     def _get_op(self, cr, uid, ids, field_names, arg, context):  # op = OrderPoint
684         res = {}
685         for val in self.browse(cr, uid, ids, context=context):
686             res[val.id]={}
687             cr.execute("SELECT product_min_qty, product_max_qty, product_uom  \
688                         FROM stock_warehouse_orderpoint \
689                         WHERE warehouse_id = %s AND product_id = %s AND active = 'TRUE'", (val.warehouse_id.id, val.product_id.id))
690             ret =  cr.fetchone() or [0.0,0.0,False]
691             coef = 1
692             round_value = 1
693             if ret[2]:
694                 coef = self._to_default_uom_factor(cr, uid, val, ret[2], context)
695                 res_coef, round_value = self._from_default_uom_factor(cr, uid, val, val.product_uom.id, context=context)
696                 coef = coef * res_coef
697             res[val.id]['minimum_op'] = rounding(ret[0]*coef, round_value)
698             res[val.id]['maximum_op'] = ret[1]*coef
699         return res
700     
701     def onchange_company(self, cr, uid, ids, company_id):
702         result = {}
703         if company_id:
704             result['warehouse_id'] = False
705         return {'value': result}
706
707     def onchange_uom(self, cr, uid, ids, product_uom, ):
708         if not product_uom:
709             return {}
710         ret={}
711         val1 = self.browse(cr, uid, ids)
712         val = val1[0]
713         coeff_uom2def = self._to_default_uom_factor(cr, uid, val, val.active_uom.id, {})
714         coeff_def2uom, round_value = self._from_default_uom_factor( cr, uid, val, product_uom, {})
715         coeff = coeff_uom2def * coeff_def2uom
716         ret['planned_outgoing'] = rounding(coeff * val.planned_outgoing, round_value)
717         ret['to_procure'] = rounding(coeff * val.to_procure, round_value)
718         ret['active_uom'] = product_uom
719         return {'value': ret}
720
721     _columns = {
722         'company_id': fields.many2one('res.company', 'Company', required = True),
723         'history' : fields.text('Procurement History', readonly=True, help = "History of procurement or internal supply of this planning line."),
724         'state' : fields.selection([('draft','Draft'),('done','Done')],'State',readonly=True),
725         'period_id': fields.many2one('stock.period' , 'Period', required=True, \
726                 help = 'Period for this planning. Requisition will be created for beginning of the period.'),
727         'warehouse_id' : fields.many2one('stock.warehouse','Warehouse', required=True), 
728         'product_id': fields.many2one('product.product' , 'Product', required=True, help = 'Product which this planning is created for.'),
729         'product_uom_categ' : fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uom domain
730         'product_uom' : fields.many2one('product.uom', 'UoM', required=True, help = "Unit of Measure used to show the quanities of stock calculation." \
731                         "You can use units form default category or from second category (UoS category)."),
732         'product_uos_categ' : fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uos domain
733 # Field used in onchange_uom to check what uom was before change to recalculate quantities acording to old uom (active_uom) and new uom.
734         'active_uom' :fields.many2one('product.uom',  string = "Active UoM"), 
735         'planned_outgoing' : fields.float('Planned Out', required=True,  \
736                 help = 'Enter planned outgoing quantity from selected Warehouse during the selected Period of selected Product. '\
737                         'To plan this value look at Confirmed Out or Sales Forecasts. This value should be equal or greater than Confirmed Out.'),
738         'company_forecast': fields.function(_get_forecast, method=True, string ='Company Forecast', multi = 'company', \
739                 help = 'All sales forecasts for whole company (for all Warehouses) of selected Product during selected Period.'),
740         'warehouse_forecast': fields.function(_get_forecast, method=True, string ='Warehouse Forecast',  multi = 'warehouse',\
741                 help = 'All sales forecasts for selected Warehouse of selected Product during selected Period.'),
742         'stock_simulation': fields.float('Stock Simulation', readonly =True, \
743                 help = 'Stock simulation at the end of selected Period.\n For current period it is: \n' \
744                        'Initial Stock - Already Out + Already In - Expected Out + Incoming Left.\n' \
745                         'For periods ahead it is: \nInitial Stock - Planned Out Before + Incoming Before - Planned Out + Planned In.'),
746         'incoming': fields.float('Confirmed In', readonly=True, \
747                 help = 'Quantity of all confirmed incoming moves in calculated Period.'),
748         'outgoing': fields.float('Confirmed Out', readonly=True, \
749                 help = 'Quantity of all confirmed outgoing moves in calculated Period.'),
750         'incoming_left': fields.float('Incoming Left', readonly=True,  \
751                 help = 'Quantity left to Planned incoming quantity. This is calculated difference between Planned In and Confirmed In. ' \
752                         'For current period Already In is also calculated. This value is used to create procurement for lacking quantity.'),
753         'outgoing_left': fields.float('Expected Out', readonly=True, \
754                 help = 'Quantity expected to go out in selected period. As a difference between Planned Out and Confirmed Out. ' \
755                         'For current period Already Out is also calculated'),
756         'to_procure': fields.float(string='Planned In', required=True, \
757                 help = 'Enter quantity which (by your plan) should come in. Change this value and observe Stock simulation. ' \
758                         'This value should be equal or greater than Confirmed In.'),
759         'line_time' : fields.function(_get_past_future, method=True,type='char', string='Past/Future'),
760         'minimum_op' : fields.function(_get_op, method=True, type='float', string = 'Minimum Rule', multi= 'minimum', \
761                             help = 'Minimum quantity set in Minimum Stock Rules for this Warhouse'),
762         'maximum_op' : fields.function(_get_op, method=True, type='float', string = 'Maximum Rule', multi= 'maximum', \
763                             help = 'Maximum quantity set in Minimum Stock Rules for this Warhouse'),
764         'outgoing_before' : fields.float('Planned Out Before', readonly=True, \
765                             help= 'Planned Out in periods before calculated. '\
766                                     'Between start date of current period and one day before start of calculated period.'),
767         'incoming_before': fields.float('Incoming Before', readonly = True, \
768                             help= 'Confirmed incoming in periods before calculated (Including Already In). '\
769                                     'Between start date of current period and one day before start of calculated period.'),
770         'stock_start' : fields.float('Initial Stock', readonly=True, \
771                             help= 'Stock quantity one day before current period.'),
772         'already_out' : fields.float('Already Out', readonly=True, \
773                             help= 'Quantity which is already dispatched out of this warehouse in current period.'),
774         'already_in' : fields.float('Already In', readonly=True, \
775                             help= 'Quantity which is already picked up to this warehouse in current period.'),
776         'stock_only' : fields.boolean("Stock Location Only", help = "Check to calculate stock location of selected warehouse only. " \
777                                         "If not selected calculation is made for input, stock and output location of warehouse."),
778         "procure_to_stock" : fields.boolean("Procure To Stock Location", help = "Chect to make procurement to stock location of selected warehouse. " \
779                                         "If not selected procurement will be made into input location of warehouse."),
780         "confirmed_forecasts_only" : fields.boolean("Validated Forecasts", help = "Check to take validated forecasts only. " \
781                     "If not checked system takes validated and draft forecasts."),
782         'supply_warehouse_id' : fields.many2one('stock.warehouse','Source Warehouse', help = "Warehouse used as source in supply pick move created by 'Supply from Another Warhouse'."), 
783         "stock_supply_location" : fields.boolean("Stock Supply Location", help = "Check to supply from Stock location of Supply Warehouse. " \
784                 "If not checked supply will be made from Output location of Supply Warehouse. Used in 'Supply from Another Warhouse' with Supply Warehouse."),
785
786     }
787
788     _defaults = {
789         'state': lambda *args: 'draft' ,
790         'to_procure' : 0.0,
791         'planned_outgoing' : 0.0,
792         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.planning', context=c),
793     }   
794     
795     _order = 'period_id'
796
797     def _to_default_uom_factor(self, cr, uid, val, uom_id, context):
798         uom_obj = self.pool.get('product.uom')
799         uom = uom_obj.browse(cr, uid, uom_id, context=context)
800         coef =  uom.factor
801         if uom.category_id.id != val.product_id.uom_id.category_id.id:
802             coef = coef / val.product_id.uos_coeff
803         return val.product_id.uom_id.factor / coef
804
805
806     def _from_default_uom_factor(self, cr, uid, val, uom_id, context):
807         uom_obj = self.pool.get('product.uom')
808         uom = uom_obj.browse(cr, uid, uom_id, context=context)
809         res = uom.factor
810         if uom.category_id.id != val.product_id.uom_id.category_id.id:
811             res = res / val.product_id.uos_coeff
812         return res / val.product_id.uom_id.factor, uom.rounding
813
814     def calculate_planning(self, cr, uid, ids, context, *args):
815         one_minute = RelativeDateTime(minutes=1)
816         current_date_beginning_c = mx.DateTime.today()
817         current_date_end_c = current_date_beginning_c  + RelativeDateTime(days=1, minutes=-1)  # to get hour 23:59:00 
818         current_date_beginning = current_date_beginning_c.strftime('%Y-%m-%d %H:%M:%S')
819         current_date_end = current_date_end_c.strftime('%Y-%m-%d %H:%M:%S')
820         for val in self.browse(cr, uid, ids, context=context):
821             day = mx.DateTime.strptime(val.period_id.date_start, '%Y-%m-%d %H:%M:%S')
822             dbefore = mx.DateTime.DateTime(day.year, day.month, day.day) - one_minute
823             day_before_calculated_period = dbefore.strftime('%Y-%m-%d %H:%M:%S')   # one day before start of calculated period
824             cr.execute("SELECT date_start \
825                     FROM stock_period AS period \
826                     LEFT JOIN stock_planning AS planning \
827                     ON (planning.period_id = period.id) \
828                     WHERE (period.date_stop >= %s) AND (period.date_start <= %s) AND \
829                         planning.product_id = %s", (current_date_end, current_date_end, val.product_id.id,)) #
830             date = cr.fetchone()
831             start_date_current_period = date and date[0] or False
832             start_date_current_period = start_date_current_period or current_date_beginning
833             day = mx.DateTime.strptime(start_date_current_period, '%Y-%m-%d %H:%M:%S')
834             dbefore = mx.DateTime.DateTime(day.year, day.month, day.day) - one_minute
835             date_for_start = dbefore.strftime('%Y-%m-%d %H:%M:%S')   # one day before current period
836             already_out = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='out', done = True, context=context),
837             already_in = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='in', done = True, context=context),
838             outgoing = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='out', done = False, context=context),
839             incoming = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='in', done = False, context=context),
840             outgoing_before = self._get_outgoing_before(cr, uid, val, start_date_current_period, day_before_calculated_period, context=context),
841             incoming_before = self._get_in_out(cr, uid, val, start_date_current_period, day_before_calculated_period, direction='in', done = False, context=context),
842             stock_start = self._get_stock_start(cr, uid, val, date_for_start, context=context),
843             if start_date_current_period == val.period_id.date_start:   # current period is calculated
844                 current=True
845             else:
846                 current=False
847             factor, round_value = self._from_default_uom_factor(cr, uid, val, val.product_uom.id, context=context)
848             self.write(cr, uid, ids, {
849                 'already_out': rounding(already_out[0]*factor,round_value),
850                 'already_in': rounding(already_in[0]*factor,round_value),
851                 'outgoing': rounding(outgoing[0]*factor,round_value),
852                 'incoming': rounding(incoming[0]*factor,round_value),
853                 'outgoing_before' : rounding(outgoing_before[0]*factor,round_value),
854                 'incoming_before': rounding((incoming_before[0]+ (not current and already_in[0]))*factor,round_value),
855                 'outgoing_left': rounding(val.planned_outgoing - (outgoing[0] + (current and already_out[0]))*factor,round_value),
856                 'incoming_left': rounding(val.to_procure - (incoming[0] + (current and already_in[0]))*factor,round_value),
857                 'stock_start' : rounding(stock_start[0]*factor,round_value),
858                 'stock_simulation': rounding(val.to_procure - val.planned_outgoing + (stock_start[0]+ incoming_before[0] - outgoing_before[0] \
859                                      + (not current and already_in[0]))*factor,round_value),
860             })
861         return True
862
863 # method below converts quantities and uoms to general OpenERP standard with UoM Qty, UoM, UoS Qty, UoS.
864 # from stock_planning standard where you have one Qty and one UoM (any from UoM or UoS category) 
865 # so if UoM is from UoM category it is used as UoM in standard and if product has UoS the UoS will be calcualated.
866 # If UoM is from UoS category it is recalculated to basic UoS from product (in planning you can use any UoS from UoS category) 
867 # and basic UoM is calculated.
868     def _qty_to_standard(self, cr, uid, val, context):
869         uos = False
870         uos_qty = 0.0
871         if val.product_uom.category_id.id == val.product_id.uom_id.category_id.id:
872             uom_qty = val.incoming_left
873             uom = val.product_uom.id
874             if val.product_id.uos_id:
875                 uos = val.product_id.uos_id.id
876                 coeff_uom2def = self._to_default_uom_factor(cr, uid, val, val.product_uom.id, {})
877                 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val, uos, {})
878                 uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
879         elif val.product_uom.category_id.id == val.product_id.uos_id.category_id.id:
880             coeff_uom2def = self._to_default_uom_factor(cr, uid, val, val.product_uom.id, {})
881             uos = val.product_id.uos_id.id
882             coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val, uos, {})
883             uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
884             uom = val.product_id.uom_id.id
885             coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val, uom, {})
886             uom_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
887         return uom_qty, uom, uos_qty, uos
888     
889     def procure_incomming_left(self, cr, uid, ids, context, *args):
890         for obj in self.browse(cr, uid, ids):
891             if obj.incoming_left <= 0:
892                 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
893             uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
894             user = self.pool.get('res.users').browse(cr, uid, uid, context)
895             proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
896                         'company_id' : obj.company_id.id,
897                         'name': _('Manual planning for ') + obj.period_id.name,
898                         'origin': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
899                         'date_planned': obj.period_id.date_start,
900                         'product_id': obj.product_id.id,
901                         'product_qty': uom_qty,
902                         'product_uom': uom,
903                         'product_uos_qty': uos_qty,
904                         'product_uos': uos,
905                         'location_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or obj.warehouse_id.lot_input_id.id, 
906                         'procure_method': 'make_to_order',
907                         'note' : _("Procurement created in MPS by user: ") + str(user.login) + _("  Creation Date: ") + \
908                                             time.strftime('%Y-%m-%d %H:%M:%S') + \
909                                         _("\nFor period: ") + obj.period_id.name + _(" according to state:") + \
910                                         _("\n Warehouse Forecast: ") + str(obj.warehouse_forecast) + \
911                                         _("\n Initial Stock: ") + str(obj.stock_start) + \
912                                         _("\n Planned Out: ") + str(obj.planned_outgoing) + _("    Planned In: ") + str(obj.to_procure) + \
913                                         _("\n Already Out: ") + str(obj.already_out) + _("    Already In: ") +  str(obj.already_in) + \
914                                         _("\n Confirmed Out: ") + str(obj.outgoing) + _("    Confirmed In: ") + str(obj.incoming) + \
915                                         _("\n Planned Out Before: ") + str(obj.outgoing_before) + _("    Confirmed In Before: ") + \
916                                                                                             str(obj.incoming_before) + \
917                                         _("\n Expected Out: ") + str(obj.outgoing_left) + _("    Incoming Left: ") + str(obj.incoming_left) + \
918                                         _("\n Stock Simulation: ") +  str(obj.stock_simulation) + _("    Minimum stock: ") + str(obj.minimum_op),
919
920                             }, context=context)
921             wf_service = netsvc.LocalService("workflow")
922             wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
923             self.calculate_planning(cr, uid, ids, context)
924             prev_text = obj.history or ""
925             self.write(cr, uid, ids, {
926                     'history' : prev_text + _('Requisition (') + str(user.login) + ", " + time.strftime('%Y.%m.%d %H:%M) ') + str(obj.incoming_left) + \
927                     " " + obj.product_uom.name + "\n",
928                 })
929         return True
930
931     def internal_supply(self, cr, uid, ids, context, *args):
932         for obj in self.browse(cr, uid, ids):
933             if obj.incoming_left <= 0:
934                 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
935             if not obj.supply_warehouse_id:
936                 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse !'))
937             if obj.supply_warehouse_id.id == obj.warehouse_id.id:
938                 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse different than calculated (destination) Warehouse !'))
939             uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
940             user = self.pool.get('res.users').browse(cr, uid, uid, context)
941             picking_id = self.pool.get('stock.picking').create(cr, uid, {
942                             'origin': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
943                             'type': 'internal',
944                             'state': 'auto',
945                             'date' :  obj.period_id.date_start,
946                             'move_type': 'direct',
947                             'invoice_state':  'none',
948                             'company_id': obj.company_id.id,
949                             'note': _("Pick created from MPS by user: ") + str(user.login) + _("  Creation Date: ") + \
950                                             time.strftime('%Y-%m-%d %H:%M:%S') + \
951                                         _("\nFor period: ") + obj.period_id.name + _(" according to state:") + \
952                                         _("\n Warehouse Forecast: ") + str(obj.warehouse_forecast) + \
953                                         _("\n Initial Stock: ") + str(obj.stock_start) + \
954                                         _("\n Planned Out: ") + str(obj.planned_outgoing) + _("    Planned In: ") + str(obj.to_procure) + \
955                                         _("\n Already Out: ") + str(obj.already_out) + _("    Already In: ") +  str(obj.already_in) + \
956                                         _("\n Confirmed Out: ") + str(obj.outgoing) + _("    Confirmed In: ") + str(obj.incoming) + \
957                                         _("\n Planned Out Before: ") + str(obj.outgoing_before) + _("    Confirmed In Before: ") + \
958                                                                                             str(obj.incoming_before) + \
959                                         _("\n Expected Out: ") + str(obj.outgoing_left) + _("    Incoming Left: ") + str(obj.incoming_left) + \
960                                         _("\n Stock Simulation: ") +  str(obj.stock_simulation) + _("    Minimum stock: ") + str(obj.minimum_op),
961                         })
962
963             move_id = self.pool.get('stock.move').create(cr, uid, {
964                         'name': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
965                         'picking_id': picking_id,
966                         'product_id': obj.product_id.id,
967                         'date_planned': obj.period_id.date_start,
968                         'product_qty': uom_qty,
969                         'product_uom': uom,
970                         'product_uos_qty': uos_qty,
971                         'product_uos': uos,
972                         'location_id': obj.stock_supply_location and obj.supply_warehouse_id.lot_stock_id.id or \
973                                                                 obj.supply_warehouse_id.lot_output_id.id,
974                         'location_dest_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or \
975                                                                 obj.warehouse_id.lot_input_id.id,
976                         'tracking_id': False,
977                         'company_id': obj.company_id.id,
978                     })
979             wf_service = netsvc.LocalService("workflow")
980             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
981
982         self.calculate_planning(cr, uid, ids, context)
983         prev_text = obj.history or ""
984         pick_name = self.pool.get('stock.picking').browse(cr, uid, picking_id).name
985         self.write(cr, uid, ids, {
986               'history' : prev_text + _('Pick List ')+ pick_name + " (" + str(user.login) + ", " + time.strftime('%Y.%m.%d %H:%M) ') \
987                 + str(obj.incoming_left) +" " + obj.product_uom.name + "\n",
988                 })
989
990         return True
991    
992     def product_id_change(self, cr, uid, ids, product_id):
993         ret={}
994         if product_id:
995             product_rec =  self.pool.get('product.product').browse(cr, uid, product_id)
996             ret['product_uom'] = product_rec.uom_id.id
997             ret['active_uom'] = product_rec.uom_id.id
998             ret['product_uom_categ'] = product_rec.uom_id.category_id.id
999             ret['product_uos_categ'] = product_rec.uos_id and product_rec.uos_id.category_id.id or False
1000         else:
1001             ret['product_uom'] = False
1002             ret['product_uom_categ'] = False
1003             ret['product_uos_categ'] = False
1004         res = {'value': ret}
1005         return res
1006
1007 stock_planning()
1008
1009 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: