[MERGE] forward port of branch 8.0 up to e883193
[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', 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', 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', 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', copy=True),
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', 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     _columns = {
159         'name': fields.char('Name'),
160         'code': fields.char('Reference', size=16),
161         '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."),
162         'type': fields.selection([('normal','Manufacture this product as a normal bill of material'),('phantom','Sell and ship this product as a set of components(phantom)')], 'BoM Type', required=True,
163                 help= "Set: When processing a sales order for this product, the delivery order will contain the raw materials, instead of the finished product."),
164         'position': fields.char('Internal Reference', help="Reference to a position in an external plan."),
165         'product_tmpl_id': fields.many2one('product.template', 'Product', domain="[('type', '!=', 'service')]", required=True),
166         'product_id': fields.many2one('product.product', 'Product Variant',
167             domain="['&', ('product_tmpl_id','=',product_tmpl_id), ('type','!=', 'service')]",
168             help="If a product variant is defined the BOM is available only for this product."),
169         'bom_line_ids': fields.one2many('mrp.bom.line', 'bom_id', 'BoM Lines', copy=True),
170         'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
171         '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"),
172         'date_start': fields.date('Valid From', help="Validity of this BoM. Keep empty if it's always valid."),
173         'date_stop': fields.date('Valid Until', help="Validity of this BoM. Keep empty if it's always valid."),
174         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
175         'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. "\
176                 "The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
177         'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
178         'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% during the production process."),
179         'property_ids': fields.many2many('mrp.property', string='Properties'),
180         'company_id': fields.many2one('res.company', 'Company', required=True),
181     }
182
183     def _get_uom_id(self, cr, uid, *args):
184         return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
185     _defaults = {
186         'active': lambda *a: 1,
187         'product_qty': lambda *a: 1.0,
188         'product_efficiency': lambda *a: 1.0,
189         'product_rounding': lambda *a: 0.0,
190         'type': lambda *a: 'normal',
191         'product_uom': _get_uom_id,
192         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
193     }
194     _order = "sequence"
195
196     def _bom_find(self, cr, uid, product_tmpl_id=None, product_id=None, properties=None, context=None):
197         """ Finds BoM for particular product and product uom.
198         @param product_tmpl_id: Selected product.
199         @param product_uom: Unit of measure of a product.
200         @param properties: List of related properties.
201         @return: False or BoM id.
202         """
203         if properties is None:
204             properties = []
205         if product_id:
206             if not product_tmpl_id:
207                 product_tmpl_id = self.pool['product.product'].browse(cr, uid, product_id, context=context).product_tmpl_id.id
208             domain = [
209                 '|',
210                     ('product_id', '=', product_id),
211                     '&',
212                         ('product_id', '=', False),
213                         ('product_tmpl_id', '=', product_tmpl_id)
214             ]
215         elif product_tmpl_id:
216             domain = [('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl_id)]
217         else:
218             # neither product nor template, makes no sense to search
219             return False
220         domain = domain + [ '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
221                             '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
222         # order to prioritize bom with product_id over the one without
223         ids = self.search(cr, uid, domain, order='product_id', context=context)
224         # Search a BoM which has all properties specified, or if you can not find one, you could
225         # pass a BoM without any properties
226         bom_empty_prop = False
227         for bom in self.pool.get('mrp.bom').browse(cr, uid, ids, context=context):
228             if not set(map(int, bom.property_ids or [])) - set(properties or []):
229                 if properties and not bom.property_ids:
230                     bom_empty_prop = bom.id
231                 else:
232                     return bom.id
233         return bom_empty_prop
234
235     def _bom_explode(self, cr, uid, bom, product, factor, properties=None, level=0, routing_id=False, previous_products=None, master_bom=None, context=None):
236         """ Finds Products and Work Centers for related BoM for manufacturing order.
237         @param bom: BoM of particular product template.
238         @param product: Select a particular variant of the BoM. If False use BoM without variants.
239         @param factor: Factor represents the quantity, but in UoM of the BoM, taking into account the numbers produced by the BoM
240         @param properties: A List of properties Ids.
241         @param level: Depth level to find BoM lines starts from 10.
242         @param previous_products: List of product previously use by bom explore to avoid recursion
243         @param master_bom: When recursion, used to display the name of the master bom
244         @return: result: List of dictionaries containing product details.
245                  result2: List of dictionaries containing Work Center details.
246         """
247         uom_obj = self.pool.get("product.uom")
248         routing_obj = self.pool.get('mrp.routing')
249         master_bom = master_bom or bom
250
251
252         def _factor(factor, product_efficiency, product_rounding):
253             factor = factor / (product_efficiency or 1.0)
254             factor = _common.ceiling(factor, product_rounding)
255             if factor < product_rounding:
256                 factor = product_rounding
257             return factor
258
259         factor = _factor(factor, bom.product_efficiency, bom.product_rounding)
260
261         result = []
262         result2 = []
263
264         routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
265         if routing:
266             for wc_use in routing.workcenter_lines:
267                 wc = wc_use.workcenter_id
268                 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
269                 mult = (d + (m and 1.0 or 0.0))
270                 cycle = mult * wc_use.cycle_nbr
271                 result2.append({
272                     'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_tmpl_id.name_get()[0][1]),
273                     'workcenter_id': wc.id,
274                     'sequence': level + (wc_use.sequence or 0),
275                     'cycle': cycle,
276                     '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)),
277                 })
278
279         for bom_line_id in bom.bom_line_ids:
280             if bom_line_id.date_start and bom_line_id.date_start > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) or \
281                 bom_line_id.date_stop and bom_line_id.date_stop > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT):
282                     continue
283             # all bom_line_id variant values must be in the product
284             if bom_line_id.attribute_value_ids:
285                 if not product or (set(map(int,bom_line_id.attribute_value_ids or [])) - set(map(int,product.attribute_value_ids))):
286                     continue
287
288             if previous_products and bom_line_id.product_id.product_tmpl_id.id in previous_products:
289                 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]))
290
291             quantity = _factor(bom_line_id.product_qty * factor, bom_line_id.product_efficiency, bom_line_id.product_rounding)
292             bom_id = self._bom_find(cr, uid, product_id=bom_line_id.product_id.id, properties=properties, context=context)
293
294             #If BoM should not behave like PhantoM, just add the product, otherwise explode further
295             if bom_line_id.type != "phantom" and (not bom_id or self.browse(cr, uid, bom_id, context=context).type != "phantom"):
296                 result.append({
297                     'name': bom_line_id.product_id.name,
298                     'product_id': bom_line_id.product_id.id,
299                     'product_qty': quantity,
300                     'product_uom': bom_line_id.product_uom.id,
301                     'product_uos_qty': bom_line_id.product_uos and _factor(bom_line_id.product_uos_qty * factor, bom_line_id.product_efficiency, bom_line_id.product_rounding) or False,
302                     'product_uos': bom_line_id.product_uos and bom_line_id.product_uos.id or False,
303                 })
304             elif bom_id:
305                 all_prod = [bom.product_tmpl_id.id] + (previous_products or [])
306                 bom2 = self.browse(cr, uid, bom_id, context=context)
307                 # We need to convert to units/UoM of chosen BoM
308                 factor2 = uom_obj._compute_qty(cr, uid, bom_line_id.product_uom.id, quantity, bom2.product_uom.id)
309                 quantity2 = factor2 / bom2.product_qty
310                 res = self._bom_explode(cr, uid, bom2, bom_line_id.product_id, quantity2,
311                     properties=properties, level=level + 10, previous_products=all_prod, master_bom=master_bom, context=context)
312                 result = result + res[0]
313                 result2 = result2 + res[1]
314             else:
315                 raise osv.except_osv(_('Invalid Action!'), _('BoM "%s" contains a phantom BoM line but the product "%s" does not have any BoM defined.') % (master_bom.name,bom_line_id.product_id.name_get()[0][1]))
316
317         return result, result2
318
319     def copy_data(self, cr, uid, id, default=None, context=None):
320         if default is None:
321             default = {}
322         bom_data = self.read(cr, uid, id, [], context=context)
323         default.update(name=_("%s (copy)") % (bom_data['name']))
324         return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
325
326     def onchange_uom(self, cr, uid, ids, product_tmpl_id, product_uom, context=None):
327         res = {'value': {}}
328         if not product_uom or not product_tmpl_id:
329             return res
330         product = self.pool.get('product.template').browse(cr, uid, product_tmpl_id, context=context)
331         uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
332         if uom.category_id.id != product.uom_id.category_id.id:
333             res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
334             res['value'].update({'product_uom': product.uom_id.id})
335         return res
336
337     def onchange_product_tmpl_id(self, cr, uid, ids, product_tmpl_id, product_qty=0, context=None):
338         """ Changes UoM and name if product_id changes.
339         @param product_id: Changed product_id
340         @return:  Dictionary of changed values
341         """
342         res = {}
343         if product_tmpl_id:
344             prod = self.pool.get('product.template').browse(cr, uid, product_tmpl_id, context=context)
345             res['value'] = {
346                 'name': prod.name,
347                 'product_uom': prod.uom_id.id,
348             }
349         return res
350
351 class mrp_bom_line(osv.osv):
352     _name = 'mrp.bom.line'
353     _order = "sequence"
354
355     def _get_child_bom_lines(self, cr, uid, ids, field_name, arg, context=None):
356         """If the BOM line refers to a BOM, return the ids of the child BOM lines"""
357         bom_obj = self.pool['mrp.bom']
358         res = {}
359         for bom_line in self.browse(cr, uid, ids, context=context):
360             bom_id = bom_obj._bom_find(cr, uid,
361                 product_tmpl_id=bom_line.product_id.product_tmpl_id.id,
362                 product_id=bom_line.product_id.id, context=context)
363             if bom_id:
364                 child_bom = bom_obj.browse(cr, uid, bom_id, context=context)
365                 res[bom_line.id] = [x.id for x in child_bom.bom_line_ids]
366             else:
367                 res[bom_line.id] = False
368         return res
369
370     _columns = {
371         'type': fields.selection([('normal', 'Normal'), ('phantom', 'Phantom')], 'BoM Line Type', required=True,
372                 help="Phantom: this product line will not appear in the raw materials of manufacturing orders,"
373                      "it will be directly replaced by the raw materials of its own BoM, without triggering"
374                      "an extra manufacturing order."),
375         'product_id': fields.many2one('product.product', 'Product', required=True),
376         'product_uos_qty': fields.float('Product UOS Qty'),
377         '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."),
378         'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
379         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True,
380             help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"),
381         
382         'date_start': fields.date('Valid From', help="Validity of component. Keep empty if it's always valid."),
383         'date_stop': fields.date('Valid Until', help="Validity of component. Keep empty if it's always valid."),
384         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying."),
385         '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."),
386         'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
387         'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
388         'property_ids': fields.many2many('mrp.property', string='Properties'), #Not used
389
390         'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True, required=True),
391         'attribute_value_ids': fields.many2many('product.attribute.value', string='Variants', help="BOM Product Variants needed form apply this line."),
392         'child_line_ids': fields.function(_get_child_bom_lines, relation="mrp.bom.line", string="BOM lines of the referred bom", type="one2many")
393     }
394
395     def _get_uom_id(self, cr, uid, *args):
396         return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
397     _defaults = {
398         'product_qty': lambda *a: 1.0,
399         'product_efficiency': lambda *a: 1.0,
400         'product_rounding': lambda *a: 0.0,
401         'type': lambda *a: 'normal',
402         'product_uom': _get_uom_id,
403     }
404     _sql_constraints = [
405         ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
406             'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
407     ]
408
409     def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
410         res = {'value': {}}
411         if not product_uom or not product_id:
412             return res
413         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
414         uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
415         if uom.category_id.id != product.uom_id.category_id.id:
416             res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
417             res['value'].update({'product_uom': product.uom_id.id})
418         return res
419
420     def onchange_product_id(self, cr, uid, ids, product_id, product_qty=0, context=None):
421         """ Changes UoM if product_id changes.
422         @param product_id: Changed product_id
423         @return:  Dictionary of changed values
424         """
425         res = {}
426         if product_id:
427             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
428             res['value'] = {
429                 'product_uom': prod.uom_id.id,
430                 'product_uos_qty': 0,
431                 'product_uos': False
432             }
433             if prod.uos_id.id:
434                 res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
435                 res['value']['product_uos'] = prod.uos_id.id
436         return res
437
438 class mrp_production(osv.osv):
439     """
440     Production Orders / Manufacturing Orders
441     """
442     _name = 'mrp.production'
443     _description = 'Manufacturing Order'
444     _date_name = 'date_planned'
445     _inherit = ['mail.thread', 'ir.needaction_mixin']
446
447     def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
448         """ Calculates total hours and total no. of cycles for a production order.
449         @param prop: Name of field.
450         @param unknow_none:
451         @return: Dictionary of values.
452         """
453         result = {}
454         for prod in self.browse(cr, uid, ids, context=context):
455             result[prod.id] = {
456                 'hour_total': 0.0,
457                 'cycle_total': 0.0,
458             }
459             for wc in prod.workcenter_lines:
460                 result[prod.id]['hour_total'] += wc.hour
461                 result[prod.id]['cycle_total'] += wc.cycle
462         return result
463
464     def _src_id_default(self, cr, uid, ids, context=None):
465         try:
466             location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
467             self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
468         except (orm.except_orm, ValueError):
469             location_id = False
470         return location_id
471
472     def _dest_id_default(self, cr, uid, ids, context=None):
473         try:
474             location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
475             self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
476         except (orm.except_orm, ValueError):
477             location_id = False
478         return location_id
479
480     def _get_progress(self, cr, uid, ids, name, arg, context=None):
481         """ Return product quantity percentage """
482         result = dict.fromkeys(ids, 100)
483         for mrp_production in self.browse(cr, uid, ids, context=context):
484             if mrp_production.product_qty:
485                 done = 0.0
486                 for move in mrp_production.move_created_ids2:
487                     if not move.scrapped and move.product_id == mrp_production.product_id:
488                         done += move.product_qty
489                 result[mrp_production.id] = done / mrp_production.product_qty * 100
490         return result
491
492     def _moves_assigned(self, cr, uid, ids, name, arg, context=None):
493         """ Test whether all the consume lines are assigned """
494         res = {}
495         for production in self.browse(cr, uid, ids, context=context):
496             res[production.id] = True
497             states = [x.state != 'assigned' for x in production.move_lines if x]
498             if any(states) or len(states) == 0: #When no moves, ready_production will be False, but test_ready will pass
499                 res[production.id] = False
500         return res
501
502     def _mrp_from_move(self, cr, uid, ids, context=None):
503         """ Return mrp"""
504         res = []
505         for move in self.browse(cr, uid, ids, context=context):
506             res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
507         return res
508
509     _columns = {
510         'name': fields.char('Reference', required=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
511         'origin': fields.char('Source Document', readonly=True, states={'draft': [('readonly', False)]},
512             help="Reference of the document that generated this production order request.", copy=False),
513         'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
514             select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
515
516         'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}, 
517                                       domain=[('type','!=','service')]),
518         'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
519         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
520         'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
521         'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
522         'progress': fields.function(_get_progress, type='float',
523             string='Production progress'),
524
525         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
526             readonly=True, states={'draft': [('readonly', False)]},
527             help="Location where the system will look for components."),
528         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
529             readonly=True, states={'draft': [('readonly', False)]},
530             help="Location where the system will stock the finished products."),
531         'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
532         'date_start': fields.datetime('Start Date', select=True, readonly=True, copy=False),
533         'date_finished': fields.datetime('End Date', select=True, readonly=True, copy=False),
534         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', readonly=True, states={'draft': [('readonly', False)]},
535             help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
536         'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
537             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."),
538         'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True, copy=False),
539         'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
540             domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
541         'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
542             domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
543         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
544             domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
545         'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
546             domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
547         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
548             readonly=True),
549         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
550             readonly=True, states={'draft': [('readonly', False)]}),
551         'state': fields.selection(
552             [('draft', 'New'), ('cancel', 'Cancelled'), ('confirmed', 'Awaiting Raw Materials'),
553                 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
554             string='Status', readonly=True,
555             track_visibility='onchange', copy=False,
556             help="When the production order is created the status is set to 'Draft'.\n\
557                 If the order is confirmed the status is set to 'Waiting Goods'.\n\
558                 If any exceptions are there, the status is set to 'Picking Exception'.\n\
559                 If the stock is available then the status is set to 'Ready to Produce'.\n\
560                 When the production gets started then the status is set to 'In Production'.\n\
561                 When the production is over, the status is set to 'Done'."),
562         'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
563         'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
564         'user_id': fields.many2one('res.users', 'Responsible'),
565         'company_id': fields.many2one('res.company', 'Company', required=True),
566         'ready_production': fields.function(_moves_assigned, type='boolean', store={'stock.move': (_mrp_from_move, ['state'], 10)}),
567     }
568
569     _defaults = {
570         'priority': lambda *a: '1',
571         'state': lambda *a: 'draft',
572         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
573         'product_qty': lambda *a: 1.0,
574         'user_id': lambda self, cr, uid, c: uid,
575         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
576         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
577         'location_src_id': _src_id_default,
578         'location_dest_id': _dest_id_default
579     }
580
581     _sql_constraints = [
582         ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
583     ]
584
585     _order = 'priority desc, date_planned asc'
586
587     def _check_qty(self, cr, uid, ids, context=None):
588         for order in self.browse(cr, uid, ids, context=context):
589             if order.product_qty <= 0:
590                 return False
591         return True
592
593     _constraints = [
594         (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
595     ]
596
597     def unlink(self, cr, uid, ids, context=None):
598         for production in self.browse(cr, uid, ids, context=context):
599             if production.state not in ('draft', 'cancel'):
600                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
601         return super(mrp_production, self).unlink(cr, uid, ids, context=context)
602
603     def location_id_change(self, cr, uid, ids, src, dest, context=None):
604         """ Changes destination location if source location is changed.
605         @param src: Source location id.
606         @param dest: Destination location id.
607         @return: Dictionary of values.
608         """
609         if dest:
610             return {}
611         if src:
612             return {'value': {'location_dest_id': src}}
613         return {}
614
615     def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
616         """ Finds UoM of changed product.
617         @param product_id: Id of changed product.
618         @return: Dictionary of values.
619         """
620         result = {}
621         if not product_id:
622             return {'value': {
623                 'product_uom': False,
624                 'bom_id': False,
625                 'routing_id': False,
626                 'product_uos_qty': 0,
627                 'product_uos': False
628             }}
629         bom_obj = self.pool.get('mrp.bom')
630         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
631         bom_id = bom_obj._bom_find(cr, uid, product_id=product.id, properties=[], context=context)
632         routing_id = False
633         if bom_id:
634             bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
635             routing_id = bom_point.routing_id.id or False
636         product_uom_id = product.uom_id and product.uom_id.id or False
637         result['value'] = {'product_uos_qty': 0, 'product_uos': False, 'product_uom': product_uom_id, 'bom_id': bom_id, 'routing_id': routing_id}
638         if product.uos_id.id:
639             result['value']['product_uos_qty'] = product_qty * product.uos_coeff
640             result['value']['product_uos'] = product.uos_id.id
641         return result
642
643     def bom_id_change(self, cr, uid, ids, bom_id, context=None):
644         """ Finds routing for changed BoM.
645         @param product: Id of product.
646         @return: Dictionary of values.
647         """
648         if not bom_id:
649             return {'value': {
650                 'routing_id': False
651             }}
652         bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
653         routing_id = bom_point.routing_id.id or False
654         result = {
655             'routing_id': routing_id
656         }
657         return {'value': result}
658
659
660     def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
661         """ Compute product_lines and workcenter_lines from BoM structure
662         @return: product_lines
663         """
664         if properties is None:
665             properties = []
666         results = []
667         bom_obj = self.pool.get('mrp.bom')
668         uom_obj = self.pool.get('product.uom')
669         prod_line_obj = self.pool.get('mrp.production.product.line')
670         workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
671         for production in self.browse(cr, uid, ids, context=context):
672             #unlink product_lines
673             prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
674             #unlink workcenter_lines
675             workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
676             # search BoM structure and route
677             bom_point = production.bom_id
678             bom_id = production.bom_id.id
679             if not bom_point:
680                 bom_id = bom_obj._bom_find(cr, uid, product_id=production.product_id.id, properties=properties, context=context)
681                 if bom_id:
682                     bom_point = bom_obj.browse(cr, uid, bom_id)
683                     routing_id = bom_point.routing_id.id or False
684                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
685     
686             if not bom_id:
687                 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
688
689             # get components and workcenter_lines from BoM structure
690             factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
691             # product_lines, workcenter_lines
692             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, context=context)
693
694             # reset product_lines in production order
695             for line in results:
696                 line['production_id'] = production.id
697                 prod_line_obj.create(cr, uid, line)
698
699             #reset workcenter_lines in production order
700             for line in results2:
701                 line['production_id'] = production.id
702                 workcenter_line_obj.create(cr, uid, line)
703         return results
704
705     def action_compute(self, cr, uid, ids, properties=None, context=None):
706         """ Computes bills of material of a product.
707         @param properties: List containing dictionaries of properties.
708         @return: No. of products.
709         """
710         return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
711
712     def action_cancel(self, cr, uid, ids, context=None):
713         """ Cancels the production order and related stock moves.
714         @return: True
715         """
716         if context is None:
717             context = {}
718         move_obj = self.pool.get('stock.move')
719         for production in self.browse(cr, uid, ids, context=context):
720             if production.move_created_ids:
721                 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
722             move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
723         self.write(cr, uid, ids, {'state': 'cancel'})
724         # Put related procurements in exception
725         proc_obj = self.pool.get("procurement.order")
726         procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
727         if procs:
728             proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
729         return True
730
731     def action_ready(self, cr, uid, ids, context=None):
732         """ Changes the production state to Ready and location id of stock move.
733         @return: True
734         """
735         move_obj = self.pool.get('stock.move')
736         self.write(cr, uid, ids, {'state': 'ready'})
737
738         for production in self.browse(cr, uid, ids, context=context):
739             if not production.move_created_ids:
740                 self._make_production_produce_line(cr, uid, production, context=context)
741
742             if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
743                 move_obj.write(cr, uid, [production.move_prod_id.id],
744                         {'location_id': production.location_dest_id.id})
745         return True
746
747     def action_production_end(self, cr, uid, ids, context=None):
748         """ Changes production state to Finish and writes finished date.
749         @return: True
750         """
751         for production in self.browse(cr, uid, ids):
752             self._costs_generate(cr, uid, production)
753         write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
754         # Check related procurements
755         proc_obj = self.pool.get("procurement.order")
756         procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
757         proc_obj.check(cr, uid, procs, context=context)
758         return write_res
759
760     def test_production_done(self, cr, uid, ids):
761         """ Tests whether production is done or not.
762         @return: True or False
763         """
764         res = True
765         for production in self.browse(cr, uid, ids):
766             if production.move_lines:
767                 res = False
768
769             if production.move_created_ids:
770                 res = False
771         return res
772
773     def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
774         """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
775             it's always equal to the quantity encoded in the production order or the production wizard, but if the
776             module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
777             and its quantity.
778         :param production_id: ID of the mrp.order
779         :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
780         :return: The factor to apply to the quantity that we should produce for the given production order.
781         """
782         return 1
783
784     def _get_produced_qty(self, cr, uid, production, context=None):
785         ''' returns the produced quantity of product 'production.product_id' for the given production, in the product UoM
786         '''
787         produced_qty = 0
788         for produced_product in production.move_created_ids2:
789             if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
790                 continue
791             produced_qty += produced_product.product_qty
792         return produced_qty
793
794     def _get_consumed_data(self, cr, uid, production, context=None):
795         ''' returns a dictionary containing for each raw material of the given production, its quantity already consumed (in the raw material UoM)
796         '''
797         consumed_data = {}
798         # Calculate already consumed qtys
799         for consumed in production.move_lines2:
800             if consumed.scrapped:
801                 continue
802             if not consumed_data.get(consumed.product_id.id, False):
803                 consumed_data[consumed.product_id.id] = 0
804             consumed_data[consumed.product_id.id] += consumed.product_qty
805         return consumed_data
806
807     def _calculate_qty(self, cr, uid, production, product_qty=0.0, context=None):
808         """
809             Calculates the quantity still needed to produce an extra number of products
810             product_qty is in the uom of the product
811         """
812         quant_obj = self.pool.get("stock.quant")
813         uom_obj = self.pool.get("product.uom")
814         produced_qty = self._get_produced_qty(cr, uid, production, context=context)
815         consumed_data = self._get_consumed_data(cr, uid, production, context=context)
816
817         #In case no product_qty is given, take the remaining qty to produce for the given production
818         if not product_qty:
819             product_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id) - produced_qty
820         production_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id)
821
822         scheduled_qty = {}
823         for scheduled in production.product_lines:
824             if scheduled.product_id.type == 'service':
825                 continue
826             qty = uom_obj._compute_qty(cr, uid, scheduled.product_uom.id, scheduled.product_qty, scheduled.product_id.uom_id.id)
827             if scheduled_qty.get(scheduled.product_id.id):
828                 scheduled_qty[scheduled.product_id.id] += qty
829             else:
830                 scheduled_qty[scheduled.product_id.id] = qty
831         dicts = {}
832         # Find product qty to be consumed and consume it
833         for product_id in scheduled_qty.keys():
834
835             consumed_qty = consumed_data.get(product_id, 0.0)
836             
837             # qty available for consume and produce
838             sched_product_qty = scheduled_qty[product_id]
839             qty_avail = sched_product_qty - consumed_qty
840             if qty_avail <= 0.0:
841                 # there will be nothing to consume for this raw material
842                 continue
843
844             if not dicts.get(product_id):
845                 dicts[product_id] = {}
846
847             # total qty of consumed product we need after this consumption
848             if product_qty + produced_qty <= production_qty:
849                 total_consume = ((product_qty + produced_qty) * sched_product_qty / production_qty)
850             else:
851                 total_consume = sched_product_qty
852             qty = total_consume - consumed_qty
853
854             # Search for quants related to this related move
855             for move in production.move_lines:
856                 if qty <= 0.0:
857                     break
858                 if move.product_id.id != product_id:
859                     continue
860
861                 q = min(move.product_qty, qty)
862                 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, q, domain=[('qty', '>', 0.0)],
863                                                      prefered_domain_list=[[('reservation_id', '=', move.id)]], context=context)
864                 for quant, quant_qty in quants:
865                     if quant:
866                         lot_id = quant.lot_id.id
867                         if not product_id in dicts.keys():
868                             dicts[product_id] = {lot_id: quant_qty}
869                         elif lot_id in dicts[product_id].keys():
870                             dicts[product_id][lot_id] += quant_qty
871                         else:
872                             dicts[product_id][lot_id] = quant_qty
873                         qty -= quant_qty
874             if qty > 0:
875                 if dicts[product_id].get(False):
876                     dicts[product_id][False] += qty
877                 else:
878                     dicts[product_id][False] = qty
879
880         consume_lines = []
881         for prod in dicts.keys():
882             for lot, qty in dicts[prod].items():
883                 consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
884         return consume_lines
885
886     def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
887         """ To produce final product based on production mode (consume/consume&produce).
888         If Production mode is consume, all stock move lines of raw materials will be done/consumed.
889         If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
890         and stock move lines of final product will be also done/produced.
891         @param production_id: the ID of mrp.production object
892         @param production_qty: specify qty to produce in the uom of the production order
893         @param production_mode: specify production mode (consume/consume&produce).
894         @param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
895         @return: True
896         """
897         stock_mov_obj = self.pool.get('stock.move')
898         uom_obj = self.pool.get("product.uom")
899         production = self.browse(cr, uid, production_id, context=context)
900         production_qty_uom = uom_obj._compute_qty(cr, uid, production.product_uom.id, production_qty, production.product_id.uom_id.id)
901
902         main_production_move = False
903         if production_mode == 'consume_produce':
904             # To produce remaining qty of final product
905             produced_products = {}
906             for produced_product in production.move_created_ids2:
907                 if produced_product.scrapped:
908                     continue
909                 if not produced_products.get(produced_product.product_id.id, False):
910                     produced_products[produced_product.product_id.id] = 0
911                 produced_products[produced_product.product_id.id] += produced_product.product_qty
912
913             for produce_product in production.move_created_ids:
914                 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
915                 lot_id = False
916                 if wiz:
917                     lot_id = wiz.lot_id.id
918                 new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty_uom),
919                                                          location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
920                 stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
921                 if produce_product.product_id.id == production.product_id.id and new_moves:
922                     main_production_move = new_moves[0]
923
924         if production_mode in ['consume', 'consume_produce']:
925             if wiz:
926                 consume_lines = []
927                 for cons in wiz.consume_lines:
928                     consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
929             else:
930                 consume_lines = self._calculate_qty(cr, uid, production, production_qty_uom, context=context)
931             for consume in consume_lines:
932                 remaining_qty = consume['product_qty']
933                 for raw_material_line in production.move_lines:
934                     if remaining_qty <= 0:
935                         break
936                     if consume['product_id'] != raw_material_line.product_id.id:
937                         continue
938                     consumed_qty = min(remaining_qty, raw_material_line.product_qty)
939                     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)
940                     remaining_qty -= consumed_qty
941                 if remaining_qty:
942                     #consumed more in wizard than previously planned
943                     product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context)
944                     extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context)
945                     if extra_move_id:
946                         stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
947
948         self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
949         self.signal_workflow(cr, uid, [production_id], 'button_produce_done')
950         return True
951
952     def _costs_generate(self, cr, uid, production):
953         """ Calculates total costs at the end of the production.
954         @param production: Id of production order.
955         @return: Calculated amount.
956         """
957         amount = 0.0
958         analytic_line_obj = self.pool.get('account.analytic.line')
959         for wc_line in production.workcenter_lines:
960             wc = wc_line.workcenter_id
961             if wc.costs_journal_id and wc.costs_general_account_id:
962                 # Cost per hour
963                 value = wc_line.hour * wc.costs_hour
964                 account = wc.costs_hour_account_id.id
965                 if value and account:
966                     amount += value
967                     # we user SUPERUSER_ID as we do not garantee an mrp user
968                     # has access to account analytic lines but still should be
969                     # able to produce orders
970                     analytic_line_obj.create(cr, SUPERUSER_ID, {
971                         'name': wc_line.name + ' (H)',
972                         'amount': value,
973                         'account_id': account,
974                         'general_account_id': wc.costs_general_account_id.id,
975                         'journal_id': wc.costs_journal_id.id,
976                         'ref': wc.code,
977                         'product_id': wc.product_id.id,
978                         'unit_amount': wc_line.hour,
979                         'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
980                     })
981                 # Cost per cycle
982                 value = wc_line.cycle * wc.costs_cycle
983                 account = wc.costs_cycle_account_id.id
984                 if value and account:
985                     amount += value
986                     analytic_line_obj.create(cr, SUPERUSER_ID, {
987                         'name': wc_line.name + ' (C)',
988                         'amount': value,
989                         'account_id': account,
990                         'general_account_id': wc.costs_general_account_id.id,
991                         'journal_id': wc.costs_journal_id.id,
992                         'ref': wc.code,
993                         'product_id': wc.product_id.id,
994                         'unit_amount': wc_line.cycle,
995                         'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
996                     })
997         return amount
998
999     def action_in_production(self, cr, uid, ids, context=None):
1000         """ Changes state to In Production and writes starting date.
1001         @return: True
1002         """
1003         return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
1004
1005     def consume_lines_get(self, cr, uid, ids, *args):
1006         res = []
1007         for order in self.browse(cr, uid, ids, context={}):
1008             res += [x.id for x in order.move_lines]
1009         return res
1010
1011     def test_ready(self, cr, uid, ids):
1012         res = True
1013         for production in self.browse(cr, uid, ids):
1014             if production.move_lines and not production.ready_production:
1015                 res = False
1016         return res
1017
1018     
1019     
1020     def _make_production_produce_line(self, cr, uid, production, context=None):
1021         stock_move = self.pool.get('stock.move')
1022         source_location_id = production.product_id.property_stock_production.id
1023         destination_location_id = production.location_dest_id.id
1024         data = {
1025             'name': production.name,
1026             'date': production.date_planned,
1027             'product_id': production.product_id.id,
1028             'product_uom': production.product_uom.id,
1029             'product_uom_qty': production.product_qty,
1030             'product_uos_qty': production.product_uos and production.product_uos_qty or False,
1031             'product_uos': production.product_uos and production.product_uos.id or False,
1032             'location_id': source_location_id,
1033             'location_dest_id': destination_location_id,
1034             'move_dest_id': production.move_prod_id.id,
1035             'company_id': production.company_id.id,
1036             'production_id': production.id,
1037             'origin': production.name,
1038         }
1039         move_id = stock_move.create(cr, uid, data, context=context)
1040         #a phantom bom cannot be used in mrp order so it's ok to assume the list returned by action_confirm
1041         #is 1 element long, so we can take the first.
1042         return stock_move.action_confirm(cr, uid, [move_id], context=context)[0]
1043
1044     def _get_raw_material_procure_method(self, cr, uid, product, context=None):
1045         '''This method returns the procure_method to use when creating the stock move for the production raw materials'''
1046         warehouse_obj = self.pool['stock.warehouse']
1047         try:
1048             mto_route = warehouse_obj._get_mto_route(cr, uid, context=context)
1049         except:
1050             return "make_to_stock"
1051         routes = product.route_ids + product.categ_id.total_route_ids
1052         if mto_route in [x.id for x in routes]:
1053             return "make_to_order"
1054         return "make_to_stock"
1055
1056     def _create_previous_move(self, cr, uid, move_id, product, source_location_id, dest_location_id, context=None):
1057         '''
1058         When the routing gives a different location than the raw material location of the production order, 
1059         we should create an extra move from the raw material location to the location of the routing, which 
1060         precedes the consumption line (chained).  The picking type depends on the warehouse in which this happens
1061         and the type of locations. 
1062         '''
1063         loc_obj = self.pool.get("stock.location")
1064         stock_move = self.pool.get('stock.move')
1065         type_obj = self.pool.get('stock.picking.type')
1066         # Need to search for a picking type
1067         move = stock_move.browse(cr, uid, move_id, context=context)
1068         src_loc = loc_obj.browse(cr, uid, source_location_id, context=context)
1069         dest_loc = loc_obj.browse(cr, uid, dest_location_id, context=context)
1070         code = stock_move.get_code_from_locs(cr, uid, move, src_loc, dest_loc, context=context)
1071         if code == 'outgoing':
1072             check_loc = src_loc
1073         else:
1074             check_loc = dest_loc
1075         wh = loc_obj.get_warehouse(cr, uid, check_loc, context=context)
1076         domain = [('code', '=', code)]
1077         if wh: 
1078             domain += [('warehouse_id', '=', wh)]
1079         types = type_obj.search(cr, uid, domain, context=context)
1080         move = stock_move.copy(cr, uid, move_id, default = {
1081             'location_id': source_location_id,
1082             'location_dest_id': dest_location_id,
1083             'procure_method': self._get_raw_material_procure_method(cr, uid, product, context=context),
1084             'raw_material_production_id': False, 
1085             'move_dest_id': move_id,
1086             'picking_type_id': types and types[0] or False,
1087         }, context=context)
1088         return move
1089
1090     def _make_consume_line_from_data(self, cr, uid, production, product, uom_id, qty, uos_id, uos_qty, context=None):
1091         stock_move = self.pool.get('stock.move')
1092         loc_obj = self.pool.get('stock.location')
1093         # Internal shipment is created for Stockable and Consumer Products
1094         if product.type not in ('product', 'consu'):
1095             return False
1096         # Take routing location as a Source Location.
1097         source_location_id = production.location_src_id.id
1098         prod_location_id = source_location_id
1099         prev_move= False
1100         if production.bom_id.routing_id and production.bom_id.routing_id.location_id and production.bom_id.routing_id.location_id.id != source_location_id:
1101             source_location_id = production.bom_id.routing_id.location_id.id
1102             prev_move = True
1103
1104         destination_location_id = production.product_id.property_stock_production.id
1105         move_id = stock_move.create(cr, uid, {
1106             'name': production.name,
1107             'date': production.date_planned,
1108             'product_id': product.id,
1109             'product_uom_qty': qty,
1110             'product_uom': uom_id,
1111             'product_uos_qty': uos_id and uos_qty or False,
1112             'product_uos': uos_id or False,
1113             'location_id': source_location_id,
1114             'location_dest_id': destination_location_id,
1115             'company_id': production.company_id.id,
1116             'procure_method': prev_move and 'make_to_stock' or self._get_raw_material_procure_method(cr, uid, product, context=context), #Make_to_stock avoids creating procurement
1117             'raw_material_production_id': production.id,
1118             #this saves us a browse in create()
1119             'price_unit': product.standard_price,
1120             'origin': production.name,
1121             'warehouse_id': loc_obj.get_warehouse(cr, uid, production.location_src_id, context=context),
1122         }, context=context)
1123         
1124         if prev_move:
1125             prev_move = self._create_previous_move(cr, uid, move_id, product, prod_location_id, source_location_id, context=context)
1126             stock_move.action_confirm(cr, uid, [prev_move], context=context)
1127         return move_id
1128
1129     def _make_production_consume_line(self, cr, uid, line, context=None):
1130         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)
1131
1132
1133     def _make_service_procurement(self, cr, uid, line, context=None):
1134         prod_obj = self.pool.get('product.product')
1135         if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
1136             vals = {
1137                 'name': line.production_id.name,
1138                 'origin': line.production_id.name,
1139                 'company_id': line.production_id.company_id.id,
1140                 'date_planned': line.production_id.date_planned,
1141                 'product_id': line.product_id.id,
1142                 'product_qty': line.product_qty,
1143                 'product_uom': line.product_uom.id,
1144                 'product_uos_qty': line.product_uos_qty,
1145                 'product_uos': line.product_uos.id,
1146                 }
1147             proc_obj = self.pool.get("procurement.order")
1148             proc = proc_obj.create(cr, uid, vals, context=context)
1149             proc_obj.run(cr, uid, [proc], context=context)
1150
1151
1152     def action_confirm(self, cr, uid, ids, context=None):
1153         """ Confirms production order.
1154         @return: Newly generated Shipment Id.
1155         """
1156         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)])
1157         self.action_compute(cr, uid, uncompute_ids, context=context)
1158         for production in self.browse(cr, uid, ids, context=context):
1159             self._make_production_produce_line(cr, uid, production, context=context)
1160
1161             stock_moves = []
1162             for line in production.product_lines:
1163                 if line.product_id.type != 'service':
1164                     stock_move_id = self._make_production_consume_line(cr, uid, line, context=context)
1165                     stock_moves.append(stock_move_id)
1166                 else:
1167                     self._make_service_procurement(cr, uid, line, context=context)
1168             if stock_moves:
1169                 self.pool.get('stock.move').action_confirm(cr, uid, stock_moves, context=context)
1170             production.write({'state': 'confirmed'}, context=context)
1171         return 0
1172
1173     def action_assign(self, cr, uid, ids, context=None):
1174         """
1175         Checks the availability on the consume lines of the production order
1176         """
1177         from openerp import workflow
1178         move_obj = self.pool.get("stock.move")
1179         for production in self.browse(cr, uid, ids, context=context):
1180             move_obj.action_assign(cr, uid, [x.id for x in production.move_lines], context=context)
1181             if self.pool.get('mrp.production').test_ready(cr, uid, [production.id]):
1182                 workflow.trg_validate(uid, 'mrp.production', production.id, 'moves_ready', cr)
1183
1184
1185     def force_production(self, cr, uid, ids, *args):
1186         """ Assigns products.
1187         @param *args: Arguments
1188         @return: True
1189         """
1190         from openerp import workflow
1191         move_obj = self.pool.get('stock.move')
1192         for order in self.browse(cr, uid, ids):
1193             move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
1194             if self.pool.get('mrp.production').test_ready(cr, uid, [order.id]):
1195                 workflow.trg_validate(uid, 'mrp.production', order.id, 'moves_ready', cr)
1196         return True
1197
1198
1199 class mrp_production_workcenter_line(osv.osv):
1200     _name = 'mrp.production.workcenter.line'
1201     _description = 'Work Order'
1202     _order = 'sequence'
1203     _inherit = ['mail.thread']
1204
1205     _columns = {
1206         'name': fields.char('Work Order', required=True),
1207         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1208         'cycle': fields.float('Number of Cycles', digits=(16, 2)),
1209         'hour': fields.float('Number of Hours', digits=(16, 2)),
1210         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1211         'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1212             track_visibility='onchange', select=True, ondelete='cascade', required=True),
1213     }
1214     _defaults = {
1215         'sequence': lambda *a: 1,
1216         'hour': lambda *a: 0,
1217         'cycle': lambda *a: 0,
1218     }
1219
1220 class mrp_production_product_line(osv.osv):
1221     _name = 'mrp.production.product.line'
1222     _description = 'Production Scheduled Product'
1223     _columns = {
1224         'name': fields.char('Name', required=True),
1225         'product_id': fields.many2one('product.product', 'Product', required=True),
1226         'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1227         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1228         'product_uos_qty': fields.float('Product UOS Quantity'),
1229         'product_uos': fields.many2one('product.uom', 'Product UOS'),
1230         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1231     }
1232
1233 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: