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