wrong domain in action_reception_picking_move in addons/stock/stock_view.xml
[odoo/odoo.git] / addons / mrp / mrp.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23 import openerp.addons.decimal_precision as dp
24 from openerp.osv import fields, osv, orm
25 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
26 from openerp.tools import float_compare
27 from openerp.tools.translate import _
28 from openerp import tools, SUPERUSER_ID
29 from openerp.addons.product import _common
30
31
32 class mrp_property_group(osv.osv):
33     """
34     Group of mrp properties.
35     """
36     _name = 'mrp.property.group'
37     _description = 'Property Group'
38     _columns = {
39         'name': fields.char('Property Group', size=64, required=True),
40         'description': fields.text('Description'),
41     }
42
43 class mrp_property(osv.osv):
44     """
45     Properties of mrp.
46     """
47     _name = 'mrp.property'
48     _description = 'Property'
49     _columns = {
50         'name': fields.char('Name', size=64, required=True),
51         'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
52         'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
53         'description': fields.text('Description'),
54     }
55     _defaults = {
56         'composition': lambda *a: 'min',
57     }
58 #----------------------------------------------------------
59 # Work Centers
60 #----------------------------------------------------------
61 # capacity_hour : capacity per hour. default: 1.0.
62 #          Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
63 # unit_per_cycle : how many units are produced for one cycle
64
65 class mrp_workcenter(osv.osv):
66     _name = 'mrp.workcenter'
67     _description = 'Work Center'
68     _inherits = {'resource.resource':"resource_id"}
69     _columns = {
70         'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
71         'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this Work Center can do in parallel. If this Work Center represents a team of 5 workers, the capacity per cycle is 5."),
72         'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
73         'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
74         'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
75         'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
76         'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','!=','view')],
77             help="Fill this only if you want automatic analytic accounting entries on production orders."),
78         'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
79         'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','!=','view')],
80             help="Fill this only if you want automatic analytic accounting entries on production orders."),
81         'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
82         'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','!=','view')]),
83         'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
84         'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
85     }
86     _defaults = {
87         'capacity_per_cycle': 1.0,
88         'resource_type': 'material',
89      }
90
91     def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
92         value = {}
93
94         if product_id:
95             cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
96             value = {'costs_hour': cost.standard_price}
97         return {'value': value}
98
99 class mrp_routing(osv.osv):
100     """
101     For specifying the routings of Work Centers.
102     """
103     _name = 'mrp.routing'
104     _description = 'Routing'
105     _columns = {
106         'name': fields.char('Name', size=64, required=True),
107         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
108         'code': fields.char('Code', size=8),
109
110         'note': fields.text('Description'),
111         'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
112
113         'location_id': fields.many2one('stock.location', 'Production Location',
114             help="Keep empty if you produce at the location where the finished products are needed." \
115                 "Set a location if you produce at a fixed location. This can be a partner location " \
116                 "if you subcontract the manufacturing operations."
117         ),
118         'company_id': fields.many2one('res.company', 'Company'),
119     }
120     _defaults = {
121         'active': lambda *a: 1,
122         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
123     }
124
125 class mrp_routing_workcenter(osv.osv):
126     """
127     Defines working cycles and hours of a Work Center using routings.
128     """
129     _name = 'mrp.routing.workcenter'
130     _description = 'Work Center Usage'
131     _order = 'sequence'
132     _columns = {
133         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
134         'name': fields.char('Name', size=64, required=True),
135         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
136         'cycle_nbr': fields.float('Number of Cycles', required=True,
137             help="Number of iterations this work center has to do in the specified operation of the routing."),
138         'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for this Work Center to achieve the operation of the specified routing."),
139         'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
140              help="Routing indicates all the Work Centers used, for how long and/or cycles." \
141                 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
142         'note': fields.text('Description'),
143         'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
144     }
145     _defaults = {
146         'cycle_nbr': lambda *a: 1.0,
147         'hour_nbr': lambda *a: 0.0,
148     }
149
150 class mrp_bom(osv.osv):
151     """
152     Defines bills of material for a product.
153     """
154     _name = 'mrp.bom'
155     _description = 'Bill of Material'
156     _inherit = ['mail.thread']
157
158     def _child_compute(self, cr, uid, ids, name, arg, context=None):
159         """ Gets child bom.
160         @param self: The object pointer
161         @param cr: The current row, from the database cursor,
162         @param uid: The current user ID for security checks
163         @param ids: List of selected IDs
164         @param name: Name of the field
165         @param arg: User defined argument
166         @param context: A standard dictionary for contextual values
167         @return:  Dictionary of values
168         """
169         result = {}
170         if context is None:
171             context = {}
172         bom_obj = self.pool.get('mrp.bom')
173         bom_id = context and context.get('active_id', False) or False
174         cr.execute('select id from mrp_bom')
175         if all(bom_id != r[0] for r in cr.fetchall()):
176             ids.sort()
177             bom_id = ids[0]
178         bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
179         for bom in self.browse(cr, uid, ids, context=context):
180             if (bom_parent) or (bom.id == bom_id):
181                 result[bom.id] = map(lambda x: x.id, bom.bom_line_ids)
182             else:
183                 result[bom.id] = []
184             if bom.bom_line_ids:
185                 continue
186             ok = ((name=='child_complete_ids'))
187             if (bom.type=='phantom' or ok):
188                 sids = bom_obj.search(cr, uid, [('product_tmpl_id','=',bom.product_tmpl_id.id)])
189                 if sids:
190                     bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
191                     result[bom.id] += map(lambda x: x.id, bom2.bom_line_ids)
192         return result
193
194     _columns = {
195         'name': fields.char('Name', size=64),
196         'code': fields.char('Reference', size=16),
197         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the bills of material without removing it."),
198         'type': fields.selection([('normal', 'Normal'), ('phantom', 'Set')], 'BoM Type', required=True,
199                 help= "Set: When processing a sales order for this product, the delivery order will contain the raw materials, instead of the finished product."),
200         'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
201         'product_tmpl_id': fields.many2one('product.template', 'Product', required=True),
202         'product_id': fields.many2one('product.product', 'Product Variant',
203             domain="[('product_tmpl_id','=',product_tmpl_id)]",
204             help="If a product variant is defined the BOM is available only for this product."),
205         'bom_line_ids': fields.one2many('mrp.bom.line', 'bom_id', 'BoM Lines'),
206         
207         'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
208         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"),
209         'date_start': fields.date('Valid From', help="Validity of this BoM. Keep empty if it's always valid."),
210         'date_stop': fields.date('Valid Until', help="Validity of this BoM. Keep empty if it's always valid."),
211         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
212         'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. "\
213                 "The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
214         'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
215         'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% during the production process."),
216         'property_ids': fields.many2many('mrp.property', string='Properties'),
217         'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
218         'company_id': fields.many2one('res.company', 'Company', required=True),
219     }
220
221     def _get_uom_id(self, cr, uid, *args):
222         return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
223     _defaults = {
224         'active': lambda *a: 1,
225         'product_qty': lambda *a: 1.0,
226         'product_efficiency': lambda *a: 1.0,
227         'product_rounding': lambda *a: 0.0,
228         'type': lambda *a: 'normal',
229         'product_uom': _get_uom_id,
230         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
231     }
232     _order = "sequence"
233
234     def _bom_find(self, cr, uid, product_uom, product_tmpl_id=None, product_id=None, properties=None):
235         """ Finds BoM for particular product and product uom.
236         @param product_tmpl_id: Selected product.
237         @param product_uom: Unit of measure of a product.
238         @param properties: List of related properties.
239         @return: False or BoM id.
240         """
241         if properties is None:
242             properties = []
243         domain = None
244         if product_id:
245             domain = ['|',('product_id', '=', product_id),('product_tmpl_id.product_variant_ids', '=', product_id)]
246         else:
247             domain = [('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl_id)]
248         if product_uom:
249             domain +=  [('product_uom','=',product_uom)]
250         domain = domain + [ '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
251                             '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
252         ids = self.search(cr, uid, domain)
253         for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
254             if not set(map(int,bom.property_ids or [])) - set(properties or []):
255                 return bom.id
256         return False
257
258     def _bom_explode(self, cr, uid, bom, product, factor, properties=None, level=0, routing_id=False, previous_products=None, master_bom=None):
259         """ Finds Products and Work Centers for related BoM for manufacturing order.
260         @param bom: BoM of particular product template.
261         @param product: Select a particular variant of the BoM. If False use BoM without variants.
262         @param factor: Factor of product UoM.
263         @param properties: A List of properties Ids.
264         @param level: Depth level to find BoM lines starts from 10.
265         @param previous_products: List of product previously use by bom explore to avoid recursion
266         @param master_bom: When recursion, used to display the name of the master bom
267         @return: result: List of dictionaries containing product details.
268                  result2: List of dictionaries containing Work Center details.
269         """
270         routing_obj = self.pool.get('mrp.routing')
271         all_prod = [] + (previous_products or [])
272         master_bom = master_bom or bom
273
274         def _factor(factor, product_efficiency, product_rounding):
275             factor = factor / (product_efficiency or 1.0)
276             factor = _common.ceiling(factor, product_rounding)
277             if factor < product_rounding:
278                 factor = product_rounding
279             return factor
280
281         factor = _factor(factor, bom.product_efficiency, bom.product_rounding)
282
283         result = []
284         result2 = []
285
286         routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
287         if routing:
288             for wc_use in routing.workcenter_lines:
289                 wc = wc_use.workcenter_id
290                 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
291                 mult = (d + (m and 1.0 or 0.0))
292                 cycle = mult * wc_use.cycle_nbr
293                 result2.append({
294                     'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_tmpl_id.name_get()[0][1]),
295                     'workcenter_id': wc.id,
296                     'sequence': level + (wc_use.sequence or 0),
297                     'cycle': cycle,
298                     'hour': float(wc_use.hour_nbr * mult + ((wc.time_start or 0.0) + (wc.time_stop or 0.0) + cycle * (wc.time_cycle or 0.0)) * (wc.time_efficiency or 1.0)),
299                 })
300
301         for bom_line_id in bom.bom_line_ids:
302             if bom_line_id.date_start and bom_line_id.date_start > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) or \
303                 bom_line_id.date_stop and bom_line_id.date_stop > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT):
304                     continue
305             # check properties
306             if set(map(int,bom_line_id.property_ids or [])) - set(properties or []):
307                 continue
308             # all bom_line_id variant values must be in the product
309             if bom_line_id.attribute_value_ids:
310                 if not product or (set(map(int,bom_line_id.attribute_value_ids or [])) - set(map(int,product.attribute_value_ids))):
311                     continue
312
313             if bom_line_id.product_id.id in all_prod:
314                 raise osv.except_osv(_('Invalid Action!'), _('BoM "%s" contains a BoM line with a product recursion: "%s".') % (master_bom.name,bom_line_id.product_id.name_get()[0][1]))
315             all_prod.append(bom_line_id.product_id.id)
316             
317             if bom_line_id.type != "phantom":
318                 result.append({
319                     'name': bom_line_id.product_id.name,
320                     'product_id': bom_line_id.product_id.id,
321                     'product_qty': _factor(bom_line_id.product_qty * factor, bom_line_id.product_efficiency, bom_line_id.product_rounding),
322                     'product_uom': bom_line_id.product_uom.id,
323                     'product_uos_qty': bom_line_id.product_uos and bom_line_id.product_uos_qty * factor or False,
324                     'product_uos': bom_line_id.product_uos and bom_line_id.product_uos.id or False,
325                 })
326             else:
327                 bom_id = self._bom_find(cr, uid, bom_line_id.product_uom.id, product_id=bom_line_id.product_id.id, properties=properties)
328                 if bom_id:
329                     bom2 = self.browse(cr, uid, bom_id)  
330                     res = self._bom_explode(cr, uid, bom2, bom_line_id.product_id, factor,
331                         properties=properties, level=level + 10, previous_products=all_prod, master_bom=master_bom)
332                     result = result + res[0]
333                     result2 = result2 + res[1]
334                 else:
335                     raise osv.except_osv(_('Invalid Action!'), _('BoM "%s" contains a phantom BoM line but the product "%s" don\'t have any BoM defined.') % (master_bom.name,bom_line_id.product_id.name_get()[0][1]))
336
337         return result, result2
338
339     def copy_data(self, cr, uid, id, default=None, context=None):
340         if default is None:
341             default = {}
342         bom_data = self.read(cr, uid, id, [], context=context)
343         default.update(name=_("%s (copy)") % (bom_data['name']))
344         return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
345
346     def onchange_uom(self, cr, uid, ids, product_tmpl_id, product_uom, context=None):
347         res = {'value': {}}
348         if not product_uom or not product_tmpl_id:
349             return res
350         product = self.pool.get('product.template').browse(cr, uid, product_tmpl_id, context=context)
351         uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
352         if uom.category_id.id != product.uom_id.category_id.id:
353             res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
354             res['value'].update({'product_uom': product.uom_id.id})
355         return res
356
357     def onchange_product_tmpl_id(self, cr, uid, ids, product_tmpl_id, product_qty=0, context=None):
358         """ Changes UoM and name if product_id changes.
359         @param product_id: Changed product_id
360         @return:  Dictionary of changed values
361         """
362         res = {}
363         if product_tmpl_id:
364             prod = self.pool.get('product.template').browse(cr, uid, product_tmpl_id, context=context)
365             res['value'] = {
366                 'name': prod.name,
367                 'product_uom': prod.uom_id.id,
368             }
369         return res
370
371 class mrp_bom_line(osv.osv):
372     _name = 'mrp.bom.line'
373     _order = "sequence"
374
375     _columns = {
376         'type': fields.selection([('normal', 'Normal'), ('phantom', 'Phantom')], 'BoM Line Type', required=True,
377                 help="Phantom: this product line will not appear in the raw materials of manufacturing orders,"
378                      "it will be directly replaced by the raw materials of its own BoM, without triggering"
379                      "an extra manufacturing order."),
380         'product_id': fields.many2one('product.product', 'Product', required=True),
381         'product_uos_qty': fields.float('Product UOS Qty'),
382         '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."),
383         'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
384         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True,
385             help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"),
386         
387         'date_start': fields.date('Valid From', help="Validity of component. Keep empty if it's always valid."),
388         'date_stop': fields.date('Valid Until', help="Validity of component. Keep empty if it's always valid."),
389         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying."),
390         'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
391         'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
392         'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
393         'property_ids': fields.many2many('mrp.property', string='Properties'),
394
395         'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True, required=True),
396         'attribute_value_ids': fields.many2many('product.attribute.value', string='Variants', help="BOM Product Variants needed form apply this line."),
397     }
398
399     def _get_uom_id(self, cr, uid, *args):
400         return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
401     _defaults = {
402         'product_qty': lambda *a: 1.0,
403         'product_efficiency': lambda *a: 1.0,
404         'product_rounding': lambda *a: 0.0,
405         'type': lambda *a: 'normal',
406         'product_uom': _get_uom_id,
407     }
408     _sql_constraints = [
409         ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
410             'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
411     ]
412
413     def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
414         res = {'value': {}}
415         if not product_uom or not product_id:
416             return res
417         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
418         uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
419         if uom.category_id.id != product.uom_id.category_id.id:
420             res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
421             res['value'].update({'product_uom': product.uom_id.id})
422         return res
423
424     def onchange_product_id(self, cr, uid, ids, product_id, product_qty=0, context=None):
425         """ Changes UoM if product_id changes.
426         @param product_id: Changed product_id
427         @return:  Dictionary of changed values
428         """
429         res = {}
430         if product_id:
431             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
432             res['value'] = {
433                 'product_uom': prod.uom_id.id,
434                 'product_uos_qty': 0,
435                 'product_uos': False
436             }
437             if prod.uos_id.id:
438                 res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
439                 res['value']['product_uos'] = prod.uos_id.id
440         return res
441
442 class mrp_production(osv.osv):
443     """
444     Production Orders / Manufacturing Orders
445     """
446     _name = 'mrp.production'
447     _description = 'Manufacturing Order'
448     _date_name = 'date_planned'
449     _inherit = ['mail.thread', 'ir.needaction_mixin']
450
451     def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
452         """ Calculates total hours and total no. of cycles for a production order.
453         @param prop: Name of field.
454         @param unknow_none:
455         @return: Dictionary of values.
456         """
457         result = {}
458         for prod in self.browse(cr, uid, ids, context=context):
459             result[prod.id] = {
460                 'hour_total': 0.0,
461                 'cycle_total': 0.0,
462             }
463             for wc in prod.workcenter_lines:
464                 result[prod.id]['hour_total'] += wc.hour
465                 result[prod.id]['cycle_total'] += wc.cycle
466         return result
467
468     def _src_id_default(self, cr, uid, ids, context=None):
469         try:
470             location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
471             self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
472         except (orm.except_orm, ValueError):
473             location_id = False
474         return location_id
475
476     def _dest_id_default(self, cr, uid, ids, context=None):
477         try:
478             location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
479             self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
480         except (orm.except_orm, ValueError):
481             location_id = False
482         return location_id
483
484     def _get_progress(self, cr, uid, ids, name, arg, context=None):
485         """ Return product quantity percentage """
486         result = dict.fromkeys(ids, 100)
487         for mrp_production in self.browse(cr, uid, ids, context=context):
488             if mrp_production.product_qty:
489                 done = 0.0
490                 for move in mrp_production.move_created_ids2:
491                     if not move.scrapped and move.product_id == mrp_production.product_id:
492                         done += move.product_qty
493                 result[mrp_production.id] = done / mrp_production.product_qty * 100
494         return result
495
496     def _moves_assigned(self, cr, uid, ids, name, arg, context=None):
497         """ Test whether all the consume lines are assigned """
498         res = {}
499         for production in self.browse(cr, uid, ids, context=context):
500             res[production.id] = True
501             states = [x.state != 'assigned' for x in production.move_lines if x]
502             if any(states) or len(states) == 0:
503                 res[production.id] = False
504         return res
505
506     def _mrp_from_move(self, cr, uid, ids, context=None):
507         """ Return mrp"""
508         res = []
509         for move in self.browse(cr, uid, ids, context=context):
510             res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
511         return res
512
513     _columns = {
514         'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
515         'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
516             help="Reference of the document that generated this production order request."),
517         'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
518             select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
519
520         'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
521         'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
522         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
523         'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
524         'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
525         'progress': fields.function(_get_progress, type='float',
526             string='Production progress'),
527
528         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
529             readonly=True, states={'draft': [('readonly', False)]},
530             help="Location where the system will look for components."),
531         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
532             readonly=True, states={'draft': [('readonly', False)]},
533             help="Location where the system will stock the finished products."),
534         'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}),
535         'date_start': fields.datetime('Start Date', select=True, readonly=True),
536         'date_finished': fields.datetime('End Date', select=True, readonly=True),
537         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', readonly=True, states={'draft': [('readonly', False)]},
538             help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
539         'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
540             help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production plannification."),
541         'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
542         'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
543             domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
544         'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
545             domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
546         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
547             domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
548         'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
549             domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
550         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
551             readonly=True),
552         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
553             readonly=True, states={'draft': [('readonly', False)]}),
554         'state': fields.selection(
555             [('draft', 'New'), ('cancel', 'Cancelled'), ('confirmed', 'Awaiting Raw Materials'),
556                 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
557             string='Status', readonly=True,
558             track_visibility='onchange',
559             help="When the production order is created the status is set to 'Draft'.\n\
560                 If the order is confirmed the status is set to 'Waiting Goods'.\n\
561                 If any exceptions are there, the status is set to 'Picking Exception'.\n\
562                 If the stock is available then the status is set to 'Ready to Produce'.\n\
563                 When the production gets started then the status is set to 'In Production'.\n\
564                 When the production is over, the status is set to 'Done'."),
565         'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
566         'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
567         'user_id': fields.many2one('res.users', 'Responsible'),
568         'company_id': fields.many2one('res.company', 'Company', required=True),
569         'ready_production': fields.function(_moves_assigned, type='boolean', store={'stock.move': (_mrp_from_move, ['state'], 10)}),
570     }
571
572     _defaults = {
573         'priority': lambda *a: '1',
574         'state': lambda *a: 'draft',
575         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
576         'product_qty': lambda *a: 1.0,
577         'user_id': lambda self, cr, uid, c: uid,
578         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
579         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
580         'location_src_id': _src_id_default,
581         'location_dest_id': _dest_id_default
582     }
583
584     _sql_constraints = [
585         ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
586     ]
587
588     _order = 'priority desc, date_planned asc'
589
590     def _check_qty(self, cr, uid, ids, context=None):
591         for order in self.browse(cr, uid, ids, context=context):
592             if order.product_qty <= 0:
593                 return False
594         return True
595
596     _constraints = [
597         (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
598     ]
599
600     def unlink(self, cr, uid, ids, context=None):
601         for production in self.browse(cr, uid, ids, context=context):
602             if production.state not in ('draft', 'cancel'):
603                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
604         return super(mrp_production, self).unlink(cr, uid, ids, context=context)
605
606     def copy(self, cr, uid, id, default=None, context=None):
607         if default is None:
608             default = {}
609         default.update({
610             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
611             'move_lines': [],
612             'move_lines2': [],
613             'move_created_ids': [],
614             'move_created_ids2': [],
615             'product_lines': [],
616             'move_prod_id': False,
617         })
618         return super(mrp_production, self).copy(cr, uid, id, default, context)
619
620     def location_id_change(self, cr, uid, ids, src, dest, context=None):
621         """ Changes destination location if source location is changed.
622         @param src: Source location id.
623         @param dest: Destination location id.
624         @return: Dictionary of values.
625         """
626         if dest:
627             return {}
628         if src:
629             return {'value': {'location_dest_id': src}}
630         return {}
631
632     def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
633         """ Finds UoM of changed product.
634         @param product_id: Id of changed product.
635         @return: Dictionary of values.
636         """
637         result = {}
638         if not product_id:
639             return {'value': {
640                 'product_uom': False,
641                 'bom_id': False,
642                 'routing_id': False,
643                 'product_uos_qty': 0,
644                 'product_uos': False
645             }}
646         bom_obj = self.pool.get('mrp.bom')
647         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
648         bom_id = bom_obj._bom_find(cr, uid, product.uom_id and product.uom_id.id, product_id=product.id, properties=[])
649         routing_id = False
650         if bom_id:
651             bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
652             routing_id = bom_point.routing_id.id or False
653         product_uom_id = product.uom_id and product.uom_id.id or False
654         result['value'] = {'product_uos_qty': 0, 'product_uos': False, 'product_uom': product_uom_id, 'bom_id': bom_id, 'routing_id': routing_id}
655         if product.uos_id.id:
656             result['value']['product_uos_qty'] = product_qty * product.uos_coeff
657             result['value']['product_uos'] = product.uos_id.id
658         return result
659
660     def bom_id_change(self, cr, uid, ids, bom_id, context=None):
661         """ Finds routing for changed BoM.
662         @param product: Id of product.
663         @return: Dictionary of values.
664         """
665         if not bom_id:
666             return {'value': {
667                 'routing_id': False
668             }}
669         bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
670         routing_id = bom_point.routing_id.id or False
671         result = {
672             'routing_id': routing_id
673         }
674         return {'value': result}
675
676
677     def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
678         """ Compute product_lines and workcenter_lines from BoM structure
679         @return: product_lines
680         """
681         if properties is None:
682             properties = []
683         results = []
684         bom_obj = self.pool.get('mrp.bom')
685         uom_obj = self.pool.get('product.uom')
686         prod_line_obj = self.pool.get('mrp.production.product.line')
687         workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
688         for production in self.browse(cr, uid, ids, context=context):
689             #unlink product_lines
690             prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
691             #unlink workcenter_lines
692             workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
693             # search BoM structure and route
694             bom_point = production.bom_id
695             bom_id = production.bom_id.id
696             if not bom_point:
697                 bom_id = bom_obj._bom_find(cr, uid, production.product_uom.id, product_id=production.product_id.id, properties=properties)
698                 if bom_id:
699                     bom_point = bom_obj.browse(cr, uid, bom_id)
700                     routing_id = bom_point.routing_id.id or False
701                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
702     
703             if not bom_id:
704                 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
705
706             # get components and workcenter_lines from BoM structure
707             factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
708             # product_lines, workcenter_lines
709             results, results2 = bom_obj._bom_explode(cr, uid, bom_point, production.product_id, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
710             # reset product_lines in production order
711             for line in results:
712                 line['production_id'] = production.id
713                 prod_line_obj.create(cr, uid, line)
714
715             #reset workcenter_lines in production order
716             for line in results2:
717                 line['production_id'] = production.id
718                 workcenter_line_obj.create(cr, uid, line)
719         return results
720
721     def action_compute(self, cr, uid, ids, properties=None, context=None):
722         """ Computes bills of material of a product.
723         @param properties: List containing dictionaries of properties.
724         @return: No. of products.
725         """
726         return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
727
728     def action_cancel(self, cr, uid, ids, context=None):
729         """ Cancels the production order and related stock moves.
730         @return: True
731         """
732         if context is None:
733             context = {}
734         move_obj = self.pool.get('stock.move')
735         for production in self.browse(cr, uid, ids, context=context):
736             if production.move_created_ids:
737                 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
738             move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
739         self.write(cr, uid, ids, {'state': 'cancel'})
740         # Put related procurements in exception
741         proc_obj = self.pool.get("procurement.order")
742         procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
743         if procs:
744             proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
745         return True
746
747     def action_ready(self, cr, uid, ids, context=None):
748         """ Changes the production state to Ready and location id of stock move.
749         @return: True
750         """
751         move_obj = self.pool.get('stock.move')
752         self.write(cr, uid, ids, {'state': 'ready'})
753
754         for production in self.browse(cr, uid, ids, context=context):
755             if not production.move_created_ids:
756                 self._make_production_produce_line(cr, uid, production, context=context)
757
758             if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
759                 move_obj.write(cr, uid, [production.move_prod_id.id],
760                         {'location_id': production.location_dest_id.id})
761         return True
762
763     def action_production_end(self, cr, uid, ids, context=None):
764         """ Changes production state to Finish and writes finished date.
765         @return: True
766         """
767         for production in self.browse(cr, uid, ids):
768             self._costs_generate(cr, uid, production)
769         write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
770         # Check related procurements
771         proc_obj = self.pool.get("procurement.order")
772         procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
773         proc_obj.check(cr, uid, procs, context=context)
774         return write_res
775
776     def test_production_done(self, cr, uid, ids):
777         """ Tests whether production is done or not.
778         @return: True or False
779         """
780         res = True
781         for production in self.browse(cr, uid, ids):
782             if production.move_lines:
783                 res = False
784
785             if production.move_created_ids:
786                 res = False
787         return res
788
789     def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
790         """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
791             it's always equal to the quantity encoded in the production order or the production wizard, but if the
792             module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
793             and its quantity.
794         :param production_id: ID of the mrp.order
795         :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
796         :return: The factor to apply to the quantity that we should produce for the given production order.
797         """
798         return 1
799
800     def _get_produced_qty(self, cr, uid, production, context=None):
801         ''' returns the produced quantity of product 'production.product_id' for the given production, in the product UoM
802         '''
803         produced_qty = 0
804         for produced_product in production.move_created_ids2:
805             if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
806                 continue
807             produced_qty += produced_product.product_qty
808         return produced_qty
809
810     def _get_consumed_data(self, cr, uid, production, context=None):
811         ''' returns a dictionary containing for each raw material of the given production, its quantity already consumed (in the raw material UoM)
812         '''
813         consumed_data = {}
814         # Calculate already consumed qtys
815         for consumed in production.move_lines2:
816             if consumed.scrapped:
817                 continue
818             if not consumed_data.get(consumed.product_id.id, False):
819                 consumed_data[consumed.product_id.id] = 0
820             consumed_data[consumed.product_id.id] += consumed.product_qty
821         return consumed_data
822
823     def _calculate_qty(self, cr, uid, production, product_qty=0.0, context=None):
824         """
825             Calculates the quantity still needed to produce an extra number of products
826         """
827         quant_obj = self.pool.get("stock.quant")
828         produced_qty = self._get_produced_qty(cr, uid, production, context=context)
829         consumed_data = self._get_consumed_data(cr, uid, production, context=context)
830
831         #In case no product_qty is given, take the remaining qty to produce for the given production
832         if not product_qty:
833             product_qty = production.product_qty - produced_qty
834
835         dicts = {}
836         # Find product qty to be consumed and consume it
837         for scheduled in production.product_lines:
838             product_id = scheduled.product_id.id
839
840             consumed_qty = consumed_data.get(product_id, 0.0)
841             
842             # qty available for consume and produce
843             qty_avail = scheduled.product_qty - consumed_qty
844             if qty_avail <= 0.0:
845                 # there will be nothing to consume for this raw material
846                 continue
847
848             if not dicts.get(product_id):
849                 dicts[product_id] = {}
850
851             # total qty of consumed product we need after this consumption
852             total_consume = ((product_qty + produced_qty) * scheduled.product_qty / production.product_qty)
853             qty = total_consume - consumed_qty
854
855             # Search for quants related to this related move
856             for move in production.move_lines:
857                 if qty <= 0.0:
858                     break
859                 if move.product_id.id != product_id:
860                     continue
861
862                 q = min(move.product_qty, qty)
863                 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, scheduled.product_id, q, domain=[('qty', '>', 0.0)],
864                                                      prefered_domain_list=[[('reservation_id', '=', move.id)], [('reservation_id', '=', False)]], context=context)
865                 for quant, quant_qty in quants:
866                     if quant:
867                         lot_id = quant.lot_id.id
868                         if not product_id in dicts.keys():
869                             dicts[product_id] = {lot_id: quant_qty}
870                         elif lot_id in dicts[product_id].keys():
871                             dicts[product_id][lot_id] += quant_qty
872                         else:
873                             dicts[product_id][lot_id] = quant_qty
874                         qty -= quant_qty
875             if qty > 0:
876                 if dicts[product_id].get(False):
877                     dicts[product_id][False] += qty
878                 else:
879                     dicts[product_id][False] = qty
880
881         consume_lines = []
882         for prod in dicts.keys():
883             for lot, qty in dicts[prod].items():
884                 consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
885         return consume_lines
886
887     def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
888         """ To produce final product based on production mode (consume/consume&produce).
889         If Production mode is consume, all stock move lines of raw materials will be done/consumed.
890         If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
891         and stock move lines of final product will be also done/produced.
892         @param production_id: the ID of mrp.production object
893         @param production_qty: specify qty to produce
894         @param production_mode: specify production mode (consume/consume&produce).
895         @param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
896         @return: True
897         """
898         stock_mov_obj = self.pool.get('stock.move')
899         production = self.browse(cr, uid, production_id, context=context)
900         if not production.move_lines and production.state == 'ready':
901             # trigger workflow if not products to consume (eg: services)
902             self.signal_button_produce(cr, uid, [production_id])
903
904         produced_qty = self._get_produced_qty(cr, uid, production, context=context)
905
906         main_production_move = False
907         if production_mode == 'consume_produce':
908             # To produce remaining qty of final product
909             #vals = {'state':'confirmed'}
910             #final_product_todo = [x.id for x in production.move_created_ids]
911             #stock_mov_obj.write(cr, uid, final_product_todo, vals)
912             #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
913             produced_products = {}
914             for produced_product in production.move_created_ids2:
915                 if produced_product.scrapped:
916                     continue
917                 if not produced_products.get(produced_product.product_id.id, False):
918                     produced_products[produced_product.product_id.id] = 0
919                 produced_products[produced_product.product_id.id] += produced_product.product_qty
920
921             for produce_product in production.move_created_ids:
922                 produced_qty = produced_products.get(produce_product.product_id.id, 0)
923                 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
924                 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
925                 if float_compare(rest_qty, (subproduct_factor * production_qty), precision_rounding=produce_product.product_id.uom_id.rounding) < 0:
926                     prod_name = produce_product.product_id.name_get()[0][1]
927                     raise osv.except_osv(_('Warning!'), _('You are going to produce total %s quantities of "%s".\nBut you can only produce up to total %s quantities.') % ((subproduct_factor * production_qty), prod_name, rest_qty))
928                 if float_compare(rest_qty, 0, precision_rounding=produce_product.product_id.uom_id.rounding) > 0:
929                     lot_id = False
930                     if wiz:
931                         lot_id = wiz.lot_id.id
932                     new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
933                     stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
934                     if produce_product.product_id.id == production.product_id.id and new_moves:
935                         main_production_move = new_moves[0]
936
937         if production_mode in ['consume', 'consume_produce']:
938             if wiz:
939                 consume_lines = []
940                 for cons in wiz.consume_lines:
941                     consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
942             else:
943                 consume_lines = self._calculate_qty(cr, uid, production, production_qty, context=context)
944             for consume in consume_lines:
945                 remaining_qty = consume['product_qty']
946                 for raw_material_line in production.move_lines:
947                     if remaining_qty <= 0:
948                         break
949                     if consume['product_id'] != raw_material_line.product_id.id:
950                         continue
951                     consumed_qty = min(remaining_qty, raw_material_line.product_qty)
952                     stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id, restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
953                     remaining_qty -= consumed_qty
954                 if remaining_qty:
955                     #consumed more in wizard than previously planned
956                     product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context)
957                     extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context)
958                     if extra_move_id:
959                         stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
960
961         self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
962         self.signal_button_produce_done(cr, uid, [production_id])
963         return True
964
965     def _costs_generate(self, cr, uid, production):
966         """ Calculates total costs at the end of the production.
967         @param production: Id of production order.
968         @return: Calculated amount.
969         """
970         amount = 0.0
971         analytic_line_obj = self.pool.get('account.analytic.line')
972         for wc_line in production.workcenter_lines:
973             wc = wc_line.workcenter_id
974             if wc.costs_journal_id and wc.costs_general_account_id:
975                 # Cost per hour
976                 value = wc_line.hour * wc.costs_hour
977                 account = wc.costs_hour_account_id.id
978                 if value and account:
979                     amount += value
980                     # we user SUPERUSER_ID as we do not garantee an mrp user
981                     # has access to account analytic lines but still should be
982                     # able to produce orders
983                     analytic_line_obj.create(cr, SUPERUSER_ID, {
984                         'name': wc_line.name + ' (H)',
985                         'amount': value,
986                         'account_id': account,
987                         'general_account_id': wc.costs_general_account_id.id,
988                         'journal_id': wc.costs_journal_id.id,
989                         'ref': wc.code,
990                         'product_id': wc.product_id.id,
991                         'unit_amount': wc_line.hour,
992                         'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
993                     })
994                 # Cost per cycle
995                 value = wc_line.cycle * wc.costs_cycle
996                 account = wc.costs_cycle_account_id.id
997                 if value and account:
998                     amount += value
999                     analytic_line_obj.create(cr, SUPERUSER_ID, {
1000                         'name': wc_line.name + ' (C)',
1001                         'amount': value,
1002                         'account_id': account,
1003                         'general_account_id': wc.costs_general_account_id.id,
1004                         'journal_id': wc.costs_journal_id.id,
1005                         'ref': wc.code,
1006                         'product_id': wc.product_id.id,
1007                         'unit_amount': wc_line.cycle,
1008                         'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
1009                     })
1010         return amount
1011
1012     def action_in_production(self, cr, uid, ids, context=None):
1013         """ Changes state to In Production and writes starting date.
1014         @return: True
1015         """
1016         return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
1017
1018     def consume_lines_get(self, cr, uid, ids, *args):
1019         res = []
1020         for order in self.browse(cr, uid, ids, context={}):
1021             res += [x.id for x in order.move_lines]
1022         return res
1023
1024     def test_ready(self, cr, uid, ids):
1025         res = False
1026         for production in self.browse(cr, uid, ids):
1027             if production.ready_production:
1028                 res = True
1029         return res
1030     
1031     
1032     def _make_production_produce_line(self, cr, uid, production, context=None):
1033         stock_move = self.pool.get('stock.move')
1034         source_location_id = production.product_id.property_stock_production.id
1035         destination_location_id = production.location_dest_id.id
1036         data = {
1037             'name': production.name,
1038             'date': production.date_planned,
1039             'product_id': production.product_id.id,
1040             'product_uom': production.product_uom.id,
1041             'product_uom_qty': production.product_qty,
1042             'product_uos_qty': production.product_uos and production.product_uos_qty or False,
1043             'product_uos': production.product_uos and production.product_uos.id or False,
1044             'location_id': source_location_id,
1045             'location_dest_id': destination_location_id,
1046             'move_dest_id': production.move_prod_id.id,
1047             'company_id': production.company_id.id,
1048             'production_id': production.id,
1049             'origin': production.name,
1050         }
1051         move_id = stock_move.create(cr, uid, data, context=context)
1052         #a phantom bom cannot be used in mrp order so it's ok to assume the list returned by action_confirm
1053         #is 1 element long, so we can take the first.
1054         return stock_move.action_confirm(cr, uid, [move_id], context=context)[0]
1055
1056     def _get_raw_material_procure_method(self, cr, uid, product, context=None):
1057         '''This method returns the procure_method to use when creating the stock move for the production raw materials'''
1058         try:
1059             mto_route = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1]
1060         except:
1061             return "make_to_stock"
1062         routes = product.route_ids + product.categ_id.total_route_ids
1063         if mto_route in [x.id for x in routes]:
1064             return "make_to_order"
1065         return "make_to_stock"
1066
1067     def _make_consume_line_from_data(self, cr, uid, production, product, uom_id, qty, uos_id, uos_qty, context=None):
1068         stock_move = self.pool.get('stock.move')
1069         # Internal shipment is created for Stockable and Consumer Products
1070         if product.type not in ('product', 'consu'):
1071             return False
1072         # Take routing location as a Source Location.
1073         source_location_id = production.location_src_id.id
1074         if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1075             source_location_id = production.bom_id.routing_id.location_id.id
1076
1077         destination_location_id = production.product_id.property_stock_production.id
1078         if not source_location_id:
1079             source_location_id = production.location_src_id.id
1080         move_id = stock_move.create(cr, uid, {
1081             'name': production.name,
1082             'date': production.date_planned,
1083             'product_id': product.id,
1084             'product_uom_qty': qty,
1085             'product_uom': uom_id,
1086             'product_uos_qty': uos_id and uos_qty or False,
1087             'product_uos': uos_id or False,
1088             'location_id': source_location_id,
1089             'location_dest_id': destination_location_id,
1090             'company_id': production.company_id.id,
1091             'procure_method': self._get_raw_material_procure_method(cr, uid, product, context=context),
1092             'raw_material_production_id': production.id,
1093             #this saves us a browse in create()
1094             'price_unit': product.standard_price,
1095             'origin': production.name,
1096         })
1097         return move_id
1098
1099     def _make_production_consume_line(self, cr, uid, line, context=None):
1100         return self._make_consume_line_from_data(cr, uid, line.production_id, line.product_id, line.product_uom.id, line.product_qty, line.product_uos.id, line.product_uos_qty, context=context)
1101
1102     def action_confirm(self, cr, uid, ids, context=None):
1103         """ Confirms production order.
1104         @return: Newly generated Shipment Id.
1105         """
1106         uncompute_ids = filter(lambda x: x, [not x.product_lines and x.id or False for x in self.browse(cr, uid, ids, context=context)])
1107         self.action_compute(cr, uid, uncompute_ids, context=context)
1108         for production in self.browse(cr, uid, ids, context=context):
1109             self._make_production_produce_line(cr, uid, production, context=context)
1110
1111             stock_moves = []
1112             for line in production.product_lines:
1113                 stock_move_id = self._make_production_consume_line(cr, uid, line, context=context)
1114                 if stock_move_id:
1115                     stock_moves.append(stock_move_id)
1116             if stock_moves:
1117                 self.pool.get('stock.move').action_confirm(cr, uid, stock_moves, context=context)
1118             production.write({'state': 'confirmed'}, context=context)
1119         return 0
1120
1121     def action_assign(self, cr, uid, ids, context=None):
1122         """
1123         Checks the availability on the consume lines of the production order
1124         """
1125         move_obj = self.pool.get("stock.move")
1126         for production in self.browse(cr, uid, ids, context=context):
1127             move_obj.action_assign(cr, uid, [x.id for x in production.move_lines], context=context)
1128
1129
1130     def force_production(self, cr, uid, ids, *args):
1131         """ Assigns products.
1132         @param *args: Arguments
1133         @return: True
1134         """
1135         move_obj = self.pool.get('stock.move')
1136         for order in self.browse(cr, uid, ids):
1137             move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
1138         return True
1139
1140
1141 class mrp_production_workcenter_line(osv.osv):
1142     _name = 'mrp.production.workcenter.line'
1143     _description = 'Work Order'
1144     _order = 'sequence'
1145     _inherit = ['mail.thread']
1146
1147     _columns = {
1148         'name': fields.char('Work Order', size=64, required=True),
1149         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1150         'cycle': fields.float('Number of Cycles', digits=(16, 2)),
1151         'hour': fields.float('Number of Hours', digits=(16, 2)),
1152         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1153         'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1154             track_visibility='onchange', select=True, ondelete='cascade', required=True),
1155     }
1156     _defaults = {
1157         'sequence': lambda *a: 1,
1158         'hour': lambda *a: 0,
1159         'cycle': lambda *a: 0,
1160     }
1161
1162 class mrp_production_product_line(osv.osv):
1163     _name = 'mrp.production.product.line'
1164     _description = 'Production Scheduled Product'
1165     _columns = {
1166         'name': fields.char('Name', size=64, required=True),
1167         'product_id': fields.many2one('product.product', 'Product', required=True),
1168         'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1169         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1170         'product_uos_qty': fields.float('Product UOS Quantity'),
1171         'product_uos': fields.many2one('product.uom', 'Product UOS'),
1172         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1173     }
1174
1175 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: