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