[IMP] stock_planning: Cleaning module, removed comments.
[odoo/odoo.git] / addons / stock_planning / stock_planning.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import time
23 import mx.DateTime
24 from mx.DateTime import RelativeDateTime, now, DateTime, localtime
25
26 from osv import osv, fields
27 import netsvc
28 from tools.translate import _
29
30 def rounding(fl, round_value):
31     if not round_value:
32         return fl
33     return round(fl / round_value) * round_value
34
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"
40     _columns = {
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')
45     }
46     _defaults = {
47         'state': 'draft'
48     }
49     
50     def button_open(self, cr, uid, ids, context):
51         self.write(cr, uid, ids, {'state': 'open'})
52         return True
53     
54     def button_close(self, cr, uid, ids, context):
55         self.write(cr, uid, ids, {'state': 'close'})
56         return True
57
58 stock_period()
59
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"
64
65     _columns = {
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),
122     }
123     _defaults = {
124         'user_id': lambda obj, cr, uid, context: uid,
125         'state': 'draft',
126         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.sale.forecast', context=c),
127     }
128
129     def action_validate(self, cr, uid, ids, *args):
130         self.write(cr, uid, ids, {'state':'validated','user_id':uid})
131         return True
132
133     def unlink(self, cr, uid, ids, context=None):
134         forecasts = self.read(cr, uid, ids, ['state'])
135         unlink_ids = []
136         for t in forecasts:
137             if t['state'] in ('draft'):
138                 unlink_ids.append(t['id'])
139             else:
140                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Validated Sale Forecasts !'))
141         osv.osv.unlink(self, cr, uid, unlink_ids,context=context)
142         return True
143
144     def onchange_company(self, cr, uid, ids, company_id=False):
145         result = {}
146         if not company_id:
147             return result
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}
153
154     def product_id_change(self, cr, uid, ids, product_id=False):
155         ret = {}
156         if product_id:
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
162         else:
163             ret['product_uom'] = False
164             ret['product_uom_categ'] = False
165             ret['product_uos_categ'] = False
166         res = {'value': ret}
167         return res
168
169     def onchange_uom(self, cr, uid, ids, product_uom=False, product_qty=0.0, active_uom=False):
170         ret = {}
171         if not ids:
172             return {}
173         if product_uom:
174             val1 = self.browse(cr, uid, ids)
175             val = val1[0]
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}
182
183     def product_amt_change(self, cr, uid, ids, product_amt = 0.0, product_uom=False):
184         ret = {}
185         if not ids:
186             return {}
187         round_value = 1
188         if product_amt:
189             coeff_def2uom = 1
190             val1 = self.browse(cr, uid, ids)
191             val = val1[0]
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)
195         res = {'value': ret}
196         return res
197
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)
201         coef = uom.factor
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
205
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)
209         res = uom.factor
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
213
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
219         return ret
220
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
226         return ret
227
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
232         return ret
233
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))
246                 else:
247                     shops = False
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)
255                 else:
256                     dept_users = False
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):
259                     if period:
260                         so_period_ids = so_obj.search(cr, uid, [('date_order','>=',period.date_start),('date_order','<=',period.date_stop) ], context = context)
261                         if so_period_ids:
262                             if obj.analyzed_user_id:
263                                 user_set = str(obj.analyzed_user_id.id)
264
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
267                             if dept_users:
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
270                             if shops:
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],
297         })
298         return True
299
300
301 stock_sale_forecast()
302
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"
307
308     def _get_in_out(self, cr, uid, val, date_start, date_stop, direction, done, context=None):
309         if not context:
310             context = {}
311         product_obj = self.pool.get('product.product')
312         mapping = {'in': {
313                         'field': "incoming_qty",
314                         'adapter': lambda x: x,
315                   },
316                   'out': {
317                         'field': "outgoing_qty",
318                         'adapter': lambda x: -x,
319                   },
320         }
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
329         if done:
330             c1 = context.copy()
331             c1.update({ 'states':('done',), 'what':(direction,) })
332             prod_ids = [prod_id]
333             st = product_obj.get_product_available(cr,uid, prod_ids, context=c1)
334             res = mapping[direction]['adapter'](st.get(prod_id,0.0))
335         else:
336             product = product_obj.read(cr, uid, prod_id,[], context)
337             product_qty = product[mapping[direction]['field']]
338             res = mapping[direction]['adapter'](product_qty)
339         return res
340
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)
352         return res
353
354     def _to_planning_uom(self, cr, uid, val, qtys, context):
355         res_qty = 0
356         if qtys:
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)
363         return res_qty
364
365
366     def _get_forecast(self, cr, uid, ids, field_names, arg, context=None):
367         res = {}
368         for val in self.browse(cr, uid, ids):
369             res[val.id] = {}
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)
378
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)
387         return res
388
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
399         return res
400
401     def _get_past_future(self, cr, uid, ids, field_names, arg, context):
402         res = {}
403         for val in self.browse(cr, uid, ids, context=context):
404             if val.period_id.date_stop < time.strftime('%Y-%m-%d'):
405                 res[val.id] = 'Past'
406             else:
407                 res[val.id] = 'Future'
408         return res
409
410     def _get_op(self, cr, uid, ids, field_names, arg, context=None):  # op = OrderPoint
411         res = {}
412         for val in self.browse(cr, uid, ids, context=context):
413             res[val.id]={}
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]
418             coef = 1
419             round_value = 1
420             if ret[2]:
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
426         return res
427
428     def onchange_company(self, cr, uid, ids, company_id=False):
429         result = {}
430         if company_id:
431             result['warehouse_id'] = False
432         return {'value': result}
433
434     def onchange_uom(self, cr, uid, ids, product_uom=False):
435         ret = {}
436         if not product_uom:
437             return {}
438         if not ids:
439             return {}
440         val1 = self.browse(cr, uid, ids)
441         val = val1[0]
442         if val.active_uom:
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}
450
451     _columns = {
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."),
515
516     }
517
518     _defaults = {
519         'state': lambda *args: 'draft' ,
520         'to_procure': 0.0,
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),
523     }
524
525     _order = 'period_id'
526
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)
530         coef = uom.factor
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
534
535
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)
539         res = uom.factor
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
543
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,)) #
560             date = cr.fetchone()
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
574                 current = True
575             else:
576                 current = False
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),
590             })
591         return True
592
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):
599         uos = False
600         uos_qty = 0.0
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
618
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,
632                         'product_uom': uom,
633                         'product_uos_qty': uos_qty,
634                         'product_uos': uos,
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),
649
650                             }, context=context)
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",
658                 })
659         return True
660
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,
673                             'type': 'internal',
674                             'state': 'auto',
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),
691                         })
692
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,
699                         'product_uom': uom,
700                         'product_uos_qty': uos_qty,
701                         'product_uos': uos,
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,
708                     })
709             wf_service = netsvc.LocalService("workflow")
710             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
711
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",
718                 })
719
720         return True
721
722     def product_id_change(self, cr, uid, ids, product_id):
723         ret = {}
724         if 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
730         else:
731             ret['product_uom'] = False
732             ret['product_uom_categ'] = False
733             ret['product_uos_categ'] = False
734         res = {'value': ret}
735         return res
736
737 stock_planning()
738
739 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: