[MERGE] stock_planning: a few bugfixes, better help/tooltips, cleanup, most of it...
[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 from datetime import datetime
24 from dateutil.relativedelta import relativedelta
25
26 from osv import osv, fields
27 import netsvc
28 from tools.translate import _
29 import logging
30
31 _logger = logging.getLogger('mps') 
32
33
34 def rounding(fl, round_value):
35     if not round_value:
36         return fl
37     return round(fl / round_value) * round_value
38
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"
44     _order = "date_start"
45     _columns = {
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')
50     }
51     _defaults = {
52         'state': 'draft'
53     }
54     
55     def button_open(self, cr, uid, ids, context=None):
56         self.write(cr, uid, ids, {'state': 'open'})
57         return True
58     
59     def button_close(self, cr, uid, ids, context=None):
60         self.write(cr, uid, ids, {'state': 'close'})
61         return True
62
63 stock_period()
64
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"
69
70     _columns = {
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),
127     }
128     _defaults = {
129         'user_id': lambda obj, cr, uid, context: uid,
130         'state': 'draft',
131         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.sale.forecast', context=c),
132     }
133
134     def action_validate(self, cr, uid, ids, *args):
135         self.write(cr, uid, ids, {'state': 'validated','user_id': uid})
136         return True
137
138     def unlink(self, cr, uid, ids, context=None):
139         forecasts = self.read(cr, uid, ids, ['state'])
140         unlink_ids = []
141         for t in forecasts:
142             if t['state'] in ('draft'):
143                 unlink_ids.append(t['id'])
144             else:
145                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Validated Sale Forecasts !'))
146         osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
147         return True
148
149     def onchange_company(self, cr, uid, ids, company_id=False):
150         result = {}
151         if not company_id:
152             return result
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}
158
159     def product_id_change(self, cr, uid, ids, product_id=False):
160         ret = {}
161         if product_id:
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
167         else:
168             ret['product_uom'] = False
169             ret['product_uom_categ'] = False
170             ret['product_uos_categ'] = False
171         res = {'value': ret}
172         return res
173
174     def onchange_uom(self, cr, uid, ids, product_uom=False, product_qty=0.0, 
175                      active_uom=False, product_id=False):
176         ret = {}
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}
184
185     def product_amt_change(self, cr, uid, ids, product_amt=0.0, product_uom=False, product_id=False):
186         round_value = 1
187         qty = 0.0
188         if product_amt and product_id:
189             product = self.pool.get('product.product').browse(cr, uid, product_id)
190             coeff_def2uom = 1
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}}
195         return res
196
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)
202         coef = uom.factor
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
206
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)
212         res = uom.factor
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
216
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
222         return ret
223
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
229         return ret
230
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
235         return ret
236
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:
245                 shops = users = None
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):
252                     if period:
253                         so_period_ids = so_obj.search(cr, uid, [('date_order','>=',period.date_start),('date_order','<=',period.date_stop) ], context = context)
254                         if so_period_ids:
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
258                             if users:
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
261                             if shops:
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],
288         })
289         return True
290
291 stock_sale_forecast()
292
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"
297
298     def _get_in_out(self, cr, uid, val, date_start, date_stop, direction, done, context=None):
299         if context is None:
300             context = {}
301         product_obj = self.pool.get('product.product')
302         mapping = {'in': {
303                         'field': "incoming_qty",
304                         'adapter': lambda x: x,
305                   },
306                   'out': {
307                         'field': "outgoing_qty",
308                         'adapter': lambda x: -x,
309                   },
310         }
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
319         if done:
320             context.update({ 'states':('done',), 'what':(direction,) })
321             prod_ids = [prod_id]
322             st = product_obj.get_product_available(cr, uid, prod_ids, context=context)
323             res = mapping[direction]['adapter'](st.get(prod_id,0.0))
324         else:
325             product = product_obj.read(cr, uid, prod_id,[], context)
326             product_qty = product[mapping[direction]['field']]
327             res = mapping[direction]['adapter'](product_qty)
328         return res
329
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)
341         return res
342
343     def _to_default_uom(self, cr, uid, val, qtys, context=None):
344         res_qty = 0
345         if qtys:
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
349         return res_qty
350
351     def _to_form_uom(self, cr, uid, val, qtys, context=None):
352         res_qty = 0
353         if qtys:
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)
359         return res_qty
360
361
362     def _get_forecast(self, cr, uid, ids, field_names, arg, context=None):
363         res = {}
364         for val in self.browse(cr, uid, ids, context=context):
365             res[val.id] = {}
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)
374
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)
383         return res
384
385     def _get_stock_start(self, cr, uid, val, date, context=None):
386         if context is None:
387             context = {}
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
397         return res
398
399     def _get_past_future(self, cr, uid, ids, field_names, arg, context=None):
400         res = {}
401         for val in self.browse(cr, uid, ids, context=context):
402             if val.period_id.date_stop < time.strftime('%Y-%m-%d'):
403                 res[val.id] = 'Past'
404             else:
405                 res[val.id] = 'Future'
406         return res
407
408     def _get_op(self, cr, uid, ids, field_names, arg, context=None):  # op = OrderPoint
409         res = {}
410         for val in self.browse(cr, uid, ids, context=context):
411             res[val.id]={}
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]
416             coef = 1
417             round_value = 1
418             if ret[2]:
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)
424         return res
425
426     def onchange_company(self, cr, uid, ids, company_id=False):
427         result = {}
428         if company_id:
429             result['warehouse_id'] = False
430         return {'value': result}
431
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):
434         ret = {}
435         if not product_uom:
436             return {}
437         if active_uom:
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}
445
446     _columns = {
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."),
510
511     }
512
513     _defaults = {
514         'state': 'draft' ,
515         'to_procure': 0.0,
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),
518     }
519
520     _order = 'period_id'
521
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)
527         coef = uom.factor
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
531
532
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)
538         res = uom.factor
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
542
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,)) #
562             date = cr.fetchone()
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
577                 current = True
578             else:
579                 current = False
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),
593             })
594         return True
595
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):
602         uos = False
603         uos_qty = 0.0
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
621
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,
635                         'product_uom': uom,
636                         'product_uos_qty': uos_qty,
637                         'product_uos': uos,
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 \
641                                         \n For period: %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)
654                             }, context=context)
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)
662                 })
663
664         return True
665
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),
678                         'type': 'internal',
679                         'state': 'auto',
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)
698                         })
699
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,
706                         'product_uom': uom,
707                         'product_uos_qty': uos_qty,
708                         'product_uos': uos,
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,
715                     })
716             wf_service = netsvc.LocalService("workflow")
717             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
718
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)
725                 })
726
727         return True
728
729     def product_id_change(self, cr, uid, ids, product_id):
730         ret = {}
731         if 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
737         else:
738             ret['product_uom'] = False
739             ret['product_uom_categ'] = False
740             ret['product_uos_categ'] = False
741         res = {'value': ret}
742         return res
743
744 stock_planning()
745
746 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: