1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
24 from mx.DateTime import RelativeDateTime, now, DateTime, localtime
26 from osv import osv, fields
28 from tools.translate import _
30 def rounding(fl, round_value):
33 return round(fl / round_value) * round_value
35 # Periods have no company_id field as they can be shared across similar companies.
36 # If somone thinks different it can be improved.
37 class stock_period(osv.osv):
38 _name = "stock.period"
39 _description = "stock period"
41 'name': fields.char('Period Name', size=64, required=True),
42 'date_start': fields.datetime('Start Date', required=True),
43 'date_stop': fields.datetime('End Date', required=True),
44 'state': fields.selection([('draft','Draft'), ('open','Open'),('close','Close')], 'State')
50 def button_open(self, cr, uid, ids, context):
51 self.write(cr, uid, ids, {'state': 'open'})
54 def button_close(self, cr, uid, ids, context):
55 self.write(cr, uid, ids, {'state': 'close'})
60 # Stock and Sales Forecast object. Previously stock_planning_sale_prevision.
61 # A lot of changes in 1.1
62 class stock_sale_forecast(osv.osv):
63 _name = "stock.sale.forecast"
66 'company_id':fields.many2one('res.company', 'Company', required=True),
67 'create_uid': fields.many2one('res.users', 'Responsible'),
68 'name': fields.char('Name', size=64, readonly=True, states={'draft': [('readonly',False)]}),
69 'user_id': fields.many2one('res.users', 'Created/Validated by',readonly=True, \
70 help='Shows who created this forecast, or who validated.'),
71 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, readonly=True, states={'draft': [('readonly',False)]}, \
72 help='Shows which warehouse this forecast concerns. '\
73 'If during stock planning you will need sales forecast for all warehouses choose any warehouse now.'),
74 'period_id': fields.many2one('stock.period', 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
75 help = 'Shows which period this forecast concerns.'),
76 'product_id': fields.many2one('product.product', 'Product', readonly=True, required=True, states={'draft':[('readonly',False)]}, \
77 help = 'Shows which product this forecast concerns.'),
78 'product_qty': fields.float('Product Quantity', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
79 help= 'Forecasted quantity.'),
80 'product_amt': fields.float('Product Amount', readonly=True, states={'draft':[('readonly',False)]}, \
81 help='Forecast value which will be converted to Product Quantity according to prices.'),
82 'product_uom_categ': fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uom domain
83 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
84 help = "Unit of Measure used to show the quanities of stock calculation." \
85 "You can use units form default category or from second category (UoS category)."),
86 'product_uos_categ' : fields.many2one('product.uom.categ', 'Product UoS Category'), # Invisible field for product_uos domain
87 # Field used in onchange_uom to check what uom was before change and recalculate quantities acording to old uom (active_uom) and new uom.
88 'active_uom': fields.many2one('product.uom', string = "Active UoM"),
89 'state': fields.selection([('draft','Draft'),('validated','Validated')],'State',readonly=True),
90 'analyzed_period1_id': fields.many2one('stock.period', 'Period1', readonly=True, states={'draft':[('readonly',False)]},),
91 'analyzed_period2_id': fields.many2one('stock.period', 'Period2', readonly=True, states={'draft':[('readonly',False)]},),
92 'analyzed_period3_id': fields.many2one('stock.period', 'Period3', readonly=True, states={'draft':[('readonly',False)]},),
93 'analyzed_period4_id': fields.many2one('stock.period', 'Period4', readonly=True, states={'draft':[('readonly',False)]},),
94 'analyzed_period5_id': fields.many2one('stock.period' , 'Period5', readonly=True, states={'draft':[('readonly',False)]},),
95 'analyzed_user_id': fields.many2one('res.users', 'This User', required=False, readonly=True, states={'draft':[('readonly',False)]},),
96 'analyzed_dept_id': fields.many2one('hr.department', 'This Department', required=False, \
97 readonly=True, states={'draft':[('readonly',False)]},),
98 'analyzed_warehouse_id': fields.many2one('stock.warehouse' , 'This Warehouse', required=False, \
99 readonly=True, states={'draft':[('readonly',False)]}),
100 'analyze_company': fields.boolean('Per Company', readonly=True, states={'draft':[('readonly',False)]}, \
101 help = "Check this box to see the sales for whole company."),
102 'analyzed_period1_per_user': fields.float('This User Period1', readonly=True),
103 'analyzed_period2_per_user': fields.float('This User Period2', readonly=True),
104 'analyzed_period3_per_user': fields.float('This User Period3', readonly=True),
105 'analyzed_period4_per_user': fields.float('This User Period4', readonly=True),
106 'analyzed_period5_per_user': fields.float('This User Period5', readonly=True),
107 'analyzed_period1_per_dept': fields.float('This Dept Period1', readonly=True),
108 'analyzed_period2_per_dept': fields.float('This Dept Period2', readonly=True),
109 'analyzed_period3_per_dept': fields.float('This Dept Period3', readonly=True),
110 'analyzed_period4_per_dept': fields.float('This Dept Period4', readonly=True),
111 'analyzed_period5_per_dept': fields.float('This Dept Period5', readonly=True),
112 'analyzed_period1_per_warehouse': fields.float('This Warehouse Period1', readonly=True),
113 'analyzed_period2_per_warehouse': fields.float('This Warehouse Period2', readonly=True),
114 'analyzed_period3_per_warehouse': fields.float('This Warehouse Period3', readonly=True),
115 'analyzed_period4_per_warehouse': fields.float('This Warehouse Period4', readonly=True),
116 'analyzed_period5_per_warehouse': fields.float('This Warehouse Period5', readonly=True),
117 'analyzed_period1_per_company': fields.float('This Copmany Period1', readonly=True),
118 'analyzed_period2_per_company': fields.float('This Company Period2', readonly=True),
119 'analyzed_period3_per_company': fields.float('This Company Period3', readonly=True),
120 'analyzed_period4_per_company': fields.float('This Company Period4', readonly=True),
121 'analyzed_period5_per_company': fields.float('This Company Period5', readonly=True),
124 'user_id': lambda obj, cr, uid, context: uid,
126 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.sale.forecast', context=c),
129 def action_validate(self, cr, uid, ids, *args):
130 self.write(cr, uid, ids, {'state':'validated','user_id':uid})
133 def unlink(self, cr, uid, ids, context=None):
134 forecasts = self.read(cr, uid, ids, ['state'])
137 if t['state'] in ('draft'):
138 unlink_ids.append(t['id'])
140 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Validated Sale Forecasts !'))
141 osv.osv.unlink(self, cr, uid, unlink_ids,context=context)
144 def onchange_company(self, cr, uid, ids, company_id=False):
148 result['warehouse_id'] = False
149 result['analyzed_user_id'] = False
150 result['analyzed_dept_id'] = False
151 result['analyzed_warehouse_id'] = False
152 return {'value': result}
154 def product_id_change(self, cr, uid, ids, product_id=False):
157 product_rec = self.pool.get('product.product').browse(cr, uid, product_id)
158 ret['product_uom'] = product_rec.uom_id.id
159 ret['product_uom_categ'] = product_rec.uom_id.category_id.id
160 ret['product_uos_categ'] = product_rec.uos_id and product_rec.uos_id.category_id.id or False
161 ret['active_uom'] = product_rec.uom_id.id
163 ret['product_uom'] = False
164 ret['product_uom_categ'] = False
165 ret['product_uos_categ'] = False
169 def onchange_uom(self, cr, uid, ids, product_uom=False, product_qty=0.0, active_uom=False):
174 val1 = self.browse(cr, uid, ids)
176 coeff_uom2def = self._to_default_uom_factor(cr, uid, val, active_uom, {})
177 coeff_def2uom, round_value = self._from_default_uom_factor( cr, uid, val, product_uom, {})
178 coeff = coeff_uom2def * coeff_def2uom
179 ret['product_qty'] = rounding(coeff * product_qty, round_value)
180 ret['active_uom'] = product_uom
181 return {'value': ret}
183 def product_amt_change(self, cr, uid, ids, product_amt = 0.0, product_uom=False):
190 val1 = self.browse(cr, uid, ids)
192 if (product_uom != val.product_id.uom_id.id):
193 coeff_def2uom, round_value = self._from_default_uom_factor( cr, uid, val, product_uom, {})
194 ret['product_qty'] = rounding(coeff_def2uom * product_amt/(val.product_id.product_tmpl_id.list_price), round_value)
198 def _to_default_uom_factor(self, cr, uid, val, uom_id, context=None):
199 uom_obj = self.pool.get('product.uom')
200 uom = uom_obj.browse(cr, uid, uom_id, context=context)
202 if uom.category_id.id <> val.product_id.uom_id.category_id.id:
203 coef = coef / val.product_id.uos_coeff
204 return val.product_id.uom_id.factor / coef
206 def _from_default_uom_factor(self, cr, uid, val, uom_id, context):
207 uom_obj = self.pool.get('product.uom')
208 uom = uom_obj.browse(cr, uid, uom_id, context=context)
210 if uom.category_id.id <> val.product_id.uom_id.category_id.id:
211 res = res / val.product_id.uos_coeff
212 return res / val.product_id.uom_id.factor, uom.rounding
214 def _sales_per_users(self, cr, uid, so, so_line, company, users):
215 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) " \
216 "WHERE (sol.id IN %s) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN %s) AND (s.company_id=%s) " \
217 "AND (s.user_id IN %s) " ,(tuple(so_line), tuple(so), company, tuple(users)))
218 ret = cr.fetchone()[0] or 0.0
221 def _sales_per_warehouse(self, cr, uid, so, so_line, company, shops):
222 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) " \
223 "WHERE (sol.id IN %s) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN %s)AND (s.company_id=%s) " \
224 "AND (s.shop_id IN %s)" ,(tuple(so_line), tuple(so), company, tuple(shops)))
225 ret = cr.fetchone()[0] or 0.0
228 def _sales_per_company(self, cr, uid, so, so_line, company):
229 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) " \
230 "WHERE (sol.id IN %s) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN %s) AND (s.company_id=%s)", (tuple(so_line), tuple(so), company))
231 ret = cr.fetchone()[0] or 0.0
234 def calculate_sales_history(self, cr, uid, ids, context, *args):
235 sales = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],]
236 for obj in self.browse(cr, uid, ids):
237 periods = obj.analyzed_period1_id, obj.analyzed_period2_id, obj.analyzed_period3_id, obj.analyzed_period4_id, obj.analyzed_period5_id
238 so_obj = self.pool.get('sale.order')
239 so_line_obj = self.pool.get('sale.order.line')
240 so_line_product_ids = so_line_obj.search(cr, uid, [('product_id','=', obj.product_id.id)], context = context)
241 if so_line_product_ids:
242 so_line_product_set = ','.join(map(str,so_line_product_ids))
243 if obj.analyzed_warehouse_id:
244 shops = self.pool.get('sale.shop').search(cr, uid,[('warehouse_id','=', obj.analyzed_warehouse_id.id)], context = context)
245 shops_set = ','.join(map(str,shops))
248 if obj.analyzed_dept_id:
249 dept_obj = self.pool.get('hr.department')
250 dept_id = obj.analyzed_dept_id.id and [obj.analyzed_dept_id.id] or []
251 dept_ids = dept_obj.search(cr,uid,[('parent_id','child_of',dept_id)])
252 cr.execute("SELECT user_id FROM resource_resource rsc, hr_employee emp WHERE (emp.resource_id = rsc.id and emp.department_id IN %s)" ,(tuple(dept_ids),))
253 dept_users = [x for x, in cr.fetchall()]
254 dept_users_set = map(str,dept_users)
257 factor, round_value = self._from_default_uom_factor(cr, uid, obj, obj.product_uom.id, context=context)
258 for i, period in enumerate(periods):
260 so_period_ids = so_obj.search(cr, uid, [('date_order','>=',period.date_start),('date_order','<=',period.date_stop) ], context = context)
262 if obj.analyzed_user_id:
263 user_set = str(obj.analyzed_user_id.id)
265 sales[i][0] = self._sales_per_users(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, user_set)
266 sales[i][0] *= factor
268 sales[i][1] = self._sales_per_users(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, dept_users_set)
269 sales[i][1] *= factor
271 sales[i][2] = self._sales_per_warehouse(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, shops_set)
272 sales[i][2] *= factor
273 if obj.analyze_company:
274 sales[i][3] = self._sales_per_company(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, )
275 sales[i][3] *= factor
276 self.write(cr, uid, ids, {
277 'analyzed_period1_per_user': sales[0][0],
278 'analyzed_period2_per_user': sales[1][0],
279 'analyzed_period3_per_user': sales[2][0],
280 'analyzed_period4_per_user': sales[3][0],
281 'analyzed_period5_per_user': sales[4][0],
282 'analyzed_period1_per_dept': sales[0][1],
283 'analyzed_period2_per_dept': sales[1][1],
284 'analyzed_period3_per_dept': sales[2][1],
285 'analyzed_period4_per_dept': sales[3][1],
286 'analyzed_period5_per_dept': sales[4][1],
287 'analyzed_period1_per_warehouse': sales[0][2],
288 'analyzed_period2_per_warehouse': sales[1][2],
289 'analyzed_period3_per_warehouse': sales[2][2],
290 'analyzed_period4_per_warehouse': sales[3][2],
291 'analyzed_period5_per_warehouse': sales[4][2],
292 'analyzed_period1_per_company': sales[0][3],
293 'analyzed_period2_per_company': sales[1][3],
294 'analyzed_period3_per_company': sales[2][3],
295 'analyzed_period4_per_company': sales[3][3],
296 'analyzed_period5_per_company': sales[4][3],
301 stock_sale_forecast()
303 # The main Stock Planning object
304 # A lot of changes by contributor in ver 1.1
305 class stock_planning(osv.osv):
306 _name = "stock.planning"
308 def _get_in_out(self, cr, uid, val, date_start, date_stop, direction, done, context=None):
311 product_obj = self.pool.get('product.product')
313 'field': "incoming_qty",
314 'adapter': lambda x: x,
317 'field': "outgoing_qty",
318 'adapter': lambda x: -x,
321 context['from_date'] = date_start
322 context['to_date'] = date_stop
323 locations = [val.warehouse_id.lot_stock_id.id,]
324 if not val.stock_only:
325 locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
326 context['location'] = locations
327 context['compute_child'] = True
328 prod_id = val.product_id.id
331 c1.update({ 'states':('done',), 'what':(direction,) })
333 st = product_obj.get_product_available(cr,uid, prod_ids, context=c1)
334 res = mapping[direction]['adapter'](st.get(prod_id,0.0))
336 product = product_obj.read(cr, uid, prod_id,[], context)
337 product_qty = product[mapping[direction]['field']]
338 res = mapping[direction]['adapter'](product_qty)
341 def _get_outgoing_before(self, cr, uid, val, date_start, date_stop, context=None):
342 cr.execute("SELECT sum(planning.planned_outgoing), planning.product_uom \
343 FROM stock_planning AS planning \
344 LEFT JOIN stock_period AS period \
345 ON (planning.period_id = period.id) \
346 WHERE (period.date_stop >= %s) AND (period.date_stop <= %s) \
347 AND (planning.product_id = %s) AND (planning.company_id = %s) \
348 GROUP BY planning.product_uom", \
349 (date_start, date_stop, val.product_id.id, val.company_id.id,))
350 planning_qtys = cr.fetchall()
351 res = self._to_planning_uom(cr, uid, val, planning_qtys, context)
354 def _to_planning_uom(self, cr, uid, val, qtys, context):
357 uom_obj = self.pool.get('product.uom')
358 for qty, prod_uom in qtys:
359 coef = self._to_default_uom_factor(cr, uid, val, prod_uom, context=context)
360 res_coef, round_value = self._from_default_uom_factor(cr, uid, val, val.product_uom.id, context=context)
361 coef = coef * res_coef
362 res_qty += rounding(qty * coef, round_value)
366 def _get_forecast(self, cr, uid, ids, field_names, arg, context=None):
368 for val in self.browse(cr, uid, ids):
370 valid_part = val.confirmed_forecasts_only and " AND state = 'validated'" or ""
371 cr.execute('SELECT sum(product_qty), product_uom \
372 FROM stock_sale_forecast \
373 WHERE product_id = %s AND period_id = %s AND company_id = %s '+valid_part+ \
374 'GROUP BY product_uom', \
375 (val.product_id.id,val.period_id.id, val.company_id.id))
376 company_qtys = cr.fetchall()
377 res[val.id]['company_forecast'] = self._to_planning_uom(cr, uid, val, company_qtys, context)
379 cr.execute('SELECT sum(product_qty), product_uom \
380 FROM stock_sale_forecast \
381 WHERE product_id = %s and period_id = %s AND warehouse_id = %s ' + valid_part + \
382 'GROUP BY product_uom', \
383 (val.product_id.id,val.period_id.id, val.warehouse_id.id))
384 warehouse_qtys = cr.fetchall()
385 res[val.id]['warehouse_forecast'] = self._to_planning_uom(cr, uid, val, warehouse_qtys, context)
386 res[val.id]['warehouse_forecast'] = rounding(res[val.id]['warehouse_forecast'], val.product_id.uom_id.rounding)
389 def _get_stock_start(self, cr, uid, val, date, context=None):
390 context['from_date'] = None
391 context['to_date'] = date
392 locations = [val.warehouse_id.lot_stock_id.id,]
393 if not val.stock_only:
394 locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
395 context['location'] = locations
396 context['compute_child'] = True
397 product_obj = self.pool.get('product.product').read(cr, uid,val.product_id.id,[], context)
398 res = product_obj['qty_available'] # value for stock_start
401 def _get_past_future(self, cr, uid, ids, field_names, arg, context):
403 for val in self.browse(cr, uid, ids, context=context):
404 if val.period_id.date_stop < time.strftime('%Y-%m-%d'):
407 res[val.id] = 'Future'
410 def _get_op(self, cr, uid, ids, field_names, arg, context=None): # op = OrderPoint
412 for val in self.browse(cr, uid, ids, context=context):
414 cr.execute("SELECT product_min_qty, product_max_qty, product_uom \
415 FROM stock_warehouse_orderpoint \
416 WHERE warehouse_id = %s AND product_id = %s AND active = 'TRUE'", (val.warehouse_id.id, val.product_id.id))
417 ret = cr.fetchone() or [0.0,0.0,False]
421 coef = self._to_default_uom_factor(cr, uid, val, ret[2], context)
422 res_coef, round_value = self._from_default_uom_factor(cr, uid, val, val.product_uom.id, context=context)
423 coef = coef * res_coef
424 res[val.id]['minimum_op'] = rounding(ret[0]*coef, round_value)
425 res[val.id]['maximum_op'] = ret[1]*coef
428 def onchange_company(self, cr, uid, ids, company_id=False):
431 result['warehouse_id'] = False
432 return {'value': result}
434 def onchange_uom(self, cr, uid, ids, product_uom=False):
440 val1 = self.browse(cr, uid, ids)
443 coeff_uom2def = self._to_default_uom_factor(cr, uid, val, val.active_uom.id, {})
444 coeff_def2uom, round_value = self._from_default_uom_factor( cr, uid, val, product_uom, {})
445 coeff = coeff_uom2def * coeff_def2uom
446 ret['planned_outgoing'] = rounding(coeff * val.planned_outgoing, round_value)
447 ret['to_procure'] = rounding(coeff * val.to_procure, round_value)
448 ret['active_uom'] = product_uom
449 return {'value': ret}
452 'company_id': fields.many2one('res.company', 'Company', required = True),
453 'history': fields.text('Procurement History', readonly=True, help = "History of procurement or internal supply of this planning line."),
454 'state' : fields.selection([('draft','Draft'),('done','Done')],'State',readonly=True),
455 'period_id': fields.many2one('stock.period' , 'Period', required=True, \
456 help = 'Period for this planning. Requisition will be created for beginning of the period.'),
457 'warehouse_id': fields.many2one('stock.warehouse','Warehouse', required=True),
458 'product_id': fields.many2one('product.product' , 'Product', required=True, help = 'Product which this planning is created for.'),
459 'product_uom_categ' : fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uom domain
460 'product_uom': fields.many2one('product.uom', 'UoM', required=True, help = "Unit of Measure used to show the quanities of stock calculation." \
461 "You can use units form default category or from second category (UoS category)."),
462 'product_uos_categ': fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uos domain
463 # Field used in onchange_uom to check what uom was before change to recalculate quantities acording to old uom (active_uom) and new uom.
464 'active_uom': fields.many2one('product.uom', string = "Active UoM"),
465 'planned_outgoing': fields.float('Planned Out', required=True, \
466 help = 'Enter planned outgoing quantity from selected Warehouse during the selected Period of selected Product. '\
467 'To plan this value look at Confirmed Out or Sales Forecasts. This value should be equal or greater than Confirmed Out.'),
468 'company_forecast': fields.function(_get_forecast, method=True, string ='Company Forecast', multi = 'company', \
469 help = 'All sales forecasts for whole company (for all Warehouses) of selected Product during selected Period.'),
470 'warehouse_forecast': fields.function(_get_forecast, method=True, string ='Warehouse Forecast', multi = 'warehouse',\
471 help = 'All sales forecasts for selected Warehouse of selected Product during selected Period.'),
472 'stock_simulation': fields.float('Stock Simulation', readonly =True, \
473 help = 'Stock simulation at the end of selected Period.\n For current period it is: \n' \
474 'Initial Stock - Already Out + Already In - Expected Out + Incoming Left.\n' \
475 'For periods ahead it is: \nInitial Stock - Planned Out Before + Incoming Before - Planned Out + Planned In.'),
476 'incoming': fields.float('Confirmed In', readonly=True, \
477 help = 'Quantity of all confirmed incoming moves in calculated Period.'),
478 'outgoing': fields.float('Confirmed Out', readonly=True, \
479 help = 'Quantity of all confirmed outgoing moves in calculated Period.'),
480 'incoming_left': fields.float('Incoming Left', readonly=True, \
481 help = 'Quantity left to Planned incoming quantity. This is calculated difference between Planned In and Confirmed In. ' \
482 'For current period Already In is also calculated. This value is used to create procurement for lacking quantity.'),
483 'outgoing_left': fields.float('Expected Out', readonly=True, \
484 help = 'Quantity expected to go out in selected period. As a difference between Planned Out and Confirmed Out. ' \
485 'For current period Already Out is also calculated'),
486 'to_procure': fields.float(string='Planned In', required=True, \
487 help = 'Enter quantity which (by your plan) should come in. Change this value and observe Stock simulation. ' \
488 'This value should be equal or greater than Confirmed In.'),
489 'line_time': fields.function(_get_past_future, method=True,type='char', string='Past/Future'),
490 'minimum_op': fields.function(_get_op, method=True, type='float', string = 'Minimum Rule', multi= 'minimum', \
491 help = 'Minimum quantity set in Minimum Stock Rules for this Warhouse'),
492 'maximum_op': fields.function(_get_op, method=True, type='float', string = 'Maximum Rule', multi= 'maximum', \
493 help = 'Maximum quantity set in Minimum Stock Rules for this Warhouse'),
494 'outgoing_before': fields.float('Planned Out Before', readonly=True, \
495 help= 'Planned Out in periods before calculated. '\
496 'Between start date of current period and one day before start of calculated period.'),
497 'incoming_before': fields.float('Incoming Before', readonly = True, \
498 help= 'Confirmed incoming in periods before calculated (Including Already In). '\
499 'Between start date of current period and one day before start of calculated period.'),
500 'stock_start': fields.float('Initial Stock', readonly=True, \
501 help= 'Stock quantity one day before current period.'),
502 'already_out': fields.float('Already Out', readonly=True, \
503 help= 'Quantity which is already dispatched out of this warehouse in current period.'),
504 'already_in': fields.float('Already In', readonly=True, \
505 help= 'Quantity which is already picked up to this warehouse in current period.'),
506 'stock_only': fields.boolean("Stock Location Only", help = "Check to calculate stock location of selected warehouse only. " \
507 "If not selected calculation is made for input, stock and output location of warehouse."),
508 "procure_to_stock": fields.boolean("Procure To Stock Location", help = "Chect to make procurement to stock location of selected warehouse. " \
509 "If not selected procurement will be made into input location of warehouse."),
510 "confirmed_forecasts_only": fields.boolean("Validated Forecasts", help = "Check to take validated forecasts only. " \
511 "If not checked system takes validated and draft forecasts."),
512 'supply_warehouse_id': fields.many2one('stock.warehouse','Source Warehouse', help = "Warehouse used as source in supply pick move created by 'Supply from Another Warhouse'."),
513 "stock_supply_location": fields.boolean("Stock Supply Location", help = "Check to supply from Stock location of Supply Warehouse. " \
514 "If not checked supply will be made from Output location of Supply Warehouse. Used in 'Supply from Another Warhouse' with Supply Warehouse."),
519 'state': lambda *args: 'draft' ,
521 'planned_outgoing': 0.0,
522 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.planning', context=c),
527 def _to_default_uom_factor(self, cr, uid, val, uom_id, context=None):
528 uom_obj = self.pool.get('product.uom')
529 uom = uom_obj.browse(cr, uid, uom_id, context=context)
531 if uom.category_id.id != val.product_id.uom_id.category_id.id:
532 coef = coef / val.product_id.uos_coeff
533 return val.product_id.uom_id.factor / coef
536 def _from_default_uom_factor(self, cr, uid, val, uom_id, context=None):
537 uom_obj = self.pool.get('product.uom')
538 uom = uom_obj.browse(cr, uid, uom_id, context=context)
540 if uom.category_id.id != val.product_id.uom_id.category_id.id:
541 res = res / val.product_id.uos_coeff
542 return res / val.product_id.uom_id.factor, uom.rounding
544 def calculate_planning(self, cr, uid, ids, context, *args):
545 one_minute = RelativeDateTime(minutes=1)
546 current_date_beginning_c = mx.DateTime.today()
547 current_date_end_c = current_date_beginning_c + RelativeDateTime(days=1, minutes=-1) # to get hour 23:59:00
548 current_date_beginning = current_date_beginning_c.strftime('%Y-%m-%d %H:%M:%S')
549 current_date_end = current_date_end_c.strftime('%Y-%m-%d %H:%M:%S')
550 for val in self.browse(cr, uid, ids, context=context):
551 day = mx.DateTime.strptime(val.period_id.date_start, '%Y-%m-%d %H:%M:%S')
552 dbefore = mx.DateTime.DateTime(day.year, day.month, day.day) - one_minute
553 day_before_calculated_period = dbefore.strftime('%Y-%m-%d %H:%M:%S') # one day before start of calculated period
554 cr.execute("SELECT date_start \
555 FROM stock_period AS period \
556 LEFT JOIN stock_planning AS planning \
557 ON (planning.period_id = period.id) \
558 WHERE (period.date_stop >= %s) AND (period.date_start <= %s) AND \
559 planning.product_id = %s", (current_date_end, current_date_end, val.product_id.id,)) #
561 start_date_current_period = date and date[0] or False
562 start_date_current_period = start_date_current_period or current_date_beginning
563 day = mx.DateTime.strptime(start_date_current_period, '%Y-%m-%d %H:%M:%S')
564 dbefore = mx.DateTime.DateTime(day.year, day.month, day.day) - one_minute
565 date_for_start = dbefore.strftime('%Y-%m-%d %H:%M:%S') # one day before current period
566 already_out = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='out', done = True, context=context),
567 already_in = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='in', done = True, context=context),
568 outgoing = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='out', done = False, context=context),
569 incoming = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='in', done = False, context=context),
570 outgoing_before = self._get_outgoing_before(cr, uid, val, start_date_current_period, day_before_calculated_period, context=context),
571 incoming_before = self._get_in_out(cr, uid, val, start_date_current_period, day_before_calculated_period, direction='in', done = False, context=context),
572 stock_start = self._get_stock_start(cr, uid, val, date_for_start, context=context),
573 if start_date_current_period == val.period_id.date_start: # current period is calculated
577 factor, round_value = self._from_default_uom_factor(cr, uid, val, val.product_uom.id, context=context)
578 self.write(cr, uid, ids, {
579 'already_out': rounding(already_out[0]*factor,round_value),
580 'already_in': rounding(already_in[0]*factor,round_value),
581 'outgoing': rounding(outgoing[0]*factor,round_value),
582 'incoming': rounding(incoming[0]*factor,round_value),
583 'outgoing_before' : rounding(outgoing_before[0]*factor,round_value),
584 'incoming_before': rounding((incoming_before[0]+ (not current and already_in[0]))*factor,round_value),
585 'outgoing_left': rounding(val.planned_outgoing - (outgoing[0] + (current and already_out[0]))*factor,round_value),
586 'incoming_left': rounding(val.to_procure - (incoming[0] + (current and already_in[0]))*factor,round_value),
587 'stock_start': rounding(stock_start[0]*factor,round_value),
588 'stock_simulation': rounding(val.to_procure - val.planned_outgoing + (stock_start[0]+ incoming_before[0] - outgoing_before[0] \
589 + (not current and already_in[0]))*factor,round_value),
593 # method below converts quantities and uoms to general OpenERP standard with UoM Qty, UoM, UoS Qty, UoS.
594 # from stock_planning standard where you have one Qty and one UoM (any from UoM or UoS category)
595 # so if UoM is from UoM category it is used as UoM in standard and if product has UoS the UoS will be calcualated.
596 # If UoM is from UoS category it is recalculated to basic UoS from product (in planning you can use any UoS from UoS category)
597 # and basic UoM is calculated.
598 def _qty_to_standard(self, cr, uid, val, context):
601 if val.product_uom.category_id.id == val.product_id.uom_id.category_id.id:
602 uom_qty = val.incoming_left
603 uom = val.product_uom.id
604 if val.product_id.uos_id:
605 uos = val.product_id.uos_id.id
606 coeff_uom2def = self._to_default_uom_factor(cr, uid, val, val.product_uom.id, {})
607 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val, uos, {})
608 uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
609 elif val.product_uom.category_id.id == val.product_id.uos_id.category_id.id:
610 coeff_uom2def = self._to_default_uom_factor(cr, uid, val, val.product_uom.id, {})
611 uos = val.product_id.uos_id.id
612 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val, uos, {})
613 uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
614 uom = val.product_id.uom_id.id
615 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val, uom, {})
616 uom_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
617 return uom_qty, uom, uos_qty, uos
619 def procure_incomming_left(self, cr, uid, ids, context, *args):
620 for obj in self.browse(cr, uid, ids):
621 if obj.incoming_left <= 0:
622 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
623 uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
624 user = self.pool.get('res.users').browse(cr, uid, uid, context)
625 proc_id = self.pool.get('procurement.order').create(cr, uid, {
626 'company_id' : obj.company_id.id,
627 'name': _('Manual planning for ') + obj.period_id.name,
628 'origin': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
629 'date_planned': obj.period_id.date_start,
630 'product_id': obj.product_id.id,
631 'product_qty': uom_qty,
633 'product_uos_qty': uos_qty,
635 'location_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or obj.warehouse_id.lot_input_id.id,
636 'procure_method': 'make_to_order',
637 'note' : _("Procurement created in MPS by user: ") + str(user.login) + _(" Creation Date: ") + \
638 time.strftime('%Y-%m-%d %H:%M:%S') + \
639 _("\nFor period: ") + obj.period_id.name + _(" according to state:") + \
640 _("\n Warehouse Forecast: ") + str(obj.warehouse_forecast) + \
641 _("\n Initial Stock: ") + str(obj.stock_start) + \
642 _("\n Planned Out: ") + str(obj.planned_outgoing) + _(" Planned In: ") + str(obj.to_procure) + \
643 _("\n Already Out: ") + str(obj.already_out) + _(" Already In: ") + str(obj.already_in) + \
644 _("\n Confirmed Out: ") + str(obj.outgoing) + _(" Confirmed In: ") + str(obj.incoming) + \
645 _("\n Planned Out Before: ") + str(obj.outgoing_before) + _(" Confirmed In Before: ") + \
646 str(obj.incoming_before) + \
647 _("\n Expected Out: ") + str(obj.outgoing_left) + _(" Incoming Left: ") + str(obj.incoming_left) + \
648 _("\n Stock Simulation: ") + str(obj.stock_simulation) + _(" Minimum stock: ") + str(obj.minimum_op),
651 wf_service = netsvc.LocalService("workflow")
652 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
653 self.calculate_planning(cr, uid, ids, context)
654 prev_text = obj.history or ""
655 self.write(cr, uid, ids, {
656 'history' : prev_text + _('Requisition (') + str(user.login) + ", " + time.strftime('%Y.%m.%d %H:%M) ') + str(obj.incoming_left) + \
657 " " + obj.product_uom.name + "\n",
661 def internal_supply(self, cr, uid, ids, context, *args):
662 for obj in self.browse(cr, uid, ids):
663 if obj.incoming_left <= 0:
664 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
665 if not obj.supply_warehouse_id:
666 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse !'))
667 if obj.supply_warehouse_id.id == obj.warehouse_id.id:
668 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse different than calculated (destination) Warehouse !'))
669 uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
670 user = self.pool.get('res.users').browse(cr, uid, uid, context)
671 picking_id = self.pool.get('stock.picking').create(cr, uid, {
672 'origin': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
675 'date' : obj.period_id.date_start,
676 'move_type': 'direct',
677 'invoice_state': 'none',
678 'company_id': obj.company_id.id,
679 'note': _("Pick created from MPS by user: ") + str(user.login) + _(" Creation Date: ") + \
680 time.strftime('%Y-%m-%d %H:%M:%S') + \
681 _("\nFor period: ") + obj.period_id.name + _(" according to state:") + \
682 _("\n Warehouse Forecast: ") + str(obj.warehouse_forecast) + \
683 _("\n Initial Stock: ") + str(obj.stock_start) + \
684 _("\n Planned Out: ") + str(obj.planned_outgoing) + _(" Planned In: ") + str(obj.to_procure) + \
685 _("\n Already Out: ") + str(obj.already_out) + _(" Already In: ") + str(obj.already_in) + \
686 _("\n Confirmed Out: ") + str(obj.outgoing) + _(" Confirmed In: ") + str(obj.incoming) + \
687 _("\n Planned Out Before: ") + str(obj.outgoing_before) + _(" Confirmed In Before: ") + \
688 str(obj.incoming_before) + \
689 _("\n Expected Out: ") + str(obj.outgoing_left) + _(" Incoming Left: ") + str(obj.incoming_left) + \
690 _("\n Stock Simulation: ") + str(obj.stock_simulation) + _(" Minimum stock: ") + str(obj.minimum_op),
693 move_id = self.pool.get('stock.move').create(cr, uid, {
694 'name': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
695 'picking_id': picking_id,
696 'product_id': obj.product_id.id,
697 'date_planned': obj.period_id.date_start,
698 'product_qty': uom_qty,
700 'product_uos_qty': uos_qty,
702 'location_id': obj.stock_supply_location and obj.supply_warehouse_id.lot_stock_id.id or \
703 obj.supply_warehouse_id.lot_output_id.id,
704 'location_dest_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or \
705 obj.warehouse_id.lot_input_id.id,
706 'tracking_id': False,
707 'company_id': obj.company_id.id,
709 wf_service = netsvc.LocalService("workflow")
710 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
712 self.calculate_planning(cr, uid, ids, context)
713 prev_text = obj.history or ""
714 pick_name = self.pool.get('stock.picking').browse(cr, uid, picking_id).name
715 self.write(cr, uid, ids, {
716 'history' : prev_text + _('Pick List ')+ pick_name + " (" + str(user.login) + ", " + time.strftime('%Y.%m.%d %H:%M) ') \
717 + str(obj.incoming_left) +" " + obj.product_uom.name + "\n",
722 def product_id_change(self, cr, uid, ids, product_id):
725 product_rec = self.pool.get('product.product').browse(cr, uid, product_id)
726 ret['product_uom'] = product_rec.uom_id.id
727 ret['active_uom'] = product_rec.uom_id.id
728 ret['product_uom_categ'] = product_rec.uom_id.category_id.id
729 ret['product_uos_categ'] = product_rec.uos_id and product_rec.uos_id.category_id.id or False
731 ret['product_uom'] = False
732 ret['product_uom_categ'] = False
733 ret['product_uos_categ'] = False
739 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: