[Merge]
[odoo/odoo.git] / addons / mrp / mrp.py
index 25aaf85..ec42948 100644 (file)
@@ -39,15 +39,15 @@ class mrp_workcenter(osv.osv):
     _description = 'Work Center'
     _inherits = {'resource.resource':"resource_id"}
     _columns = {
-        'note': fields.text('Description', help="Description of the work center. Explain here what's a cycle according to this work center."),
-        '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."),
+        'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
+        '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."),
         'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
         'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
         'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
-        'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work center per hour."),
+        'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
         'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
             help="Complete this only if you want automatic analytic accounting entries on production orders."),
-        'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work center per cycle."),
+        'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
         'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
             help="Complete this only if you want automatic analytic accounting entries on production orders."),
         'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
@@ -67,7 +67,7 @@ class mrp_workcenter(osv.osv):
 
         if product_id:
             cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
-            value = {'costs_hour' :cost.standard_price}
+            value = {'costs_hour': cost.standard_price}
         return {'value': value}
 
 mrp_workcenter()
@@ -75,7 +75,7 @@ mrp_workcenter()
 
 class mrp_routing(osv.osv):
     """
-    For specifying the routings of workcenters.
+    For specifying the routings of Work Centers.
     """
     _name = 'mrp.routing'
     _description = 'Routing'
@@ -102,22 +102,22 @@ mrp_routing()
 
 class mrp_routing_workcenter(osv.osv):
     """
-    Defines working cycles and hours of a workcenter using routings.
+    Defines working cycles and hours of a Work Center using routings.
     """
     _name = 'mrp.routing.workcenter'
-    _description = 'Workcenter Usage'
+    _description = 'Work Center Usage'
     _columns = {
         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
         'name': fields.char('Name', size=64, required=True),
-        'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing work centers."),
+        'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
         'cycle_nbr': fields.float('Number of Cycles', required=True,
             help="Number of iterations this work center has to do in the specified operation of the routing."),
-        '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."),
+        '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."),
         'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
-             help="Routing indicates all the workcenters used, for how long and/or cycles." \
-                "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
+             help="Routing indicates all the Work Centers used, for how long and/or cycles." \
+                "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
         'note': fields.text('Description'),
-        'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company'),
+        'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
     }
     _defaults = {
         'cycle_nbr': lambda *a: 1.0,
@@ -132,7 +132,7 @@ class mrp_bom(osv.osv):
     _name = 'mrp.bom'
     _description = 'Bill of Material'
 
-    def _child_compute(self, cr, uid, ids, name, arg, context={}):
+    def _child_compute(self, cr, uid, ids, name, arg, context=None):
         """ Gets child bom.
         @param self: The object pointer
         @param cr: The current row, from the database cursor,
@@ -144,13 +144,15 @@ class mrp_bom(osv.osv):
         @return:  Dictionary of values
         """
         result = {}
+        if context is None:
+            context = {}
         bom_obj = self.pool.get('mrp.bom')
         bom_id = context and context.get('active_id', False) or False
         cr.execute('select id from mrp_bom')
         if all(bom_id != r[0] for r in cr.fetchall()):
             ids.sort()
             bom_id = ids[0]
-        bom_parent = bom_obj.browse(cr, uid, bom_id)
+        bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
         for bom in self.browse(cr, uid, ids, context=context):
             if (bom_parent) or (bom.id == bom_id):
                 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
@@ -167,14 +169,14 @@ class mrp_bom(osv.osv):
 
         return result
 
-    def _compute_type(self, cr, uid, ids, field_name, arg, context):
+    def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
         """ Sets particular method for the selected bom type.
         @param field_name: Name of the field
         @param arg: User defined argument
         @return:  Dictionary of values
         """
         res = dict(map(lambda x: (x,''), ids))
-        for line in self.browse(cr, uid, ids):
+        for line in self.browse(cr, uid, ids, context=context):
             if line.type == 'phantom' and not line.bom_id:
                 res[line.id] = 'set'
                 continue
@@ -195,7 +197,7 @@ class mrp_bom(osv.osv):
                                  help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
                                  "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
                                  "If a Phantom BoM is used for a root product, it will be sold and shipped as a set of components, instead of being produced."),
-        'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
+        'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
         'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
         'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
@@ -212,7 +214,7 @@ class mrp_bom(osv.osv):
         '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."),
         'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
         'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
-        'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
+        'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
         'company_id': fields.many2one('res.company','Company',required=True),
     }
     _defaults = {
@@ -229,7 +231,7 @@ class mrp_bom(osv.osv):
             'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
     ]
 
-    def _check_recursion(self, cr, uid, ids):
+    def _check_recursion(self, cr, uid, ids, context=None):
         level = 100
         while len(ids):
             cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
@@ -238,19 +240,40 @@ class mrp_bom(osv.osv):
                 return False
             level -= 1
         return True
+
+    def _check_product(self, cr, uid, ids, context=None):
+        all_prod = []
+        boms = self.browse(cr, uid, ids, context=context)
+        def check_bom(boms):
+            res = True
+            for bom in boms:
+                if bom.product_id.id in all_prod:
+                    res = res and False
+                all_prod.append(bom.product_id.id)
+                lines = bom.bom_lines
+                if lines:
+                    newboms = [a for a in lines if a not in boms]
+                    res = res and check_bom(newboms)
+            return res
+        return check_bom(boms)
+
     _constraints = [
-        (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
+        (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id']),
+        (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
     ]
 
-
-    def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
+    def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
         """ Changes UoM and name if product_id changes.
         @param name: Name of the field
         @param product_id: Changed product_id
         @return:  Dictionary of changed values
         """
+        if context is None:
+            context = {}
+            context['lang'] = self.pool.get('res.users').browse(cr,uid,uid).context_lang
+            
         if product_id:
-            prod = self.pool.get('product.product').browse(cr, uid, [product_id])[0]
+            prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
             v = {'product_uom': prod.uom_id.id}
             if not name:
                 v['name'] = prod.name
@@ -279,14 +302,14 @@ class mrp_bom(osv.osv):
         return result
 
     def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
-        """ Finds Products and Workcenters for related BoM for manufacturing order.
+        """ Finds Products and Work Centers for related BoM for manufacturing order.
         @param bom: BoM of particular product.
         @param factor: Factor of product UoM.
         @param properties: A List of properties Ids.
         @param addthis: If BoM found then True else False.
         @param level: Depth level to find BoM lines starts from 10.
         @return: result: List of dictionaries containing product details.
-                 result2: List of dictionaries containing workcenter details.
+                 result2: List of dictionaries containing Work Center details.
         """
         factor = factor / (bom.product_efficiency or 1.0)
         factor = rounding(factor, bom.product_rounding)
@@ -340,7 +363,7 @@ class mrp_bom(osv.osv):
         if context is None:
             context = {}
         bom_data = self.read(cr, uid, id, [], context=context)
-        default.update({'name': bom_data['name'] + _(' (copy)')})
+        default.update({'name': bom_data['name'] + ' ' + _('Copy'), 'bom_id':False})
         return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
 
 mrp_bom()
@@ -366,9 +389,10 @@ class mrp_bom_revision(osv.osv):
 mrp_bom_revision()
 
 def rounding(f, r):
+    import math
     if not r:
         return f
-    return round(f / r) * r
+    return math.ceil(f / r) * r
 
 class mrp_production(osv.osv):
     """
@@ -378,7 +402,7 @@ class mrp_production(osv.osv):
     _description = 'Manufacturing Order'
     _date_name  = 'date_planned'
 
-    def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
+    def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
         """ Calculates total hours and total no. of cycles for a production order.
         @param prop: Name of field.
         @param unknow_none:
@@ -395,7 +419,7 @@ class mrp_production(osv.osv):
                 result[prod.id]['cycle_total'] += wc.cycle
         return result
 
-    def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
+    def _production_date_end(self, cr, uid, ids, prop, unknow_none, context=None):
         """ Finds production end date.
         @param prop: Name of field.
         @param unknow_none:
@@ -406,7 +430,7 @@ class mrp_production(osv.osv):
             result[prod.id] = prod.date_planned
         return result
 
-    def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
+    def _production_date(self, cr, uid, ids, prop, unknow_none, context=None):
         """ Finds production planned date.
         @param prop: Name of field.
         @param unknow_none:
@@ -422,27 +446,27 @@ class mrp_production(osv.osv):
         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
 
-        'product_id': fields.many2one('product.product', 'Product', required=True, ),
+        'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft':[('readonly',False)]}),
         'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
         'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
 
         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
-            help="Location where the system will look for components."),
+            readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will look for components."),
         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
-            help="Location where the system will stock the finished products."),
+            readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will stock the finished products."),
 
-        'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
-        'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
+        'date_planned_end': fields.function(_production_date_end, type='date', string='Scheduled End Date'),
+        'date_planned_date': fields.function(_production_date, type='date', string='Scheduled Date'),
         'date_planned': fields.datetime('Scheduled date', required=True, select=1),
-        'date_start': fields.datetime('Start Date'),
-        'date_finished': fields.datetime('End Date'),
+        'date_start': fields.datetime('Start Date', select=True),
+        'date_finished': fields.datetime('End Date', select=True),
 
-        'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
-        'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', help="The list of operations (list of workcenters) to produce the finished product. The routing is mainly used to compute workcenter costs during operations and to plan future loads on workcenters based on production plannification."),
+        'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]}),
+        'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]}, 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."),
 
-        'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
+        'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True, ondelete="restrict",
             help="This is the internal picking list that brings the finished product to the production plan"),
         'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
         'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
@@ -454,8 +478,8 @@ class mrp_production(osv.osv):
         'state': fields.selection([('draft','Draft'),('picking_except', 'Picking Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','In Production'),('cancel','Cancelled'),('done','Done')],'State', readonly=True,
                                     help='When the production order is created the state is set to \'Draft\'.\n If the order is confirmed the state is set to \'Waiting Goods\'.\n If any exceptions are there, the state is set to \'Picking Exception\'.\
                                     \nIf the stock is available then the state is set to \'Ready to Produce\'.\n When the production gets started then the state is set to \'In Production\'.\n When the production is over, the state is set to \'Done\'.'),
-        'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
-        'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
+        'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
+        'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
         'company_id': fields.many2one('res.company','Company',required=True),
     }
     _defaults = {
@@ -468,8 +492,8 @@ class mrp_production(osv.osv):
     }
     _order = 'priority desc, date_planned asc';
 
-    def _check_qty(self, cr, uid, ids):
-        orders = self.browse(cr, uid, ids)
+    def _check_qty(self, cr, uid, ids, context=None):
+        orders = self.browse(cr, uid, ids, context=context)
         for order in orders:
             if order.product_qty <= 0:
                 return False
@@ -503,7 +527,7 @@ class mrp_production(osv.osv):
         })
         return super(mrp_production, self).copy(cr, uid, id, default, context)
 
-    def location_id_change(self, cr, uid, ids, src, dest, context={}):
+    def location_id_change(self, cr, uid, ids, src, dest, context=None):
         """ Changes destination location if source location is changed.
         @param src: Source location id.
         @param dest: Destination location id.
@@ -571,6 +595,7 @@ class mrp_production(osv.osv):
         """
         results = []
         bom_obj = self.pool.get('mrp.bom')
+        uom_obj = self.pool.get('product.uom')
         prod_line_obj = self.pool.get('mrp.production.product.line')
         workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
         for production in self.browse(cr, uid, ids):
@@ -588,7 +613,7 @@ class mrp_production(osv.osv):
             if not bom_id:
                 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
 
-            factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
+            factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
             res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
             results = res[0]
             results2 = res[1]
@@ -600,12 +625,18 @@ class mrp_production(osv.osv):
                 workcenter_line_obj.create(cr, uid, line)
         return len(results)
 
-    def action_cancel(self, cr, uid, ids):
+    def action_cancel(self, cr, uid, ids, context=None):
         """ Cancels the production order and related stock moves.
         @return: True
         """
+        if context is None:
+            context = {}
         move_obj = self.pool.get('stock.move')
-        for production in self.browse(cr, uid, ids):
+        for production in self.browse(cr, uid, ids, context=context):
+            if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
+                raise osv.except_osv(
+                    _('Could not cancel manufacturing order !'),
+                    _('You must first cancel related internal picking attached to this manufacturing order.'))
             if production.move_created_ids:
                 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
             move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
@@ -644,10 +675,10 @@ class mrp_production(osv.osv):
         res = True
         for production in self.browse(cr, uid, ids):
             if production.move_lines:
-               res = False
+                res = False
 
             if production.move_created_ids:
-               res = False
+                res = False
         return res
 
     def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
@@ -660,11 +691,9 @@ class mrp_production(osv.osv):
         @param production_mode: specify production mode (consume/consume&produce).
         @return: True
         """
-
         stock_mov_obj = self.pool.get('stock.move')
-        production = self.browse(cr, uid, production_id)
+        production = self.browse(cr, uid, production_id, context=context)
 
-        final_product_todo = []
 
         produced_qty = 0
         if production_mode == 'consume_produce':
@@ -702,13 +731,14 @@ class mrp_production(osv.osv):
                         if consumed_qty == 0:
                             consumed_qty = production_qty * f.product_qty / production.product_qty
                         if consumed_qty > 0:
-                            stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
+                            stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, raw_product.location_id.id, context=context)
 
         if production_mode == 'consume_produce':
             # To produce remaining qty of final product
-            vals = {'state':'confirmed'}
-            final_product_todo = [x.id for x in production.move_created_ids]
-            stock_mov_obj.write(cr, uid, final_product_todo, vals)
+            #vals = {'state':'confirmed'}
+            #final_product_todo = [x.id for x in production.move_created_ids]
+            #stock_mov_obj.write(cr, uid, final_product_todo, vals)
+            #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
             produced_products = {}
             for produced_product in production.move_created_ids2:
                 if produced_product.scrapped:
@@ -721,7 +751,7 @@ class mrp_production(osv.osv):
                 produced_qty = produced_products.get(produce_product.product_id.id, 0)
                 rest_qty = production.product_qty - produced_qty
                 if rest_qty <= production_qty:
-                   production_qty = rest_qty
+                    production_qty = rest_qty
                 if rest_qty > 0 :
                     stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)