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