[ADD]: Added tooltips for production order and procurement order.
[odoo/odoo.git] / addons / mrp / mrp.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 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 from osv import fields
23 from osv import osv
24 import ir
25
26 import netsvc
27 import time
28 from mx import DateTime
29 from tools.translate import _
30
31 #----------------------------------------------------------
32 # Work Centers
33 #----------------------------------------------------------
34 # capacity_hour : capacity per hour. default: 1.0.
35 #          Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
36 # unit_per_cycle : how many units are produced for one cycle
37 #
38 # TODO: Work Center may be recursive ?
39 #
40 class mrp_workcenter(osv.osv):
41     _name = 'mrp.workcenter'
42     _description = 'Work Center'
43     _columns = {
44         'name': fields.char('Work Center Name', size=64, required=True),
45         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the work center without removing it."),
46         'type': fields.selection([('machine','Machine'),('hr','Human Resource'),('tool','Tool')], 'Type', required=True),
47         'code': fields.char('Code', size=16),
48         'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="The normal working time of the workcenter."),
49         'note': fields.text('Description', help="Description of the workcenter. Explain here what's a cycle according to this workcenter."),
50
51         'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this workcenter can do in parallel. If this workcenter represents a team of 5 workers, the capacity per cycle is 5."),
52
53         'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
54         'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
55         'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
56         'time_efficiency': fields.float('Efficiency factor', help="Factor to adjust the work center cycle and before and after production durations"),
57
58         'costs_hour': fields.float('Cost per hour'),
59         'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
60             help="Complete this only if you want automatic analytic accounting entries on production orders."),
61         'costs_cycle': fields.float('Cost per cycle'),
62         'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
63             help="Complete this only if you want automatic analytic accounting entries on production orders."),
64         'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
65         'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
66         'company_id': fields.many2one('res.company','Company',required=True),  
67     }
68     _defaults = {
69         'active': lambda *a: 1,
70         'type': lambda *a: 'machine',
71         'time_efficiency': lambda *a: 1.0,
72         'capacity_per_cycle': lambda *a: 1.0,
73         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.workcenter', c)
74     }
75 mrp_workcenter()
76
77
78 class mrp_property_group(osv.osv):
79     _name = 'mrp.property.group'
80     _description = 'Property Group'
81     _columns = {
82         'name': fields.char('Property Group', size=64, required=True),
83         'description': fields.text('Description'),
84     }
85 mrp_property_group()
86
87 class mrp_property(osv.osv):
88     _name = 'mrp.property'
89     _description = 'Property'
90     _columns = {
91         'name': fields.char('Name', size=64, required=True),
92         'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
93         'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
94         'description': fields.text('Description'),
95     }
96     _defaults = {
97         'composition': lambda *a: 'min',
98     }
99 mrp_property()
100
101 class mrp_routing(osv.osv):
102     _name = 'mrp.routing'
103     _description = 'Routing'
104     _columns = {
105         'name': fields.char('Name', size=64, required=True),
106         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the routing without removing it."),
107         'code': fields.char('Code', size=8),
108
109         'note': fields.text('Description'),
110         'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
111
112         'location_id': fields.many2one('stock.location', 'Production Location',
113             help="Keep empty if you produce at the location where the finished products are needed." \
114                 "Set a location if you produce at a fixed location. This can be a partner location " \
115                 "if you subcontract the manufacturing operations."
116         ),
117     }
118     _defaults = {
119         'active': lambda *a: 1,
120     }
121 mrp_routing()
122
123 class mrp_routing_workcenter(osv.osv):
124     _name = 'mrp.routing.workcenter'
125     _description = 'Routing workcenter usage'
126     _columns = {
127         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
128         'name': fields.char('Name', size=64, required=True),
129         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing workcenters."),
130         'cycle_nbr': fields.float('Number of Cycles', required=True,
131             help="Time in hours for doing one cycle."),
132         'hour_nbr': fields.float('Number of Hours', required=True, help="Cost per hour"),
133         'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade', 
134              help="Routing indicates all the workcenters used, for how long and/or cycles." \
135                 "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
136         'note': fields.text('Description')
137     }
138     _defaults = {
139         'cycle_nbr': lambda *a: 1.0,
140         'hour_nbr': lambda *a: 0.0,
141     }
142 mrp_routing_workcenter()
143
144 class mrp_bom(osv.osv):
145     _name = 'mrp.bom'
146     _description = 'Bills of Material'
147     def _child_compute(self, cr, uid, ids, name, arg, context={}):
148         result = {}
149         for bom in self.browse(cr, uid, ids, context=context):
150             result[bom.id] = map(lambda x: x.id, bom.bom_lines)
151             if bom.bom_lines:
152                 continue
153             ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
154             if bom.type=='phantom' or ok:
155                 sids = self.pool.get('mrp.bom').search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
156                 if sids:
157                     bom2 = self.pool.get('mrp.bom').browse(cr, uid, sids[0], context=context)
158                     result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
159         return result
160     def _compute_type(self, cr, uid, ids, field_name, arg, context):
161         res = dict(map(lambda x: (x,''), ids))
162         for line in self.browse(cr, uid, ids):
163             if line.type=='phantom' and not line.bom_id:
164                 res[line.id] = 'set'
165                 continue
166             if line.bom_lines or line.type=='phantom':
167                 continue
168             if line.product_id.supply_method=='produce':
169                 if line.product_id.procure_method=='make_to_stock':
170                     res[line.id] = 'stock'
171                 else:
172                     res[line.id] = 'order'
173         return res
174     _columns = {
175         'name': fields.char('Name', size=64, required=True),
176         'code': fields.char('Code', size=16),
177         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the bills of material without removing it."),
178         'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True, 
179                                  help= "If a sub-product is used in several products, it can be useful to create its own BoM."\
180                                  "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type."\
181                                  "If a Phantom BoM is used for a root product, it will be sold and shipped as a set of components, instead of being produced."),
182         'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
183         'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
184         'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
185         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
186         'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
187         'product_id': fields.many2one('product.product', 'Product', required=True),
188         'product_uos_qty': fields.float('Product UOS Qty'),
189         'product_uos': fields.many2one('product.uom', 'Product UOS', help="Product UOS (Unit of Sale) is the unit of measurement for the invoicing and promotion of stock."),
190         'product_qty': fields.float('Product Qty', required=True),
191         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
192         'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
193         'product_efficiency': fields.float('Product Efficiency', required=True, help="Material efficiency. A factor of 0.9 means a loss of 10% in the production."),
194         'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
195         'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
196         'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of workcenters) to produce the finished product. The routing is mainly used to compute workcenter costs during operations and to plan future loads on workcenters based on production planning."),
197         'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
198         'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
199         'revision_type': fields.selection([('numeric','numeric indices'),('alpha','alphabetical indices')], 'Index type'),
200         'child_ids': fields.function(_child_compute,relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
201         'child_complete_ids': fields.function(_child_compute,relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
202         'company_id': fields.many2one('res.company','Company',required=True),
203     }
204     _defaults = {
205         'active': lambda *a: 1,
206         'product_efficiency': lambda *a: 1.0,
207         'product_qty': lambda *a: 1.0,
208         'product_rounding': lambda *a: 1.0,
209         'type': lambda *a: 'normal',
210         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', c)
211     }
212     _order = "sequence"
213     _sql_constraints = [
214         ('bom_qty_zero', 'CHECK (product_qty>0)',  'All product quantities must be greater than 0.\n' \
215             'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
216     ]
217
218     def _check_recursion(self, cr, uid, ids):
219         level = 500
220         while len(ids):
221             cr.execute('select distinct bom_id from mrp_bom where id in ('+','.join(map(str, ids))+')')
222             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
223             if not level:
224                 return False
225             level -= 1
226         return True
227     _constraints = [
228         (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
229     ]
230
231
232     def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
233         if product_id:
234             prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0]
235             v = {'product_uom':prod.uom_id.id}
236             if not name:
237                 v['name'] = prod.name
238             return {'value': v}
239         return {}
240
241     def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
242         bom_result = False
243         # Why searching on BoM without parent ?
244         cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
245         ids = map(lambda x: x[0], cr.fetchall())
246         max_prop = 0
247         result = False
248         for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
249             prop = 0
250             for prop_id in bom.property_ids:
251                 if prop_id.id in properties:
252                     prop+=1
253             if (prop>max_prop) or ((max_prop==0) and not result):
254                 result = bom.id
255                 max_prop = prop
256         return result
257
258     def _bom_explode(self, cr, uid, bom, factor, properties, addthis=False, level=0):
259         factor = factor / (bom.product_efficiency or 1.0)
260         factor = rounding(factor, bom.product_rounding)
261         if factor<bom.product_rounding:
262             factor = bom.product_rounding
263         result = []
264         result2 = []
265         phantom=False
266         if bom.type=='phantom' and not bom.bom_lines:
267             newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
268             if newbom:
269                 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
270                 result = result + res[0]
271                 result2 = result2 + res[1]
272                 phantom=True
273             else:
274                 phantom=False
275         if not phantom:
276             if addthis and not bom.bom_lines:
277                 result.append(
278                 {
279                     'name': bom.product_id.name,
280                     'product_id': bom.product_id.id,
281                     'product_qty': bom.product_qty * factor,
282                     'product_uom': bom.product_uom.id,
283                     'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
284                     'product_uos': bom.product_uos and bom.product_uos.id or False,
285                 })
286             if bom.routing_id:
287                 for wc_use in bom.routing_id.workcenter_lines:
288                     wc = wc_use.workcenter_id
289                     d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
290                     mult = (d + (m and 1.0 or 0.0))
291                     cycle = mult * wc_use.cycle_nbr
292                     result2.append({
293                         'name': bom.routing_id.name,
294                         'workcenter_id': wc.id,
295                         'sequence': level+(wc_use.sequence or 0),
296                         'cycle': cycle,
297                         'hour': float(wc_use.hour_nbr*mult + (wc.time_start+wc.time_stop+cycle*wc.time_cycle) * (wc.time_efficiency or 1.0)),
298                     })
299             for bom2 in bom.bom_lines:
300                 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
301                 result = result + res[0]
302                 result2 = result2 + res[1]
303         return result, result2
304
305     def set_indices(self, cr, uid, ids, context = {}):
306         if not ids or (ids and not ids[0]):
307             return True
308         res = self.read(cr, uid, ids, ['revision_ids', 'revision_type'])
309         rev_ids = res[0]['revision_ids']
310         idx = 1
311         new_idx = []
312         for rev_id in rev_ids:
313             if res[0]['revision_type'] == 'numeric':
314                 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : idx})
315             else:
316                 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : "%c"%(idx+96,)})
317             idx+=1
318         return True
319
320 mrp_bom()
321
322 class mrp_bom_revision(osv.osv):
323     _name = 'mrp.bom.revision'
324     _description = 'Bill of material revisions'
325     _columns = {
326         'name': fields.char('Modification name', size=64, required=True),
327         'description': fields.text('Description'),
328         'date': fields.date('Modification Date'),
329         'indice': fields.char('Revision', size=16),
330         'last_indice': fields.char('last indice', size=64),
331         'author_id': fields.many2one('res.users', 'Author'),
332         'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
333     }
334
335     _defaults = {
336         'author_id': lambda x,y,z,c: z,
337         'date': lambda *a: time.strftime('%Y-%m-%d'),
338     }
339
340 mrp_bom_revision()
341
342 def rounding(f, r):
343     if not r:
344         return f
345     return round(f / r) * r
346
347 class mrp_production(osv.osv):
348     _name = 'mrp.production'
349     _description = 'Production'
350     _date_name  = 'date_planned'
351
352     def _get_sale_order(self,cr,uid,ids,field_name=False):
353         move_obj=self.pool.get('stock.move')
354         def get_parent_move(move_id):
355             move = move_obj.browse(cr,uid,move_id)
356             if move.move_dest_id:
357                 return get_parent_move(move.move_dest_id.id)
358             return move_id
359         productions=self.read(cr,uid,ids,['id','move_prod_id'])
360         res={}
361         for production in productions:
362             res[production['id']]=False
363             if production.get('move_prod_id',False):
364                 parent_move_line=get_parent_move(production['move_prod_id'][0])
365                 if parent_move_line:
366                     move = move_obj.browse(cr,uid,parent_move_line)
367                     #TODO: fix me sale module can not be used here,
368                     #as may be mrp can be installed without sale module
369                     if field_name=='name':
370                         res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.name or False
371                     if field_name=='client_order_ref':
372                         res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.client_order_ref or False
373         return res
374
375     def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
376         result = {}
377         for prod in self.browse(cr, uid, ids, context=context):
378             result[prod.id] = {
379                 'hour_total': 0.0,
380                 'cycle_total': 0.0,
381             }
382             for wc in prod.workcenter_lines:
383                 result[prod.id]['hour_total'] += wc.hour
384                 result[prod.id]['cycle_total'] += wc.cycle
385         return result
386
387     def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
388         result = {}
389         for prod in self.browse(cr, uid, ids, context=context):
390             result[prod.id] = prod.date_planned
391         return result
392
393     def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
394         result = {}
395         for prod in self.browse(cr, uid, ids, context=context):
396             result[prod.id] = prod.date_planned[:10]
397         return result
398
399     def _sale_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
400         return self._get_sale_order(cr,uid,ids,field_name='name')
401
402     def _sale_ref_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
403         return self._get_sale_order(cr,uid,ids,field_name='client_order_ref')
404
405     _columns = {
406         'name': fields.char('Reference', size=64, required=True),
407         'origin': fields.char('Origin', size=64, help="Reference of the document that generated this production order request."),
408         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
409
410         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
411         'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
412         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
413         'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
414         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
415
416         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
417             help="Location where the system will look for components."),
418         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
419             help="Location where the system will stock the finished products."),
420
421         'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End'),
422         'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
423         'date_planned': fields.datetime('Scheduled date', required=True, select=1),
424         'date_start': fields.datetime('Start Date'),
425         'date_finnished': fields.datetime('End Date'),
426
427         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
428         'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', help="The list of operations (list of workcenters) to produce the finished product. The routing is mainly used to compute workcenter costs during operations and to plan futur loads on workcenters based on production plannification."),
429
430         'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
431             help="This is the internal picking list that brings the finished product to the production plan"),
432         'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
433         'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products Consummed'),
434
435         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created'),
436         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
437         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
438         'state': fields.selection([('draft','Draft'),('picking_except', 'Picking Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','In Production'),('cancel','Cancelled'),('done','Done')],'State', readonly=True,
439                                     help='When the production order is created the state is set to \'Draft\'.\n If the order is confirmed the state is set to \'Waiting Goods\'.\n If any exceptions are there, the state is set to \'Packing Exception\'.\
440                                     \nIf the stock is available then the state is set to \'Ready to Produce\'.\n When the production get started then the state is set to \'In Production\'.\n When the production is over, the state is set to \'Done\'.'),
441         'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder'),
442         'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder'),
443
444         'sale_name': fields.function(_sale_name_calc, method=True, type='char', string='Sale Name', help='Gives the name of sale order.'),
445         'sale_ref': fields.function(_sale_ref_calc, method=True, type='char', string='Sale Reference', help='Gives the name of Customer Reference from sale order.'),
446         'company_id': fields.many2one('res.company','Company',required=True),
447     }
448     _defaults = {
449         'priority': lambda *a: '1',
450         'state': lambda *a: 'draft',
451         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
452         'product_qty':  lambda *a: 1.0,
453         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.production') or '/',
454         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', c),
455     }
456     _order = 'date_planned asc, priority desc';
457     def unlink(self, cr, uid, ids, context=None):
458         productions = self.read(cr, uid, ids, ['state'])
459         unlink_ids = []
460         for s in productions:
461             if s['state'] in ['draft','cancel']:
462                 unlink_ids.append(s['id'])
463             else:
464                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
465         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
466
467     def copy(self, cr, uid, id, default=None,context=None):
468         if not default:
469             default = {}
470         default.update({
471             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
472             'move_lines' : [],
473             'move_created_ids': [],
474             'state': 'draft'
475         })
476         return super(mrp_production, self).copy(cr, uid, id, default, context)
477
478     def location_id_change(self, cr, uid, ids, src, dest, context={}):
479         if dest:
480             return {}
481         if src:
482             return {'value': {'location_dest_id': src}}
483         return {}
484
485     def product_id_change(self, cr, uid, ids, product):
486         if not product:
487             return {}
488         res = self.pool.get('product.product').read(cr, uid, [product], ['uom_id'])[0]
489         uom = res['uom_id'] and res['uom_id'][0]
490         result = {'product_uom':uom}
491         return {'value':result}
492
493     def bom_id_change(self, cr, uid, ids, product):
494         if not product:
495             return {}
496         res = self.pool.get('mrp.bom').read(cr, uid, [product], ['routing_id'])[0]
497         routing_id = res['routing_id'] and res['routing_id'][0]
498         result = {'routing_id':routing_id}
499         return {'value':result}
500
501     def action_picking_except(self, cr, uid, ids):
502         self.write(cr, uid, ids, {'state':'picking_except'})
503         return True
504
505     def action_compute(self, cr, uid, ids, properties=[]):
506         results = []
507         for production in self.browse(cr, uid, ids):
508             cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
509             cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
510             bom_point = production.bom_id
511             bom_id = production.bom_id.id
512             if not bom_point:
513                 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
514                 if bom_id:
515                     bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id)
516                     routing_id = bom_point.routing_id.id or False
517                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
518
519             if not bom_id:
520                 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
521
522             #if bom_point.routing_id and bom_point.routing_id.location_id:
523             #   self.write(cr, uid, [production.id], {'location_src_id': bom_point.routing_id.location_id.id})
524
525             factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
526             res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
527             results = res[0]
528             results2 = res[1]
529             for line in results:
530                 line['production_id'] = production.id
531                 self.pool.get('mrp.production.product.line').create(cr, uid, line)
532             for line in results2:
533                 line['production_id'] = production.id
534                 self.pool.get('mrp.production.workcenter.line').create(cr, uid, line)
535         return len(results)
536
537     def action_cancel(self, cr, uid, ids):
538         for production in self.browse(cr, uid, ids):
539             if production.move_created_ids:
540                 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_created_ids])
541             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_lines])
542         self.write(cr, uid, ids, {'state':'cancel'}) #,'move_lines':[(6,0,[])]})
543         return True
544
545     #XXX: may be a bug here; lot_lines are unreserved for a few seconds;
546     #     between the end of the picking list and the call to this function
547     def action_ready(self, cr, uid, ids):
548         self.write(cr, uid, ids, {'state':'ready'})
549         for production in self.browse(cr, uid, ids):
550             if production.move_prod_id:
551                 self.pool.get('stock.move').write(cr, uid, [production.move_prod_id.id],
552                         {'location_id':production.location_dest_id.id})
553         return True
554
555     #TODO Review materials in function in_prod and prod_end.
556     def action_production_end(self, cr, uid, ids):
557         move_ids = []
558         for production in self.browse(cr, uid, ids):
559             for res in production.move_lines:
560                 for move in production.move_created_ids:
561                     #XXX must use the orm
562                     cr.execute('INSERT INTO stock_move_history_ids \
563                             (parent_id, child_id) VALUES (%s,%s)',
564                             (res.id, move.id))
565                 move_ids.append(res.id)
566             vals= {'state':'confirmed'}
567             new_moves = [x.id for x in production.move_created_ids]
568             self.pool.get('stock.move').write(cr, uid, new_moves, vals)
569             if not production.date_finnished:
570                 self.write(cr, uid, [production.id],
571                         {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
572             self.pool.get('stock.move').check_assign(cr, uid, new_moves)
573             self.pool.get('stock.move').action_done(cr, uid, new_moves)
574             self._costs_generate(cr, uid, production)
575         self.pool.get('stock.move').action_done(cr, uid, move_ids)
576         self.write(cr,  uid, ids, {'state': 'done'})
577         return True
578
579     def _costs_generate(self, cr, uid, production):
580         amount = 0.0
581         for wc_line in production.workcenter_lines:
582             wc = wc_line.workcenter_id
583             if wc.costs_journal_id and wc.costs_general_account_id:
584                 value = wc_line.hour * wc.costs_hour
585                 account = wc.costs_hour_account_id.id
586                 if value and account:
587                     amount += value
588                     self.pool.get('account.analytic.line').create(cr, uid, {
589                         'name': wc_line.name+' (H)',
590                         'amount': value,
591                         'account_id': account,
592                         'general_account_id': wc.costs_general_account_id.id,
593                         'journal_id': wc.costs_journal_id.id,
594                         'code': wc.code
595                     } )
596             if wc.costs_journal_id and wc.costs_general_account_id:
597                 value = wc_line.cycle * wc.costs_cycle
598                 account = wc.costs_cycle_account_id.id
599                 if value and account:
600                     amount += value
601                     self.pool.get('account.analytic.line').create(cr, uid, {
602                         'name': wc_line.name+' (C)',
603                         'amount': value,
604                         'account_id': account,
605                         'general_account_id': wc.costs_general_account_id.id,
606                         'journal_id': wc.costs_journal_id.id,
607                         'code': wc.code
608                     } )
609         return amount
610
611     def action_in_production(self, cr, uid, ids):
612         move_ids = []
613         for production in self.browse(cr, uid, ids):
614             for res in production.move_lines:
615                 move_ids.append(res.id)
616             if not production.date_start:
617                 self.write(cr, uid, [production.id],
618                         {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
619         self.pool.get('stock.move').action_done(cr, uid, move_ids)
620         self.write(cr, uid, ids, {'state': 'in_production'})
621         return True
622
623     def test_if_product(self, cr, uid, ids):
624         res = True
625         for production in self.browse(cr, uid, ids):
626             if not production.product_lines:
627                 if not self.action_compute(cr, uid, [production.id]):
628                     res = False
629         return res
630
631     def _get_auto_picking(self, cr, uid, production):
632         return True
633
634     def action_confirm(self, cr, uid, ids):
635         picking_id=False
636         proc_ids = []
637         for production in self.browse(cr, uid, ids):
638             if not production.product_lines:
639                 self.action_compute(cr, uid, [production.id])
640                 production = self.browse(cr, uid, [production.id])[0]
641             routing_loc = None
642             pick_type = 'internal'
643             address_id = False
644             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
645                 routing_loc = production.bom_id.routing_id.location_id
646                 if routing_loc.usage<>'internal':
647                     pick_type = 'out'
648                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
649                 routing_loc = routing_loc.id
650             picking_id = self.pool.get('stock.picking').create(cr, uid, {
651                 'origin': (production.origin or '').split(':')[0] +':'+production.name,
652                 'type': pick_type,
653                 'move_type': 'one',
654                 'state': 'auto',
655                 'address_id': address_id,
656                 'auto_picking': self._get_auto_picking(cr, uid, production),
657                 'company_id': production.company_id.id,
658             })
659
660             source = production.product_id.product_tmpl_id.property_stock_production.id
661             data = {
662                 'name':'PROD:'+production.name,
663                 'date_planned': production.date_planned,
664                 'product_id': production.product_id.id,
665                 'product_qty': production.product_qty,
666                 'product_uom': production.product_uom.id,
667                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
668                 'product_uos': production.product_uos and production.product_uos.id or False,
669                 'location_id': source,
670                 'location_dest_id': production.location_dest_id.id,
671                 'move_dest_id': production.move_prod_id.id,
672                 'state': 'waiting',
673                 'company_id': production.company_id.id,
674             }
675             res_final_id = self.pool.get('stock.move').create(cr, uid, data)
676
677             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
678             moves = []
679             for line in production.product_lines:
680                 move_id=False
681                 newdate = production.date_planned
682                 if line.product_id.type in ('product', 'consu'):
683                     res_dest_id = self.pool.get('stock.move').create(cr, uid, {
684                         'name':'PROD:'+production.name,
685                         'date_planned': production.date_planned,
686                         'product_id': line.product_id.id,
687                         'product_qty': line.product_qty,
688                         'product_uom': line.product_uom.id,
689                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
690                         'product_uos': line.product_uos and line.product_uos.id or False,
691                         'location_id': routing_loc or production.location_src_id.id,
692                         'location_dest_id': source,
693                         'move_dest_id': res_final_id,
694                         'state': 'waiting',
695                         'company_id': production.company_id.id,
696                     })
697                     moves.append(res_dest_id)
698                     move_id = self.pool.get('stock.move').create(cr, uid, {
699                         'name':'PROD:'+production.name,
700                         'picking_id':picking_id,
701                         'product_id': line.product_id.id,
702                         'product_qty': line.product_qty,
703                         'product_uom': line.product_uom.id,
704                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
705                         'product_uos': line.product_uos and line.product_uos.id or False,
706                         'date_planned': newdate,
707                         'move_dest_id': res_dest_id,
708                         'location_id': production.location_src_id.id,
709                         'location_dest_id': routing_loc or production.location_src_id.id,
710                         'state': 'waiting',
711                         'company_id': production.company_id.id,
712                     })
713                 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
714                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
715                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
716                     'date_planned': newdate,
717                     'product_id': line.product_id.id,
718                     'product_qty': line.product_qty,
719                     'product_uom': line.product_uom.id,
720                     'product_uos_qty': line.product_uos and line.product_qty or False,
721                     'product_uos': line.product_uos and line.product_uos.id or False,
722                     'location_id': production.location_src_id.id,
723                     'procure_method': line.product_id.procure_method,
724                     'move_id': move_id,
725                     'company_id': production.company_id.id,
726                 })
727                 wf_service = netsvc.LocalService("workflow")
728                 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
729                 proc_ids.append(proc_id)
730             wf_service = netsvc.LocalService("workflow")
731             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
732             self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
733         return picking_id
734
735     def force_production(self, cr, uid, ids, *args):
736         pick_obj = self.pool.get('stock.picking')
737         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
738         return True
739
740 mrp_production()
741
742
743 class stock_move(osv.osv):
744     _name = 'stock.move'
745     _inherit = 'stock.move'
746     _columns = {
747         'production_id': fields.many2one('mrp.production', 'Production', select=True),
748     }
749 stock_move()
750
751 class mrp_production_workcenter_line(osv.osv):
752     _name = 'mrp.production.workcenter.line'
753     _description = 'Work Orders'
754     _order = 'sequence'
755     _columns = {
756         'name': fields.char('Work Order', size=64, required=True),
757         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
758         'cycle': fields.float('Nbr of cycles', digits=(16,2)),
759         'hour': fields.float('Nbr of hours', digits=(16,2)),
760         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
761         'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade'),
762     }
763     _defaults = {
764         'sequence': lambda *a: 1,
765         'hour': lambda *a: 0,
766         'cycle': lambda *a: 0,
767     }
768 mrp_production_workcenter_line()
769
770 class mrp_production_product_line(osv.osv):
771     _name = 'mrp.production.product.line'
772     _description = 'Production scheduled products'
773     _columns = {
774         'name': fields.char('Name', size=64, required=True),
775         'product_id': fields.many2one('product.product', 'Product', required=True),
776         'product_qty': fields.float('Product Qty', required=True),
777         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
778         'product_uos_qty': fields.float('Product UOS Qty'),
779         'product_uos': fields.many2one('product.uom', 'Product UOS'),
780         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
781     }
782 mrp_production_product_line()
783
784 # ------------------------------------------------------------------
785 # Procurement
786 # ------------------------------------------------------------------
787 #
788 # Produce, Buy or Find products and place a move
789 #     then wizard for picking lists & move
790 #
791 class mrp_procurement(osv.osv):
792     _name = "mrp.procurement"
793     _description = "Procurement"
794     _order = 'priority,date_planned'
795     _columns = {
796         'name': fields.char('Name', size=64, required=True, help='Requisition name. It differs when done by scheduler else same as origin name.'),
797         'origin': fields.char('Origin', size=64,
798             help="Reference of the document that created this Requisition.\n"
799             "This is automatically completed by Open ERP."),
800         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
801         'date_planned': fields.datetime('Scheduled date', required=True),
802         'date_close': fields.datetime('Date Closed'),
803         'product_id': fields.many2one('product.product', 'Product', required=True),
804         'product_qty': fields.float('Quantity', required=True, states={'draft':[('readonly',False)]}, readonly=True),
805         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
806         'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
807         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
808         'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
809
810         'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
811
812         'close_move': fields.boolean('Close Move at end', required=True),
813         'location_id': fields.many2one('stock.location', 'Location', required=True),
814         'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Requisition Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
815             readonly=True, required=True, help="If you encode manually a Requisition, you probably want to use" \
816             " a make to order method."),
817
818         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
819         'note': fields.text('Note'),
820
821         'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
822
823         'message': fields.char('Latest error', size=64),
824         'state': fields.selection([
825             ('draft','Draft'),
826             ('confirmed','Confirmed'),
827             ('exception','Exception'),
828             ('running','Running'),
829             ('cancel','Cancel'),
830             ('ready','Ready'),
831             ('done','Done'),
832             ('waiting','Waiting')], 'State', required=True,
833             help='When a procurement is created the state is set to \'Draft\'.\n If the procurement is confirmed, the state is set to \'Confirmed\'.\
834             \nAfter confirming the state is set to \'Running\'.\n If any exception arises in the order then the state is set to \'Exception\'.\n Once the exception is removed the state becomes \'Ready\'.\n It is in \'Waiting\'. state when the procurement is waiting for another one to finish.'),
835         'note' : fields.text('Note'),
836         'company_id': fields.many2one('res.company','Company',required=True),  
837     }
838     _defaults = {
839         'state': lambda *a: 'draft',
840         'priority': lambda *a: '1',
841         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
842         'close_move': lambda *a: 0,
843         'procure_method': lambda *a: 'make_to_order',
844         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.procurement', c)
845     }
846
847     def unlink(self, cr, uid, ids, context=None):
848         procurements = self.read(cr, uid, ids, ['state'])
849         unlink_ids = []
850         for s in procurements:
851             if s['state'] in ['draft','cancel']:
852                 unlink_ids.append(s['id'])
853             else:
854                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Requisition Order(s) which are in %s State!' % s['state']))
855         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
856
857     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
858         if product_id:
859             w=self.pool.get('product.product').browse(cr,uid,product_id, context)
860             v = {
861                 'product_uom':w.uom_id.id,
862                 'product_uos':w.uos_id and w.uos_id.id or w.uom_id.id
863             }
864             return {'value': v}
865         return {}
866
867     def check_product(self, cr, uid, ids):
868         for procurement in self.browse(cr, uid, ids):
869             if procurement.product_id.type in ('product', 'consu'):
870                 return True
871         return False
872
873     def check_move_cancel(self, cr, uid, ids, context={}):
874         res = True
875         ok = False
876         for procurement in self.browse(cr, uid, ids, context):
877             if procurement.move_id:
878                 ok = True
879                 if not procurement.move_id.state=='cancel':
880                     res = False
881         return res and ok
882
883     def check_move_done(self, cr, uid, ids, context={}):
884         res = True
885         for proc in self.browse(cr, uid, ids, context):
886             if proc.move_id:
887                 if not proc.move_id.state=='done':
888                     res = False
889         return res
890
891     #
892     # This method may be overrided by objects that override mrp.procurment
893     # for computing their own purpose
894     #
895     def _quantity_compute_get(self, cr, uid, proc, context={}):
896         if proc.product_id.type=='product':
897             if proc.move_id.product_uos:
898                 return proc.move_id.product_uos_qty
899         return False
900
901     def _uom_compute_get(self, cr, uid, proc, context={}):
902         if proc.product_id.type=='product':
903             if proc.move_id.product_uos:
904                 return proc.move_id.product_uos.id
905         return False
906
907     #
908     # Return the quantity of product shipped/produced/served, wich may be
909     # different from the planned quantity
910     #
911     def quantity_get(self, cr, uid, id, context={}):
912         proc = self.browse(cr, uid, id, context)
913         result = self._quantity_compute_get(cr, uid, proc, context)
914         if not result:
915             result = proc.product_qty
916         return result
917
918     def uom_get(self, cr, uid, id, context=None):
919         proc = self.browse(cr, uid, id, context)
920         result = self._uom_compute_get(cr, uid, proc, context)
921         if not result:
922             result = proc.product_uom.id
923         return result
924
925     def check_waiting(self, cr, uid, ids, context=[]):
926         for procurement in self.browse(cr, uid, ids, context=context):
927             if procurement.move_id and procurement.move_id.state=='auto':
928                 return True
929         return False
930
931     def check_produce_service(self, cr, uid, procurement, context=[]):
932         return True
933
934     def check_produce_product(self, cr, uid, procurement, context=[]):
935         properties = [x.id for x in procurement.property_ids]
936         bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
937         if not bom_id:
938             cr.execute('update mrp_procurement set message=%s where id=%s', (_('No BoM defined for this product !'), procurement.id))
939             return False
940         return True
941
942     def check_make_to_stock(self, cr, uid, ids, context={}):
943         ok = True
944         for procurement in self.browse(cr, uid, ids, context=context):
945             if procurement.product_id.type=='service':
946                 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
947             else:
948                 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
949         return ok
950
951     def check_produce(self, cr, uid, ids, context={}):
952         res = True
953         user = self.pool.get('res.users').browse(cr, uid, uid)
954         for procurement in self.browse(cr, uid, ids):
955             if procurement.product_id.product_tmpl_id.supply_method<>'produce':
956                 if procurement.product_id.seller_ids:
957                     partner = procurement.product_id.seller_ids[0].name
958                     if user.company_id and user.company_id.partner_id:
959                         if partner.id == user.company_id.partner_id.id:
960                             return True
961                 return False
962             if procurement.product_id.product_tmpl_id.type=='service':
963                 res = res and self.check_produce_service(cr, uid, procurement, context)
964             else:
965                 res = res and self.check_produce_product(cr, uid, procurement, context)
966             if not res:
967                 return False
968         return res
969
970     def check_buy(self, cr, uid, ids):
971         user = self.pool.get('res.users').browse(cr, uid, uid)
972         for procurement in self.browse(cr, uid, ids):
973             if procurement.product_id.product_tmpl_id.supply_method<>'buy':
974                 return False
975             if not procurement.product_id.seller_ids:
976                 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No supplier defined for this product !'), procurement.id))
977                 return False
978             partner = procurement.product_id.seller_ids[0].name
979             if user.company_id and user.company_id.partner_id:
980                 if partner.id == user.company_id.partner_id.id:
981                     return False
982             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
983             if not address_id:
984                 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No address defined for the supplier'), procurement.id))
985                 return False
986         return True
987
988     def test_cancel(self, cr, uid, ids):
989         for record in self.browse(cr, uid, ids):
990             if record.move_id and record.move_id.state=='cancel':
991                 return True
992         return False
993
994     def action_confirm(self, cr, uid, ids, context={}):
995         for procurement in self.browse(cr, uid, ids):
996             if procurement.product_qty <= 0.00:
997                 raise osv.except_osv(_('Data Insufficient !'), _('Please check the Quantity of Requisition Order(s), it should not be less than 1!'))
998             if procurement.product_id.type in ('product', 'consu'):
999                 if not procurement.move_id:
1000                     source = procurement.location_id.id
1001                     if procurement.procure_method=='make_to_order':
1002                         source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
1003                     id = self.pool.get('stock.move').create(cr, uid, {
1004                         'name': 'PROC:'+procurement.name,
1005                         'location_id': source,
1006                         'location_dest_id': procurement.location_id.id,
1007                         'product_id': procurement.product_id.id,
1008                         'product_qty':procurement.product_qty,
1009                         'product_uom': procurement.product_uom.id,
1010                         'date_planned': procurement.date_planned,
1011                         'state':'confirmed',
1012                         'company_id': procurement.company_id.id,
1013                     })
1014                     self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
1015                 else:
1016                     # TODO: check this
1017                     if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('waiting',):
1018                         id = self.pool.get('stock.move').write(cr, uid, [procurement.move_id.id], {'state':'confirmed'})
1019         self.write(cr, uid, ids, {'state':'confirmed','message':''})
1020         return True
1021
1022     def action_move_assigned(self, cr, uid, ids):
1023         self.write(cr, uid, ids, {'state':'running','message':_('from stock: products assigned.')})
1024         return True
1025
1026     def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
1027         return True
1028
1029     def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
1030         ok = True
1031         if procurement.move_id:
1032             id = procurement.move_id.id
1033             if not (procurement.move_id.state in ('done','assigned','cancel')):
1034                 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
1035                 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%s', (procurement.product_id.id,))
1036                 if not cr.fetchone()[0]:
1037                     cr.execute('update mrp_procurement set message=%s where id=%s', (_('from stock and no minimum orderpoint rule defined'), procurement.id))
1038         return ok
1039
1040     def action_produce_assign_service(self, cr, uid, ids, context={}):
1041         for procurement in self.browse(cr, uid, ids):
1042             self.write(cr, uid, [procurement.id], {'state':'running'})
1043         return True
1044
1045     def action_produce_assign_product(self, cr, uid, ids, context={}):
1046         produce_id = False
1047         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1048         for procurement in self.browse(cr, uid, ids):
1049             res_id = procurement.move_id.id
1050             loc_id = procurement.location_id.id
1051             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - DateTime.RelativeDateTime(days=procurement.product_id.product_tmpl_id.produce_delay or 0.0)
1052             newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
1053             produce_id = self.pool.get('mrp.production').create(cr, uid, {
1054                 'origin': procurement.origin,
1055                 'product_id': procurement.product_id.id,
1056                 'product_qty': procurement.product_qty,
1057                 'product_uom': procurement.product_uom.id,
1058                 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
1059                 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
1060                 'location_src_id': procurement.location_id.id,
1061                 'location_dest_id': procurement.location_id.id,
1062                 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
1063                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1064                 'move_prod_id': res_id,
1065                 'company_id': procurement.company_id.id,
1066             })
1067             self.write(cr, uid, [procurement.id], {'state':'running'})
1068             bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
1069                     [produce_id], properties=[x.id for x in procurement.property_ids])
1070             wf_service = netsvc.LocalService("workflow")
1071             wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
1072             self.pool.get('stock.move').write(cr, uid, [res_id],
1073                     {'location_id':procurement.location_id.id})
1074         return produce_id
1075
1076     def action_po_assign(self, cr, uid, ids, context={}):
1077         purchase_id = False
1078         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1079         for procurement in self.browse(cr, uid, ids):
1080             res_id = procurement.move_id.id
1081             partner = procurement.product_id.seller_ids[0].name
1082             partner_id = partner.id
1083             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1084             pricelist_id = partner.property_product_pricelist_purchase.id
1085
1086             uom_id = procurement.product_id.uom_po_id.id
1087
1088             qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1089             if procurement.product_id.seller_ids[0].qty:
1090                 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1091
1092             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
1093
1094             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
1095             newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1096             newdate = newdate - procurement.product_id.seller_ids[0].delay
1097
1098             context.update({'lang':partner.lang})
1099             product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1100
1101             line = {
1102                 'name': product.name,
1103                 'product_qty': qty,
1104                 'product_id': procurement.product_id.id,
1105                 'product_uom': uom_id,
1106                 'price_unit': price,
1107                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1108                 'move_dest_id': res_id,
1109                 'notes':product.description_purchase,
1110             }
1111
1112             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1113             taxes = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, taxes_ids)
1114             line.update({
1115                 'taxes_id':[(6,0,taxes)]
1116             })
1117             purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1118                 'origin': procurement.origin,
1119                 'partner_id': partner_id,
1120                 'partner_address_id': address_id,
1121                 'location_id': procurement.location_id.id,
1122                 'pricelist_id': pricelist_id,
1123                 'order_line': [(0,0,line)],
1124                 'company_id': procurement.company_id.id,
1125                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1126             })
1127             self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1128         return purchase_id
1129
1130     def action_cancel(self, cr, uid, ids):
1131         todo = []
1132         todo2 = []
1133         for proc in self.browse(cr, uid, ids):
1134             if proc.close_move:
1135                 if proc.move_id.state not in ('done','cancel'):
1136                     todo2.append(proc.move_id.id)
1137             else:
1138                 if proc.move_id and proc.move_id.state=='waiting':
1139                     todo.append(proc.move_id.id)
1140         if len(todo2):
1141             self.pool.get('stock.move').action_cancel(cr, uid, todo2)
1142         if len(todo):
1143             self.pool.get('stock.move').write(cr, uid, todo, {'state':'assigned'})
1144         self.write(cr, uid, ids, {'state':'cancel'})
1145         wf_service = netsvc.LocalService("workflow")
1146         for id in ids:
1147             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1148         return True
1149
1150     def action_check_finnished(self, cr, uid, ids):
1151         return self.check_move_done(cr, uid, ids)
1152
1153     def action_check(self, cr, uid, ids):
1154         ok = False
1155         for procurement in self.browse(cr, uid, ids):
1156             if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1157                 self.action_done(cr, uid, [procurement.id])
1158                 ok = True
1159         return ok
1160
1161     def action_ready(self, cr, uid, ids):
1162         res = self.write(cr, uid, ids, {'state':'ready'})
1163         return res
1164
1165     def action_done(self, cr, uid, ids):
1166         for procurement in self.browse(cr, uid, ids):
1167             if procurement.move_id:
1168                 if procurement.close_move and (procurement.move_id.state <> 'done'):
1169                     self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1170         res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1171         wf_service = netsvc.LocalService("workflow")
1172         for id in ids:
1173             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1174         return res
1175
1176     def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1177         '''
1178         use_new_cursor: False or the dbname
1179         '''
1180         if not context:
1181             context={}
1182         self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1183         self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1184                 use_new_cursor=use_new_cursor, context=context)
1185 mrp_procurement()
1186
1187
1188 class stock_warehouse_orderpoint(osv.osv):
1189     _name = "stock.warehouse.orderpoint"
1190     _description = "Orderpoint minimum rule"
1191     _columns = {
1192         'name': fields.char('Name', size=32, required=True),
1193         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the orderpoint without removing it."),
1194         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1195         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
1196         'location_id': fields.many2one('stock.location', 'Location', required=True),
1197         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
1198         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1199         'product_min_qty': fields.float('Min Quantity', required=True,
1200             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1201             "a requisition to bring the virtual stock to the Max Quantity."),
1202         'product_max_qty': fields.float('Max Quantity', required=True,
1203             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1204             "a requisition to bring the virtual stock to the Max Quantity."),
1205         'qty_multiple': fields.integer('Qty Multiple', required=True,
1206             help="The requisition quantity will by rounded up to this multiple."),
1207         'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order'),
1208         'company_id': fields.many2one('res.company','Company',required=True),   
1209     }
1210     _defaults = {
1211         'active': lambda *a: 1,
1212         'logic': lambda *a: 'max',
1213         'qty_multiple': lambda *a: 1,
1214         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1215         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1216         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', c)
1217     }
1218     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1219         if warehouse_id:
1220             w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1221             v = {'location_id':w.lot_stock_id.id}
1222             return {'value': v}
1223         return {}
1224     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1225         if product_id:
1226             prod=self.pool.get('product.product').browse(cr,uid,product_id)
1227             v = {'product_uom':prod.uom_id.id}
1228             return {'value': v}
1229         return {}
1230     def copy(self, cr, uid, id, default=None,context={}):
1231         if not default:
1232             default = {}
1233         default.update({
1234             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.warehouse.orderpoint') or '',
1235         })
1236         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
1237 stock_warehouse_orderpoint()
1238
1239
1240 class StockMove(osv.osv):
1241     _inherit = 'stock.move'
1242     _columns = {
1243         'procurements': fields.one2many('mrp.procurement', 'move_id', 'Requisitions'),
1244     }
1245     def copy(self, cr, uid, id, default=None, context=None):
1246         default = default or {}
1247         default['procurements'] = []
1248         return super(StockMove, self).copy(cr, uid, id, default, context)
1249
1250     def _action_explode(self, cr, uid, move, context={}):
1251         if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1252             bis = self.pool.get('mrp.bom').search(cr, uid, [
1253                 ('product_id','=',move.product_id.id),
1254                 ('bom_id','=',False),
1255                 ('type','=','phantom')])
1256             if bis:
1257                 factor = move.product_qty
1258                 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1259                 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1260                 dest = move.product_id.product_tmpl_id.property_stock_production.id
1261                 state = 'confirmed'
1262                 if move.state=='assigned':
1263                     state='assigned'
1264                 for line in res[0]:
1265                     print 'Line :',line
1266                     valdef = {
1267                         'picking_id': move.picking_id.id,
1268                         'product_id': line['product_id'],
1269                         'product_uom': line['product_uom'],
1270                         'product_qty': line['product_qty'],
1271                         'product_uos': line['product_uos'],
1272                         'product_uos_qty': line['product_uos_qty'],
1273                         'move_dest_id': move.id,
1274                         'state': state,
1275                         'name': line['name'],
1276                         'location_dest_id': dest,
1277                         'move_history_ids': [(6,0,[move.id])],
1278                         'move_history_ids2': [(6,0,[])],
1279                         'procurements': [],
1280                     }
1281                     mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1282                     prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1283                     proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1284                         'name': (move.picking_id.origin or ''),
1285                         'origin': (move.picking_id.origin or ''),
1286                         'date_planned': move.date_planned,
1287                         'product_id': line['product_id'],
1288                         'product_qty': line['product_qty'],
1289                         'product_uom': line['product_uom'],
1290                         'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1291                         'product_uos':  line['product_uos'],
1292                         'location_id': move.location_id.id,
1293                         'procure_method': prodobj.procure_method,
1294                         'move_id': mid,
1295                         'company_id': line['company_id'],
1296                     })
1297                     wf_service = netsvc.LocalService("workflow")
1298                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1299                 self.pool.get('stock.move').write(cr, uid, [move.id], {
1300                     'location_id': move.location_dest_id.id,
1301                     'auto_validate': True,
1302                     'picking_id': False,
1303                     'location_id': dest,
1304                     'state': 'waiting'
1305                 })
1306                 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1307                     wf_service = netsvc.LocalService("workflow")
1308                     wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1309         return True
1310 StockMove()
1311
1312
1313 class StockPicking(osv.osv):
1314     _inherit = 'stock.picking'
1315
1316     def test_finnished(self, cursor, user, ids):
1317         wf_service = netsvc.LocalService("workflow")
1318         res = super(StockPicking, self).test_finnished(cursor, user, ids)
1319         for picking in self.browse(cursor, user, ids):
1320             for move in picking.move_lines:
1321                 if move.state == 'done' and move.procurements:
1322                     for procurement in move.procurements:
1323                         wf_service.trg_validate(user, 'mrp.procurement',
1324                                 procurement.id, 'button_check', cursor)
1325         return res
1326
1327     #
1328     # Explode picking by replacing phantom BoMs
1329     #
1330     def action_explode(self, cr, uid, picks, *args):
1331         for move in self.pool.get('stock.move').browse(cr, uid, picks):
1332             self.pool.get('stock.move')._action_explode(cr, uid, move)
1333         return picks
1334
1335 StockPicking()
1336
1337 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1338