[IMP]: stock, mrp: Changed logic of track lots, Improvement in functions of split...
[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     def action_start(self, cr, uid, ids, context=None):
585         self.write(cr, uid, ids, {'state':'in_production'})
586         return True
587
588     #TODO Review materials in function in_prod and prod_end.
589     def action_production_end(self, cr, uid, ids):
590 #        move_ids = []
591         for production in self.browse(cr, uid, ids):
592             for res in production.move_lines:
593                 for move in production.move_created_ids:
594                     #XXX must use the orm
595                     cr.execute('INSERT INTO stock_move_history_ids \
596                             (parent_id, child_id) VALUES (%s,%s)',
597                             (res.id, move.id))
598 #                move_ids.append(res.id)
599             vals= {'state':'confirmed'}
600             new_moves = [x.id for x in production.move_created_ids]
601             self.pool.get('stock.move').write(cr, uid, new_moves, vals)
602             if not production.date_finnished:
603                 self.write(cr, uid, [production.id],
604                         {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
605             self.pool.get('stock.move').check_assign(cr, uid, new_moves)
606             self.pool.get('stock.move').action_done(cr, uid, new_moves)
607             self._costs_generate(cr, uid, production)
608         self.pool.get('stock.move').action_done(cr, uid, move_ids)
609         self.write(cr,  uid, ids, {'state': 'done'})
610         return True
611
612     def _costs_generate(self, cr, uid, production):
613         amount = 0.0
614         for wc_line in production.workcenter_lines:
615             wc = wc_line.workcenter_id
616             if wc.costs_journal_id and wc.costs_general_account_id:
617                 value = wc_line.hour * wc.costs_hour
618                 account = wc.costs_hour_account_id.id
619                 if value and account:
620                     amount += value
621                     self.pool.get('account.analytic.line').create(cr, uid, {
622                         'name': wc_line.name+' (H)',
623                         'amount': value,
624                         'account_id': account,
625                         'general_account_id': wc.costs_general_account_id.id,
626                         'journal_id': wc.costs_journal_id.id,
627                         'code': wc.code
628                     } )
629             if wc.costs_journal_id and wc.costs_general_account_id:
630                 value = wc_line.cycle * wc.costs_cycle
631                 account = wc.costs_cycle_account_id.id
632                 if value and account:
633                     amount += value
634                     self.pool.get('account.analytic.line').create(cr, uid, {
635                         'name': wc_line.name+' (C)',
636                         'amount': value,
637                         'account_id': account,
638                         'general_account_id': wc.costs_general_account_id.id,
639                         'journal_id': wc.costs_journal_id.id,
640                         'code': wc.code
641                     } )
642         return amount
643
644     def action_in_production(self, cr, uid, ids):
645         move_ids = []
646         for production in self.browse(cr, uid, ids):
647             for res in production.move_lines:
648                 move_ids.append(res.id)
649             if not production.date_start:
650                 self.write(cr, uid, [production.id],
651                         {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
652 #        self.pool.get('stock.move').action_done(cr, uid, move_ids)
653         self.write(cr, uid, ids, {'state': 'in_production'})
654         return True
655
656     def test_if_product(self, cr, uid, ids):
657         res = True
658         for production in self.browse(cr, uid, ids):
659             if not production.product_lines:
660                 if not self.action_compute(cr, uid, [production.id]):
661                     res = False
662         return res
663
664     def _get_auto_picking(self, cr, uid, production):
665         return True
666
667     def action_confirm(self, cr, uid, ids):
668         picking_id=False
669         proc_ids = []
670         for production in self.browse(cr, uid, ids):
671             if not production.product_lines:
672                 self.action_compute(cr, uid, [production.id])
673                 production = self.browse(cr, uid, [production.id])[0]
674             routing_loc = None
675             pick_type = 'internal'
676             address_id = False
677             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
678                 routing_loc = production.bom_id.routing_id.location_id
679                 if routing_loc.usage<>'internal':
680                     pick_type = 'out'
681                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
682                 routing_loc = routing_loc.id
683             picking_id = self.pool.get('stock.picking').create(cr, uid, {
684                 'origin': (production.origin or '').split(':')[0] +':'+production.name,
685                 'type': pick_type,
686                 'move_type': 'one',
687                 'state': 'auto',
688                 'address_id': address_id,
689                 'auto_picking': self._get_auto_picking(cr, uid, production),
690                 'company_id': production.company_id.id,
691             })
692
693             source = production.product_id.product_tmpl_id.property_stock_production.id
694             data = {
695                 'name':'PROD:'+production.name,
696                 'date_planned': production.date_planned,
697                 'product_id': production.product_id.id,
698                 'product_qty': production.product_qty,
699                 'product_uom': production.product_uom.id,
700                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
701                 'product_uos': production.product_uos and production.product_uos.id or False,
702                 'location_id': source,
703                 'location_dest_id': production.location_dest_id.id,
704                 'move_dest_id': production.move_prod_id.id,
705                 'state': 'waiting',
706                 'company_id': production.company_id.id,
707             }
708             res_final_id = self.pool.get('stock.move').create(cr, uid, data)
709
710             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
711             moves = []
712             for line in production.product_lines:
713                 move_id=False
714                 newdate = production.date_planned
715                 if line.product_id.type in ('product', 'consu'):
716                     res_dest_id = self.pool.get('stock.move').create(cr, uid, {
717                         'name':'PROD:'+production.name,
718                         'date_planned': production.date_planned,
719                         'product_id': line.product_id.id,
720                         'product_qty': line.product_qty,
721                         'product_uom': line.product_uom.id,
722                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
723                         'product_uos': line.product_uos and line.product_uos.id or False,
724                         'location_id': routing_loc or production.location_src_id.id,
725                         'location_dest_id': source,
726                         'move_dest_id': res_final_id,
727                         'state': 'waiting',
728                         'company_id': production.company_id.id,
729                     })
730                     moves.append(res_dest_id)
731                     move_id = self.pool.get('stock.move').create(cr, uid, {
732                         'name':'PROD:'+production.name,
733                         'picking_id':picking_id,
734                         'product_id': line.product_id.id,
735                         'product_qty': line.product_qty,
736                         'product_uom': line.product_uom.id,
737                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
738                         'product_uos': line.product_uos and line.product_uos.id or False,
739                         'date_planned': newdate,
740                         'move_dest_id': res_dest_id,
741                         'location_id': production.location_src_id.id,
742                         'location_dest_id': routing_loc or production.location_src_id.id,
743                         'state': 'waiting',
744                         'company_id': production.company_id.id,
745                     })
746                 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
747                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
748                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
749                     'date_planned': newdate,
750                     'product_id': line.product_id.id,
751                     'product_qty': line.product_qty,
752                     'product_uom': line.product_uom.id,
753                     'product_uos_qty': line.product_uos and line.product_qty or False,
754                     'product_uos': line.product_uos and line.product_uos.id or False,
755                     'location_id': production.location_src_id.id,
756                     'procure_method': line.product_id.procure_method,
757                     'move_id': move_id,
758                     'company_id': production.company_id.id,
759                 })
760                 wf_service = netsvc.LocalService("workflow")
761                 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
762                 proc_ids.append(proc_id)
763             wf_service = netsvc.LocalService("workflow")
764             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
765             self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
766         return picking_id
767
768     def force_production(self, cr, uid, ids, *args):
769         pick_obj = self.pool.get('stock.picking')
770         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
771         return True
772
773 mrp_production()
774
775
776 class stock_move(osv.osv):
777     _name = 'stock.move'
778     _inherit = 'stock.move'
779     _columns = {
780         'production_id': fields.many2one('mrp.production', 'Production', select=True),
781     }
782 stock_move()
783
784 class mrp_production_workcenter_line(osv.osv):
785     _name = 'mrp.production.workcenter.line'
786     _description = 'Work Orders'
787     _order = 'sequence'
788     _columns = {
789         'name': fields.char('Work Order', size=64, required=True),
790         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
791         'cycle': fields.float('Nbr of cycles', digits=(16,2)),
792         'hour': fields.float('Nbr of hours', digits=(16,2)),
793         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
794         'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade'),
795     }
796     _defaults = {
797         'sequence': lambda *a: 1,
798         'hour': lambda *a: 0,
799         'cycle': lambda *a: 0,
800     }
801 mrp_production_workcenter_line()
802
803 class mrp_production_product_line(osv.osv):
804     _name = 'mrp.production.product.line'
805     _description = 'Production scheduled products'
806     _columns = {
807         'name': fields.char('Name', size=64, required=True),
808         'product_id': fields.many2one('product.product', 'Product', required=True),
809         'product_qty': fields.float('Product Qty', required=True),
810         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
811         'product_uos_qty': fields.float('Product UOS Qty'),
812         'product_uos': fields.many2one('product.uom', 'Product UOS'),
813         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
814     }
815 mrp_production_product_line()
816
817 # ------------------------------------------------------------------
818 # Procurement
819 # ------------------------------------------------------------------
820 #
821 # Produce, Buy or Find products and place a move
822 #     then wizard for picking lists & move
823 #
824 class mrp_procurement(osv.osv):
825     _name = "mrp.procurement"
826     _description = "Procurement"
827     _order = 'priority,date_planned'
828     _columns = {
829         'name': fields.char('Reason', size=64, required=True, help='Requisition name.'),
830         'origin': fields.char('Source Document', size=64,
831             help="Reference of the document that created this Requisition.\n"
832             "This is automatically completed by Open ERP."),
833         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
834         'date_planned': fields.datetime('Scheduled date', required=True),
835         'date_close': fields.datetime('Date Closed'),
836         'product_id': fields.many2one('product.product', 'Product', required=True, states={'draft':[('readonly',False)]}, readonly=True),
837         'product_qty': fields.float('Quantity', required=True, states={'draft':[('readonly',False)]}, readonly=True),
838         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
839         'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
840         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
841         'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
842
843         'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
844
845         'close_move': fields.boolean('Close Move at end', required=True),
846         'location_id': fields.many2one('stock.location', 'Location', required=True, states={'draft':[('readonly',False)]}, readonly=True),
847         'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
848             readonly=True, required=True, help="If you encode manually a Procurement, you probably want to use" \
849             " a make to order method."),
850
851         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
852         'note': fields.text('Note'),
853
854         'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
855
856         'message': fields.char('Latest error', size=64, help="Exception occurred while computing procurement orders."),
857         'state': fields.selection([
858             ('draft','Draft'),
859             ('confirmed','Confirmed'),
860             ('exception','Exception'),
861             ('running','Running'),
862             ('cancel','Cancel'),
863             ('ready','Ready'),
864             ('done','Done'),
865             ('waiting','Waiting')], 'State', required=True,
866             help='When a procurement is created the state is set to \'Draft\'.\n If the procurement is confirmed, the state is set to \'Confirmed\'.\
867             \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.'),
868         'note' : fields.text('Note'),
869         'company_id': fields.many2one('res.company','Company',required=True),
870     }
871     _defaults = {
872         'state': lambda *a: 'draft',
873         'priority': lambda *a: '1',
874         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
875         'close_move': lambda *a: 0,
876         'procure_method': lambda *a: 'make_to_order',
877         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.procurement', context=c)
878     }
879
880     def unlink(self, cr, uid, ids, context=None):
881         procurements = self.read(cr, uid, ids, ['state'])
882         unlink_ids = []
883         for s in procurements:
884             if s['state'] in ['draft','cancel']:
885                 unlink_ids.append(s['id'])
886             else:
887                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Requisition Order(s) which are in %s State!' % s['state']))
888         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
889
890     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
891         if product_id:
892             w=self.pool.get('product.product').browse(cr,uid,product_id, context)
893             v = {
894                 'product_uom':w.uom_id.id,
895                 'product_uos':w.uos_id and w.uos_id.id or w.uom_id.id
896             }
897             return {'value': v}
898         return {}
899
900     def check_product(self, cr, uid, ids):
901         for procurement in self.browse(cr, uid, ids):
902             if procurement.product_id.type in ('product', 'consu'):
903                 return True
904         return False
905
906     def check_move_cancel(self, cr, uid, ids, context={}):
907         res = True
908         ok = False
909         for procurement in self.browse(cr, uid, ids, context):
910             if procurement.move_id:
911                 ok = True
912                 if not procurement.move_id.state=='cancel':
913                     res = False
914         return res and ok
915
916     def check_move_done(self, cr, uid, ids, context={}):
917         res = True
918         for proc in self.browse(cr, uid, ids, context):
919             if proc.move_id:
920                 if not proc.move_id.state=='done':
921                     res = False
922         return res
923
924     #
925     # This method may be overrided by objects that override mrp.procurment
926     # for computing their own purpose
927     #
928     def _quantity_compute_get(self, cr, uid, proc, context={}):
929         if proc.product_id.type=='product':
930             if proc.move_id.product_uos:
931                 return proc.move_id.product_uos_qty
932         return False
933
934     def _uom_compute_get(self, cr, uid, proc, context={}):
935         if proc.product_id.type=='product':
936             if proc.move_id.product_uos:
937                 return proc.move_id.product_uos.id
938         return False
939
940     #
941     # Return the quantity of product shipped/produced/served, wich may be
942     # different from the planned quantity
943     #
944     def quantity_get(self, cr, uid, id, context={}):
945         proc = self.browse(cr, uid, id, context)
946         result = self._quantity_compute_get(cr, uid, proc, context)
947         if not result:
948             result = proc.product_qty
949         return result
950
951     def uom_get(self, cr, uid, id, context=None):
952         proc = self.browse(cr, uid, id, context)
953         result = self._uom_compute_get(cr, uid, proc, context)
954         if not result:
955             result = proc.product_uom.id
956         return result
957
958     def check_waiting(self, cr, uid, ids, context=[]):
959         for procurement in self.browse(cr, uid, ids, context=context):
960             if procurement.move_id and procurement.move_id.state=='auto':
961                 return True
962         return False
963
964     def check_produce_service(self, cr, uid, procurement, context=[]):
965         return True
966
967     def check_produce_product(self, cr, uid, procurement, context=[]):
968         properties = [x.id for x in procurement.property_ids]
969         bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
970         if not bom_id:
971             cr.execute('update mrp_procurement set message=%s where id=%s', (_('No BoM defined for this product !'), procurement.id))
972             return False
973         return True
974
975     def check_make_to_stock(self, cr, uid, ids, context={}):
976         ok = True
977         for procurement in self.browse(cr, uid, ids, context=context):
978             if procurement.product_id.type=='service':
979                 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
980             else:
981                 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
982         return ok
983
984     def check_produce(self, cr, uid, ids, context={}):
985         res = True
986         user = self.pool.get('res.users').browse(cr, uid, uid)
987         for procurement in self.browse(cr, uid, ids):
988             if procurement.product_id.product_tmpl_id.supply_method<>'produce':
989                 if procurement.product_id.seller_ids:
990                     partner = procurement.product_id.seller_ids[0].name
991                     if user.company_id and user.company_id.partner_id:
992                         if partner.id == user.company_id.partner_id.id:
993                             return True
994                 return False
995             if procurement.product_id.product_tmpl_id.type=='service':
996                 res = res and self.check_produce_service(cr, uid, procurement, context)
997             else:
998                 res = res and self.check_produce_product(cr, uid, procurement, context)
999             if not res:
1000                 return False
1001         return res
1002
1003     def check_buy(self, cr, uid, ids):
1004         user = self.pool.get('res.users').browse(cr, uid, uid)
1005         for procurement in self.browse(cr, uid, ids):
1006             if procurement.product_id.product_tmpl_id.supply_method<>'buy':
1007                 return False
1008             if not procurement.product_id.seller_ids:
1009                 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No supplier defined for this product !'), procurement.id))
1010                 return False
1011             partner = procurement.product_id.seller_ids[0].name
1012             if user.company_id and user.company_id.partner_id:
1013                 if partner.id == user.company_id.partner_id.id:
1014                     return False
1015             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
1016             if not address_id:
1017                 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No address defined for the supplier'), procurement.id))
1018                 return False
1019         return True
1020
1021     def test_cancel(self, cr, uid, ids):
1022         for record in self.browse(cr, uid, ids):
1023             if record.move_id and record.move_id.state=='cancel':
1024                 return True
1025         return False
1026
1027     def action_confirm(self, cr, uid, ids, context={}):
1028         for procurement in self.browse(cr, uid, ids):
1029             if procurement.product_qty <= 0.00:
1030                 raise osv.except_osv(_('Data Insufficient !'), _('Please check the Quantity of Requisition Order(s), it should not be less than 1!'))
1031             if procurement.product_id.type in ('product', 'consu'):
1032                 if not procurement.move_id:
1033                     source = procurement.location_id.id
1034                     if procurement.procure_method=='make_to_order':
1035                         source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
1036                     id = self.pool.get('stock.move').create(cr, uid, {
1037                         'name': 'PROC:'+procurement.name,
1038                         'location_id': source,
1039                         'location_dest_id': procurement.location_id.id,
1040                         'product_id': procurement.product_id.id,
1041                         'product_qty':procurement.product_qty,
1042                         'product_uom': procurement.product_uom.id,
1043                         'date_planned': procurement.date_planned,
1044                         'state':'confirmed',
1045                         'company_id': procurement.company_id.id,
1046                     })
1047                     self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
1048                 else:
1049                     # TODO: check this
1050                     if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('waiting',):
1051                         id = self.pool.get('stock.move').write(cr, uid, [procurement.move_id.id], {'state':'confirmed'})
1052         self.write(cr, uid, ids, {'state':'confirmed','message':''})
1053         return True
1054
1055     def action_move_assigned(self, cr, uid, ids, context={}):
1056         self.write(cr, uid, ids, {'state':'running','message':_('from stock: products assigned.')})
1057         return True
1058
1059     def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
1060         return True
1061
1062     def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
1063         ok = True
1064         if procurement.move_id:
1065             id = procurement.move_id.id
1066             if not (procurement.move_id.state in ('done','assigned','cancel')):
1067                 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
1068                 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%s', (procurement.product_id.id,))
1069                 if not cr.fetchone()[0]:
1070                     cr.execute('update mrp_procurement set message=%s where id=%s', (_('from stock and no minimum orderpoint rule defined'), procurement.id))
1071         return ok
1072
1073     def action_produce_assign_service(self, cr, uid, ids, context={}):
1074         for procurement in self.browse(cr, uid, ids):
1075             self.write(cr, uid, [procurement.id], {'state':'running'})
1076         return True
1077
1078     def action_produce_assign_product(self, cr, uid, ids, context={}):
1079         produce_id = False
1080         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1081         for procurement in self.browse(cr, uid, ids):
1082             res_id = procurement.move_id.id
1083             loc_id = procurement.location_id.id
1084             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)
1085             newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
1086             produce_id = self.pool.get('mrp.production').create(cr, uid, {
1087                 'origin': procurement.origin,
1088                 'product_id': procurement.product_id.id,
1089                 'product_qty': procurement.product_qty,
1090                 'product_uom': procurement.product_uom.id,
1091                 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
1092                 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
1093                 'location_src_id': procurement.location_id.id,
1094                 'location_dest_id': procurement.location_id.id,
1095                 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
1096                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1097                 'move_prod_id': res_id,
1098                 'company_id': procurement.company_id.id,
1099             })
1100             self.write(cr, uid, [procurement.id], {'state':'running'})
1101             bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
1102                     [produce_id], properties=[x.id for x in procurement.property_ids])
1103             wf_service = netsvc.LocalService("workflow")
1104             wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
1105             self.pool.get('stock.move').write(cr, uid, [res_id],
1106                     {'location_id':procurement.location_id.id})
1107         return produce_id
1108
1109     def action_po_assign(self, cr, uid, ids, context={}):
1110         purchase_id = False
1111         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1112         for procurement in self.browse(cr, uid, ids):
1113             res_id = procurement.move_id.id
1114             partner = procurement.product_id.seller_ids[0].name
1115             partner_id = partner.id
1116             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1117             pricelist_id = partner.property_product_pricelist_purchase.id
1118
1119             uom_id = procurement.product_id.uom_po_id.id
1120
1121             qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1122             if procurement.product_id.seller_ids[0].qty:
1123                 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1124
1125             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
1126
1127             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
1128             newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1129             newdate = newdate - procurement.product_id.seller_ids[0].delay
1130
1131             #Passing partner_id to context for purchase order line integrity of Line name
1132             context.update({'lang':partner.lang, 'partner_id':partner_id})
1133
1134             product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1135
1136             line = {
1137                 'name': product.partner_ref,
1138                 'product_qty': qty,
1139                 'product_id': procurement.product_id.id,
1140                 'product_uom': uom_id,
1141                 'price_unit': price,
1142                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1143                 'move_dest_id': res_id,
1144                 'notes':product.description_purchase,
1145             }
1146
1147             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1148             taxes = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, taxes_ids)
1149             line.update({
1150                 'taxes_id':[(6,0,taxes)]
1151             })
1152             purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1153                 'origin': procurement.origin,
1154                 'partner_id': partner_id,
1155                 'partner_address_id': address_id,
1156                 'location_id': procurement.location_id.id,
1157                 'pricelist_id': pricelist_id,
1158                 'order_line': [(0,0,line)],
1159                 'company_id': procurement.company_id.id,
1160                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1161             })
1162             self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1163         return purchase_id
1164
1165     def action_cancel(self, cr, uid, ids):
1166         todo = []
1167         todo2 = []
1168         for proc in self.browse(cr, uid, ids):
1169             if proc.close_move:
1170                 if proc.move_id.state not in ('done','cancel'):
1171                     todo2.append(proc.move_id.id)
1172             else:
1173                 if proc.move_id and proc.move_id.state=='waiting':
1174                     todo.append(proc.move_id.id)
1175         if len(todo2):
1176             self.pool.get('stock.move').action_cancel(cr, uid, todo2)
1177         if len(todo):
1178             self.pool.get('stock.move').write(cr, uid, todo, {'state':'assigned'})
1179         self.write(cr, uid, ids, {'state':'cancel'})
1180         wf_service = netsvc.LocalService("workflow")
1181         for id in ids:
1182             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1183         return True
1184
1185     def action_check_finnished(self, cr, uid, ids):
1186         return self.check_move_done(cr, uid, ids)
1187
1188     def action_check(self, cr, uid, ids):
1189         ok = False
1190         for procurement in self.browse(cr, uid, ids):
1191             if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1192                 self.action_done(cr, uid, [procurement.id])
1193                 ok = True
1194         return ok
1195
1196     def action_ready(self, cr, uid, ids):
1197         res = self.write(cr, uid, ids, {'state':'ready'})
1198         return res
1199
1200     def action_done(self, cr, uid, ids):
1201         for procurement in self.browse(cr, uid, ids):
1202             if procurement.move_id:
1203                 if procurement.close_move and (procurement.move_id.state <> 'done'):
1204                     self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1205         res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1206         wf_service = netsvc.LocalService("workflow")
1207         for id in ids:
1208             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1209         return res
1210
1211     def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1212         '''
1213         use_new_cursor: False or the dbname
1214         '''
1215         if not context:
1216             context={}
1217         self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1218         self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1219                 use_new_cursor=use_new_cursor, context=context)
1220 mrp_procurement()
1221
1222
1223 class stock_warehouse_orderpoint(osv.osv):
1224     _name = "stock.warehouse.orderpoint"
1225     _description = "Orderpoint minimum rule"
1226     _columns = {
1227         'name': fields.char('Name', size=32, required=True),
1228         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the orderpoint without removing it."),
1229         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1230         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
1231         'location_id': fields.many2one('stock.location', 'Location', required=True),
1232         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
1233         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1234         'product_min_qty': fields.float('Min Quantity', required=True,
1235             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1236             "a requisition to bring the virtual stock to the Max Quantity."),
1237         'product_max_qty': fields.float('Max Quantity', required=True,
1238             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1239             "a requisition to bring the virtual stock to the Max Quantity."),
1240         'qty_multiple': fields.integer('Qty Multiple', required=True,
1241             help="The requisition quantity will by rounded up to this multiple."),
1242         'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order'),
1243         'company_id': fields.many2one('res.company','Company',required=True),
1244     }
1245     _defaults = {
1246         'active': lambda *a: 1,
1247         'logic': lambda *a: 'max',
1248         'qty_multiple': lambda *a: 1,
1249         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1250         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1251         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=c)
1252     }
1253     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1254         if warehouse_id:
1255             w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1256             v = {'location_id':w.lot_stock_id.id}
1257             return {'value': v}
1258         return {}
1259     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1260         if product_id:
1261             prod=self.pool.get('product.product').browse(cr,uid,product_id)
1262             v = {'product_uom':prod.uom_id.id}
1263             return {'value': v}
1264         return {}
1265     def copy(self, cr, uid, id, default=None,context={}):
1266         if not default:
1267             default = {}
1268         default.update({
1269             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.warehouse.orderpoint') or '',
1270         })
1271         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
1272 stock_warehouse_orderpoint()
1273
1274
1275 class StockMove(osv.osv):
1276     _inherit = 'stock.move'
1277     _columns = {
1278         'procurements': fields.one2many('mrp.procurement', 'move_id', 'Requisitions'),
1279     }
1280     def copy(self, cr, uid, id, default=None, context=None):
1281         default = default or {}
1282         default['procurements'] = []
1283         return super(StockMove, self).copy(cr, uid, id, default, context)
1284
1285     def _action_explode(self, cr, uid, move, context={}):
1286         if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1287             bis = self.pool.get('mrp.bom').search(cr, uid, [
1288                 ('product_id','=',move.product_id.id),
1289                 ('bom_id','=',False),
1290                 ('type','=','phantom')])
1291             if bis:
1292                 factor = move.product_qty
1293                 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1294                 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1295                 dest = move.product_id.product_tmpl_id.property_stock_production.id
1296                 state = 'confirmed'
1297                 if move.state=='assigned':
1298                     state='assigned'
1299                 for line in res[0]:
1300                     print 'Line :',line
1301                     valdef = {
1302                         'picking_id': move.picking_id.id,
1303                         'product_id': line['product_id'],
1304                         'product_uom': line['product_uom'],
1305                         'product_qty': line['product_qty'],
1306                         'product_uos': line['product_uos'],
1307                         'product_uos_qty': line['product_uos_qty'],
1308                         'move_dest_id': move.id,
1309                         'state': state,
1310                         'name': line['name'],
1311                         'location_dest_id': dest,
1312                         'move_history_ids': [(6,0,[move.id])],
1313                         'move_history_ids2': [(6,0,[])],
1314                         'procurements': [],
1315                     }
1316                     mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1317                     prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1318                     proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1319                         'name': (move.picking_id.origin or ''),
1320                         'origin': (move.picking_id.origin or ''),
1321                         'date_planned': move.date_planned,
1322                         'product_id': line['product_id'],
1323                         'product_qty': line['product_qty'],
1324                         'product_uom': line['product_uom'],
1325                         'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1326                         'product_uos':  line['product_uos'],
1327                         'location_id': move.location_id.id,
1328                         'procure_method': prodobj.procure_method,
1329                         'move_id': mid,
1330                         'company_id': line['company_id'],
1331                     })
1332                     wf_service = netsvc.LocalService("workflow")
1333                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1334                 self.pool.get('stock.move').write(cr, uid, [move.id], {
1335                     'location_id': move.location_dest_id.id,
1336                     'auto_validate': True,
1337                     'picking_id': False,
1338                     'location_id': dest,
1339                     'state': 'waiting'
1340                 })
1341                 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1342                     wf_service = netsvc.LocalService("workflow")
1343                     wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1344         return True
1345     
1346     def _track_lines(self, cr, uid, ids, data, context=None):
1347         production_ids = []
1348         new_move = super(StockMove, self)._track_lines(cr, uid, ids, data, context=context)
1349         if new_move:
1350             production_obj = self.pool.get('mrp.production')
1351             move_obj = self.pool.get('stock.move')
1352             move = move_obj.browse(cr, uid, ids)
1353             production_ids = production_obj.search(cr, uid, [('move_lines', 'in', [move.id])])
1354             for new in new_move:
1355                 production_obj.write(cr, uid, production_ids, {'move_lines': [(4, new)]})
1356                 move_obj.action_done(cr, uid, [new])
1357         return {}
1358     
1359     def consume_moves(self, cr, uid, ids, product_qty, location_id, context=None):
1360         new_move = super(StockMove, self).consume_moves(cr, uid, ids, product_qty, location_id, context=context)
1361         if new_move:
1362             production_obj = self.pool.get('mrp.production')
1363             move_obj = self.pool.get('stock.move')
1364             move = move_obj.browse(cr, uid, ids)
1365             production_ids = production_obj.search(cr, uid, [('move_lines', 'in', [move.id])])
1366             production_obj.write(cr, uid, production_ids, {'move_lines': [(4, new_move)]})
1367         return {}
1368     
1369     def scrap_moves(self, cr, uid, ids, product_qty, location_id, context=None):
1370         new_move = super(StockMove, self).scrap_moves(cr, uid, ids, product_qty, location_id, context=context)
1371         if new_move:
1372             production_obj = self.pool.get('mrp.production')
1373             move_obj = self.pool.get('stock.move')
1374             move = move_obj.browse(cr, uid, ids)
1375             production_ids = production_obj.search(cr, uid, [('move_lines', 'in', [move.id])])
1376             production_obj.write(cr, uid, production_ids, {'move_lines': [(4, new_move)]})
1377         return {}
1378
1379 StockMove()
1380
1381
1382 class StockPicking(osv.osv):
1383     _inherit = 'stock.picking'
1384
1385     def test_finnished(self, cursor, user, ids):
1386         wf_service = netsvc.LocalService("workflow")
1387         res = super(StockPicking, self).test_finnished(cursor, user, ids)
1388         for picking in self.browse(cursor, user, ids):
1389             for move in picking.move_lines:
1390                 if move.state == 'done' and move.procurements:
1391                     for procurement in move.procurements:
1392                         wf_service.trg_validate(user, 'mrp.procurement',
1393                                 procurement.id, 'button_check', cursor)
1394         return res
1395
1396     #
1397     # Explode picking by replacing phantom BoMs
1398     #
1399     def action_explode(self, cr, uid, picks, *args):
1400         for move in self.pool.get('stock.move').browse(cr, uid, picks):
1401             self.pool.get('stock.move')._action_explode(cr, uid, move)
1402         return picks
1403
1404 StockPicking()
1405
1406 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1407