[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         move_obj = self.pool.get('stock.move')
997         for procurement in self.browse(cr, uid, ids):
998             res_id = procurement.move_id.id or False
999             if not res_id:
1000                 raise osv.except_osv(_('Warning !'), _('No reservation is defined for the selected procurement.'))
1001             move_obj.write(cr, uid, [res_id],{'location_id':procurement.location_id.id})
1002             loc_id = procurement.location_id.id
1003             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)
1004             newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
1005             produce_id = self.pool.get('mrp.production').create(cr, uid, {
1006                 'origin': procurement.origin,
1007                 'product_id': procurement.product_id.id,
1008                 'product_qty': procurement.product_qty,
1009                 'product_uom': procurement.product_uom.id,
1010                 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
1011                 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
1012                 'location_src_id': procurement.location_id.id,
1013                 'location_dest_id': procurement.location_id.id,
1014                 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
1015                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1016                 'move_prod_id': res_id,
1017             })
1018
1019             self.write(cr, uid, [procurement.id], {'state':'running'})
1020             bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
1021                     [produce_id], properties=[x.id for x in procurement.property_ids])
1022             wf_service = netsvc.LocalService("workflow")
1023             wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
1024         return produce_id
1025
1026     def action_po_assign(self, cr, uid, ids, context={}):
1027         purchase_id = False
1028         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1029         for procurement in self.browse(cr, uid, ids):
1030             res_id = procurement.move_id.id or False
1031             partner = procurement.product_id.seller_ids[0].name
1032             partner_id = partner.id
1033             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1034             pricelist_id = partner.property_product_pricelist_purchase.id
1035
1036             uom_id = procurement.product_id.uom_po_id.id
1037
1038             qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1039             if procurement.product_id.seller_ids[0].qty:
1040                 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1041
1042             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id]
1043
1044             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
1045             newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1046             newdate = newdate - procurement.product_id.seller_ids[0].delay
1047
1048             #Passing partner_id to context for purchase order line integrity of Line name
1049             context.update({'lang':partner.lang, 'partner_id':partner_id})
1050
1051             product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1052
1053             line = {
1054                 'name': product.partner_ref,
1055                 'product_qty': qty,
1056                 'product_id': procurement.product_id.id,
1057                 'product_uom': uom_id,
1058                 'price_unit': price,
1059                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1060                 'move_dest_id': res_id,
1061                 'notes':product.description_purchase,
1062             }
1063
1064             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1065             taxes = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, taxes_ids)
1066             line.update({
1067                 'taxes_id':[(6,0,taxes)]
1068             })
1069             purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1070                 'origin': procurement.origin,
1071                 'partner_id': partner_id,
1072                 'partner_address_id': address_id,
1073                 'location_id': procurement.location_id.id,
1074                 'pricelist_id': pricelist_id,
1075                 'order_line': [(0,0,line)],
1076                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1077             })
1078             self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1079         return purchase_id
1080
1081     def action_cancel(self, cr, uid, ids):
1082         todo = []
1083         todo2 = []
1084         for proc in self.browse(cr, uid, ids):
1085             if proc.close_move and proc.move_id:
1086                 if proc.move_id.state not in ('done','cancel'):
1087                     todo2.append(proc.move_id.id)
1088             else:
1089                 if proc.move_id and proc.move_id.state=='waiting':
1090                     todo.append(proc.move_id.id)
1091         if len(todo2):
1092             self.pool.get('stock.move').action_cancel(cr, uid, todo2)
1093         if len(todo):
1094             self.pool.get('stock.move').write(cr, uid, todo, {'state':'assigned'})
1095         self.write(cr, uid, ids, {'state':'cancel'})
1096         wf_service = netsvc.LocalService("workflow")
1097         for id in ids:
1098             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1099         return True
1100
1101     def action_check_finnished(self, cr, uid, ids):
1102         return self.check_move_done(cr, uid, ids)
1103
1104     def action_check(self, cr, uid, ids):
1105         ok = False
1106         for procurement in self.browse(cr, uid, ids):
1107             if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1108                 self.action_done(cr, uid, [procurement.id])
1109                 ok = True
1110         return ok
1111
1112     def action_ready(self, cr, uid, ids):
1113         res = self.write(cr, uid, ids, {'state':'ready'})
1114         return res
1115
1116     def action_done(self, cr, uid, ids):
1117         for procurement in self.browse(cr, uid, ids):
1118             if procurement.move_id:
1119                 if procurement.close_move and (procurement.move_id.state <> 'done'):
1120                     self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1121         res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1122         wf_service = netsvc.LocalService("workflow")
1123         for id in ids:
1124             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1125         return res
1126
1127     def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1128         '''
1129         use_new_cursor: False or the dbname
1130         '''
1131         if not context:
1132             context={}
1133         self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1134         self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1135                 use_new_cursor=use_new_cursor, context=context)
1136 mrp_procurement()
1137
1138
1139 class stock_warehouse_orderpoint(osv.osv):
1140     _name = "stock.warehouse.orderpoint"
1141     _description = "Orderpoint minimum rule"
1142     _columns = {
1143         'name': fields.char('Name', size=32, required=True),
1144         'active': fields.boolean('Active'),
1145         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1146         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
1147         'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
1148         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')], ondelete="cascade"),
1149         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1150         'product_min_qty': fields.float('Min Quantity', required=True,
1151             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1152             "a procurement to bring the virtual stock to the Max Quantity."),
1153         'product_max_qty': fields.float('Max Quantity', required=True,
1154             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1155             "a procurement to bring the virtual stock to the Max Quantity."),
1156         'qty_multiple': fields.integer('Qty Multiple', required=True,
1157             help="The procurement quantity will by rounded up to this multiple."),
1158         'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order', ondelete="set null")
1159     }
1160     _defaults = {
1161         'active': lambda *a: 1,
1162         'logic': lambda *a: 'max',
1163         'qty_multiple': lambda *a: 1,
1164         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1165         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1166     }
1167
1168     _sql_constraints = [
1169         ( 'qty_multiple_check', 'CHECK( qty_multiple > 0 )', _('Qty Multiple must be greater than zero.')),
1170     ]
1171
1172     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1173         if warehouse_id:
1174             w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1175             v = {'location_id':w.lot_stock_id.id}
1176             return {'value': v}
1177         return {}
1178     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1179         if product_id:
1180             prod=self.pool.get('product.product').browse(cr,uid,product_id)
1181             v = {'product_uom':prod.uom_id.id}
1182             return {'value': v}
1183         return {}
1184     def copy(self, cr, uid, id, default=None,context={}):
1185         if not default:
1186             default = {}
1187         default.update({
1188             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.warehouse.orderpoint') or '',
1189         })
1190         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
1191 stock_warehouse_orderpoint()
1192
1193
1194 class StockMove(osv.osv):
1195     _inherit = 'stock.move'
1196     _columns = {
1197         'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1198     }
1199     def copy(self, cr, uid, id, default=None, context=None):
1200         default = default or {}
1201         default['procurements'] = []
1202         return super(StockMove, self).copy(cr, uid, id, default, context)
1203
1204     def _action_explode(self, cr, uid, move, context={}):
1205         if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1206             bis = self.pool.get('mrp.bom').search(cr, uid, [
1207                 ('product_id','=',move.product_id.id),
1208                 ('bom_id','=',False),
1209                 ('type','=','phantom')])
1210             if bis:
1211                 factor = move.product_qty
1212                 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1213                 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1214                 state = 'confirmed'
1215                 if move.state=='assigned':
1216                     state='assigned'
1217                 for line in res[0]:
1218                     valdef = {
1219                         'picking_id': move.picking_id.id,
1220                         'product_id': line['product_id'],
1221                         'product_uom': line['product_uom'],
1222                         'product_qty': line['product_qty'],
1223                         'product_uos': line['product_uos'],
1224                         'product_uos_qty': line['product_uos_qty'],
1225                         'move_dest_id': move.id,
1226                         'state': state,
1227                         'name': line['name'],
1228                         'move_history_ids': [(6,0,[move.id])],
1229                         'move_history_ids2': [(6,0,[])],
1230                         'procurements': []
1231                     }
1232                     mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1233                     prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1234                     proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1235                         'name': (move.picking_id.origin or ''),
1236                         'origin': (move.picking_id.origin or ''),
1237                         'date_planned': move.date_planned,
1238                         'product_id': line['product_id'],
1239                         'product_qty': line['product_qty'],
1240                         'product_uom': line['product_uom'],
1241                         'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1242                         'product_uos':  line['product_uos'],
1243                         'location_id': move.location_id.id,
1244                         'procure_method': prodobj.procure_method,
1245                         'move_id': mid,
1246                     })
1247                     wf_service = netsvc.LocalService("workflow")
1248                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1249                 self.pool.get('stock.move').write(cr, uid, [move.id], {
1250                     'location_id': move.location_dest_id.id, # src and dest locations identical to have correct inventory, dummy move
1251                     'auto_validate': True,
1252                     'picking_id': False,
1253                     'state': 'waiting'
1254                 })
1255                 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1256                     wf_service = netsvc.LocalService("workflow")
1257                     wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1258         return True
1259 StockMove()
1260
1261
1262 class StockPicking(osv.osv):
1263     _inherit = 'stock.picking'
1264
1265     def test_finnished(self, cursor, user, ids):
1266         wf_service = netsvc.LocalService("workflow")
1267         res = super(StockPicking, self).test_finnished(cursor, user, ids)
1268         for picking in self.browse(cursor, user, ids):
1269             for move in picking.move_lines:
1270                 if move.state == 'done' and move.procurements:
1271                     for procurement in move.procurements:
1272                         wf_service.trg_validate(user, 'mrp.procurement',
1273                                 procurement.id, 'button_check', cursor)
1274         return res
1275
1276     #
1277     # Explode picking by replacing phantom BoMs
1278     #
1279     def action_explode(self, cr, uid, picks, *args):
1280         for move in self.pool.get('stock.move').browse(cr, uid, picks):
1281             self.pool.get('stock.move')._action_explode(cr, uid, move)
1282         return picks
1283
1284 StockPicking()
1285
1286 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1287