[FIX] stock_planning: Fix for onchange of unsaved records and misc improvements ...
[odoo/odoo.git] / addons / stock_planning / stock_planning.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import time
24 import mx.DateTime
25 from mx.DateTime import RelativeDateTime, now, DateTime, localtime
26
27 from osv import osv, fields
28 import netsvc
29 from tools.translate import _
30 from tools import config
31
32
33 def rounding(fl, round_value):
34     if not round_value:
35         return fl
36     return round(fl / round_value) * round_value
37
38 # Object creating periods quickly
39 # changed that stop_date is created with hour 23:59:00 when it was 00:00:00 stop date was excluded from period
40 class stock_period_createlines(osv.osv_memory):
41     _name = "stock.period.createlines"
42
43     def _get_new_period_start(self, cr, uid, context=None):
44         cr.execute("select max(date_stop) from stock_period")
45         result = cr.fetchone()
46         last_date = result and result[0] or False
47         if last_date:
48             period_start = mx.DateTime.strptime(last_date,"%Y-%m-%d %H:%M:%S")+ RelativeDateTime(days=1)
49             period_start = period_start - RelativeDateTime(hours=period_start.hour, minutes=period_start.minute, seconds=period_start.second)
50         else:
51             period_start = mx.DateTime.today()
52         return period_start.strftime('%Y-%m-%d')
53
54
55     _columns = {
56         'name': fields.char('Period Name', size=64),
57         'date_start': fields.date('Start Date', required=True),
58         'date_stop': fields.date('End Date', required=True),
59         'period_ids': fields.one2many('stock.period', 'planning_id', 'Periods'),
60     }
61     _defaults={
62         'date_start': _get_new_period_start,
63     }
64
65     def create_period_weekly(self, cr, uid, ids, context=None):
66         res = self.create_period(cr, uid, ids, context, 6, 'Weekly')
67         return {
68                 'view_type': 'form',
69                 "view_mode": 'tree',
70                 'res_model': 'stock.period',
71                 'type': 'ir.actions.act_window',
72             }
73
74     def create_period_monthly(self,cr, uid, ids, interval=1, context=None):
75         for p in self.browse(cr, uid, ids, context=context):
76             dt = p.date_start
77             ds = mx.DateTime.strptime(p.date_start, '%Y-%m-%d')
78             while ds.strftime('%Y-%m-%d') < p.date_stop:
79                 de = ds + RelativeDateTime(months=interval, minutes=-1)
80                 self.pool.get('stock.period').create(cr, uid, {
81                     'name': ds.strftime('%Y/%m'),
82                     'date_start': ds.strftime('%Y-%m-%d'),
83                     'date_stop': de.strftime('%Y-%m-%d %H:%M:%S'),
84                 })
85                 ds = ds + RelativeDateTime(months=interval)
86         return {
87                 'view_type': 'form',
88                 "view_mode": 'tree',
89                 'res_model': 'stock.period',
90                 'type': 'ir.actions.act_window',
91                 }
92
93     def create_period(self, cr, uid, ids, interval=0, name='Daily', context=None):
94         for p in self.browse(cr, uid, ids, context=context):
95             dt = p.date_start
96             ds = mx.DateTime.strptime(p.date_start, '%Y-%m-%d')
97             while ds.strftime('%Y-%m-%d') < p.date_stop:
98                 de = ds + RelativeDateTime(days=interval, minutes =-1)
99                 if name =='Daily':
100                     new_name=de.strftime('%Y-%m-%d')
101                 if name =="Weekly":
102                     new_name = de.strftime('%Y, week %W')
103                 self.pool.get('stock.period').create(cr, uid, {
104                     'name': new_name,
105                     'date_start': ds.strftime('%Y-%m-%d'),
106                     'date_stop': de.strftime('%Y-%m-%d %H:%M:%S'),
107                 })
108                 ds = ds + RelativeDateTime(days=interval) + 1
109         return {
110                 'view_type': 'form',
111                 "view_mode": 'tree',
112                 'res_model': 'stock.period',
113                 'type': 'ir.actions.act_window',
114             }
115 stock_period_createlines()
116
117 # Periods have no company_id field as they can be shared across similar companies.
118 # If somone thinks different it can be improved.
119 class stock_period(osv.osv):
120     _name = "stock.period"
121     _description = "stock period"
122     _columns = {
123         'name': fields.char('Period Name', size=64, required=True),
124         'date_start': fields.datetime('Start Date', required=True),
125         'date_stop': fields.datetime('End Date', required=True),
126         'state': fields.selection([('draft','Draft'), ('open','Open'),('close','Close')], 'State')
127     }
128     _defaults = {
129         'state': 'draft'
130     }
131
132 stock_period()
133
134 # Creates forecasts records for products from selected Product Category for selected 'Warehouse - Period'
135 # Object added by contributor in ver 1.1
136 class stock_sale_forecast_createlines(osv.osv_memory):
137     _name = "stock.sale.forecast.createlines"
138     _description = "stock.sale.forecast.createlines"
139
140 # FIXME Add some period sugestion like below
141
142 #    def _get_latest_period(self,cr,uid,context={}):
143 #        cr.execute("select max(date_stop) from stock_period")
144 #        result=cr.fetchone()
145 #        return result and result[0] or False
146
147
148     _columns = {
149         'company_id': fields.many2one('res.company', 'Company', required=True, select=1),
150         'warehouse_id1': fields.many2one('stock.warehouse' , 'Warehouse', required=True, \
151                                 help='Warehouse which forecasts will concern. '\
152                                    'If during stock planning you will need sales forecast for all warehouses choose any warehouse now.'),
153         'period_id1': fields.many2one('stock.period' , 'Period', required=True, help = 'Period which forecasts will concern.' ),
154         'product_categ_id1': fields.many2one('product.category' , 'Product Category', required=True, \
155                                 help ='Product Category of products which created forecasts will concern.'),
156         'copy_forecast': fields.boolean('Copy Last Forecast', help="Copy quantities from last Stock and Sale Forecast."),
157                 }
158
159     _defaults = {
160         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.sale.forecast.createlines', context=c),
161     }
162
163     def create_forecast(self, cr, uid, ids, context=None):
164         product_obj = self.pool.get('product.product')
165         forecast_obj = self.pool.get('stock.sale.forecast')
166         mod_obj = self.pool.get('ir.model.data')
167         prod_categ_obj = self.pool.get('product.category')
168         template_obj = self.pool.get('product.template')
169         for f in self.browse(cr, uid, ids, context=context):
170             categ_ids =  f.product_categ_id1.id and [f.product_categ_id1.id] or []
171             prod_categ_ids = prod_categ_obj.search(cr, uid, [('parent_id','child_of', categ_ids)])
172             templates_ids = template_obj.search(cr, uid, [('categ_id','in',prod_categ_ids)])
173             products_ids = product_obj.search(cr, uid, [('product_tmpl_id','in',templates_ids)])
174             if len(products_ids) == 0:
175                 raise osv.except_osv(_('Error !'), _('No products in selected category !'))
176             copy = f.copy_forecast
177             for p in product_obj.browse(cr, uid, products_ids,{}):
178                 if len(forecast_obj.search(cr, uid, [('product_id','=',p.id) , \
179                                                        ('period_id','=',f.period_id1.id), \
180                                                        ('user_id','=',uid), \
181                                                        ('warehouse_id','=',f.warehouse_id1.id)]))== 0:
182                     forecast_qty = 0.0
183 # Not sure if it is expected quantity for this feature (copying forecast from previous period)
184 # because it can take incidental forecast of this warehouse, this product and this user (creating, writing or validating forecast).
185 # It takes only one forecast line (no sum). If user creates only one forecast per period it will be OK. If not I have no idea how to count it.
186
187                     prod_uom = False
188                     if copy:
189                         cr.execute("SELECT period.date_stop, forecast.product_qty, forecast.product_uom \
190                                     FROM stock_sale_forecast AS forecast \
191                                     LEFT JOIN stock_period AS period \
192                                     ON forecast.period_id = period.id \
193                                     WHERE (forecast.user_id = %s OR forecast.create_uid = %s OR forecast.write_uid = %s) \
194                                         AND forecast.warehouse_id = %s AND forecast.product_id = %s \
195                                         AND period.date_stop < %s \
196                                     ORDER BY period.date_stop DESC",
197                                     (uid, uid, uid, f.warehouse_id1.id, p.id, f.period_id1.date_stop) )
198                         ret = cr.fetchone()
199                         if ret:
200                             forecast_qty = ret[1]
201                             prod_uom = ret[2]
202                     prod_uom = prod_uom or p.uom_id.id
203                     prod_uos_categ = False
204                     if p.uos_id:
205                         prod_uos_categ = p.uos_id.category_id.id
206                     forecast_obj.create(cr, uid, {
207                         'company_id': f.warehouse_id1.company_id.id,
208                         'period_id': f.period_id1.id,
209                         'warehouse_id': f.warehouse_id1.id,
210                         'product_id': p.id,
211                         'product_qty': forecast_qty,
212                         'product_amt': 0.0,
213                         'product_uom': prod_uom,
214                         'active_uom': prod_uom,
215                         'product_uom_categ': p.uom_id.category_id.id,
216                         'product_uos_categ': prod_uos_categ,
217                      })
218         result = mod_obj._get_id(cr, uid, 'stock_planning', 'view_stock_sale_forecast_filter')
219         id = mod_obj.read(cr, uid, result, ['res_id'], context=context)
220
221         return {
222                 'view_type': 'form',
223                 "view_mode": 'tree',
224                 'res_model': 'stock.sale.forecast',
225                 'type': 'ir.actions.act_window',
226                 'search_view_id': id['res_id'],
227             }
228
229 stock_sale_forecast_createlines()
230
231
232 # Stock and Sales Forecast object. Previously stock_planning_sale_prevision.
233 # A lot of changes in 1.1
234 class stock_sale_forecast(osv.osv):
235     _name = "stock.sale.forecast"
236
237     _columns = {
238         'company_id':fields.many2one('res.company', 'Company', required=True),
239         'create_uid': fields.many2one('res.users', 'Responsible'),
240         'name': fields.char('Name', size=64, readonly=True, states={'draft': [('readonly',False)]}),
241         'user_id': fields.many2one('res.users' , 'Created/Validated by',readonly=True, \
242                                         help='Shows who created this forecast, or who validated.'),
243         'warehouse_id': fields.many2one('stock.warehouse' , 'Warehouse', required=True, readonly=True, states={'draft': [('readonly',False)]}, \
244                                         help='Shows which warehouse this forecast concerns. '\
245                                          'If during stock planning you will need sales forecast for all warehouses choose any warehouse now.'),
246         'period_id': fields.many2one('stock.period' , 'Period', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
247                                         help = 'Shows which period this forecast concerns.'),
248         'product_id': fields.many2one('product.product' , 'Product', readonly=True, required=True, states={'draft':[('readonly',False)]}, \
249                                         help = 'Shows which product this forecast concerns.'),
250         'product_qty': fields.float('Product Quantity', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
251                                         help= 'Forecasted quantity.'),
252         'product_amt': fields.float('Product Amount', readonly=True, states={'draft':[('readonly',False)]}, \
253                                         help='Forecast value which will be converted to Product Quantity according to prices.'),
254         'product_uom_categ': fields.many2one('product.uom.categ', 'Product UoM Category'),  # Invisible field for product_uom domain
255         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, readonly=True, states={'draft':[('readonly',False)]}, \
256                         help = "Unit of Measure used to show the quanities of stock calculation." \
257                         "You can use units form default category or from second category (UoS category)."),
258         'product_uos_categ' : fields.many2one('product.uom.categ', 'Product UoS Category'), # Invisible field for product_uos domain
259 # Field used in onchange_uom to check what uom was before change and recalculate quantities acording to old uom (active_uom) and new uom.
260         'active_uom': fields.many2one('product.uom',  string = "Active UoM"),
261         'state': fields.selection([('draft','Draft'),('validated','Validated')],'State',readonly=True),
262         'analyzed_period1_id': fields.many2one('stock.period' , 'Period1', readonly=True, states={'draft':[('readonly',False)]},),
263         'analyzed_period2_id': fields.many2one('stock.period' , 'Period2', readonly=True, states={'draft':[('readonly',False)]},),
264         'analyzed_period3_id': fields.many2one('stock.period' , 'Period3', readonly=True, states={'draft':[('readonly',False)]},),
265         'analyzed_period4_id': fields.many2one('stock.period' , 'Period4', readonly=True, states={'draft':[('readonly',False)]},),
266         'analyzed_period5_id': fields.many2one('stock.period' , 'Period5', readonly=True, states={'draft':[('readonly',False)]},),
267         'analyzed_user_id': fields.many2one('res.users' , 'This User', required=False, readonly=True, states={'draft':[('readonly',False)]},),
268         'analyzed_dept_id': fields.many2one('hr.department' , 'This Department', required=False, \
269                                     readonly=True, states={'draft':[('readonly',False)]},),
270         'analyzed_warehouse_id': fields.many2one('stock.warehouse' , 'This Warehouse', required=False, \
271                                     readonly=True, states={'draft':[('readonly',False)]}),
272         'analyze_company': fields.boolean('Per Company', readonly=True, states={'draft':[('readonly',False)]}, \
273                                     help = "Check this box to see the sales for whole company."),
274         'analyzed_period1_per_user': fields.float('This User Period1', readonly=True),
275         'analyzed_period2_per_user': fields.float('This User Period2', readonly=True),
276         'analyzed_period3_per_user': fields.float('This User Period3', readonly=True),
277         'analyzed_period4_per_user': fields.float('This User Period4', readonly=True),
278         'analyzed_period5_per_user': fields.float('This User Period5', readonly=True),
279         'analyzed_period1_per_dept': fields.float('This Dept Period1', readonly=True),
280         'analyzed_period2_per_dept': fields.float('This Dept Period2', readonly=True),
281         'analyzed_period3_per_dept': fields.float('This Dept Period3', readonly=True),
282         'analyzed_period4_per_dept': fields.float('This Dept Period4', readonly=True),
283         'analyzed_period5_per_dept': fields.float('This Dept Period5', readonly=True),
284         'analyzed_period1_per_warehouse': fields.float('This Warehouse Period1', readonly=True),
285         'analyzed_period2_per_warehouse': fields.float('This Warehouse Period2', readonly=True),
286         'analyzed_period3_per_warehouse': fields.float('This Warehouse Period3', readonly=True),
287         'analyzed_period4_per_warehouse': fields.float('This Warehouse Period4', readonly=True),
288         'analyzed_period5_per_warehouse': fields.float('This Warehouse Period5', readonly=True),
289         'analyzed_period1_per_company': fields.float('This Copmany Period1', readonly=True),
290         'analyzed_period2_per_company': fields.float('This Company Period2', readonly=True),
291         'analyzed_period3_per_company': fields.float('This Company Period3', readonly=True),
292         'analyzed_period4_per_company': fields.float('This Company Period4', readonly=True),
293         'analyzed_period5_per_company': fields.float('This Company Period5', readonly=True),
294     }
295     _defaults = {
296         'user_id': lambda obj, cr, uid, context: uid,
297         'state': 'draft',
298         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.sale.forecast', context=c),
299     }
300
301     def action_validate(self, cr, uid, ids, *args):
302         self.write(cr, uid, ids, {'state':'validated','user_id':uid})
303         return True
304
305     def unlink(self, cr, uid, ids, context=None):
306         forecasts = self.read(cr, uid, ids, ['state'])
307         unlink_ids = []
308         for t in forecasts:
309             if t['state'] in ('draft'):
310                 unlink_ids.append(t['id'])
311             else:
312                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Validated Sale Forecasts !'))
313         osv.osv.unlink(self, cr, uid, unlink_ids,context=context)
314         return True
315
316     def onchange_company(self, cr, uid, ids, company_id=False):
317         result = {}
318         if not company_id:
319             return result
320         result['warehouse_id'] = False
321         result['analyzed_user_id'] = False
322         result['analyzed_dept_id'] = False
323         result['analyzed_warehouse_id'] = False
324         return {'value': result}
325
326     def product_id_change(self, cr, uid, ids, product_id=False):
327         ret = {}
328         if product_id:
329             product_rec =  self.pool.get('product.product').browse(cr, uid, product_id)
330             ret['product_uom'] = product_rec.uom_id.id
331             ret['product_uom_categ'] = product_rec.uom_id.category_id.id
332             ret['product_uos_categ'] = product_rec.uos_id and product_rec.uos_id.category_id.id or False
333             ret['active_uom'] = product_rec.uom_id.id
334         else:
335             ret['product_uom'] = False
336             ret['product_uom_categ'] = False
337             ret['product_uos_categ'] = False
338         res = {'value': ret}
339         return res
340
341     def onchange_uom(self, cr, uid, ids, product_uom=False, product_qty=0.0, active_uom=False):
342         ret = {}
343         if not ids:
344             return {}
345         if product_uom:
346             val1 = self.browse(cr, uid, ids)
347             val = val1[0]
348             coeff_uom2def = self._to_default_uom_factor(cr, uid, val, active_uom, {})
349             coeff_def2uom, round_value = self._from_default_uom_factor( cr, uid, val, product_uom, {})
350             coeff = coeff_uom2def * coeff_def2uom
351             ret['product_qty'] = rounding(coeff * product_qty, round_value)
352             ret['active_uom'] = product_uom
353         return {'value': ret}
354
355     def product_amt_change(self, cr, uid, ids, product_amt = 0.0, product_uom=False):
356         ret = {}
357         if not ids:
358             return {}
359         round_value = 1
360         if product_amt:
361             coeff_def2uom = 1
362             val1 = self.browse(cr, uid, ids)
363             val = val1[0]
364             if (product_uom != val.product_id.uom_id.id):
365                 coeff_def2uom, round_value = self._from_default_uom_factor( cr, uid, val, product_uom, {})
366             ret['product_qty'] = rounding(coeff_def2uom * product_amt/(val.product_id.product_tmpl_id.list_price), round_value)
367         res = {'value': ret}
368         return res
369
370     def _to_default_uom_factor(self, cr, uid, val, uom_id, context=None):
371         uom_obj = self.pool.get('product.uom')
372         uom = uom_obj.browse(cr, uid, uom_id, context=context)
373         coef =  uom.factor
374         if uom.category_id.id <> val.product_id.uom_id.category_id.id:
375             coef = coef / val.product_id.uos_coeff
376         return val.product_id.uom_id.factor / coef
377
378     def _from_default_uom_factor(self, cr, uid, val, uom_id, context):
379         uom_obj = self.pool.get('product.uom')
380         uom = uom_obj.browse(cr, uid, uom_id, context=context)
381         res = uom.factor
382         if uom.category_id.id <> val.product_id.uom_id.category_id.id:
383             res = res / val.product_id.uos_coeff
384         return res / val.product_id.uom_id.factor, uom.rounding
385
386     def _sales_per_users(self, cr, uid, so, so_line, company, users):
387         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) " \
388                    "WHERE (sol.id IN %s) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN %s) AND (s.company_id=%s) " \
389                     "AND (s.user_id IN %s) " ,(tuple(so_line), tuple(so), company, tuple(users)))
390         ret = cr.fetchone()[0] or 0.0
391         return ret
392
393     def _sales_per_warehouse(self, cr, uid, so, so_line, company, shops):
394         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) " \
395                    "WHERE (sol.id IN %s) AND (s.state NOT IN (\'draft\',\'cancel\')) AND (s.id IN %s)AND (s.company_id=%s) " \
396                     "AND (s.shop_id IN %s)" ,(tuple(so_line), tuple(so), company, tuple(shops)))
397         ret = cr.fetchone()[0] or 0.0
398         return ret
399
400     def _sales_per_company(self, cr, uid, so, so_line, company):
401         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) " \
402                    "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))
403         ret = cr.fetchone()[0] or 0.0
404         return ret
405
406     def calculate_sales_history(self, cr, uid, ids, context, *args):
407         sales = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],]
408         for obj in self.browse(cr, uid, ids):
409             periods = obj.analyzed_period1_id, obj.analyzed_period2_id, obj.analyzed_period3_id, obj.analyzed_period4_id, obj.analyzed_period5_id
410             so_obj = self.pool.get('sale.order')
411             so_line_obj = self.pool.get('sale.order.line')
412             so_line_product_ids = so_line_obj.search(cr, uid, [('product_id','=', obj.product_id.id)], context = context)
413             if so_line_product_ids:
414                 so_line_product_set = ','.join(map(str,so_line_product_ids))
415                 if obj.analyzed_warehouse_id:
416                     shops = self.pool.get('sale.shop').search(cr, uid,[('warehouse_id','=', obj.analyzed_warehouse_id.id)], context = context)
417                     shops_set = ','.join(map(str,shops))
418                 else:
419                     shops = False
420                 if obj.analyzed_dept_id:
421                     dept_obj = self.pool.get('hr.department')
422                     dept_id =  obj.analyzed_dept_id.id and [obj.analyzed_dept_id.id] or []
423                     dept_ids = dept_obj.search(cr,uid,[('parent_id','child_of',dept_id)])
424 #                    dept_ids_set = ','.join(map(str,dept_ids))
425                     cr.execute("SELECT user_id FROM hr_department_user_rel WHERE (department_id IN %s)" ,(tuple(dept_ids),))
426                     dept_users = [x for x, in cr.fetchall()]
427                     dept_users_set =  ','.join(map(str,dept_users))
428                 else:
429                     dept_users = False
430                 factor, round_value = self._from_default_uom_factor(cr, uid, obj, obj.product_uom.id, context=context)
431                 for i, period in enumerate(periods):
432                     if period:
433                         so_period_ids = so_obj.search(cr, uid, [('date_order','>=',period.date_start),('date_order','<=',period.date_stop) ], context = context)
434                         if so_period_ids:
435                            # so_period_set = ','.join(map(str,so_period_ids))
436                             if obj.analyzed_user_id:
437                                 user_set = str(obj.analyzed_user_id.id)
438
439                                 sales[i][0] = self._sales_per_users(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, user_set)
440                                 sales[i][0] *= factor
441                             if dept_users:
442                                 sales[i][1] = self._sales_per_users(cr, uid, so_period_ids,  so_line_product_ids, obj.company_id.id, dept_users_set)
443                                 sales[i][1] *= factor
444                             if shops:
445                                 sales[i][2] = self._sales_per_warehouse(cr, uid, so_period_ids,  so_line_product_ids, obj.company_id.id, shops_set)
446                                 sales[i][2] *= factor
447                             if obj.analyze_company:
448                                 sales[i][3] = self._sales_per_company(cr, uid, so_period_ids, so_line_product_ids, obj.company_id.id, )
449                                 sales[i][3] *= factor
450         self.write(cr, uid, ids, {
451             'analyzed_period1_per_user': sales[0][0],
452             'analyzed_period2_per_user': sales[1][0],
453             'analyzed_period3_per_user': sales[2][0],
454             'analyzed_period4_per_user': sales[3][0],
455             'analyzed_period5_per_user': sales[4][0],
456             'analyzed_period1_per_dept': sales[0][1],
457             'analyzed_period2_per_dept': sales[1][1],
458             'analyzed_period3_per_dept': sales[2][1],
459             'analyzed_period4_per_dept': sales[3][1],
460             'analyzed_period5_per_dept': sales[4][1],
461             'analyzed_period1_per_warehouse': sales[0][2],
462             'analyzed_period2_per_warehouse': sales[1][2],
463             'analyzed_period3_per_warehouse': sales[2][2],
464             'analyzed_period4_per_warehouse': sales[3][2],
465             'analyzed_period5_per_warehouse': sales[4][2],
466             'analyzed_period1_per_company': sales[0][3],
467             'analyzed_period2_per_company': sales[1][3],
468             'analyzed_period3_per_company': sales[2][3],
469             'analyzed_period4_per_company': sales[3][3],
470             'analyzed_period5_per_company': sales[4][3],
471         })
472         return True
473
474
475 stock_sale_forecast()
476
477 # Creates stock planning records for products from selected Product Category for selected 'Warehouse - Period'
478 # Object added by contributor in ver 1.1
479 class stock_planning_createlines(osv.osv_memory):
480     _name = "stock.planning.createlines"
481
482     def onchange_company(self, cr, uid, ids, company_id=False):
483         result = {}
484         if company_id:
485             result['warehouse_id2'] = False
486         return {'value': result}
487
488     _columns = {
489         'company_id': fields.many2one('res.company', 'Company', required=True),
490         'period_id2': fields.many2one('stock.period' , 'Period', required=True, help = 'Period which planning will concern.'),
491         'warehouse_id2': fields.many2one('stock.warehouse' , 'Warehouse', required=True, help = 'Warehouse which planning will concern.'),
492         'product_categ_id2': fields.many2one('product.category' , 'Product Category', \
493                         help = 'Planning will be created for products from Product Category selected by this field. '\
494                                'This field is ignored when you check \"All Forecasted Product\" box.' ),
495         'forecasted_products': fields.boolean('All Products with Forecast', \
496                      help = "Check this box to create planning for all products having any forecast for selected Warehouse and Period. "\
497                             "Product Category field will be ignored."),
498     }
499
500     _defaults = {
501         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.planning', context=c),
502     }
503
504     def create_planning(self,cr, uid, ids, context=None):
505         if context is None:
506             context = {}
507         product_obj = self.pool.get('product.product')
508         planning_obj = self.pool.get('stock.planning')
509         mod_obj = self.pool.get('ir.model.data')
510         for f in self.browse(cr, uid, ids, context=context):
511             if f.forecasted_products:
512                 cr.execute("SELECT product_id \
513                                 FROM stock_sale_forecast \
514                                 WHERE (period_id = %s) AND (warehouse_id = %s)", (f.period_id2.id, f.warehouse_id2.id))
515                 products_id1 = [x for x, in cr.fetchall()]
516             else:
517                 prod_categ_obj = self.pool.get('product.category')
518                 template_obj = self.pool.get('product.template')
519                 categ_ids = f.product_categ_id2.id and [f.product_categ_id2.id] or []
520                 prod_categ_ids = prod_categ_obj.search(cr,uid,[('parent_id','child_of',categ_ids)])
521                 templates_ids = template_obj.search(cr,uid,[('categ_id','in',prod_categ_ids)])
522                 products_id1 = product_obj.search(cr,uid,[('product_tmpl_id','in',templates_ids)])
523             if len(products_id1)==0:
524                 raise osv.except_osv(_('Error !'), _('No forecasts for selected period or no products in selected category !'))
525
526             for p in product_obj.browse(cr, uid, products_id1,context=context):
527                 if len(planning_obj.search(cr, uid, [('product_id','=',p.id),
528                                                       ('period_id','=',f.period_id2.id),
529                                                       ('warehouse_id','=',f.warehouse_id2.id)]))== 0:
530                     cr.execute("SELECT period.date_stop, planning.product_uom, planning.planned_outgoing, planning.to_procure, \
531                                     planning.stock_only, planning.procure_to_stock, planning.confirmed_forecasts_only, \
532                                     planning.supply_warehouse_id, planning.stock_supply_location \
533                                 FROM stock_planning AS planning \
534                                 LEFT JOIN stock_period AS period \
535                                 ON planning.period_id = period.id \
536                                 WHERE (planning.create_uid = %s OR planning.write_uid = %s) \
537                                      AND planning.warehouse_id = %s AND planning.product_id = %s \
538                                      AND period.date_stop < %s \
539                                 ORDER BY period.date_stop DESC",
540                                     (uid, uid, f.warehouse_id2.id, p.id, f.period_id2.date_stop) )
541                     ret=cr.fetchone()
542 #                        forecast_qty = ret and ret[0] or 0.0
543                     if ret:
544 #                            raise osv.except_osv(_('Error !'), _('ret is %s %s %s %s %s %s')%(ret[0],ret[2],ret[3],ret[4],ret[5],ret[6],))
545                         prod_uom = ret[1]
546                         planned_out = ret[2]
547                         to_procure = ret[3]
548                         stock_only = ret[4]
549                         procure_to_stock = ret[5]
550                         confirmed_forecasts_only = ret[6]
551                         supply_warehouse_id = ret[7]
552                         stock_supply_location = ret[8]
553                     else:
554                         prod_uom = p.uom_id.id
555                         planned_out = False
556                         to_procure = False
557                         stock_only = False
558                         procure_to_stock = False
559                         confirmed_forecasts_only = False
560                         supply_warehouse_id = False
561                         stock_supply_location = False
562                     prod_uos_categ = False
563                     if p.uos_id:
564                         prod_uos_categ = p.uos_id.category_id.id
565                     planning_obj.create(cr, uid, {
566                         'company_id' : f.warehouse_id2.company_id.id,
567                         'period_id': f.period_id2.id,
568                         'warehouse_id' : f.warehouse_id2.id,
569                         'product_id': p.id,
570                         'product_uom' : prod_uom,
571                         'product_uom_categ' : p.uom_id.category_id.id,
572                         'product_uos_categ' : prod_uos_categ,
573                         'active_uom' : prod_uom,
574                         'planned_outgoing': planned_out,
575                         'to_procure': to_procure,
576                         'stock_only': stock_only,
577                         'procure_to_stock': procure_to_stock,
578                         'confirmed_forecasts_only': confirmed_forecasts_only,
579                         'supply_warehouse_id': supply_warehouse_id,
580                         'stock_supply_location': stock_supply_location,
581
582                     })
583         result = mod_obj._get_id(cr, uid, 'stock_planning', 'view_stock_planning_filter')
584         id = mod_obj.read(cr, uid, result, ['res_id'], context=context)
585         return {
586                 'view_type': 'form',
587                 "view_mode": 'tree',
588                 'res_model': 'stock.planning',
589                 'type': 'ir.actions.act_window',
590                 'search_view_id': id['res_id'],
591             }
592 stock_planning_createlines()
593
594
595 # The main Stock Planning object
596 # A lot of changes by contributor in ver 1.1
597 class stock_planning(osv.osv):
598     _name = "stock.planning"
599
600     def _get_in_out(self, cr, uid, val, date_start, date_stop, direction, done, context=None):
601 #        res = {}
602         if not context:
603             context = {}
604         product_obj = self.pool.get('product.product')
605         mapping = {'in': {
606                         'field': "incoming_qty",
607                         'adapter': lambda x: x,
608                   },
609                   'out': {
610                         'field': "outgoing_qty",
611                         'adapter': lambda x: -x,
612                   },
613         }
614         context['from_date'] = date_start
615         context['to_date'] = date_stop
616         locations = [val.warehouse_id.lot_stock_id.id,]
617         if not val.stock_only:
618             locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
619         context['location'] = locations
620         context['compute_child'] = True
621         prod_id = val.product_id.id
622         if done:
623             c1 = context.copy()
624             c1.update({ 'states':('done',), 'what':(direction,) })
625             prod_ids = [prod_id]
626             st = product_obj.get_product_available(cr,uid, prod_ids, context=c1)
627             res = mapping[direction]['adapter'](st.get(prod_id,0.0))
628         else:
629             product = product_obj.read(cr, uid, prod_id,[], context)
630             product_qty = product[mapping[direction]['field']]
631             res = mapping[direction]['adapter'](product_qty)
632 #            res[val.id] = product_obj['incoming_qty']
633         return res
634
635     def _get_outgoing_before(self, cr, uid, val, date_start, date_stop, context=None):
636         cr.execute("SELECT sum(planning.planned_outgoing), planning.product_uom \
637                     FROM stock_planning AS planning \
638                     LEFT JOIN stock_period AS period \
639                     ON (planning.period_id = period.id) \
640                     WHERE (period.date_stop >= %s) AND (period.date_stop <= %s) \
641                         AND (planning.product_id = %s) AND (planning.company_id = %s) \
642                     GROUP BY planning.product_uom", \
643                         (date_start, date_stop, val.product_id.id, val.company_id.id,))
644         planning_qtys = cr.fetchall()
645         res = self._to_planning_uom(cr, uid, val, planning_qtys, context)
646         return res
647
648     def _to_planning_uom(self, cr, uid, val, qtys, context):
649         res_qty = 0
650         if qtys:
651             uom_obj = self.pool.get('product.uom')
652             for qty, prod_uom in qtys:
653                 coef = self._to_default_uom_factor(cr, uid, val, prod_uom, context=context)
654                 res_coef, round_value = self._from_default_uom_factor(cr, uid, val, val.product_uom.id, context=context)
655                 coef = coef * res_coef
656                 res_qty += rounding(qty * coef, round_value)
657         return res_qty
658
659
660     def _get_forecast(self, cr, uid, ids, field_names, arg, context=None):
661         res = {}
662         for val in self.browse(cr, uid, ids):
663             res[val.id] = {}
664             valid_part = val.confirmed_forecasts_only and " AND state = 'validated'" or ""
665             cr.execute('SELECT sum(product_qty), product_uom  \
666                         FROM stock_sale_forecast \
667                         WHERE product_id = %s AND period_id = %s AND company_id = %s '+valid_part+ \
668                        'GROUP BY product_uom', \
669                             (val.product_id.id,val.period_id.id, val.company_id.id))
670             company_qtys = cr.fetchall()
671             res[val.id]['company_forecast'] = self._to_planning_uom(cr, uid, val, company_qtys, context)
672
673             cr.execute('SELECT sum(product_qty), product_uom \
674                         FROM stock_sale_forecast \
675                         WHERE product_id = %s and period_id = %s AND warehouse_id = %s ' + valid_part + \
676                        'GROUP BY product_uom', \
677                         (val.product_id.id,val.period_id.id, val.warehouse_id.id))
678             warehouse_qtys = cr.fetchall()
679             res[val.id]['warehouse_forecast'] = self._to_planning_uom(cr, uid, val, warehouse_qtys, context)
680             res[val.id]['warehouse_forecast'] = rounding(res[val.id]['warehouse_forecast'],  val.product_id.uom_id.rounding)
681         return res
682
683     def _get_stock_start(self, cr, uid, val, date, context=None):
684         context['from_date'] = None
685         context['to_date'] = date
686         locations = [val.warehouse_id.lot_stock_id.id,]
687         if not val.stock_only:
688             locations.extend([val.warehouse_id.lot_input_id.id, val.warehouse_id.lot_output_id.id])
689         context['location'] = locations
690         context['compute_child'] = True
691         product_obj =  self.pool.get('product.product').read(cr, uid,val.product_id.id,[], context)
692         res = product_obj['qty_available']     # value for stock_start
693         return res
694
695     def _get_past_future(self, cr, uid, ids, field_names, arg, context):
696         res = {}
697         for val in self.browse(cr, uid, ids, context=context):
698             if val.period_id.date_stop < time.strftime('%Y-%m-%d'):
699                 res[val.id] = 'Past'
700             else:
701                 res[val.id] = 'Future'
702         return res
703
704     def _get_op(self, cr, uid, ids, field_names, arg, context=None):  # op = OrderPoint
705         res = {}
706         for val in self.browse(cr, uid, ids, context=context):
707             res[val.id]={}
708             cr.execute("SELECT product_min_qty, product_max_qty, product_uom  \
709                         FROM stock_warehouse_orderpoint \
710                         WHERE warehouse_id = %s AND product_id = %s AND active = 'TRUE'", (val.warehouse_id.id, val.product_id.id))
711             ret = cr.fetchone() or [0.0,0.0,False]
712             coef = 1
713             round_value = 1
714             if ret[2]:
715                 coef = self._to_default_uom_factor(cr, uid, val, ret[2], context)
716                 res_coef, round_value = self._from_default_uom_factor(cr, uid, val, val.product_uom.id, context=context)
717                 coef = coef * res_coef
718             res[val.id]['minimum_op'] = rounding(ret[0]*coef, round_value)
719             res[val.id]['maximum_op'] = ret[1]*coef
720         return res
721
722     def onchange_company(self, cr, uid, ids, company_id=False):
723         result = {}
724         if company_id:
725             result['warehouse_id'] = False
726         return {'value': result}
727
728     def onchange_uom(self, cr, uid, ids, product_uom=False):
729         ret = {}
730         if not product_uom:
731             return {}
732         if not ids:
733             return {}
734         val1 = self.browse(cr, uid, ids)
735         val = val1[0]
736         if val.active_uom:
737             coeff_uom2def = self._to_default_uom_factor(cr, uid, val, val.active_uom.id, {})
738             coeff_def2uom, round_value = self._from_default_uom_factor( cr, uid, val, product_uom, {})
739             coeff = coeff_uom2def * coeff_def2uom
740             ret['planned_outgoing'] = rounding(coeff * val.planned_outgoing, round_value)
741             ret['to_procure'] = rounding(coeff * val.to_procure, round_value)
742         ret['active_uom'] = product_uom
743         return {'value': ret}
744
745     _columns = {
746         'company_id': fields.many2one('res.company', 'Company', required = True),
747         'history': fields.text('Procurement History', readonly=True, help = "History of procurement or internal supply of this planning line."),
748         'state' : fields.selection([('draft','Draft'),('done','Done')],'State',readonly=True),
749         'period_id': fields.many2one('stock.period' , 'Period', required=True, \
750                 help = 'Period for this planning. Requisition will be created for beginning of the period.'),
751         'warehouse_id': fields.many2one('stock.warehouse','Warehouse', required=True),
752         'product_id': fields.many2one('product.product' , 'Product', required=True, help = 'Product which this planning is created for.'),
753         'product_uom_categ' : fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uom domain
754         'product_uom': fields.many2one('product.uom', 'UoM', required=True, help = "Unit of Measure used to show the quanities of stock calculation." \
755                         "You can use units form default category or from second category (UoS category)."),
756         'product_uos_categ': fields.many2one('product.uom.categ', 'Product UoM Category'), # Invisible field for product_uos domain
757 # Field used in onchange_uom to check what uom was before change to recalculate quantities acording to old uom (active_uom) and new uom.
758         'active_uom': fields.many2one('product.uom',  string = "Active UoM"),
759         'planned_outgoing': fields.float('Planned Out', required=True,  \
760                 help = 'Enter planned outgoing quantity from selected Warehouse during the selected Period of selected Product. '\
761                         'To plan this value look at Confirmed Out or Sales Forecasts. This value should be equal or greater than Confirmed Out.'),
762         'company_forecast': fields.function(_get_forecast, method=True, string ='Company Forecast', multi = 'company', \
763                 help = 'All sales forecasts for whole company (for all Warehouses) of selected Product during selected Period.'),
764         'warehouse_forecast': fields.function(_get_forecast, method=True, string ='Warehouse Forecast',  multi = 'warehouse',\
765                 help = 'All sales forecasts for selected Warehouse of selected Product during selected Period.'),
766         'stock_simulation': fields.float('Stock Simulation', readonly =True, \
767                 help = 'Stock simulation at the end of selected Period.\n For current period it is: \n' \
768                        'Initial Stock - Already Out + Already In - Expected Out + Incoming Left.\n' \
769                         'For periods ahead it is: \nInitial Stock - Planned Out Before + Incoming Before - Planned Out + Planned In.'),
770         'incoming': fields.float('Confirmed In', readonly=True, \
771                 help = 'Quantity of all confirmed incoming moves in calculated Period.'),
772         'outgoing': fields.float('Confirmed Out', readonly=True, \
773                 help = 'Quantity of all confirmed outgoing moves in calculated Period.'),
774         'incoming_left': fields.float('Incoming Left', readonly=True,  \
775                 help = 'Quantity left to Planned incoming quantity. This is calculated difference between Planned In and Confirmed In. ' \
776                         'For current period Already In is also calculated. This value is used to create procurement for lacking quantity.'),
777         'outgoing_left': fields.float('Expected Out', readonly=True, \
778                 help = 'Quantity expected to go out in selected period. As a difference between Planned Out and Confirmed Out. ' \
779                         'For current period Already Out is also calculated'),
780         'to_procure': fields.float(string='Planned In', required=True, \
781                 help = 'Enter quantity which (by your plan) should come in. Change this value and observe Stock simulation. ' \
782                         'This value should be equal or greater than Confirmed In.'),
783         'line_time': fields.function(_get_past_future, method=True,type='char', string='Past/Future'),
784         'minimum_op': fields.function(_get_op, method=True, type='float', string = 'Minimum Rule', multi= 'minimum', \
785                             help = 'Minimum quantity set in Minimum Stock Rules for this Warhouse'),
786         'maximum_op': fields.function(_get_op, method=True, type='float', string = 'Maximum Rule', multi= 'maximum', \
787                             help = 'Maximum quantity set in Minimum Stock Rules for this Warhouse'),
788         'outgoing_before': fields.float('Planned Out Before', readonly=True, \
789                             help= 'Planned Out in periods before calculated. '\
790                                     'Between start date of current period and one day before start of calculated period.'),
791         'incoming_before': fields.float('Incoming Before', readonly = True, \
792                             help= 'Confirmed incoming in periods before calculated (Including Already In). '\
793                                     'Between start date of current period and one day before start of calculated period.'),
794         'stock_start': fields.float('Initial Stock', readonly=True, \
795                             help= 'Stock quantity one day before current period.'),
796         'already_out': fields.float('Already Out', readonly=True, \
797                             help= 'Quantity which is already dispatched out of this warehouse in current period.'),
798         'already_in': fields.float('Already In', readonly=True, \
799                             help= 'Quantity which is already picked up to this warehouse in current period.'),
800         'stock_only': fields.boolean("Stock Location Only", help = "Check to calculate stock location of selected warehouse only. " \
801                                         "If not selected calculation is made for input, stock and output location of warehouse."),
802         "procure_to_stock": fields.boolean("Procure To Stock Location", help = "Chect to make procurement to stock location of selected warehouse. " \
803                                         "If not selected procurement will be made into input location of warehouse."),
804         "confirmed_forecasts_only": fields.boolean("Validated Forecasts", help = "Check to take validated forecasts only. " \
805                     "If not checked system takes validated and draft forecasts."),
806         'supply_warehouse_id': fields.many2one('stock.warehouse','Source Warehouse', help = "Warehouse used as source in supply pick move created by 'Supply from Another Warhouse'."),
807         "stock_supply_location": fields.boolean("Stock Supply Location", help = "Check to supply from Stock location of Supply Warehouse. " \
808                 "If not checked supply will be made from Output location of Supply Warehouse. Used in 'Supply from Another Warhouse' with Supply Warehouse."),
809
810     }
811
812     _defaults = {
813         'state': lambda *args: 'draft' ,
814         'to_procure': 0.0,
815         'planned_outgoing': 0.0,
816         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.planning', context=c),
817     }
818
819     _order = 'period_id'
820
821     def _to_default_uom_factor(self, cr, uid, val, uom_id, context=None):
822         uom_obj = self.pool.get('product.uom')
823         uom = uom_obj.browse(cr, uid, uom_id, context=context)
824         coef = uom.factor
825         if uom.category_id.id != val.product_id.uom_id.category_id.id:
826             coef = coef / val.product_id.uos_coeff
827         return val.product_id.uom_id.factor / coef
828
829
830     def _from_default_uom_factor(self, cr, uid, val, uom_id, context=None):
831         uom_obj = self.pool.get('product.uom')
832         uom = uom_obj.browse(cr, uid, uom_id, context=context)
833         res = uom.factor
834         if uom.category_id.id != val.product_id.uom_id.category_id.id:
835             res = res / val.product_id.uos_coeff
836         return res / val.product_id.uom_id.factor, uom.rounding
837
838     def calculate_planning(self, cr, uid, ids, context, *args):
839         one_minute = RelativeDateTime(minutes=1)
840         current_date_beginning_c = mx.DateTime.today()
841         current_date_end_c = current_date_beginning_c  + RelativeDateTime(days=1, minutes=-1)  # to get hour 23:59:00
842         current_date_beginning = current_date_beginning_c.strftime('%Y-%m-%d %H:%M:%S')
843         current_date_end = current_date_end_c.strftime('%Y-%m-%d %H:%M:%S')
844         for val in self.browse(cr, uid, ids, context=context):
845             day = mx.DateTime.strptime(val.period_id.date_start, '%Y-%m-%d %H:%M:%S')
846             dbefore = mx.DateTime.DateTime(day.year, day.month, day.day) - one_minute
847             day_before_calculated_period = dbefore.strftime('%Y-%m-%d %H:%M:%S')   # one day before start of calculated period
848             cr.execute("SELECT date_start \
849                     FROM stock_period AS period \
850                     LEFT JOIN stock_planning AS planning \
851                     ON (planning.period_id = period.id) \
852                     WHERE (period.date_stop >= %s) AND (period.date_start <= %s) AND \
853                         planning.product_id = %s", (current_date_end, current_date_end, val.product_id.id,)) #
854             date = cr.fetchone()
855             start_date_current_period = date and date[0] or False
856             start_date_current_period = start_date_current_period or current_date_beginning
857             day = mx.DateTime.strptime(start_date_current_period, '%Y-%m-%d %H:%M:%S')
858             dbefore = mx.DateTime.DateTime(day.year, day.month, day.day) - one_minute
859             date_for_start = dbefore.strftime('%Y-%m-%d %H:%M:%S')   # one day before current period
860             already_out = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='out', done = True, context=context),
861             already_in = self._get_in_out(cr, uid, val, start_date_current_period, current_date_end, direction='in', done = True, context=context),
862             outgoing = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='out', done = False, context=context),
863             incoming = self._get_in_out(cr, uid, val, val.period_id.date_start, val.period_id.date_stop, direction='in', done = False, context=context),
864             outgoing_before = self._get_outgoing_before(cr, uid, val, start_date_current_period, day_before_calculated_period, context=context),
865             incoming_before = self._get_in_out(cr, uid, val, start_date_current_period, day_before_calculated_period, direction='in', done = False, context=context),
866             stock_start = self._get_stock_start(cr, uid, val, date_for_start, context=context),
867             if start_date_current_period == val.period_id.date_start:   # current period is calculated
868                 current = True
869             else:
870                 current = False
871             factor, round_value = self._from_default_uom_factor(cr, uid, val, val.product_uom.id, context=context)
872             self.write(cr, uid, ids, {
873                 'already_out': rounding(already_out[0]*factor,round_value),
874                 'already_in': rounding(already_in[0]*factor,round_value),
875                 'outgoing': rounding(outgoing[0]*factor,round_value),
876                 'incoming': rounding(incoming[0]*factor,round_value),
877                 'outgoing_before' : rounding(outgoing_before[0]*factor,round_value),
878                 'incoming_before': rounding((incoming_before[0]+ (not current and already_in[0]))*factor,round_value),
879                 'outgoing_left': rounding(val.planned_outgoing - (outgoing[0] + (current and already_out[0]))*factor,round_value),
880                 'incoming_left': rounding(val.to_procure - (incoming[0] + (current and already_in[0]))*factor,round_value),
881                 'stock_start': rounding(stock_start[0]*factor,round_value),
882                 'stock_simulation': rounding(val.to_procure - val.planned_outgoing + (stock_start[0]+ incoming_before[0] - outgoing_before[0] \
883                                      + (not current and already_in[0]))*factor,round_value),
884             })
885         return True
886
887 # method below converts quantities and uoms to general OpenERP standard with UoM Qty, UoM, UoS Qty, UoS.
888 # from stock_planning standard where you have one Qty and one UoM (any from UoM or UoS category)
889 # so if UoM is from UoM category it is used as UoM in standard and if product has UoS the UoS will be calcualated.
890 # If UoM is from UoS category it is recalculated to basic UoS from product (in planning you can use any UoS from UoS category)
891 # and basic UoM is calculated.
892     def _qty_to_standard(self, cr, uid, val, context):
893         uos = False
894         uos_qty = 0.0
895         if val.product_uom.category_id.id == val.product_id.uom_id.category_id.id:
896             uom_qty = val.incoming_left
897             uom = val.product_uom.id
898             if val.product_id.uos_id:
899                 uos = val.product_id.uos_id.id
900                 coeff_uom2def = self._to_default_uom_factor(cr, uid, val, val.product_uom.id, {})
901                 coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val, uos, {})
902                 uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
903         elif val.product_uom.category_id.id == val.product_id.uos_id.category_id.id:
904             coeff_uom2def = self._to_default_uom_factor(cr, uid, val, val.product_uom.id, {})
905             uos = val.product_id.uos_id.id
906             coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val, uos, {})
907             uos_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
908             uom = val.product_id.uom_id.id
909             coeff_def2uom, round_value = self._from_default_uom_factor(cr, uid, val, uom, {})
910             uom_qty = rounding(val.incoming_left * coeff_uom2def * coeff_def2uom, round_value)
911         return uom_qty, uom, uos_qty, uos
912
913     def procure_incomming_left(self, cr, uid, ids, context, *args):
914         for obj in self.browse(cr, uid, ids):
915             if obj.incoming_left <= 0:
916                 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
917             uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
918             user = self.pool.get('res.users').browse(cr, uid, uid, context)
919             proc_id = self.pool.get('procurement.order').create(cr, uid, {
920                         'company_id' : obj.company_id.id,
921                         'name': _('Manual planning for ') + obj.period_id.name,
922                         'origin': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
923                         'date_planned': obj.period_id.date_start,
924                         'product_id': obj.product_id.id,
925                         'product_qty': uom_qty,
926                         'product_uom': uom,
927                         'product_uos_qty': uos_qty,
928                         'product_uos': uos,
929                         'location_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or obj.warehouse_id.lot_input_id.id,
930                         'procure_method': 'make_to_order',
931                         'note' : _("Procurement created in MPS by user: ") + str(user.login) + _("  Creation Date: ") + \
932                                             time.strftime('%Y-%m-%d %H:%M:%S') + \
933                                         _("\nFor period: ") + obj.period_id.name + _(" according to state:") + \
934                                         _("\n Warehouse Forecast: ") + str(obj.warehouse_forecast) + \
935                                         _("\n Initial Stock: ") + str(obj.stock_start) + \
936                                         _("\n Planned Out: ") + str(obj.planned_outgoing) + _("    Planned In: ") + str(obj.to_procure) + \
937                                         _("\n Already Out: ") + str(obj.already_out) + _("    Already In: ") +  str(obj.already_in) + \
938                                         _("\n Confirmed Out: ") + str(obj.outgoing) + _("    Confirmed In: ") + str(obj.incoming) + \
939                                         _("\n Planned Out Before: ") + str(obj.outgoing_before) + _("    Confirmed In Before: ") + \
940                                                                                             str(obj.incoming_before) + \
941                                         _("\n Expected Out: ") + str(obj.outgoing_left) + _("    Incoming Left: ") + str(obj.incoming_left) + \
942                                         _("\n Stock Simulation: ") +  str(obj.stock_simulation) + _("    Minimum stock: ") + str(obj.minimum_op),
943
944                             }, context=context)
945             wf_service = netsvc.LocalService("workflow")
946             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
947             self.calculate_planning(cr, uid, ids, context)
948             prev_text = obj.history or ""
949             self.write(cr, uid, ids, {
950                     'history' : prev_text + _('Requisition (') + str(user.login) + ", " + time.strftime('%Y.%m.%d %H:%M) ') + str(obj.incoming_left) + \
951                     " " + obj.product_uom.name + "\n",
952                 })
953         return True
954
955     def internal_supply(self, cr, uid, ids, context, *args):
956         for obj in self.browse(cr, uid, ids):
957             if obj.incoming_left <= 0:
958                 raise osv.except_osv(_('Error !'), _('Incoming Left must be greater than 0 !'))
959             if not obj.supply_warehouse_id:
960                 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse !'))
961             if obj.supply_warehouse_id.id == obj.warehouse_id.id:
962                 raise osv.except_osv(_('Error !'), _('You must specify a Source Warehouse different than calculated (destination) Warehouse !'))
963             uom_qty, uom, uos_qty, uos = self._qty_to_standard(cr, uid, obj, context)
964             user = self.pool.get('res.users').browse(cr, uid, uid, context)
965             picking_id = self.pool.get('stock.picking').create(cr, uid, {
966                             'origin': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
967                             'type': 'internal',
968                             'state': 'auto',
969                             'date' : obj.period_id.date_start,
970                             'move_type': 'direct',
971                             'invoice_state':  'none',
972                             'company_id': obj.company_id.id,
973                             'note': _("Pick created from MPS by user: ") + str(user.login) + _("  Creation Date: ") + \
974                                             time.strftime('%Y-%m-%d %H:%M:%S') + \
975                                         _("\nFor period: ") + obj.period_id.name + _(" according to state:") + \
976                                         _("\n Warehouse Forecast: ") + str(obj.warehouse_forecast) + \
977                                         _("\n Initial Stock: ") + str(obj.stock_start) + \
978                                         _("\n Planned Out: ") + str(obj.planned_outgoing) + _("    Planned In: ") + str(obj.to_procure) + \
979                                         _("\n Already Out: ") + str(obj.already_out) + _("    Already In: ") +  str(obj.already_in) + \
980                                         _("\n Confirmed Out: ") + str(obj.outgoing) + _("    Confirmed In: ") + str(obj.incoming) + \
981                                         _("\n Planned Out Before: ") + str(obj.outgoing_before) + _("    Confirmed In Before: ") + \
982                                                                                             str(obj.incoming_before) + \
983                                         _("\n Expected Out: ") + str(obj.outgoing_left) + _("    Incoming Left: ") + str(obj.incoming_left) + \
984                                         _("\n Stock Simulation: ") +  str(obj.stock_simulation) + _("    Minimum stock: ") + str(obj.minimum_op),
985                         })
986
987             move_id = self.pool.get('stock.move').create(cr, uid, {
988                         'name': _('MPS(') + str(user.login) +') '+ obj.period_id.name,
989                         'picking_id': picking_id,
990                         'product_id': obj.product_id.id,
991                         'date_planned': obj.period_id.date_start,
992                         'product_qty': uom_qty,
993                         'product_uom': uom,
994                         'product_uos_qty': uos_qty,
995                         'product_uos': uos,
996                         'location_id': obj.stock_supply_location and obj.supply_warehouse_id.lot_stock_id.id or \
997                                                                 obj.supply_warehouse_id.lot_output_id.id,
998                         'location_dest_id': obj.procure_to_stock and obj.warehouse_id.lot_stock_id.id or \
999                                                                 obj.warehouse_id.lot_input_id.id,
1000                         'tracking_id': False,
1001                         'company_id': obj.company_id.id,
1002                     })
1003             wf_service = netsvc.LocalService("workflow")
1004             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
1005
1006         self.calculate_planning(cr, uid, ids, context)
1007         prev_text = obj.history or ""
1008         pick_name = self.pool.get('stock.picking').browse(cr, uid, picking_id).name
1009         self.write(cr, uid, ids, {
1010               'history' : prev_text + _('Pick List ')+ pick_name + " (" + str(user.login) + ", " + time.strftime('%Y.%m.%d %H:%M) ') \
1011                 + str(obj.incoming_left) +" " + obj.product_uom.name + "\n",
1012                 })
1013
1014         return True
1015
1016     def product_id_change(self, cr, uid, ids, product_id):
1017         ret = {}
1018         if product_id:
1019             product_rec =  self.pool.get('product.product').browse(cr, uid, product_id)
1020             ret['product_uom'] = product_rec.uom_id.id
1021             ret['active_uom'] = product_rec.uom_id.id
1022             ret['product_uom_categ'] = product_rec.uom_id.category_id.id
1023             ret['product_uos_categ'] = product_rec.uos_id and product_rec.uos_id.category_id.id or False
1024         else:
1025             ret['product_uom'] = False
1026             ret['product_uom_categ'] = False
1027             ret['product_uos_categ'] = False
1028         res = {'value': ret}
1029         return res
1030
1031 stock_planning()
1032
1033 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: