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 _
31 _logger = logging.getLogger('mps')
34 def rounding(fl, round_value):
37 return round(fl / round_value) * round_value
39 # Periods have no company_id field as they can be shared across similar companies.
40 # If somone thinks different it can be improved.
41 class stock_period(osv.osv):
42 _name = "stock.period"
43 _description = "stock period"
46 'name': fields.char('Period Name', size=64, required=True),
47 'date_start': fields.datetime('Start Date', required=True),
48 'date_stop': fields.datetime('End Date', required=True),
49 'state': fields.selection([('draft','Draft'), ('open','Open'),('close','Close')], 'State')
55 def button_open(self, cr, uid, ids, context=None):
56 self.write(cr, uid, ids, {'state': 'open'})
59 def button_close(self, cr, uid, ids, context=None):
60 self.write(cr, uid, ids, {'state': 'close'})
65 # Stock and Sales Forecast object. Previously stock_planning_sale_prevision.
66 # A lot of changes in 1.1
67 class stock_sale_forecast(osv.osv):
68 _name = "stock.sale.forecast"
71 'company_id':fields.many2one('res.company', 'Company', required=True),
72 'create_uid': fields.many2one('res.users', 'Responsible'),
73 'name': fields.char('Name', size=64, readonly=True, states={'draft': [('readonly',False)]}),
74 'user_id': fields.many2one('res.users', 'Created/Validated by',readonly=True, \
75 help='Shows who created this forecast, or who validated.'),
76 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, readonly=True, states={'draft': [('readonly',False)]}, \
77 help='Shows which warehouse this forecast concerns. '\
78 'If during stock planning you will need sales forecast for all warehouses choose any warehouse now.'),
79 'period_id': fields.many2one('stock.period', 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
80 help = 'Shows which period this forecast concerns.'),
81 'product_id': fields.many2one('product.product', 'Product', readonly=True, required=True, states={'draft':[('readonly',False)]}, \
82 help = 'Shows which product this forecast concerns.'),
83 'product_qty': fields.float('Forecast Quantity', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
84 help= 'Forecast Product quantity.'),
85 'product_amt': fields.float('Product Amount', readonly=True, states={'draft':[('readonly',False)]}, \
86 help='Forecast value which will be converted to Product Quantity according to prices.'),
87 'product_uom_categ': fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uom domain
88 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
89 help = "Unit of Measure used to show the quantities of stock calculation." \
90 "You can use units form default category or from second category (UoS category)."),
91 'product_uos_categ' : fields.many2one('product.uom.categ', 'Product UoS Category'), # Invisible field for product_uos domain
92 # Field used in onchange_uom to check what uom was before change and recalculate quantities according to old uom (active_uom) and new uom.
93 'active_uom': fields.many2one('product.uom', string = "Active UoM"),
94 'state': fields.selection([('draft','Draft'),('validated','Validated')],'State',readonly=True),
95 'analyzed_period1_id': fields.many2one('stock.period', 'Period1', readonly=True, states={'draft':[('readonly',False)]},),
96 'analyzed_period2_id': fields.many2one('stock.period', 'Period2', readonly=True, states={'draft':[('readonly',False)]},),
97 'analyzed_period3_id': fields.many2one('stock.period', 'Period3', readonly=True, states={'draft':[('readonly',False)]},),
98 'analyzed_period4_id': fields.many2one('stock.period', 'Period4', readonly=True, states={'draft':[('readonly',False)]},),
99 'analyzed_period5_id': fields.many2one('stock.period' , 'Period5', readonly=True, states={'draft':[('readonly',False)]},),
100 'analyzed_user_id': fields.many2one('res.users', 'This User', required=False, readonly=True, states={'draft':[('readonly',False)]},),
101 'analyzed_team_id': fields.many2one('crm.case.section', 'Sales Team', required=False, \
102 readonly=True, states={'draft':[('readonly',False)]},),
103 'analyzed_warehouse_id': fields.many2one('stock.warehouse' , 'This Warehouse', required=False, \
104 readonly=True, states={'draft':[('readonly',False)]}),
105 'analyze_company': fields.boolean('Per Company', readonly=True, states={'draft':[('readonly',False)]}, \
106 help = "Check this box to see the sales for whole company."),
107 'analyzed_period1_per_user': fields.float('This User Period1', readonly=True),
108 'analyzed_period2_per_user': fields.float('This User Period2', readonly=True),
109 'analyzed_period3_per_user': fields.float('This User Period3', readonly=True),
110 'analyzed_period4_per_user': fields.float('This User Period4', readonly=True),
111 'analyzed_period5_per_user': fields.float('This User Period5', readonly=True),
112 'analyzed_period1_per_dept': fields.float('This Dept Period1', readonly=True),
113 'analyzed_period2_per_dept': fields.float('This Dept Period2', readonly=True),
114 'analyzed_period3_per_dept': fields.float('This Dept Period3', readonly=True),
115 'analyzed_period4_per_dept': fields.float('This Dept Period4', readonly=True),
116 'analyzed_period5_per_dept': fields.float('This Dept Period5', readonly=True),
117 'analyzed_period1_per_warehouse': fields.float('This Warehouse Period1', readonly=True),
118 'analyzed_period2_per_warehouse': fields.float('This Warehouse Period2', readonly=True),
119 'analyzed_period3_per_warehouse': fields.float('This Warehouse Period3', readonly=True),
120 'analyzed_period4_per_warehouse': fields.float('This Warehouse Period4', readonly=True),
121 'analyzed_period5_per_warehouse': fields.float('This Warehouse Period5', readonly=True),
122 'analyzed_period1_per_company': fields.float('This Company Period1', readonly=True),
123 'analyzed_period2_per_company': fields.float('This Company Period2', readonly=True),
124 'analyzed_period3_per_company': fields.float('This Company Period3', readonly=True),
125 'analyzed_period4_per_company': fields.float('This Company Period4', readonly=True),
126 'analyzed_period5_per_company': fields.float('This Company Period5', readonly=True),
129 'user_id': lambda obj, cr, uid, context: uid,
131 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.sale.forecast', context=c),
134 def action_validate(self, cr, uid, ids, *args):
135 self.write(cr, uid, ids, {'state': 'validated','user_id': uid})
138 def unlink(self, cr, uid, ids, context=None):
139 forecasts = self.read(cr, uid, ids, ['state'])
142 if t['state'] in ('draft'):
143 unlink_ids.append(t['id'])
145 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Validated Sale Forecasts !'))
146 osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
149 def onchange_company(self, cr, uid, ids, company_id=False):
153 result['warehouse_id'] = False
154 result['analyzed_user_id'] = False
155 result['analyzed_team_id'] = False
156 result['analyzed_warehouse_id'] = False
157 return {'value': result}
159 def product_id_change(self, cr, uid, ids, product_id=False):
162 product_rec = self.pool.get('product.product').browse(cr, uid, product_id)
163 ret['product_uom'] = product_rec.uom_id.id
164 ret['product_uom_categ'] = product_rec.uom_id.category_id.id
165 ret['product_uos_categ'] = product_rec.uos_id and product_rec.uos_id.category_id.id or False
166 ret['active_uom'] = product_rec.uom_id.id
168 ret['product_uom'] = False
169 ret['product_uom_categ'] = False
170 ret['product_uos_categ'] = False
174 def onchange_uom(self, cr, uid, ids, product_uom=False, product_qty=0.0,
175 active_uom=False, product_id=False):
177 if product_uom and product_id:
178 coeff_uom2def = self._to_default_uom_factor(cr, uid, product_id, active_uom, {})
179 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, product_id, product_uom, {})
180 coeff = coeff_uom2def * coeff_def2uom
181 ret['product_qty'] = rounding(coeff * product_qty, round_value)
182 ret['active_uom'] = product_uom
183 return {'value': ret}
185 def product_amt_change(self, cr, uid, ids, product_amt=0.0, product_uom=False, product_id=False):
188 if product_amt and product_id:
189 product = self.pool.get('product.product').browse(cr, uid, product_id)
191 if (product_uom != product.uom_id.id):
192 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, product_id, product_uom, {})
193 qty = rounding(coeff_def2uom * product_amt/(product.product_tmpl_id.list_price), round_value)
194 res = {'value': {'product_qty': qty}}
197 def _to_default_uom_factor(self, cr, uid, product_id, uom_id, context=None):
198 uom_obj = self.pool.get('product.uom')
199 product_obj = self.pool.get('product.product')
200 product = product_obj.browse(cr, uid, product_id, context=context)
201 uom = uom_obj.browse(cr, uid, uom_id, context=context)
203 if uom.category_id.id <> product.uom_id.category_id.id:
204 coef = coef * product.uos_coeff
205 return product.uom_id.factor / coef
207 def _from_default_uom_factor(self, cr, uid, product_id, uom_id, context=None):
208 uom_obj = self.pool.get('product.uom')
209 product_obj = self.pool.get('product.product')
210 product = product_obj.browse(cr, uid, product_id, context=context)
211 uom = uom_obj.browse(cr, uid, uom_id, context=context)
213 if uom.category_id.id <> product.uom_id.category_id.id:
214 res = res * product.uos_coeff
215 return res / product.uom_id.factor, uom.rounding
217 def _sales_per_users(self, cr, uid, so, so_line, company, users):
218 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) " \
219 "WHERE (sol.id IN %s) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN %s) AND (s.company_id=%s) " \
220 "AND (s.user_id IN %s) " ,(tuple(so_line), tuple(so), company, tuple(users)))
221 ret = cr.fetchone()[0] or 0.0
224 def _sales_per_warehouse(self, cr, uid, so, so_line, company, shops):
225 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) " \
226 "WHERE (sol.id IN %s) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN %s)AND (s.company_id=%s) " \
227 "AND (s.shop_id IN %s)" ,(tuple(so_line), tuple(so), company, tuple(shops)))
228 ret = cr.fetchone()[0] or 0.0
231 def _sales_per_company(self, cr, uid, so, so_line, company):
232 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) " \
233 "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))
234 ret = cr.fetchone()[0] or 0.0
237 def calculate_sales_history(self, cr, uid, ids, context, *args):
238 sales = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],]
239 for obj in self.browse(cr, uid, ids, context=context):
240 periods = obj.analyzed_period1_id, obj.analyzed_period2_id, obj.analyzed_period3_id, obj.analyzed_period4_id, obj.analyzed_period5_id
241 so_obj = self.pool.get('sale.order')
242 so_line_obj = self.pool.get('sale.order.line')
243 so_line_product_ids = so_line_obj.search(cr, uid, [('product_id','=', obj.product_id.id)], context = context)
244 if so_line_product_ids:
246 if obj.analyzed_warehouse_id:
247 shops = self.pool.get('sale.shop').search(cr, uid,[('warehouse_id','=', obj.analyzed_warehouse_id.id)], context = context)
248 if obj.analyzed_team_id:
249 users = [u.id for u in obj.analyzed_team_id.member_ids]
250 factor, _ = self._from_default_uom_factor(cr, uid, obj.product_id.id, obj.product_uom.id, context=context)
251 for i, period in enumerate(periods):
253 so_period_ids = so_obj.search(cr, uid, [('date_order','>=',period.date_start),('date_order','<=',period.date_stop) ], context = context)
255 if obj.analyzed_user_id:
256 sales[i][0] = self._sales_per_users(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, [obj.analyzed_user_id.id])
257 sales[i][0] *= factor
259 sales[i][1] = self._sales_per_users(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, users)
260 sales[i][1] *= factor
262 sales[i][2] = self._sales_per_warehouse(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, shops)
263 sales[i][2] *= factor
264 if obj.analyze_company:
265 sales[i][3] = self._sales_per_company(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, )
266 sales[i][3] *= factor
267 self.write(cr, uid, ids, {
268 'analyzed_period1_per_user': sales[0][0],
269 'analyzed_period2_per_user': sales[1][0],
270 'analyzed_period3_per_user': sales[2][0],
271 'analyzed_period4_per_user': sales[3][0],
272 'analyzed_period5_per_user': sales[4][0],
273 'analyzed_period1_per_dept': sales[0][1],
274 'analyzed_period2_per_dept': sales[1][1],
275 'analyzed_period3_per_dept': sales[2][1],
276 'analyzed_period4_per_dept': sales[3][1],
277 'analyzed_period5_per_dept': sales[4][1],
278 'analyzed_period1_per_warehouse': sales[0][2],
279 'analyzed_period2_per_warehouse': sales[1][2],
280 'analyzed_period3_per_warehouse': sales[2][2],
281 'analyzed_period4_per_warehouse': sales[3][2],
282 'analyzed_period5_per_warehouse': sales[4][2],
283 'analyzed_period1_per_company': sales[0][3],
284 'analyzed_period2_per_company': sales[1][3],
285 'analyzed_period3_per_company': sales[2][3],
286 'analyzed_period4_per_company': sales[3][3],
287 'analyzed_period5_per_company': sales[4][3],
291 stock_sale_forecast()
293 # The main Stock Planning object
294 # A lot of changes by contributor in ver 1.1
295 class stock_planning(osv.osv):
296 _name = "stock.planning"
298 def _get_in_out(self, cr, uid, val, date_start, date_stop, direction, done, context=None):
301 product_obj = self.pool.get('product.product')
303 'field': "incoming_qty",
304 'adapter': lambda x: x,
307 'field': "outgoing_qty",
308 'adapter': lambda x: -x,
311 context['from_date'] = date_start
312 context['to_date'] = date_stop
313 locations = [val.warehouse_id.lot_stock_id.id,]
314 if not val.stock_only:
315 locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
316 context['location'] = locations
317 context['compute_child'] = True
318 prod_id = val.product_id.id
320 context.update({ 'states':('done',), 'what':(direction,) })
322 st = product_obj.get_product_available(cr, uid, prod_ids, context=context)
323 res = mapping[direction]['adapter'](st.get(prod_id,0.0))
325 product = product_obj.read(cr, uid, prod_id,[], context)
326 product_qty = product[mapping[direction]['field']]
327 res = mapping[direction]['adapter'](product_qty)
330 def _get_outgoing_before(self, cr, uid, val, date_start, date_stop, context=None):
331 cr.execute("SELECT sum(planning.planned_outgoing), planning.product_uom \
332 FROM stock_planning AS planning \
333 LEFT JOIN stock_period AS period \
334 ON (planning.period_id = period.id) \
335 WHERE (period.date_stop >= %s) AND (period.date_stop <= %s) \
336 AND (planning.product_id = %s) AND (planning.company_id = %s) \
337 GROUP BY planning.product_uom", \
338 (date_start, date_stop, val.product_id.id, val.company_id.id,))
339 planning_qtys = cr.fetchall()
340 res = self._to_default_uom(cr, uid, val, planning_qtys, context)
343 def _to_default_uom(self, cr, uid, val, qtys, context=None):
346 for qty, prod_uom in qtys:
347 coef = self._to_default_uom_factor(cr, uid, val.product_id.id, prod_uom, context=context)
348 res_qty += qty * coef
351 def _to_form_uom(self, cr, uid, val, qtys, context=None):
354 for qty, prod_uom in qtys:
355 coef = self._to_default_uom_factor(cr, uid, val.product_id.id, prod_uom, context=context)
356 res_coef, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, context=context)
357 coef = coef * res_coef
358 res_qty += rounding(qty * coef, round_value)
362 def _get_forecast(self, cr, uid, ids, field_names, arg, context=None):
364 for val in self.browse(cr, uid, ids, context=context):
366 valid_part = val.confirmed_forecasts_only and " AND state = 'validated'" or ""
367 cr.execute('SELECT sum(product_qty), product_uom \
368 FROM stock_sale_forecast \
369 WHERE product_id = %s AND period_id = %s AND company_id = %s '+valid_part+ \
370 'GROUP BY product_uom', \
371 (val.product_id.id,val.period_id.id, val.company_id.id))
372 company_qtys = cr.fetchall()
373 res[val.id]['company_forecast'] = self._to_form_uom(cr, uid, val, company_qtys, context)
375 cr.execute('SELECT sum(product_qty), product_uom \
376 FROM stock_sale_forecast \
377 WHERE product_id = %s and period_id = %s AND warehouse_id = %s ' + valid_part + \
378 'GROUP BY product_uom', \
379 (val.product_id.id,val.period_id.id, val.warehouse_id.id))
380 warehouse_qtys = cr.fetchall()
381 res[val.id]['warehouse_forecast'] = self._to_form_uom(cr, uid, val, warehouse_qtys, context)
382 # res[val.id]['warehouse_forecast'] = rounding(res[val.id]['warehouse_forecast'], val.product_id.uom_id.rounding)
385 def _get_stock_start(self, cr, uid, val, date, context=None):
388 context['from_date'] = None
389 context['to_date'] = date
390 locations = [val.warehouse_id.lot_stock_id.id,]
391 if not val.stock_only:
392 locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
393 context['location'] = locations
394 context['compute_child'] = True
395 product_obj = self.pool.get('product.product').read(cr, uid,val.product_id.id,[], context)
396 res = product_obj['qty_available'] # value for stock_start
399 def _get_past_future(self, cr, uid, ids, field_names, arg, context=None):
401 for val in self.browse(cr, uid, ids, context=context):
402 if val.period_id.date_stop < time.strftime('%Y-%m-%d'):
405 res[val.id] = 'Future'
408 def _get_op(self, cr, uid, ids, field_names, arg, context=None): # op = OrderPoint
410 for val in self.browse(cr, uid, ids, context=context):
412 cr.execute("SELECT product_min_qty, product_max_qty, product_uom \
413 FROM stock_warehouse_orderpoint \
414 WHERE warehouse_id = %s AND product_id = %s AND active = 'TRUE'", (val.warehouse_id.id, val.product_id.id))
415 ret = cr.fetchone() or [0.0,0.0,False]
419 coef = self._to_default_uom_factor(cr, uid, val.product_id.id, ret[2], context)
420 res_coef, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, context=context)
421 coef = coef * res_coef
422 res[val.id]['minimum_op'] = rounding(ret[0]*coef, round_value)
423 res[val.id]['maximum_op'] = rounding(ret[1]*coef, round_value)
426 def onchange_company(self, cr, uid, ids, company_id=False):
429 result['warehouse_id'] = False
430 return {'value': result}
432 def onchange_uom(self, cr, uid, ids, product_uom=False, product_id=False, active_uom=False,
433 planned_outgoing=0.0, to_procure=0.0):
438 coeff_uom2def = self._to_default_uom_factor(cr, uid, product_id, active_uom, {})
439 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, product_id, product_uom, {})
440 coeff = coeff_uom2def * coeff_def2uom
441 ret['planned_outgoing'] = rounding(coeff * planned_outgoing, round_value)
442 ret['to_procure'] = rounding(coeff * to_procure, round_value)
443 ret['active_uom'] = product_uom
444 return {'value': ret}
447 'company_id': fields.many2one('res.company', 'Company', required = True),
448 'history': fields.text('Procurement History', readonly=True, help = "History of procurement or internal supply of this planning line."),
449 'state' : fields.selection([('draft','Draft'),('done','Done')],'State',readonly=True),
450 'period_id': fields.many2one('stock.period' , 'Period', required=True, \
451 help = 'Period for this planning. Requisition will be created for beginning of the period.'),
452 'warehouse_id': fields.many2one('stock.warehouse','Warehouse', required=True),
453 'product_id': fields.many2one('product.product' , 'Product', required=True, help = 'Product which this planning is created for.'),
454 'product_uom_categ' : fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uom domain
455 'product_uom': fields.many2one('product.uom', 'UoM', required=True, help = "Unit of Measure used to show the quantities of stock calculation." \
456 "You can use units from default category or from second category (UoS category)."),
457 'product_uos_categ': fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uos domain
458 # Field used in onchange_uom to check what uom was before change to recalculate quantities according to old uom (active_uom) and new uom.
459 'active_uom': fields.many2one('product.uom', string = "Active UoM"), # It works only in Forecast
460 'planned_outgoing': fields.float('Planned Out', required=True, \
461 help = 'Enter planned outgoing quantity from selected Warehouse during the selected Period of selected Product. '\
462 'To plan this value look at Confirmed Out or Sales Forecasts. This value should be equal or greater than Confirmed Out.'),
463 'company_forecast': fields.function(_get_forecast, string ='Company Forecast', multi = 'company', \
464 help = 'All sales forecasts for whole company (for all Warehouses) of selected Product during selected Period.'),
465 'warehouse_forecast': fields.function(_get_forecast, string ='Warehouse Forecast', multi = 'warehouse',\
466 help = 'All sales forecasts for selected Warehouse of selected Product during selected Period.'),
467 'stock_simulation': fields.float('Stock Simulation', readonly =True, \
468 help = 'Stock simulation at the end of selected Period.\n For current period it is: \n' \
469 'Initial Stock - Already Out + Already In - Expected Out + Incoming Left.\n' \
470 'For periods ahead it is: \nInitial Stock - Planned Out Before + Incoming Before - Planned Out + Planned In.'),
471 'incoming': fields.float('Confirmed In', readonly=True, \
472 help = 'Quantity of all confirmed incoming moves in calculated Period.'),
473 'outgoing': fields.float('Confirmed Out', readonly=True, \
474 help = 'Quantity of all confirmed outgoing moves in calculated Period.'),
475 'incoming_left': fields.float('Incoming Left', readonly=True, \
476 help = 'Quantity left to Planned incoming quantity. This is calculated difference between Planned In and Confirmed In. ' \
477 'For current period Already In is also calculated. This value is used to create procurement for lacking quantity.'),
478 'outgoing_left': fields.float('Expected Out', readonly=True, \
479 help = 'Quantity expected to go out in selected period besides Confirmed Out. As a difference between Planned Out and Confirmed Out. ' \
480 'For current period Already Out is also calculated'),
481 'to_procure': fields.float(string='Planned In', required=True, \
482 help = 'Enter quantity which (by your plan) should come in. Change this value and observe Stock simulation. ' \
483 'This value should be equal or greater than Confirmed In.'),
484 'line_time': fields.function(_get_past_future, type='char', string='Past/Future'),
485 'minimum_op': fields.function(_get_op, type='float', string = 'Minimum Rule', multi= 'minimum', \
486 help = 'Minimum quantity set in Minimum Stock Rules for this Warehouse'),
487 'maximum_op': fields.function(_get_op, type='float', string = 'Maximum Rule', multi= 'maximum', \
488 help = 'Maximum quantity set in Minimum Stock Rules for this Warehouse'),
489 'outgoing_before': fields.float('Planned Out Before', readonly=True, \
490 help= 'Planned Out in periods before calculated. '\
491 'Between start date of current period and one day before start of calculated period.'),
492 'incoming_before': fields.float('Incoming Before', readonly = True, \
493 help= 'Confirmed incoming in periods before calculated (Including Already In). '\
494 'Between start date of current period and one day before start of calculated period.'),
495 'stock_start': fields.float('Initial Stock', readonly=True, \
496 help= 'Stock quantity one day before current period.'),
497 'already_out': fields.float('Already Out', readonly=True, \
498 help= 'Quantity which is already dispatched out of this warehouse in current period.'),
499 'already_in': fields.float('Already In', readonly=True, \
500 help= 'Quantity which is already picked up to this warehouse in current period.'),
501 'stock_only': fields.boolean("Stock Location Only", help = "Check to calculate stock location of selected warehouse only. " \
502 "If not selected calculation is made for input, stock and output location of warehouse."),
503 "procure_to_stock": fields.boolean("Procure To Stock Location", help = "Check to make procurement to stock location of selected warehouse. " \
504 "If not selected procurement will be made into input location of warehouse."),
505 "confirmed_forecasts_only": fields.boolean("Validated Forecasts", help = "Check to take validated forecasts only. " \
506 "If not checked system takes validated and draft forecasts."),
507 'supply_warehouse_id': fields.many2one('stock.warehouse','Source Warehouse', help = "Warehouse used as source in supply pick move created by 'Supply from Another Warehouse'."),
508 "stock_supply_location": fields.boolean("Stock Supply Location", help = "Check to supply from Stock location of Supply Warehouse. " \
509 "If not checked supply will be made from Output location of Supply Warehouse. Used in 'Supply from Another Warehouse' with Supply Warehouse."),
516 'planned_outgoing': 0.0,
517 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.planning', context=c),
522 def _to_default_uom_factor(self, cr, uid, product_id, uom_id, context=None):
523 uom_obj = self.pool.get('product.uom')
524 product_obj = self.pool.get('product.product')
525 product = product_obj.browse(cr, uid, product_id, context=context)
526 uom = uom_obj.browse(cr, uid, uom_id, context=context)
528 if uom.category_id.id != product.uom_id.category_id.id:
529 coef = coef * product.uos_coeff
530 return product.uom_id.factor / coef
533 def _from_default_uom_factor(self, cr, uid, product_id, uom_id, context=None):
534 uom_obj = self.pool.get('product.uom')
535 product_obj = self.pool.get('product.product')
536 product = product_obj.browse(cr, uid, product_id, context=context)
537 uom = uom_obj.browse(cr, uid, uom_id, context=context)
539 if uom.category_id.id != product.uom_id.category_id.id:
540 res = res * product.uos_coeff
541 return res / product.uom_id.factor, uom.rounding
543 def calculate_planning(self, cr, uid, ids, context, *args):
544 one_second = relativedelta(seconds=1)
545 today = datetime.today()
546 current_date_beginning_c = datetime(today.year, today.month, today.day)
547 current_date_end_c = current_date_beginning_c + relativedelta(days=1, seconds=-1) # to get hour 23:59:59
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 _logger.debug("Calculate Planning: current date beg: %s and end: %s", current_date_beginning, current_date_end)
551 for val in self.browse(cr, uid, ids, context=context):
552 day = datetime.strptime(val.period_id.date_start, '%Y-%m-%d %H:%M:%S')
553 dbefore = datetime(day.year, day.month, day.day) - one_second
554 day_before_calculated_period = dbefore.strftime('%Y-%m-%d %H:%M:%S') # one day before start of calculated period
555 _logger.debug("Day before calculated period: %s ", day_before_calculated_period)
556 cr.execute("SELECT date_start \
557 FROM stock_period AS period \
558 LEFT JOIN stock_planning AS planning \
559 ON (planning.period_id = period.id) \
560 WHERE (period.date_stop >= %s) AND (period.date_start <= %s) AND \
561 planning.product_id = %s", (current_date_end, current_date_end, val.product_id.id,)) #
563 start_date_current_period = date and date[0] or False
564 start_date_current_period = start_date_current_period or current_date_beginning
565 day = datetime.strptime(start_date_current_period, '%Y-%m-%d %H:%M:%S')
566 dbefore = datetime(day.year, day.month, day.day) - one_second
567 date_for_start = dbefore.strftime('%Y-%m-%d %H:%M:%S') # one day before current period
568 _logger.debug("Date for start: %s", date_for_start)
569 already_out = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='out', done=True, context=context),
570 already_in = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='in', done=True, context=context),
571 outgoing = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='out', done=False, context=context),
572 incoming = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='in', done=False, context=context),
573 outgoing_before = self._get_outgoing_before(cr, uid, val, start_date_current_period, day_before_calculated_period, context=context),
574 incoming_before = self._get_in_out(cr, uid, val, start_date_current_period, day_before_calculated_period, direction='in', done=False, context=context),
575 stock_start = self._get_stock_start(cr, uid, val, date_for_start, context=context),
576 if start_date_current_period == val.period_id.date_start: # current period is calculated
580 factor, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, context=context)
581 self.write(cr, uid, ids, {
582 'already_out': rounding(already_out[0]*factor,round_value),
583 'already_in': rounding(already_in[0]*factor,round_value),
584 'outgoing': rounding(outgoing[0]*factor,round_value),
585 'incoming': rounding(incoming[0]*factor,round_value),
586 'outgoing_before' : rounding(outgoing_before[0]*factor,round_value),
587 'incoming_before': rounding((incoming_before[0]+ (not current and already_in[0]))*factor,round_value),
588 'outgoing_left': rounding(val.planned_outgoing - (outgoing[0] + (current and already_out[0]))*factor,round_value),
589 'incoming_left': rounding(val.to_procure - (incoming[0] + (current and already_in[0]))*factor,round_value),
590 'stock_start': rounding(stock_start[0]*factor,round_value),
591 'stock_simulation': rounding(val.to_procure - val.planned_outgoing + (stock_start[0]+ incoming_before[0] - outgoing_before[0] \
592 + (not current and already_in[0]))*factor,round_value),
596 # method below converts quantities and uoms to general OpenERP standard with UoM Qty, UoM, UoS Qty, UoS.
597 # from stock_planning standard where you have one Qty and one UoM (any from UoM or UoS category)
598 # so if UoM is from UoM category it is used as UoM in standard and if product has UoS the UoS will be calcualated.
599 # If UoM is from UoS category it is recalculated to basic UoS from product (in planning you can use any UoS from UoS category)
600 # and basic UoM is calculated.
601 def _qty_to_standard(self, cr, uid, val, context=None):
604 if val.product_uom.category_id.id == val.product_id.uom_id.category_id.id:
605 uom_qty = val.incoming_left
606 uom = val.product_uom.id
607 if val.product_id.uos_id:
608 uos = val.product_id.uos_id.id
609 coeff_uom2def = self._to_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, {})
610 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, uos, {})
611 uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
612 elif val.product_uom.category_id.id == val.product_id.uos_id.category_id.id:
613 coeff_uom2def = self._to_default_uom_factor(cr, uid, val.product_id.id, val.product_uom.id, {})
614 uos = val.product_id.uos_id.id
615 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, uos, {})
616 uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
617 uom = val.product_id.uom_id.id
618 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val.product_id.id, uom, {})
619 uom_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
620 return uom_qty, uom, uos_qty, uos
622 def procure_incomming_left(self, cr, uid, ids, context, *args):
623 for obj in self.browse(cr, uid, ids, context=context):
624 if obj.incoming_left <= 0:
625 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
626 uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
627 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
628 proc_id = self.pool.get('procurement.order').create(cr, uid, {
629 'company_id' : obj.company_id.id,
630 'name': _('MPS planning for %s') %(obj.period_id.name),
631 'origin': _('MPS(%s) %s') %(user.login, obj.period_id.name),
632 'date_planned': obj.period_id.date_start,
633 'product_id': obj.product_id.id,
634 'product_qty': uom_qty,
636 'product_uos_qty': uos_qty,
638 'location_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or obj.warehouse_id.lot_input_id.id,
639 'procure_method': 'make_to_order',
640 'note' : _(' Procurement created by MPS for user: %s Creation Date: %s \
642 \n according to state: \
643 \n Warehouse Forecast: %s \
644 \n Initial Stock: %s \
645 \n Planned Out: %s Planned In: %s \
646 \n Already Out: %s Already In: %s \
647 \n Confirmed Out: %s Confirmed In: %s \
648 \n Planned Out Before: %s Confirmed In Before: %s \
649 \n Expected Out: %s Incoming Left: %s \
650 \n Stock Simulation: %s Minimum stock: %s') %(user.login, time.strftime('%Y-%m-%d %H:%M:%S'),
651 obj.period_id.name, obj.warehouse_forecast, obj.planned_outgoing, obj.stock_start, obj.to_procure,
652 obj.already_out, obj.already_in, obj.outgoing, obj.incoming, obj.outgoing_before, obj.incoming_before,
653 obj.outgoing_left, obj.incoming_left, obj.stock_simulation, obj.minimum_op)
655 wf_service = netsvc.LocalService("workflow")
656 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
657 self.calculate_planning(cr, uid, ids, context)
658 prev_text = obj.history or ""
659 self.write(cr, uid, ids, {
660 'history': _('%s Procurement (%s, %s) %s %s \n') % (prev_text, user.login, time.strftime('%Y.%m.%d %H:%M'),
661 obj.incoming_left, obj.product_uom.name)
666 def internal_supply(self, cr, uid, ids, context, *args):
667 for obj in self.browse(cr, uid, ids, context=context):
668 if obj.incoming_left <= 0:
669 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
670 if not obj.supply_warehouse_id:
671 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse !'))
672 if obj.supply_warehouse_id.id == obj.warehouse_id.id:
673 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse different than calculated (destination) Warehouse !'))
674 uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
675 user = self.pool.get('res.users').browse(cr, uid, uid, context)
676 picking_id = self.pool.get('stock.picking').create(cr, uid, {
677 'origin': _('MPS(%s) %s') %(user.login, obj.period_id.name),
680 'date': obj.period_id.date_start,
681 'move_type': 'direct',
682 'invoice_state': 'none',
683 'company_id': obj.company_id.id,
684 'note': _('Pick created from MPS by user: %s Creation Date: %s \
685 \nFor period: %s according to state: \
686 \n Warehouse Forecast: %s \
687 \n Initial Stock: %s \
688 \n Planned Out: %s Planned In: %s \
689 \n Already Out: %s Already In: %s \
690 \n Confirmed Out: %s Confirmed In: %s \
691 \n Planned Out Before: %s Confirmed In Before: %s \
692 \n Expected Out: %s Incoming Left: %s \
693 \n Stock Simulation: %s Minimum stock: %s ')
694 % (user.login, time.strftime('%Y-%m-%d %H:%M:%S'), obj.period_id.name, obj.warehouse_forecast,
695 obj.stock_start, obj.planned_outgoing, obj.to_procure, obj.already_out, obj.already_in,
696 obj.outgoing, obj.incoming, obj.outgoing_before, obj.incoming_before,
697 obj.outgoing_left, obj.incoming_left, obj.stock_simulation, obj.minimum_op)
700 move_id = self.pool.get('stock.move').create(cr, uid, {
701 'name': _('MPS(%s) %s') %(user.login, obj.period_id.name),
702 'picking_id': picking_id,
703 'product_id': obj.product_id.id,
704 'date': obj.period_id.date_start,
705 'product_qty': uom_qty,
707 'product_uos_qty': uos_qty,
709 'location_id': obj.stock_supply_location and obj.supply_warehouse_id.lot_stock_id.id or \
710 obj.supply_warehouse_id.lot_output_id.id,
711 'location_dest_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or \
712 obj.warehouse_id.lot_input_id.id,
713 'tracking_id': False,
714 'company_id': obj.company_id.id,
716 wf_service = netsvc.LocalService("workflow")
717 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
719 self.calculate_planning(cr, uid, ids, context)
720 prev_text = obj.history or ""
721 pick_name = self.pool.get('stock.picking').browse(cr, uid, picking_id).name
722 self.write(cr, uid, ids, {
723 'history': _('%s Pick List %s (%s, %s) %s %s \n') % (prev_text, pick_name, user.login, time.strftime('%Y.%m.%d %H:%M'),
724 obj.incoming_left, obj.product_uom.name)
729 def product_id_change(self, cr, uid, ids, product_id):
732 product_rec = self.pool.get('product.product').browse(cr, uid, product_id)
733 ret['product_uom'] = product_rec.uom_id.id
734 ret['active_uom'] = product_rec.uom_id.id
735 ret['product_uom_categ'] = product_rec.uom_id.category_id.id
736 ret['product_uos_categ'] = product_rec.uos_id and product_rec.uos_id.category_id.id or False
738 ret['product_uom'] = False
739 ret['product_uom_categ'] = False
740 ret['product_uos_categ'] = False
746 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: