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