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