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 ##############################################################################
23 from datetime import datetime
24 from dateutil.relativedelta import relativedelta
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=None):
51 self.write(cr, uid, ids, {'state': 'open'})
54 def button_close(self, cr, uid, ids, context=None):
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,
170 active_uom=False, product_id=False):
172 if product_uom and product_id:
173 coeff_uom2def = self._to_default_uom_factor(cr, uid, product_id, active_uom, {})
174 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, product_id, product_uom, {})
175 coeff = coeff_uom2def * coeff_def2uom
176 ret['product_qty'] = rounding(coeff * product_qty, round_value)
177 ret['active_uom'] = product_uom
178 return {'value': ret}
180 def product_amt_change(self, cr, uid, ids, product_amt=0.0, product_uom=False, product_id=False):
183 if product_amt and product_id:
184 product = self.pool.get('product.product').browse(cr, uid, product_id)
186 if (product_uom != product.uom_id.id):
187 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, product_id, product_uom, {})
188 qty = rounding(coeff_def2uom * product_amt/(product.product_tmpl_id.list_price), round_value)
189 res = {'value': {'product_qty': qty}}
192 def _to_default_uom_factor(self, cr, uid, product_id, uom_id, context=None):
193 uom_obj = self.pool.get('product.uom')
194 product_obj = self.pool.get('product.product')
195 product = product_obj.browse(cr, uid, product_id, context=context)
196 uom = uom_obj.browse(cr, uid, uom_id, context=context)
198 if uom.category_id.id <> product.uom_id.category_id.id:
199 coef = coef / product.uos_coeff
200 return product.uom_id.factor / coef
202 def _from_default_uom_factor(self, cr, uid, product_id, uom_id, context=None):
203 uom_obj = self.pool.get('product.uom')
204 product_obj = self.pool.get('product.product')
205 product = product_obj.browse(cr, uid, product_id, context=context)
206 uom = uom_obj.browse(cr, uid, uom_id, context=context)
208 if uom.category_id.id <> product.uom_id.category_id.id:
209 res = res / product.uos_coeff
210 return res / product.uom_id.factor, uom.rounding
212 def _sales_per_users(self, cr, uid, so, so_line, company, users):
213 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) " \
214 "WHERE (sol.id IN %s) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN %s) AND (s.company_id=%s) " \
215 "AND (s.user_id IN %s) " ,(tuple(so_line), tuple(so), company, tuple(users)))
216 ret = cr.fetchone()[0] or 0.0
219 def _sales_per_warehouse(self, cr, uid, so, so_line, company, shops):
220 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) " \
221 "WHERE (sol.id IN %s) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN %s)AND (s.company_id=%s) " \
222 "AND (s.shop_id IN %s)" ,(tuple(so_line), tuple(so), company, tuple(shops)))
223 ret = cr.fetchone()[0] or 0.0
226 def _sales_per_company(self, cr, uid, so, so_line, company):
227 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) " \
228 "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))
229 ret = cr.fetchone()[0] or 0.0
232 def calculate_sales_history(self, cr, uid, ids, context, *args):
233 sales = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],]
234 for obj in self.browse(cr, uid, ids, context=context):
235 periods = obj.analyzed_period1_id, obj.analyzed_period2_id, obj.analyzed_period3_id, obj.analyzed_period4_id, obj.analyzed_period5_id
236 so_obj = self.pool.get('sale.order')
237 so_line_obj = self.pool.get('sale.order.line')
238 so_line_product_ids = so_line_obj.search(cr, uid, [('product_id','=', obj.product_id.id)], context = context)
239 if so_line_product_ids:
240 so_line_product_set = ','.join(map(str,so_line_product_ids))
241 if obj.analyzed_warehouse_id:
242 shops = self.pool.get('sale.shop').search(cr, uid,[('warehouse_id','=', obj.analyzed_warehouse_id.id)], context = context)
243 shops_set = ','.join(map(str,shops))
246 if obj.analyzed_dept_id:
247 dept_obj = self.pool.get('hr.department')
248 dept_id = obj.analyzed_dept_id.id and [obj.analyzed_dept_id.id] or []
249 dept_ids = dept_obj.search(cr,uid,[('parent_id','child_of',dept_id)])
250 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),))
251 dept_users = [x for x, in cr.fetchall()]
252 dept_users_set = map(str,dept_users)
255 factor, round_value = self._from_default_uom_factor(cr, uid, obj.product_id.id, obj.product_uom.id, context=context)
256 for i, period in enumerate(periods):
258 so_period_ids = so_obj.search(cr, uid, [('date_order','>=',period.date_start),('date_order','<=',period.date_stop) ], context = context)
260 if obj.analyzed_user_id:
261 user_set = str(obj.analyzed_user_id.id)
262 sales[i][0] = self._sales_per_users(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, user_set)
263 sales[i][0] *= factor
265 sales[i][1] = self._sales_per_users(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, dept_users_set)
266 sales[i][1] *= factor
268 sales[i][2] = self._sales_per_warehouse(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, shops_set)
269 sales[i][2] *= factor
270 if obj.analyze_company:
271 sales[i][3] = self._sales_per_company(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, )
272 sales[i][3] *= factor
273 self.write(cr, uid, ids, {
274 'analyzed_period1_per_user': sales[0][0],
275 'analyzed_period2_per_user': sales[1][0],
276 'analyzed_period3_per_user': sales[2][0],
277 'analyzed_period4_per_user': sales[3][0],
278 'analyzed_period5_per_user': sales[4][0],
279 'analyzed_period1_per_dept': sales[0][1],
280 'analyzed_period2_per_dept': sales[1][1],
281 'analyzed_period3_per_dept': sales[2][1],
282 'analyzed_period4_per_dept': sales[3][1],
283 'analyzed_period5_per_dept': sales[4][1],
284 'analyzed_period1_per_warehouse': sales[0][2],
285 'analyzed_period2_per_warehouse': sales[1][2],
286 'analyzed_period3_per_warehouse': sales[2][2],
287 'analyzed_period4_per_warehouse': sales[3][2],
288 'analyzed_period5_per_warehouse': sales[4][2],
289 'analyzed_period1_per_company': sales[0][3],
290 'analyzed_period2_per_company': sales[1][3],
291 'analyzed_period3_per_company': sales[2][3],
292 'analyzed_period4_per_company': sales[3][3],
293 'analyzed_period5_per_company': sales[4][3],
297 stock_sale_forecast()
299 # The main Stock Planning object
300 # A lot of changes by contributor in ver 1.1
301 class stock_planning(osv.osv):
302 _name = "stock.planning"
304 def _get_in_out(self, cr, uid, val, date_start, date_stop, direction, done, context=None):
307 product_obj = self.pool.get('product.product')
309 'field': "incoming_qty",
310 'adapter': lambda x: x,
313 'field': "outgoing_qty",
314 'adapter': lambda x: -x,
317 context['from_date'] = date_start
318 context['to_date'] = date_stop
319 locations = [val.warehouse_id.lot_stock_id.id,]
320 if not val.stock_only:
321 locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
322 context['location'] = locations
323 context['compute_child'] = True
324 prod_id = val.product_id.id
326 context.update({ 'states':('done',), 'what':(direction,) })
328 st = product_obj.get_product_available(cr, uid, prod_ids, context=context)
329 res = mapping[direction]['adapter'](st.get(prod_id,0.0))
331 product = product_obj.read(cr, uid, prod_id,[], context)
332 product_qty = product[mapping[direction]['field']]
333 res = mapping[direction]['adapter'](product_qty)
336 def _get_outgoing_before(self, cr, uid, val, date_start, date_stop, context=None):
337 cr.execute("SELECT sum(planning.planned_outgoing), planning.product_uom \
338 FROM stock_planning AS planning \
339 LEFT JOIN stock_period AS period \
340 ON (planning.period_id = period.id) \
341 WHERE (period.date_stop >= %s) AND (period.date_stop <= %s) \
342 AND (planning.product_id = %s) AND (planning.company_id = %s) \
343 GROUP BY planning.product_uom", \
344 (date_start, date_stop, val.product_id.id, val.company_id.id,))
345 planning_qtys = cr.fetchall()
346 res = self._to_planning_uom(cr, uid, val, planning_qtys, context)
349 def _to_planning_uom(self, cr, uid, val, qtys, context=None):
352 uom_obj = self.pool.get('product.uom')
353 for qty, prod_uom in qtys:
354 coef = self._to_default_uom_factor(cr, uid, val.product_id.id, prod_uom, context=context)
355 res_coef, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, context=context)
356 coef = coef * res_coef
357 res_qty += rounding(qty * coef, round_value)
361 def _get_forecast(self, cr, uid, ids, field_names, arg, context=None):
363 for val in self.browse(cr, uid, ids, context=context):
365 valid_part = val.confirmed_forecasts_only and " AND state = 'validated'" or ""
366 cr.execute('SELECT sum(product_qty), product_uom \
367 FROM stock_sale_forecast \
368 WHERE product_id = %s AND period_id = %s AND company_id = %s '+valid_part+ \
369 'GROUP BY product_uom', \
370 (val.product_id.id,val.period_id.id, val.company_id.id))
371 company_qtys = cr.fetchall()
372 res[val.id]['company_forecast'] = self._to_planning_uom(cr, uid, val, company_qtys, context)
374 cr.execute('SELECT sum(product_qty), product_uom \
375 FROM stock_sale_forecast \
376 WHERE product_id = %s and period_id = %s AND warehouse_id = %s ' + valid_part + \
377 'GROUP BY product_uom', \
378 (val.product_id.id,val.period_id.id, val.warehouse_id.id))
379 warehouse_qtys = cr.fetchall()
380 res[val.id]['warehouse_forecast'] = self._to_planning_uom(cr, uid, val, warehouse_qtys, context)
381 res[val.id]['warehouse_forecast'] = rounding(res[val.id]['warehouse_forecast'], val.product_id.uom_id.rounding)
384 def _get_stock_start(self, cr, uid, val, date, context=None):
387 context['from_date'] = None
388 context['to_date'] = date
389 locations = [val.warehouse_id.lot_stock_id.id,]
390 if not val.stock_only:
391 locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
392 context['location'] = locations
393 context['compute_child'] = True
394 product_obj = self.pool.get('product.product').read(cr, uid,val.product_id.id,[], context)
395 res = product_obj['qty_available'] # value for stock_start
398 def _get_past_future(self, cr, uid, ids, field_names, arg, context=None):
400 for val in self.browse(cr, uid, ids, context=context):
401 if val.period_id.date_stop < time.strftime('%Y-%m-%d'):
404 res[val.id] = 'Future'
407 def _get_op(self, cr, uid, ids, field_names, arg, context=None): # op = OrderPoint
409 for val in self.browse(cr, uid, ids, context=context):
411 cr.execute("SELECT product_min_qty, product_max_qty, product_uom \
412 FROM stock_warehouse_orderpoint \
413 WHERE warehouse_id = %s AND product_id = %s AND active = 'TRUE'", (val.warehouse_id.id, val.product_id.id))
414 ret = cr.fetchone() or [0.0,0.0,False]
418 coef = self._to_default_uom_factor(cr, uid, val.product_id.id, ret[2], context)
419 res_coef, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, context=context)
420 coef = coef * res_coef
421 res[val.id]['minimum_op'] = rounding(ret[0]*coef, round_value)
422 res[val.id]['maximum_op'] = ret[1]*coef
425 def onchange_company(self, cr, uid, ids, company_id=False):
428 result['warehouse_id'] = False
429 return {'value': result}
431 def onchange_uom(self, cr, uid, ids, product_uom=False, product_id=False, active_uom=False,
432 planned_outgoing=0.0, to_procure=0.0):
437 coeff_uom2def = self._to_default_uom_factor(cr, uid, product_id, active_uom, {})
438 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, product_id, product_uom, {})
439 coeff = coeff_uom2def * coeff_def2uom
440 ret['planned_outgoing'] = rounding(coeff * planned_outgoing, round_value)
441 ret['to_procure'] = rounding(coeff * to_procure, round_value)
442 ret['active_uom'] = product_uom
443 return {'value': ret}
446 'company_id': fields.many2one('res.company', 'Company', required = True),
447 'history': fields.text('Procurement History', readonly=True, help = "History of procurement or internal supply of this planning line."),
448 'state' : fields.selection([('draft','Draft'),('done','Done')],'State',readonly=True),
449 'period_id': fields.many2one('stock.period' , 'Period', required=True, \
450 help = 'Period for this planning. Requisition will be created for beginning of the period.'),
451 'warehouse_id': fields.many2one('stock.warehouse','Warehouse', required=True),
452 'product_id': fields.many2one('product.product' , 'Product', required=True, help = 'Product which this planning is created for.'),
453 'product_uom_categ' : fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uom domain
454 'product_uom': fields.many2one('product.uom', 'UoM', required=True, help = "Unit of Measure used to show the quanities of stock calculation." \
455 "You can use units form default category or from second category (UoS category)."),
456 'product_uos_categ': fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uos domain
457 # Field used in onchange_uom to check what uom was before change to recalculate quantities acording to old uom (active_uom) and new uom.
458 'active_uom': fields.many2one('product.uom', string = "Active UoM"),
459 'planned_outgoing': fields.float('Planned Out', required=True, \
460 help = 'Enter planned outgoing quantity from selected Warehouse during the selected Period of selected Product. '\
461 'To plan this value look at Confirmed Out or Sales Forecasts. This value should be equal or greater than Confirmed Out.'),
462 'company_forecast': fields.function(_get_forecast, method=True, string ='Company Forecast', multi = 'company', \
463 help = 'All sales forecasts for whole company (for all Warehouses) of selected Product during selected Period.'),
464 'warehouse_forecast': fields.function(_get_forecast, method=True, string ='Warehouse Forecast', multi = 'warehouse',\
465 help = 'All sales forecasts for selected Warehouse of selected Product during selected Period.'),
466 'stock_simulation': fields.float('Stock Simulation', readonly =True, \
467 help = 'Stock simulation at the end of selected Period.\n For current period it is: \n' \
468 'Initial Stock - Already Out + Already In - Expected Out + Incoming Left.\n' \
469 'For periods ahead it is: \nInitial Stock - Planned Out Before + Incoming Before - Planned Out + Planned In.'),
470 'incoming': fields.float('Confirmed In', readonly=True, \
471 help = 'Quantity of all confirmed incoming moves in calculated Period.'),
472 'outgoing': fields.float('Confirmed Out', readonly=True, \
473 help = 'Quantity of all confirmed outgoing moves in calculated Period.'),
474 'incoming_left': fields.float('Incoming Left', readonly=True, \
475 help = 'Quantity left to Planned incoming quantity. This is calculated difference between Planned In and Confirmed In. ' \
476 'For current period Already In is also calculated. This value is used to create procurement for lacking quantity.'),
477 'outgoing_left': fields.float('Expected Out', readonly=True, \
478 help = 'Quantity expected to go out in selected period. As a difference between Planned Out and Confirmed Out. ' \
479 'For current period Already Out is also calculated'),
480 'to_procure': fields.float(string='Planned In', required=True, \
481 help = 'Enter quantity which (by your plan) should come in. Change this value and observe Stock simulation. ' \
482 'This value should be equal or greater than Confirmed In.'),
483 'line_time': fields.function(_get_past_future, method=True,type='char', string='Past/Future'),
484 'minimum_op': fields.function(_get_op, method=True, type='float', string = 'Minimum Rule', multi= 'minimum', \
485 help = 'Minimum quantity set in Minimum Stock Rules for this Warhouse'),
486 'maximum_op': fields.function(_get_op, method=True, type='float', string = 'Maximum Rule', multi= 'maximum', \
487 help = 'Maximum quantity set in Minimum Stock Rules for this Warhouse'),
488 'outgoing_before': fields.float('Planned Out Before', readonly=True, \
489 help= 'Planned Out in periods before calculated. '\
490 'Between start date of current period and one day before start of calculated period.'),
491 'incoming_before': fields.float('Incoming Before', readonly = True, \
492 help= 'Confirmed incoming in periods before calculated (Including Already In). '\
493 'Between start date of current period and one day before start of calculated period.'),
494 'stock_start': fields.float('Initial Stock', readonly=True, \
495 help= 'Stock quantity one day before current period.'),
496 'already_out': fields.float('Already Out', readonly=True, \
497 help= 'Quantity which is already dispatched out of this warehouse in current period.'),
498 'already_in': fields.float('Already In', readonly=True, \
499 help= 'Quantity which is already picked up to this warehouse in current period.'),
500 'stock_only': fields.boolean("Stock Location Only", help = "Check to calculate stock location of selected warehouse only. " \
501 "If not selected calculation is made for input, stock and output location of warehouse."),
502 "procure_to_stock": fields.boolean("Procure To Stock Location", help = "Chect to make procurement to stock location of selected warehouse. " \
503 "If not selected procurement will be made into input location of warehouse."),
504 "confirmed_forecasts_only": fields.boolean("Validated Forecasts", help = "Check to take validated forecasts only. " \
505 "If not checked system takes validated and draft forecasts."),
506 'supply_warehouse_id': fields.many2one('stock.warehouse','Source Warehouse', help = "Warehouse used as source in supply pick move created by 'Supply from Another Warhouse'."),
507 "stock_supply_location": fields.boolean("Stock Supply Location", help = "Check to supply from Stock location of Supply Warehouse. " \
508 "If not checked supply will be made from Output location of Supply Warehouse. Used in 'Supply from Another Warhouse' with Supply Warehouse."),
513 'state': lambda *args: 'draft' ,
515 'planned_outgoing': 0.0,
516 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.planning', context=c),
521 def _to_default_uom_factor(self, cr, uid, product_id, uom_id, context=None):
522 uom_obj = self.pool.get('product.uom')
523 product_obj = self.pool.get('product.product')
524 product = product_obj.browse(cr, uid, product_id, context=context)
525 uom = uom_obj.browse(cr, uid, uom_id, context=context)
527 if uom.category_id.id != product.uom_id.category_id.id:
528 coef = coef / product.uos_coeff
529 return product.uom_id.factor / coef
532 def _from_default_uom_factor(self, cr, uid, product_id, uom_id, context=None):
533 uom_obj = self.pool.get('product.uom')
534 product_obj = self.pool.get('product.product')
535 product = product_obj.browse(cr, uid, product_id, context=context)
536 uom = uom_obj.browse(cr, uid, uom_id, context=context)
538 if uom.category_id.id != product.uom_id.category_id.id:
539 res = res / product.uos_coeff
540 return res / product.uom_id.factor, uom.rounding
542 def calculate_planning(self, cr, uid, ids, context, *args):
543 one_minute = relativedelta(minutes=1)
544 current_date_beginning_c = datetime.today()
545 current_date_end_c = current_date_beginning_c + relativedelta(days=1, minutes=-1) # to get hour 23:59:00
546 current_date_beginning = current_date_beginning_c.strftime('%Y-%m-%d %H:%M:%S')
547 current_date_end = current_date_end_c.strftime('%Y-%m-%d %H:%M:%S')
548 for val in self.browse(cr, uid, ids, context=context):
549 day = datetime.strptime(val.period_id.date_start, '%Y-%m-%d %H:%M:%S')
550 dbefore = datetime(day.year, day.month, day.day) - one_minute
551 day_before_calculated_period = dbefore.strftime('%Y-%m-%d %H:%M:%S') # one day before start of calculated period
552 cr.execute("SELECT date_start \
553 FROM stock_period AS period \
554 LEFT JOIN stock_planning AS planning \
555 ON (planning.period_id = period.id) \
556 WHERE (period.date_stop >= %s) AND (period.date_start <= %s) AND \
557 planning.product_id = %s", (current_date_end, current_date_end, val.product_id.id,)) #
559 start_date_current_period = date and date[0] or False
560 start_date_current_period = start_date_current_period or current_date_beginning
562 day = datetime.strptime(start_date_current_period, '%Y-%m-%d %H:%M:%S')
563 dbefore = datetime(day.year, day.month, day.day) - one_minute
564 date_for_start = dbefore.strftime('%Y-%m-%d %H:%M:%S') # one day before current period
565 already_out = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='out', done=True, context=context),
566 already_in = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='in', done=True, context=context),
567 outgoing = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='out', done=False, context=context),
568 incoming = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='in', done=False, context=context),
569 outgoing_before = self._get_outgoing_before(cr, uid, val, start_date_current_period, day_before_calculated_period, context=context),
570 incoming_before = self._get_in_out(cr, uid, val, start_date_current_period, day_before_calculated_period, direction='in', done=False, context=context),
571 stock_start = self._get_stock_start(cr, uid, val, date_for_start, context=context),
572 if start_date_current_period == val.period_id.date_start: # current period is calculated
576 factor, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, context=context)
577 self.write(cr, uid, ids, {
578 'already_out': rounding(already_out[0]*factor,round_value),
579 'already_in': rounding(already_in[0]*factor,round_value),
580 'outgoing': rounding(outgoing[0]*factor,round_value),
581 'incoming': rounding(incoming[0]*factor,round_value),
582 'outgoing_before' : rounding(outgoing_before[0]*factor,round_value),
583 'incoming_before': rounding((incoming_before[0]+ (not current and already_in[0]))*factor,round_value),
584 'outgoing_left': rounding(val.planned_outgoing - (outgoing[0] + (current and already_out[0]))*factor,round_value),
585 'incoming_left': rounding(val.to_procure - (incoming[0] + (current and already_in[0]))*factor,round_value),
586 'stock_start': rounding(stock_start[0]*factor,round_value),
587 'stock_simulation': rounding(val.to_procure - val.planned_outgoing + (stock_start[0]+ incoming_before[0] - outgoing_before[0] \
588 + (not current and already_in[0]))*factor,round_value),
592 # method below converts quantities and uoms to general OpenERP standard with UoM Qty, UoM, UoS Qty, UoS.
593 # from stock_planning standard where you have one Qty and one UoM (any from UoM or UoS category)
594 # so if UoM is from UoM category it is used as UoM in standard and if product has UoS the UoS will be calcualated.
595 # If UoM is from UoS category it is recalculated to basic UoS from product (in planning you can use any UoS from UoS category)
596 # and basic UoM is calculated.
597 def _qty_to_standard(self, cr, uid, val, context=None):
600 if val.product_uom.category_id.id == val.product_id.uom_id.category_id.id:
601 uom_qty = val.incoming_left
602 uom = val.product_uom.id
603 if val.product_id.uos_id:
604 uos = val.product_id.uos_id.id
605 coeff_uom2def = self._to_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, {})
606 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, uos, {})
607 uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
608 elif val.product_uom.category_id.id == val.product_id.uos_id.category_id.id:
609 coeff_uom2def = self._to_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, {})
610 uos = val.product_id.uos_id.id
611 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, uos, {})
612 uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
613 uom = val.product_id.uom_id.id
614 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, uom, {})
615 uom_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
616 return uom_qty, uom, uos_qty, uos
618 def procure_incomming_left(self, cr, uid, ids, context, *args):
619 for obj in self.browse(cr, uid, ids, context=context):
620 if obj.incoming_left <= 0:
621 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
622 uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
623 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
624 proc_id = self.pool.get('procurement.order').create(cr, uid, {
625 'company_id' : obj.company_id.id,
626 'name': _('Manual planning for ') + obj.period_id.name,
627 'origin': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
628 'date_planned': obj.period_id.date_start,
629 'product_id': obj.product_id.id,
630 'product_qty': uom_qty,
632 'product_uos_qty': uos_qty,
634 'location_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or obj.warehouse_id.lot_input_id.id,
635 'procure_method': 'make_to_order',
636 'note' : _("Procurement created in MPS by user: ") + str(user.login) + _(" Creation Date: ") + \
637 time.strftime('%Y-%m-%d %H:%M:%S') + \
638 _("\nFor period: ") + obj.period_id.name + _(" according to state:") + \
639 _("\n Warehouse Forecast: ") + str(obj.warehouse_forecast) + \
640 _("\n Initial Stock: ") + str(obj.stock_start) + \
641 _("\n Planned Out: ") + str(obj.planned_outgoing) + _(" Planned In: ") + str(obj.to_procure) + \
642 _("\n Already Out: ") + str(obj.already_out) + _(" Already In: ") + str(obj.already_in) + \
643 _("\n Confirmed Out: ") + str(obj.outgoing) + _(" Confirmed In: ") + str(obj.incoming) + \
644 _("\n Planned Out Before: ") + str(obj.outgoing_before) + _(" Confirmed In Before: ") + \
645 str(obj.incoming_before) + \
646 _("\n Expected Out: ") + str(obj.outgoing_left) + _(" Incoming Left: ") + str(obj.incoming_left) + \
647 _("\n Stock Simulation: ") + str(obj.stock_simulation) + _(" Minimum stock: ") + str(obj.minimum_op),
650 wf_service = netsvc.LocalService("workflow")
651 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
652 self.calculate_planning(cr, uid, ids, context)
653 prev_text = obj.history or ""
654 self.write(cr, uid, ids, {
655 'history': prev_text + _('Requisition (') + str(user.login) + ", " + time.strftime('%Y.%m.%d %H:%M) ') + str(obj.incoming_left) + \
656 " " + obj.product_uom.name + "\n",
660 def internal_supply(self, cr, uid, ids, context, *args):
661 for obj in self.browse(cr, uid, ids, context=context):
662 if obj.incoming_left <= 0:
663 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
664 if not obj.supply_warehouse_id:
665 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse !'))
666 if obj.supply_warehouse_id.id == obj.warehouse_id.id:
667 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse different than calculated (destination) Warehouse !'))
668 uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
669 user = self.pool.get('res.users').browse(cr, uid, uid, context)
670 picking_id = self.pool.get('stock.picking').create(cr, uid, {
671 'origin': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
674 'date': obj.period_id.date_start,
675 'move_type': 'direct',
676 'invoice_state': 'none',
677 'company_id': obj.company_id.id,
678 'note': _("Pick created from MPS by user: ") + str(user.login) + _(" Creation Date: ") + \
679 time.strftime('%Y-%m-%d %H:%M:%S') + \
680 _("\nFor period: ") + obj.period_id.name + _(" according to state:") + \
681 _("\n Warehouse Forecast: ") + str(obj.warehouse_forecast) + \
682 _("\n Initial Stock: ") + str(obj.stock_start) + \
683 _("\n Planned Out: ") + str(obj.planned_outgoing) + _(" Planned In: ") + str(obj.to_procure) + \
684 _("\n Already Out: ") + str(obj.already_out) + _(" Already In: ") + str(obj.already_in) + \
685 _("\n Confirmed Out: ") + str(obj.outgoing) + _(" Confirmed In: ") + str(obj.incoming) + \
686 _("\n Planned Out Before: ") + str(obj.outgoing_before) + _(" Confirmed In Before: ") + \
687 str(obj.incoming_before) + \
688 _("\n Expected Out: ") + str(obj.outgoing_left) + _(" Incoming Left: ") + str(obj.incoming_left) + \
689 _("\n Stock Simulation: ") + str(obj.stock_simulation) + _(" Minimum stock: ") + str(obj.minimum_op),
692 move_id = self.pool.get('stock.move').create(cr, uid, {
693 'name': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
694 'picking_id': picking_id,
695 'product_id': obj.product_id.id,
696 'date': obj.period_id.date_start,
697 'product_qty': uom_qty,
699 'product_uos_qty': uos_qty,
701 'location_id': obj.stock_supply_location and obj.supply_warehouse_id.lot_stock_id.id or \
702 obj.supply_warehouse_id.lot_output_id.id,
703 'location_dest_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or \
704 obj.warehouse_id.lot_input_id.id,
705 'tracking_id': False,
706 'company_id': obj.company_id.id,
708 wf_service = netsvc.LocalService("workflow")
709 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
711 self.calculate_planning(cr, uid, ids, context)
712 prev_text = obj.history or ""
713 pick_name = self.pool.get('stock.picking').browse(cr, uid, picking_id).name
714 self.write(cr, uid, ids, {
715 'history' : prev_text + _('Pick List ')+ pick_name + " (" + str(user.login) + ", " + time.strftime('%Y.%m.%d %H:%M) ') \
716 + str(obj.incoming_left) +" " + obj.product_uom.name + "\n",
721 def product_id_change(self, cr, uid, ids, product_id):
724 product_rec = self.pool.get('product.product').browse(cr, uid, product_id)
725 ret['product_uom'] = product_rec.uom_id.id
726 ret['active_uom'] = product_rec.uom_id.id
727 ret['product_uom_categ'] = product_rec.uom_id.category_id.id
728 ret['product_uos_categ'] = product_rec.uos_id and product_rec.uos_id.category_id.id or False
730 ret['product_uom'] = False
731 ret['product_uom_categ'] = False
732 ret['product_uos_categ'] = False
738 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: