1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
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.
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.
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/>.
21 ##############################################################################
23 from osv import fields,osv
25 from tools import config
30 from mx.DateTime import RelativeDateTime, now, DateTime, localtime
31 from tools.translate import _
33 def rounding(fl, round_value):
36 return round(fl / round_value) * round_value
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"
44 def _get_new_period_start(self,cr,uid,context={}):
45 cr.execute("select max(date_stop) from stock_period")
47 last_date = result and result[0] or False
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)
52 period_start = mx.DateTime.today()
53 return period_start.strftime('%Y-%m-%d')
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'),
63 'date_start':_get_new_period_start,
66 def create_period_weekly(self,cr, uid, ids, context={}):
67 res=self.create_period(cr, uid, ids, context, 6, 'Weekly')
71 'res_model': 'stock.period',
72 'type': 'ir.actions.act_window',
75 def create_period_monthly(self,cr, uid, ids, context={},interval=1):
76 for p in self.browse(cr, uid, ids, context):
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'),
86 ds = ds + RelativeDateTime(months=interval)
90 'res_model': 'stock.period',
91 'type': 'ir.actions.act_window',
94 def create_period(self,cr, uid, ids, context={}, interval=0, name='Daily'):
95 for p in self.browse(cr, uid, ids, context):
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)
101 new_name=de.strftime('%Y-%m-%d')
103 new_name=de.strftime('%Y, week %W')
104 self.pool.get('stock.period').create(cr, uid, {
106 'date_start': ds.strftime('%Y-%m-%d'),
107 'date_stop': de.strftime('%Y-%m-%d %H:%M:%S'),
109 ds = ds + RelativeDateTime(days=interval) + 1
113 'res_model': 'stock.period',
114 'type': 'ir.actions.act_window',
116 stock_period_createlines()
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"
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')
129 'state' : lambda * a : 'draft'
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"
138 # FIXME Add some period sugestion like below
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
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."),
158 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.sale.forecast.createlines', context=c),
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:
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.
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) )
197 forecast_qty = ret[1]
199 prod_uom = prod_uom or p.uom_id.id
200 prod_uos_categ = False
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,
208 'product_qty' : forecast_qty,
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,
218 'res_model': 'stock.sale.forecast',
219 'type': 'ir.actions.act_window',
221 stock_sale_forecast_createlines()
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"
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),
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),
293 def action_validate(self, cr, uid, ids, *args):
294 self.write(cr, uid, ids, {'state':'validated','user_id':uid})
297 def unlink(self, cr, uid, ids, context={}):
298 forecasts = self.read(cr, uid, ids, ['state'])
301 if t['state'] in ('draft'):
302 unlink_ids.append(t['id'])
304 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Validated Sale Forecasts !'))
305 osv.osv.unlink(self, cr, uid, unlink_ids,context=context)
308 def onchange_company(self, cr, uid, ids, 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}
317 def product_id_change(self, cr, uid, ids, 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
326 ret['product_uom'] = False
327 ret['product_uom_categ'] = False
328 ret['product_uos_categ'] = False
332 def onchange_uom(self, cr, uid, ids, product_uom=False, product_qty=0.0, active_uom=False ):
334 val1 = self.browse(cr, uid, ids)
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}
343 def product_amt_change(self, cr, uid, ids, product_amt = 0.0, product_uom=False):
347 val1 = self.browse(cr, uid, ids)
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)
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)
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
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)
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
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
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
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
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))
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))
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):
418 so_period_ids = so_obj.search(cr, uid, [('date_order','>=',period.date_start), ('date_order','<=',period.date_stop)], context = context)
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)
426 sales[i][1]= self._sales_per_users(cr, uid, so_period_set, so_line_product_set, obj.company_id.id, dept_users_set)
429 sales[i][2]= self._sales_per_warehouse(cr, uid, so_period_set, so_line_product_set, obj.company_id.id, shops_set)
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, )
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],
459 stock_sale_forecast()
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"
466 def onchange_company(self, cr, uid, ids, company_id):
469 result['warehouse_id2'] = False
470 return {'value': result}
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."),
485 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.planning', context=c),
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')
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()]
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 !'))
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) )
524 # forecast_qty = ret and ret[0] or 0.0
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],))
531 procure_to_stock = ret[5]
532 confirmed_forecasts_only = ret[6]
533 supply_warehouse_id = ret[7]
534 stock_supply_location = ret[8]
536 prod_uom = p.uom_id.id
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
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,
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,
568 'res_model': 'stock.planning',
569 'type': 'ir.actions.act_window',
571 stock_planning_createlines()
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"
579 def _get_in_out(self, cr, uid, val, date_start, date_stop, direction, done, context):
584 'field': "incoming_qty",
585 'adapter': lambda x: x,
588 'field': "outgoing_qty",
589 'adapter': lambda x: -x,
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
603 c1.update({ 'states':('done',), 'what':(direction,) })
605 st = product_obj.get_product_available(cr,uid, prod_ids, context=c1)
606 res = mapping[direction]['adapter'](st.get(prod_id,0.0))
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']
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)
627 def _to_planning_uom(self, cr, uid, val, qtys, context):
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)
639 def _get_forecast(self, cr, uid, ids, field_names, arg, context):
641 for val in self.browse(cr, uid, ids):
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)
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)
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
674 def _get_past_future(self, cr, uid, ids, field_names, arg, context):
676 for val in self.browse(cr, uid, ids, context=context):
677 if val.period_id.date_stop < time.strftime('%Y-%m-%d'):
680 res[val.id] = 'Future'
683 def _get_op(self, cr, uid, ids, field_names, arg, context): # op = OrderPoint
685 for val in self.browse(cr, uid, ids, context=context):
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]
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
701 def onchange_company(self, cr, uid, ids, company_id):
704 result['warehouse_id'] = False
705 return {'value': result}
707 def onchange_uom(self, cr, uid, ids, product_uom, ):
711 val1 = self.browse(cr, uid, ids)
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}
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."),
789 'state': lambda *args: 'draft' ,
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),
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)
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
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)
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
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,)) #
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
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),
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):
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
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,
903 'product_uos_qty': uos_qty,
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),
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",
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,
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),
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,
970 'product_uos_qty': uos_qty,
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,
979 wf_service = netsvc.LocalService("workflow")
980 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
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",
992 def product_id_change(self, cr, uid, ids, 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
1001 ret['product_uom'] = False
1002 ret['product_uom_categ'] = False
1003 ret['product_uos_categ'] = False
1004 res = {'value': ret}
1009 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: