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