1# -*- coding: utf-8 -*- 2# Part of Odoo. See LICENSE file for full copyright and licensing details. 3 4import json 5import datetime 6import math 7import operator as py_operator 8import re 9 10from collections import defaultdict 11from dateutil.relativedelta import relativedelta 12from itertools import groupby 13 14from odoo import api, fields, models, _ 15from odoo.exceptions import AccessError, UserError 16from odoo.tools import float_compare, float_round, float_is_zero, format_datetime 17from odoo.tools.misc import format_date 18 19from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES 20 21SIZE_BACK_ORDER_NUMERING = 3 22 23 24class MrpProduction(models.Model): 25 """ Manufacturing Orders """ 26 _name = 'mrp.production' 27 _description = 'Production Order' 28 _date_name = 'date_planned_start' 29 _inherit = ['mail.thread', 'mail.activity.mixin'] 30 _order = 'priority desc, date_planned_start asc,id' 31 32 @api.model 33 def _get_default_picking_type(self): 34 company_id = self.env.context.get('default_company_id', self.env.company.id) 35 return self.env['stock.picking.type'].search([ 36 ('code', '=', 'mrp_operation'), 37 ('warehouse_id.company_id', '=', company_id), 38 ], limit=1).id 39 40 @api.model 41 def _get_default_location_src_id(self): 42 location = False 43 company_id = self.env.context.get('default_company_id', self.env.company.id) 44 if self.env.context.get('default_picking_type_id'): 45 location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_src_id 46 if not location: 47 location = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1).lot_stock_id 48 return location and location.id or False 49 50 @api.model 51 def _get_default_location_dest_id(self): 52 location = False 53 company_id = self.env.context.get('default_company_id', self.env.company.id) 54 if self._context.get('default_picking_type_id'): 55 location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_dest_id 56 if not location: 57 location = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1).lot_stock_id 58 return location and location.id or False 59 60 @api.model 61 def _get_default_date_planned_finished(self): 62 if self.env.context.get('default_date_planned_start'): 63 return fields.Datetime.to_datetime(self.env.context.get('default_date_planned_start')) + datetime.timedelta(hours=1) 64 return datetime.datetime.now() + datetime.timedelta(hours=1) 65 66 @api.model 67 def _get_default_date_planned_start(self): 68 if self.env.context.get('default_date_deadline'): 69 return fields.Datetime.to_datetime(self.env.context.get('default_date_deadline')) 70 return datetime.datetime.now() 71 72 @api.model 73 def _get_default_is_locked(self): 74 return self.user_has_groups('mrp.group_locked_by_default') 75 76 name = fields.Char( 77 'Reference', copy=False, readonly=True, default=lambda x: _('New')) 78 priority = fields.Selection( 79 PROCUREMENT_PRIORITIES, string='Priority', default='0', index=True, 80 help="Components will be reserved first for the MO with the highest priorities.") 81 backorder_sequence = fields.Integer("Backorder Sequence", default=0, copy=False, help="Backorder sequence, if equals to 0 means there is not related backorder") 82 origin = fields.Char( 83 'Source', copy=False, 84 states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, 85 help="Reference of the document that generated this production order request.") 86 87 product_id = fields.Many2one( 88 'product.product', 'Product', 89 domain="""[ 90 ('type', 'in', ['product', 'consu']), 91 '|', 92 ('company_id', '=', False), 93 ('company_id', '=', company_id) 94 ] 95 """, 96 readonly=True, required=True, check_company=True, 97 states={'draft': [('readonly', False)]}) 98 product_tracking = fields.Selection(related='product_id.tracking') 99 allowed_product_ids = fields.Many2many('product.product', compute='_compute_allowed_product_ids') 100 product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id') 101 product_qty = fields.Float( 102 'Quantity To Produce', 103 default=1.0, digits='Product Unit of Measure', 104 readonly=True, required=True, tracking=True, 105 states={'draft': [('readonly', False)]}) 106 product_uom_id = fields.Many2one( 107 'uom.uom', 'Product Unit of Measure', 108 readonly=True, required=True, 109 states={'draft': [('readonly', False)]}, domain="[('category_id', '=', product_uom_category_id)]") 110 lot_producing_id = fields.Many2one( 111 'stock.production.lot', string='Lot/Serial Number', copy=False, 112 domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True) 113 qty_producing = fields.Float(string="Quantity Producing", digits='Product Unit of Measure', copy=False) 114 product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id') 115 product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True) 116 picking_type_id = fields.Many2one( 117 'stock.picking.type', 'Operation Type', 118 domain="[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]", 119 default=_get_default_picking_type, required=True, check_company=True) 120 use_create_components_lots = fields.Boolean(related='picking_type_id.use_create_components_lots') 121 location_src_id = fields.Many2one( 122 'stock.location', 'Components Location', 123 default=_get_default_location_src_id, 124 readonly=True, required=True, 125 domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", 126 states={'draft': [('readonly', False)]}, check_company=True, 127 help="Location where the system will look for components.") 128 location_dest_id = fields.Many2one( 129 'stock.location', 'Finished Products Location', 130 default=_get_default_location_dest_id, 131 readonly=True, required=True, 132 domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", 133 states={'draft': [('readonly', False)]}, check_company=True, 134 help="Location where the system will stock the finished products.") 135 date_planned_start = fields.Datetime( 136 'Scheduled Date', copy=False, default=_get_default_date_planned_start, 137 help="Date at which you plan to start the production.", 138 index=True, required=True) 139 date_planned_finished = fields.Datetime( 140 'Scheduled End Date', 141 default=_get_default_date_planned_finished, 142 help="Date at which you plan to finish the production.", 143 copy=False) 144 date_deadline = fields.Datetime( 145 'Deadline', copy=False, store=True, readonly=True, compute='_compute_date_deadline', inverse='_set_date_deadline', 146 help="Informative date allowing to define when the manufacturing order should be processed at the latest to fulfill delivery on time.") 147 date_start = fields.Datetime('Start Date', copy=False, index=True, readonly=True) 148 date_finished = fields.Datetime('End Date', copy=False, index=True, readonly=True) 149 bom_id = fields.Many2one( 150 'mrp.bom', 'Bill of Material', 151 readonly=True, states={'draft': [('readonly', False)]}, 152 domain="""[ 153 '&', 154 '|', 155 ('company_id', '=', False), 156 ('company_id', '=', company_id), 157 '&', 158 '|', 159 ('product_id','=',product_id), 160 '&', 161 ('product_tmpl_id.product_variant_ids','=',product_id), 162 ('product_id','=',False), 163 ('type', '=', 'normal')]""", 164 check_company=True, 165 help="Bill of Materials allow you to define the list of required components to make a finished product.") 166 167 state = fields.Selection([ 168 ('draft', 'Draft'), 169 ('confirmed', 'Confirmed'), 170 ('progress', 'In Progress'), 171 ('to_close', 'To Close'), 172 ('done', 'Done'), 173 ('cancel', 'Cancelled')], string='State', 174 compute='_compute_state', copy=False, index=True, readonly=True, 175 store=True, tracking=True, 176 help=" * Draft: The MO is not confirmed yet.\n" 177 " * Confirmed: The MO is confirmed, the stock rules and the reordering of the components are trigerred.\n" 178 " * In Progress: The production has started (on the MO or on the WO).\n" 179 " * To Close: The production is done, the MO has to be closed.\n" 180 " * Done: The MO is closed, the stock moves are posted. \n" 181 " * Cancelled: The MO has been cancelled, can't be confirmed anymore.") 182 reservation_state = fields.Selection([ 183 ('confirmed', 'Waiting'), 184 ('assigned', 'Ready'), 185 ('waiting', 'Waiting Another Operation')], 186 string='Material Availability', 187 compute='_compute_state', copy=False, index=True, readonly=True, 188 store=True, tracking=True, 189 help=" * Ready: The material is available to start the production.\n\ 190 * Waiting: The material is not available to start the production.\n\ 191 The material availability is impacted by the manufacturing readiness\ 192 defined on the BoM.") 193 194 move_raw_ids = fields.One2many( 195 'stock.move', 'raw_material_production_id', 'Components', 196 copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, 197 domain=[('scrapped', '=', False)]) 198 move_finished_ids = fields.One2many( 199 'stock.move', 'production_id', 'Finished Products', 200 copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, 201 domain=[('scrapped', '=', False)]) 202 move_byproduct_ids = fields.One2many('stock.move', compute='_compute_move_byproduct_ids', inverse='_set_move_byproduct_ids') 203 finished_move_line_ids = fields.One2many( 204 'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product" 205 ) 206 workorder_ids = fields.One2many( 207 'mrp.workorder', 'production_id', 'Work Orders', copy=True) 208 workorder_done_count = fields.Integer('# Done Work Orders', compute='_compute_workorder_done_count') 209 move_dest_ids = fields.One2many('stock.move', 'created_production_id', 210 string="Stock Movements of Produced Goods") 211 212 unreserve_visible = fields.Boolean( 213 'Allowed to Unreserve Production', compute='_compute_unreserve_visible', 214 help='Technical field to check when we can unreserve') 215 reserve_visible = fields.Boolean( 216 'Allowed to Reserve Production', compute='_compute_unreserve_visible', 217 help='Technical field to check when we can reserve quantities') 218 user_id = fields.Many2one( 219 'res.users', 'Responsible', default=lambda self: self.env.user, 220 states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, 221 domain=lambda self: [('groups_id', 'in', self.env.ref('mrp.group_mrp_user').id)]) 222 company_id = fields.Many2one( 223 'res.company', 'Company', default=lambda self: self.env.company, 224 index=True, required=True) 225 226 qty_produced = fields.Float(compute="_get_produced_qty", string="Quantity Produced") 227 procurement_group_id = fields.Many2one( 228 'procurement.group', 'Procurement Group', 229 copy=False) 230 product_description_variants = fields.Char('Custom Description') 231 orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint') 232 propagate_cancel = fields.Boolean( 233 'Propagate cancel and split', 234 help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too') 235 delay_alert_date = fields.Datetime('Delay Alert Date', compute='_compute_delay_alert_date', search='_search_delay_alert_date') 236 json_popover = fields.Char('JSON data for the popover widget', compute='_compute_json_popover') 237 scrap_ids = fields.One2many('stock.scrap', 'production_id', 'Scraps') 238 scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move') 239 is_locked = fields.Boolean('Is Locked', default=_get_default_is_locked, copy=False) 240 is_planned = fields.Boolean('Its Operations are Planned', compute='_compute_is_planned', search='_search_is_planned') 241 242 show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots') 243 production_location_id = fields.Many2one('stock.location', "Production Location", compute="_compute_production_location", store=True) 244 picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this manufacturing order') 245 delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') 246 confirm_cancel = fields.Boolean(compute='_compute_confirm_cancel') 247 consumption = fields.Selection([ 248 ('flexible', 'Allowed'), 249 ('warning', 'Allowed with warning'), 250 ('strict', 'Blocked')], 251 required=True, 252 readonly=True, 253 default='flexible', 254 ) 255 256 mrp_production_child_count = fields.Integer("Number of generated MO", compute='_compute_mrp_production_child_count') 257 mrp_production_source_count = fields.Integer("Number of source MO", compute='_compute_mrp_production_source_count') 258 mrp_production_backorder_count = fields.Integer("Count of linked backorder", compute='_compute_mrp_production_backorder') 259 show_lock = fields.Boolean('Show Lock/unlock buttons', compute='_compute_show_lock') 260 components_availability = fields.Char( 261 string="Component Availability", compute='_compute_components_availability') 262 components_availability_state = fields.Selection([ 263 ('available', 'Available'), 264 ('expected', 'Expected'), 265 ('late', 'Late')], compute='_compute_components_availability') 266 show_lot_ids = fields.Boolean('Display the serial number shortcut on the moves', compute='_compute_show_lot_ids') 267 268 @api.depends('product_id', 'bom_id', 'company_id') 269 def _compute_allowed_product_ids(self): 270 for production in self: 271 product_domain = [ 272 ('type', 'in', ['product', 'consu']), 273 '|', 274 ('company_id', '=', False), 275 ('company_id', '=', production.company_id.id) 276 ] 277 if production.bom_id: 278 if production.bom_id.product_id: 279 product_domain += [('id', '=', production.bom_id.product_id.id)] 280 else: 281 product_domain += [('id', 'in', production.bom_id.product_tmpl_id.product_variant_ids.ids)] 282 production.allowed_product_ids = self.env['product.product'].search(product_domain) 283 284 @api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids') 285 def _compute_mrp_production_child_count(self): 286 for production in self: 287 production.mrp_production_child_count = len(production.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids - production) 288 289 @api.depends('move_dest_ids.group_id.mrp_production_ids') 290 def _compute_mrp_production_source_count(self): 291 for production in self: 292 production.mrp_production_source_count = len(production.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids - production) 293 294 @api.depends('procurement_group_id.mrp_production_ids') 295 def _compute_mrp_production_backorder(self): 296 for production in self: 297 production.mrp_production_backorder_count = len(production.procurement_group_id.mrp_production_ids) 298 299 @api.depends('move_raw_ids', 'state', 'date_planned_start', 'move_raw_ids.forecast_availability', 'move_raw_ids.forecast_expected_date') 300 def _compute_components_availability(self): 301 self.components_availability = False 302 self.components_availability_state = 'available' 303 productions = self.filtered(lambda mo: mo.state not in ['cancel', 'draft', 'done']) 304 productions.components_availability = _('Available') 305 for production in productions: 306 forecast_date = max(production.move_raw_ids.filtered('forecast_expected_date').mapped('forecast_expected_date'), default=False) 307 if any(float_compare(move.forecast_availability, move.product_qty, move.product_id.uom_id.rounding) == -1 for move in production.move_raw_ids): 308 production.components_availability = _('Not Available') 309 production.components_availability_state = 'late' 310 elif forecast_date: 311 production.components_availability = _('Exp %s', format_date(self.env, forecast_date)) 312 production.components_availability_state = 'late' if forecast_date > production.date_planned_start else 'expected' 313 314 @api.depends('move_finished_ids.date_deadline') 315 def _compute_date_deadline(self): 316 for production in self: 317 production.date_deadline = min(production.move_finished_ids.filtered('date_deadline').mapped('date_deadline'), default=production.date_deadline or False) 318 319 def _set_date_deadline(self): 320 for production in self: 321 production.move_finished_ids.date_deadline = production.date_deadline 322 323 @api.depends("workorder_ids.date_planned_start", "workorder_ids.date_planned_finished") 324 def _compute_is_planned(self): 325 for production in self: 326 if production.workorder_ids: 327 production.is_planned = any(wo.date_planned_start and wo.date_planned_finished for wo in production.workorder_ids if wo.state != 'done') 328 else: 329 production.is_planned = False 330 331 def _search_is_planned(self, operator, value): 332 if operator not in ('=', '!='): 333 raise UserError(_('Invalid domain operator %s', operator)) 334 335 if value not in (False, True): 336 raise UserError(_('Invalid domain right operand %s', value)) 337 ops = {'=': py_operator.eq, '!=': py_operator.ne} 338 ids = [] 339 for mo in self.search([]): 340 if ops[operator](value, mo.is_planned): 341 ids.append(mo.id) 342 343 return [('id', 'in', ids)] 344 345 @api.depends('move_raw_ids.delay_alert_date') 346 def _compute_delay_alert_date(self): 347 delay_alert_date_data = self.env['stock.move'].read_group([('id', 'in', self.move_raw_ids.ids), ('delay_alert_date', '!=', False)], ['delay_alert_date:max'], 'raw_material_production_id') 348 delay_alert_date_data = {data['raw_material_production_id'][0]: data['delay_alert_date'] for data in delay_alert_date_data} 349 for production in self: 350 production.delay_alert_date = delay_alert_date_data.get(production.id, False) 351 352 def _compute_json_popover(self): 353 for production in self: 354 production.json_popover = json.dumps({ 355 'popoverTemplate': 'stock.PopoverStockRescheduling', 356 'delay_alert_date': format_datetime(self.env, production.delay_alert_date, dt_format=False) if production.delay_alert_date else False, 357 'late_elements': [{ 358 'id': late_document.id, 359 'name': late_document.display_name, 360 'model': late_document._name, 361 } for late_document in production.move_raw_ids.filtered(lambda m: m.delay_alert_date).move_orig_ids._delay_alert_get_documents() 362 ] 363 }) 364 365 @api.depends('move_raw_ids.state', 'move_finished_ids.state') 366 def _compute_confirm_cancel(self): 367 """ If the manufacturing order contains some done move (via an intermediate 368 post inventory), the user has to confirm the cancellation. 369 """ 370 domain = [ 371 ('state', '=', 'done'), 372 '|', 373 ('production_id', 'in', self.ids), 374 ('raw_material_production_id', 'in', self.ids) 375 ] 376 res = self.env['stock.move'].read_group(domain, ['state', 'production_id', 'raw_material_production_id'], ['production_id', 'raw_material_production_id'], lazy=False) 377 productions_with_done_move = {} 378 for rec in res: 379 production_record = rec['production_id'] or rec['raw_material_production_id'] 380 if production_record: 381 productions_with_done_move[production_record[0]] = True 382 for production in self: 383 production.confirm_cancel = productions_with_done_move.get(production.id, False) 384 385 @api.depends('procurement_group_id') 386 def _compute_picking_ids(self): 387 for order in self: 388 order.picking_ids = self.env['stock.picking'].search([ 389 ('group_id', '=', order.procurement_group_id.id), ('group_id', '!=', False), 390 ]) 391 order.delivery_count = len(order.picking_ids) 392 393 def action_view_mo_delivery(self): 394 """ This function returns an action that display picking related to 395 manufacturing order orders. It can either be a in a list or in a form 396 view, if there is only one picking to show. 397 """ 398 self.ensure_one() 399 action = self.env["ir.actions.actions"]._for_xml_id("stock.action_picking_tree_all") 400 pickings = self.mapped('picking_ids') 401 if len(pickings) > 1: 402 action['domain'] = [('id', 'in', pickings.ids)] 403 elif pickings: 404 form_view = [(self.env.ref('stock.view_picking_form').id, 'form')] 405 if 'views' in action: 406 action['views'] = form_view + [(state,view) for state,view in action['views'] if view != 'form'] 407 else: 408 action['views'] = form_view 409 action['res_id'] = pickings.id 410 action['context'] = dict(self._context, default_origin=self.name, create=False) 411 return action 412 413 @api.depends('product_uom_id', 'product_qty', 'product_id.uom_id') 414 def _compute_product_uom_qty(self): 415 for production in self: 416 if production.product_id.uom_id != production.product_uom_id: 417 production.product_uom_qty = production.product_uom_id._compute_quantity(production.product_qty, production.product_id.uom_id) 418 else: 419 production.product_uom_qty = production.product_qty 420 421 @api.depends('product_id', 'company_id') 422 def _compute_production_location(self): 423 if not self.company_id: 424 return 425 location_by_company = self.env['stock.location'].read_group([ 426 ('company_id', 'in', self.company_id.ids), 427 ('usage', '=', 'production') 428 ], ['company_id', 'ids:array_agg(id)'], ['company_id']) 429 location_by_company = {lbc['company_id'][0]: lbc['ids'] for lbc in location_by_company} 430 for production in self: 431 if production.product_id: 432 production.production_location_id = production.product_id.with_company(production.company_id).property_stock_production 433 else: 434 production.production_location_id = location_by_company.get(production.company_id.id)[0] 435 436 @api.depends('product_id.tracking') 437 def _compute_show_lots(self): 438 for production in self: 439 production.show_final_lots = production.product_id.tracking != 'none' 440 441 def _inverse_lines(self): 442 """ Little hack to make sure that when you change something on these objects, it gets saved""" 443 pass 444 445 @api.depends('move_finished_ids.move_line_ids') 446 def _compute_lines(self): 447 for production in self: 448 production.finished_move_line_ids = production.move_finished_ids.mapped('move_line_ids') 449 450 @api.depends('workorder_ids.state') 451 def _compute_workorder_done_count(self): 452 data = self.env['mrp.workorder'].read_group([ 453 ('production_id', 'in', self.ids), 454 ('state', '=', 'done')], ['production_id'], ['production_id']) 455 count_data = dict((item['production_id'][0], item['production_id_count']) for item in data) 456 for production in self: 457 production.workorder_done_count = count_data.get(production.id, 0) 458 459 @api.depends( 460 'move_raw_ids.state', 'move_raw_ids.quantity_done', 'move_finished_ids.state', 461 'workorder_ids', 'workorder_ids.state', 'product_qty', 'qty_producing') 462 def _compute_state(self): 463 """ Compute the production state. It use the same process than stock 464 picking. It exists 3 extra steps for production: 465 - progress: At least one item is produced or consumed. 466 - to_close: The quantity produced is greater than the quantity to 467 produce and all work orders has been finished. 468 """ 469 # TODO: duplicated code with stock_picking.py 470 for production in self: 471 if not production.move_raw_ids: 472 production.state = 'draft' 473 elif all(move.state == 'draft' for move in production.move_raw_ids): 474 production.state = 'draft' 475 elif all(move.state == 'cancel' for move in production.move_raw_ids): 476 production.state = 'cancel' 477 elif all(move.state in ('cancel', 'done') for move in production.move_raw_ids): 478 production.state = 'done' 479 elif production.workorder_ids and all(wo_state in ('done', 'cancel') for wo_state in production.workorder_ids.mapped('state')): 480 production.state = 'to_close' 481 elif not production.workorder_ids and production.qty_producing >= production.product_qty: 482 production.state = 'to_close' 483 elif any(wo_state in ('progress', 'done') for wo_state in production.workorder_ids.mapped('state')): 484 production.state = 'progress' 485 elif not float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding): 486 production.state = 'progress' 487 elif any(not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding or move.product_id.uom_id.rounding) for move in production.move_raw_ids): 488 production.state = 'progress' 489 else: 490 production.state = 'confirmed' 491 492 # Compute reservation state 493 # State where the reservation does not matter. 494 production.reservation_state = False 495 # Compute reservation state according to its component's moves. 496 if production.state not in ('draft', 'done', 'cancel'): 497 relevant_move_state = production.move_raw_ids._get_relevant_state_among_moves() 498 if relevant_move_state == 'partially_available': 499 if production.bom_id.operation_ids and production.bom_id.ready_to_produce == 'asap': 500 production.reservation_state = production._get_ready_to_produce_state() 501 else: 502 production.reservation_state = 'confirmed' 503 elif relevant_move_state != 'draft': 504 production.reservation_state = relevant_move_state 505 506 @api.depends('move_raw_ids', 'state', 'move_raw_ids.product_uom_qty') 507 def _compute_unreserve_visible(self): 508 for order in self: 509 already_reserved = order.state not in ('done', 'cancel') and order.mapped('move_raw_ids.move_line_ids') 510 any_quantity_done = any(m.quantity_done > 0 for m in order.move_raw_ids) 511 512 order.unreserve_visible = not any_quantity_done and already_reserved 513 order.reserve_visible = order.state in ('confirmed', 'progress', 'to_close') and any(move.product_uom_qty and move.state in ['confirmed', 'partially_available'] for move in order.move_raw_ids) 514 515 @api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done') 516 def _get_produced_qty(self): 517 for production in self: 518 done_moves = production.move_finished_ids.filtered(lambda x: x.state != 'cancel' and x.product_id.id == production.product_id.id) 519 qty_produced = sum(done_moves.mapped('quantity_done')) 520 production.qty_produced = qty_produced 521 return True 522 523 def _compute_scrap_move_count(self): 524 data = self.env['stock.scrap'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id']) 525 count_data = dict((item['production_id'][0], item['production_id_count']) for item in data) 526 for production in self: 527 production.scrap_count = count_data.get(production.id, 0) 528 529 @api.depends('move_finished_ids') 530 def _compute_move_byproduct_ids(self): 531 for order in self: 532 order.move_byproduct_ids = order.move_finished_ids.filtered(lambda m: m.product_id != order.product_id) 533 534 def _set_move_byproduct_ids(self): 535 move_finished_ids = self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id) 536 self.move_finished_ids = move_finished_ids | self.move_byproduct_ids 537 538 @api.depends('state') 539 def _compute_show_lock(self): 540 for order in self: 541 order.show_lock = self.env.user.has_group('mrp.group_locked_by_default') and order.id is not False and order.state not in {'cancel', 'draft'} 542 543 @api.depends('state','move_raw_ids') 544 def _compute_show_lot_ids(self): 545 for order in self: 546 order.show_lot_ids = order.state != 'draft' and any(m.product_id.tracking == 'serial' for m in order.move_raw_ids) 547 548 _sql_constraints = [ 549 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'), 550 ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'), 551 ] 552 553 @api.model 554 def _search_delay_alert_date(self, operator, value): 555 late_stock_moves = self.env['stock.move'].search([('delay_alert_date', operator, value)]) 556 return ['|', ('move_raw_ids', 'in', late_stock_moves.ids), ('move_finished_ids', 'in', late_stock_moves.ids)] 557 558 @api.onchange('company_id') 559 def onchange_company_id(self): 560 if self.company_id: 561 if self.move_raw_ids: 562 self.move_raw_ids.update({'company_id': self.company_id}) 563 if self.picking_type_id and self.picking_type_id.company_id != self.company_id: 564 self.picking_type_id = self.env['stock.picking.type'].search([ 565 ('code', '=', 'mrp_operation'), 566 ('warehouse_id.company_id', '=', self.company_id.id), 567 ], limit=1).id 568 569 @api.onchange('product_id', 'picking_type_id', 'company_id') 570 def onchange_product_id(self): 571 """ Finds UoM of changed product. """ 572 if not self.product_id: 573 self.bom_id = False 574 elif not self.bom_id or self.bom_id.product_tmpl_id != self.product_tmpl_id or (self.bom_id.product_id and self.bom_id.product_id != self.product_id): 575 bom = self.env['mrp.bom']._bom_find(product=self.product_id, picking_type=self.picking_type_id, company_id=self.company_id.id, bom_type='normal') 576 if bom: 577 self.bom_id = bom.id 578 self.product_qty = self.bom_id.product_qty 579 self.product_uom_id = self.bom_id.product_uom_id.id 580 else: 581 self.bom_id = False 582 self.product_uom_id = self.product_id.uom_id.id 583 584 @api.onchange('product_qty', 'product_uom_id') 585 def _onchange_product_qty(self): 586 for workorder in self.workorder_ids: 587 workorder.product_uom_id = self.product_uom_id 588 if self._origin.product_qty: 589 workorder.duration_expected = workorder._get_duration_expected(ratio=self.product_qty / self._origin.product_qty) 590 else: 591 workorder.duration_expected = workorder._get_duration_expected() 592 if workorder.date_planned_start and workorder.duration_expected: 593 workorder.date_planned_finished = workorder.date_planned_start + relativedelta(minutes=workorder.duration_expected) 594 595 @api.onchange('bom_id') 596 def _onchange_bom_id(self): 597 if not self.product_id and self.bom_id: 598 self.product_id = self.bom_id.product_id or self.bom_id.product_tmpl_id.product_variant_ids[0] 599 self.product_qty = self.bom_id.product_qty or 1.0 600 self.product_uom_id = self.bom_id and self.bom_id.product_uom_id.id or self.product_id.uom_id.id 601 self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)] 602 self.move_finished_ids = [(2, move.id) for move in self.move_finished_ids] 603 self.picking_type_id = self.bom_id.picking_type_id or self.picking_type_id 604 605 @api.onchange('date_planned_start', 'product_id') 606 def _onchange_date_planned_start(self): 607 if self.date_planned_start and not self.is_planned: 608 date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay) 609 date_planned_finished = date_planned_finished + relativedelta(days=self.company_id.manufacturing_lead) 610 if date_planned_finished == self.date_planned_start: 611 date_planned_finished = date_planned_finished + relativedelta(hours=1) 612 self.date_planned_finished = date_planned_finished 613 self.move_raw_ids = [(1, m.id, {'date': self.date_planned_start}) for m in self.move_raw_ids] 614 self.move_finished_ids = [(1, m.id, {'date': date_planned_finished}) for m in self.move_finished_ids] 615 616 @api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id') 617 def _onchange_move_raw(self): 618 if not self.bom_id and not self._origin.product_id: 619 return 620 # Clear move raws if we are changing the product. In case of creation (self._origin is empty), 621 # we need to avoid keeping incorrect lines, so clearing is necessary too. 622 if self.product_id != self._origin.product_id: 623 self.move_raw_ids = [(5,)] 624 if self.bom_id and self.product_qty > 0: 625 # keep manual entries 626 list_move_raw = [(4, move.id) for move in self.move_raw_ids.filtered(lambda m: not m.bom_line_id)] 627 moves_raw_values = self._get_moves_raw_values() 628 move_raw_dict = {move.bom_line_id.id: move for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)} 629 for move_raw_values in moves_raw_values: 630 if move_raw_values['bom_line_id'] in move_raw_dict: 631 # update existing entries 632 list_move_raw += [(1, move_raw_dict[move_raw_values['bom_line_id']].id, move_raw_values)] 633 else: 634 # add new entries 635 list_move_raw += [(0, 0, move_raw_values)] 636 self.move_raw_ids = list_move_raw 637 else: 638 self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)] 639 640 @api.onchange('product_id') 641 def _onchange_move_finished_product(self): 642 self.move_finished_ids = [(5,)] 643 if self.product_id: 644 self._create_update_move_finished() 645 646 @api.onchange('bom_id', 'product_qty', 'product_uom_id') 647 def _onchange_move_finished(self): 648 if self.product_id and self.product_qty > 0: 649 self._create_update_move_finished() 650 else: 651 self.move_finished_ids = [(2, move.id) for move in self.move_finished_ids.filtered(lambda m: m.bom_line_id)] 652 653 @api.onchange('location_src_id', 'move_raw_ids', 'bom_id') 654 def _onchange_location(self): 655 source_location = self.location_src_id 656 self.move_raw_ids.update({ 657 'warehouse_id': source_location.get_warehouse().id, 658 'location_id': source_location.id, 659 }) 660 661 @api.onchange('location_dest_id', 'move_finished_ids', 'bom_id') 662 def _onchange_location_dest(self): 663 destination_location = self.location_dest_id 664 update_value_list = [] 665 for move in self.move_finished_ids: 666 update_value_list += [(1, move.id, ({ 667 'warehouse_id': destination_location.get_warehouse().id, 668 'location_dest_id': destination_location.id, 669 }))] 670 self.move_finished_ids = update_value_list 671 672 @api.onchange('picking_type_id') 673 def onchange_picking_type(self): 674 location = self.env.ref('stock.stock_location_stock') 675 try: 676 location.check_access_rule('read') 677 except (AttributeError, AccessError): 678 location = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).lot_stock_id 679 self.move_raw_ids.update({'picking_type_id': self.picking_type_id}) 680 self.move_finished_ids.update({'picking_type_id': self.picking_type_id}) 681 self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id 682 self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id 683 684 @api.onchange('qty_producing', 'lot_producing_id') 685 def _onchange_producing(self): 686 self._set_qty_producing() 687 688 @api.onchange('lot_producing_id') 689 def _onchange_lot_producing(self): 690 if self.product_id.tracking == 'serial': 691 if self.env['stock.move.line'].search_count([ 692 ('company_id', '=', self.company_id.id), 693 ('product_id', '=', self.product_id.id), 694 ('lot_id', '=', self.lot_producing_id.id), 695 ('state', '!=', 'cancel') 696 ]): 697 return { 698 'warning': { 699 'title': _('Warning'), 700 'message': _('Existing Serial number (%s). Please correct the serial numbers encoded.') % self.lot_producing_id.name 701 } 702 } 703 704 @api.onchange('bom_id') 705 def _onchange_workorder_ids(self): 706 if self.bom_id: 707 self._create_workorder() 708 else: 709 self.workorder_ids = False 710 711 def write(self, vals): 712 if 'workorder_ids' in self: 713 production_to_replan = self.filtered(lambda p: p.is_planned) 714 res = super(MrpProduction, self).write(vals) 715 716 for production in self: 717 if 'date_planned_start' in vals and not self.env.context.get('force_date', False): 718 if production.state in ['done', 'cancel']: 719 raise UserError(_('You cannot move a manufacturing order once it is cancelled or done.')) 720 if production.is_planned: 721 production.button_unplan() 722 move_vals = self._get_move_finished_values(self.product_id, self.product_uom_qty, self.product_uom_id) 723 production.move_finished_ids.write({'date': move_vals['date']}) 724 if vals.get('date_planned_start'): 725 production.move_raw_ids.write({'date': production.date_planned_start, 'date_deadline': production.date_planned_start}) 726 if vals.get('date_planned_finished'): 727 production.move_finished_ids.write({'date': production.date_planned_finished}) 728 if any(field in ['move_raw_ids', 'move_finished_ids', 'workorder_ids'] for field in vals) and production.state != 'draft': 729 if production.state == 'done': 730 # for some reason moves added after state = 'done' won't save group_id, reference if added in 731 # "stock_move.default_get()" 732 production.move_raw_ids.filtered(lambda move: move.additional and move.date > production.date_planned_start).write({ 733 'group_id': production.procurement_group_id.id, 734 'reference': production.name, 735 'date': production.date_planned_start, 736 'date_deadline': production.date_planned_start 737 }) 738 production.move_finished_ids.filtered(lambda move: move.additional and move.date > production.date_planned_finished).write({ 739 'reference': production.name, 740 'date': production.date_planned_finished, 741 'date_deadline': production.date_deadline 742 }) 743 production._autoconfirm_production() 744 if production in production_to_replan: 745 production._plan_workorders(replan=True) 746 if production.state == 'done' and ('lot_producing_id' in vals or 'qty_producing' in vals): 747 finished_move_lines = production.move_finished_ids.filtered( 748 lambda move: move.product_id == self.product_id and move.state == 'done').mapped('move_line_ids') 749 if 'lot_producing_id' in vals: 750 finished_move_lines.write({'lot_id': vals.get('lot_producing_id')}) 751 if 'qty_producing' in vals: 752 finished_move_lines.write({'qty_done': vals.get('qty_producing')}) 753 754 if not production.bom_id.operation_ids and vals.get('date_planned_start') and not vals.get('date_planned_finished'): 755 new_date_planned_start = fields.Datetime.to_datetime(vals.get('date_planned_start')) 756 if not production.date_planned_finished or new_date_planned_start >= production.date_planned_finished: 757 production.date_planned_finished = new_date_planned_start + datetime.timedelta(hours=1) 758 return res 759 760 @api.model 761 def create(self, values): 762 # Remove from `move_finished_ids` the by-product moves and then move `move_byproduct_ids` 763 # into `move_finished_ids` to avoid duplicate and inconsistency. 764 if values.get('move_finished_ids', False): 765 values['move_finished_ids'] = list(filter(lambda move: move[2]['byproduct_id'] is False, values['move_finished_ids'])) 766 if values.get('move_byproduct_ids', False): 767 values['move_finished_ids'] = values.get('move_finished_ids', []) + values['move_byproduct_ids'] 768 del values['move_byproduct_ids'] 769 if not values.get('name', False) or values['name'] == _('New'): 770 picking_type_id = values.get('picking_type_id') or self._get_default_picking_type() 771 picking_type_id = self.env['stock.picking.type'].browse(picking_type_id) 772 if picking_type_id: 773 values['name'] = picking_type_id.sequence_id.next_by_id() 774 else: 775 values['name'] = self.env['ir.sequence'].next_by_code('mrp.production') or _('New') 776 if not values.get('procurement_group_id'): 777 procurement_group_vals = self._prepare_procurement_group_vals(values) 778 values['procurement_group_id'] = self.env["procurement.group"].create(procurement_group_vals).id 779 production = super(MrpProduction, self).create(values) 780 (production.move_raw_ids | production.move_finished_ids).write({ 781 'group_id': production.procurement_group_id.id, 782 'origin': production.name 783 }) 784 production.move_raw_ids.write({'date': production.date_planned_start}) 785 production.move_finished_ids.write({'date': production.date_planned_finished}) 786 # Trigger move_raw creation when importing a file 787 if 'import_file' in self.env.context: 788 production._onchange_move_raw() 789 production._onchange_move_finished() 790 return production 791 792 def unlink(self): 793 if any(production.state == 'done' for production in self): 794 raise UserError(_('Cannot delete a manufacturing order in done state.')) 795 self.action_cancel() 796 not_cancel = self.filtered(lambda m: m.state != 'cancel') 797 if not_cancel: 798 productions_name = ', '.join([prod.display_name for prod in not_cancel]) 799 raise UserError(_('%s cannot be deleted. Try to cancel them before.', productions_name)) 800 801 workorders_to_delete = self.workorder_ids.filtered(lambda wo: wo.state != 'done') 802 if workorders_to_delete: 803 workorders_to_delete.unlink() 804 return super(MrpProduction, self).unlink() 805 806 def copy_data(self, default=None): 807 default = dict(default or {}) 808 # covers at least 2 cases: backorders generation (follow default logic for moves copying) 809 # and copying a done MO via the form (i.e. copy only the non-cancelled moves since no backorder = cancelled finished moves) 810 if not default or 'move_finished_ids' not in default: 811 move_finished_ids = self.move_finished_ids 812 if self.state != 'cancel': 813 move_finished_ids = self.move_finished_ids.filtered(lambda m: m.state != 'cancel' and m.product_qty != 0.0) 814 default['move_finished_ids'] = [(0, 0, move.copy_data()[0]) for move in move_finished_ids] 815 if not default or 'move_raw_ids' not in default: 816 default['move_raw_ids'] = [(0, 0, move.copy_data()[0]) for move in self.move_raw_ids.filtered(lambda m: m.product_qty != 0.0)] 817 return super(MrpProduction, self).copy_data(default=default) 818 819 def action_toggle_is_locked(self): 820 self.ensure_one() 821 self.is_locked = not self.is_locked 822 return True 823 824 def _create_workorder(self): 825 for production in self: 826 if not production.bom_id: 827 continue 828 workorders_values = [] 829 830 product_qty = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) 831 exploded_boms, dummy = production.bom_id.explode(production.product_id, product_qty / production.bom_id.product_qty, picking_type=production.bom_id.picking_type_id) 832 833 for bom, bom_data in exploded_boms: 834 # If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders. 835 if not (bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids)): 836 continue 837 for operation in bom.operation_ids: 838 workorders_values += [{ 839 'name': operation.name, 840 'production_id': production.id, 841 'workcenter_id': operation.workcenter_id.id, 842 'product_uom_id': production.product_uom_id.id, 843 'operation_id': operation.id, 844 'state': 'pending', 845 'consumption': production.consumption, 846 }] 847 production.workorder_ids = [(5, 0)] + [(0, 0, value) for value in workorders_values] 848 for workorder in production.workorder_ids: 849 workorder.duration_expected = workorder._get_duration_expected() 850 851 def _get_move_finished_values(self, product_id, product_uom_qty, product_uom, operation_id=False, byproduct_id=False): 852 group_orders = self.procurement_group_id.mrp_production_ids 853 move_dest_ids = self.move_dest_ids 854 if len(group_orders) > 1: 855 move_dest_ids |= group_orders[0].move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_dest_ids 856 date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay) 857 date_planned_finished = date_planned_finished + relativedelta(days=self.company_id.manufacturing_lead) 858 if date_planned_finished == self.date_planned_start: 859 date_planned_finished = date_planned_finished + relativedelta(hours=1) 860 return { 861 'product_id': product_id, 862 'product_uom_qty': product_uom_qty, 863 'product_uom': product_uom, 864 'operation_id': operation_id, 865 'byproduct_id': byproduct_id, 866 'name': self.name, 867 'date': date_planned_finished, 868 'date_deadline': self.date_deadline, 869 'picking_type_id': self.picking_type_id.id, 870 'location_id': self.product_id.with_company(self.company_id).property_stock_production.id, 871 'location_dest_id': self.location_dest_id.id, 872 'company_id': self.company_id.id, 873 'production_id': self.id, 874 'warehouse_id': self.location_dest_id.get_warehouse().id, 875 'origin': self.name, 876 'group_id': self.procurement_group_id.id, 877 'propagate_cancel': self.propagate_cancel, 878 'move_dest_ids': [(4, x.id) for x in self.move_dest_ids if not byproduct_id], 879 } 880 881 def _get_moves_finished_values(self): 882 moves = [] 883 for production in self: 884 if production.product_id in production.bom_id.byproduct_ids.mapped('product_id'): 885 raise UserError(_("You cannot have %s as the finished product and in the Byproducts", self.product_id.name)) 886 moves.append(production._get_move_finished_values(production.product_id.id, production.product_qty, production.product_uom_id.id)) 887 for byproduct in production.bom_id.byproduct_ids: 888 product_uom_factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) 889 qty = byproduct.product_qty * (product_uom_factor / production.bom_id.product_qty) 890 moves.append(production._get_move_finished_values( 891 byproduct.product_id.id, qty, byproduct.product_uom_id.id, 892 byproduct.operation_id.id, byproduct.id)) 893 return moves 894 895 def _create_update_move_finished(self): 896 """ This is a helper function to support complexity of onchange logic for MOs. 897 It is important that the special *2Many commands used here remain as long as function 898 is used within onchanges. 899 """ 900 # keep manual entries 901 list_move_finished = [(4, move.id) for move in self.move_finished_ids.filtered( 902 lambda m: not m.byproduct_id and m.product_id != self.product_id)] 903 list_move_finished = [] 904 moves_finished_values = self._get_moves_finished_values() 905 moves_byproduct_dict = {move.byproduct_id.id: move for move in self.move_finished_ids.filtered(lambda m: m.byproduct_id)} 906 move_finished = self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id) 907 for move_finished_values in moves_finished_values: 908 if move_finished_values.get('byproduct_id') in moves_byproduct_dict: 909 # update existing entries 910 list_move_finished += [(1, moves_byproduct_dict[move_finished_values['byproduct_id']].id, move_finished_values)] 911 elif move_finished_values.get('product_id') == self.product_id.id and move_finished: 912 list_move_finished += [(1, move_finished.id, move_finished_values)] 913 else: 914 # add new entries 915 list_move_finished += [(0, 0, move_finished_values)] 916 self.move_finished_ids = list_move_finished 917 918 def _get_moves_raw_values(self): 919 moves = [] 920 for production in self: 921 factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) / production.bom_id.product_qty 922 boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id) 923 for bom_line, line_data in lines: 924 if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom' or\ 925 bom_line.product_id.type not in ['product', 'consu']: 926 continue 927 operation = bom_line.operation_id.id or line_data['parent_line'] and line_data['parent_line'].operation_id.id 928 moves.append(production._get_move_raw_values( 929 bom_line.product_id, 930 line_data['qty'], 931 bom_line.product_uom_id, 932 operation, 933 bom_line 934 )) 935 return moves 936 937 def _get_move_raw_values(self, product_id, product_uom_qty, product_uom, operation_id=False, bom_line=False): 938 source_location = self.location_src_id 939 data = { 940 'sequence': bom_line.sequence if bom_line else 10, 941 'name': self.name, 942 'date': self.date_planned_start, 943 'date_deadline': self.date_planned_start, 944 'bom_line_id': bom_line.id if bom_line else False, 945 'picking_type_id': self.picking_type_id.id, 946 'product_id': product_id.id, 947 'product_uom_qty': product_uom_qty, 948 'product_uom': product_uom.id, 949 'location_id': source_location.id, 950 'location_dest_id': self.product_id.with_company(self.company_id).property_stock_production.id, 951 'raw_material_production_id': self.id, 952 'company_id': self.company_id.id, 953 'operation_id': operation_id, 954 'price_unit': product_id.standard_price, 955 'procure_method': 'make_to_stock', 956 'origin': self.name, 957 'state': 'draft', 958 'warehouse_id': source_location.get_warehouse().id, 959 'group_id': self.procurement_group_id.id, 960 'propagate_cancel': self.propagate_cancel, 961 } 962 return data 963 964 def _set_qty_producing(self): 965 if self.product_id.tracking == 'serial': 966 qty_producing_uom = self.product_uom_id._compute_quantity(self.qty_producing, self.product_id.uom_id, rounding_method='HALF-UP') 967 if qty_producing_uom != 1: 968 self.qty_producing = self.product_id.uom_id._compute_quantity(1, self.product_uom_id, rounding_method='HALF-UP') 969 970 for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)): 971 if move._should_bypass_set_qty_producing() or not move.product_uom: 972 continue 973 new_qty = float_round((self.qty_producing - self.qty_produced) * move.unit_factor, precision_rounding=move.product_uom.rounding) 974 move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0 975 move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty) 976 977 def _update_raw_moves(self, factor): 978 self.ensure_one() 979 update_info = [] 980 move_to_unlink = self.env['stock.move'] 981 for move in self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')): 982 old_qty = move.product_uom_qty 983 new_qty = old_qty * factor 984 if new_qty > 0: 985 move.write({'product_uom_qty': new_qty}) 986 move._action_assign() 987 update_info.append((move, old_qty, new_qty)) 988 else: 989 if move.quantity_done > 0: 990 raise UserError(_('Lines need to be deleted, but can not as you still have some quantities to consume in them. ')) 991 move._action_cancel() 992 move_to_unlink |= move 993 move_to_unlink.unlink() 994 return update_info 995 996 def _get_ready_to_produce_state(self): 997 """ returns 'assigned' if enough components are reserved in order to complete 998 the first operation of the bom. If not returns 'waiting' 999 """ 1000 self.ensure_one() 1001 first_operation = self.bom_id.operation_ids[0] 1002 if len(self.bom_id.operation_ids) == 1: 1003 moves_in_first_operation = self.move_raw_ids 1004 else: 1005 moves_in_first_operation = self.move_raw_ids.filtered(lambda move: move.operation_id == first_operation) 1006 moves_in_first_operation = moves_in_first_operation.filtered( 1007 lambda move: move.bom_line_id and 1008 not move.bom_line_id._skip_bom_line(self.product_id) 1009 ) 1010 1011 if all(move.state == 'assigned' for move in moves_in_first_operation): 1012 return 'assigned' 1013 return 'confirmed' 1014 1015 def _autoconfirm_production(self): 1016 """Automatically run `action_confirm` on `self`. 1017 1018 If the production has one of its move was added after the initial call 1019 to `action_confirm`. 1020 """ 1021 moves_to_confirm = self.env['stock.move'] 1022 for production in self: 1023 if production.state in ('done', 'cancel'): 1024 continue 1025 additional_moves = production.move_raw_ids.filtered( 1026 lambda move: move.state == 'draft' and move.additional 1027 ) 1028 additional_moves.write({ 1029 'group_id': production.procurement_group_id.id, 1030 }) 1031 additional_moves._adjust_procure_method() 1032 moves_to_confirm |= additional_moves 1033 additional_byproducts = production.move_finished_ids.filtered( 1034 lambda move: move.state == 'draft' and move.additional 1035 ) 1036 moves_to_confirm |= additional_byproducts 1037 1038 if moves_to_confirm: 1039 moves_to_confirm._action_confirm() 1040 # run scheduler for moves forecasted to not have enough in stock 1041 moves_to_confirm._trigger_scheduler() 1042 1043 self.workorder_ids.filtered(lambda w: w.state not in ['done', 'cancel'])._action_confirm() 1044 1045 def action_view_mrp_production_childs(self): 1046 self.ensure_one() 1047 mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids 1048 action = { 1049 'res_model': 'mrp.production', 1050 'type': 'ir.actions.act_window', 1051 } 1052 if len(mrp_production_ids) == 1: 1053 action.update({ 1054 'view_mode': 'form', 1055 'res_id': mrp_production_ids[0], 1056 }) 1057 else: 1058 action.update({ 1059 'name': _("%s Child MO's") % self.name, 1060 'domain': [('id', 'in', mrp_production_ids)], 1061 'view_mode': 'tree,form', 1062 }) 1063 return action 1064 1065 def action_view_mrp_production_sources(self): 1066 self.ensure_one() 1067 mrp_production_ids = self.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids.ids 1068 action = { 1069 'res_model': 'mrp.production', 1070 'type': 'ir.actions.act_window', 1071 } 1072 if len(mrp_production_ids) == 1: 1073 action.update({ 1074 'view_mode': 'form', 1075 'res_id': mrp_production_ids[0], 1076 }) 1077 else: 1078 action.update({ 1079 'name': _("MO Generated by %s") % self.name, 1080 'domain': [('id', 'in', mrp_production_ids)], 1081 'view_mode': 'tree,form', 1082 }) 1083 return action 1084 1085 def action_view_mrp_production_backorders(self): 1086 backorder_ids = self.procurement_group_id.mrp_production_ids.ids 1087 return { 1088 'res_model': 'mrp.production', 1089 'type': 'ir.actions.act_window', 1090 'name': _("Backorder MO's"), 1091 'domain': [('id', 'in', backorder_ids)], 1092 'view_mode': 'tree,form', 1093 } 1094 1095 def action_generate_serial(self): 1096 self.ensure_one() 1097 self.lot_producing_id = self.env['stock.production.lot'].create({ 1098 'product_id': self.product_id.id, 1099 'company_id': self.company_id.id 1100 }) 1101 if self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_line_ids: 1102 self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_line_ids.lot_id = self.lot_producing_id 1103 if self.product_id.tracking == 'serial': 1104 self._set_qty_producing() 1105 1106 def _action_generate_immediate_wizard(self): 1107 view = self.env.ref('mrp.view_immediate_production') 1108 return { 1109 'name': _('Immediate Production?'), 1110 'type': 'ir.actions.act_window', 1111 'view_mode': 'form', 1112 'res_model': 'mrp.immediate.production', 1113 'views': [(view.id, 'form')], 1114 'view_id': view.id, 1115 'target': 'new', 1116 'context': dict(self.env.context, default_mo_ids=[(4, mo.id) for mo in self]), 1117 } 1118 1119 def action_confirm(self): 1120 self._check_company() 1121 for production in self: 1122 if production.bom_id: 1123 production.consumption = production.bom_id.consumption 1124 if not production.move_raw_ids: 1125 raise UserError(_("Add some materials to consume before marking this MO as to do.")) 1126 # In case of Serial number tracking, force the UoM to the UoM of product 1127 if production.product_tracking == 'serial' and production.product_uom_id != production.product_id.uom_id: 1128 production.write({ 1129 'product_qty': production.product_uom_id._compute_quantity(production.product_qty, production.product_id.uom_id), 1130 'product_uom_id': production.product_id.uom_id 1131 }) 1132 for move_finish in production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id): 1133 move_finish.write({ 1134 'product_uom_qty': move_finish.product_uom._compute_quantity(move_finish.product_uom_qty, move_finish.product_id.uom_id), 1135 'product_uom': move_finish.product_id.uom_id 1136 }) 1137 production.move_raw_ids._adjust_procure_method() 1138 (production.move_raw_ids | production.move_finished_ids)._action_confirm() 1139 production.workorder_ids._action_confirm() 1140 1141 # run scheduler for moves forecasted to not have enough in stock 1142 self.move_raw_ids._trigger_scheduler() 1143 return True 1144 1145 def action_assign(self): 1146 for production in self: 1147 production.move_raw_ids._action_assign() 1148 return True 1149 1150 def button_plan(self): 1151 """ Create work orders. And probably do stuff, like things. """ 1152 orders_to_plan = self.filtered(lambda order: not order.is_planned) 1153 orders_to_confirm = orders_to_plan.filtered(lambda mo: mo.state == 'draft') 1154 orders_to_confirm.action_confirm() 1155 for order in orders_to_plan: 1156 order._plan_workorders() 1157 return True 1158 1159 def _plan_workorders(self, replan=False): 1160 """ Plan all the production's workorders depending on the workcenters 1161 work schedule. 1162 1163 :param replan: If it is a replan, only ready and pending workorder will be take in account 1164 :type replan: bool. 1165 """ 1166 self.ensure_one() 1167 1168 if not self.workorder_ids: 1169 return 1170 # Schedule all work orders (new ones and those already created) 1171 qty_to_produce = max(self.product_qty - self.qty_produced, 0) 1172 qty_to_produce = self.product_uom_id._compute_quantity(qty_to_produce, self.product_id.uom_id) 1173 start_date = max(self.date_planned_start, datetime.datetime.now()) 1174 if replan: 1175 workorder_ids = self.workorder_ids.filtered(lambda wo: wo.state in ['ready', 'pending']) 1176 # We plan the manufacturing order according to its `date_planned_start`, but if 1177 # `date_planned_start` is in the past, we plan it as soon as possible. 1178 workorder_ids.leave_id.unlink() 1179 else: 1180 workorder_ids = self.workorder_ids.filtered(lambda wo: not wo.date_planned_start) 1181 for workorder in workorder_ids: 1182 workcenters = workorder.workcenter_id | workorder.workcenter_id.alternative_workcenter_ids 1183 1184 best_finished_date = datetime.datetime.max 1185 vals = {} 1186 for workcenter in workcenters: 1187 # compute theoretical duration 1188 if workorder.workcenter_id == workcenter: 1189 duration_expected = workorder.duration_expected 1190 else: 1191 duration_expected = workorder._get_duration_expected(alternative_workcenter=workcenter) 1192 1193 from_date, to_date = workcenter._get_first_available_slot(start_date, duration_expected) 1194 # If the workcenter is unavailable, try planning on the next one 1195 if not from_date: 1196 continue 1197 # Check if this workcenter is better than the previous ones 1198 if to_date and to_date < best_finished_date: 1199 best_start_date = from_date 1200 best_finished_date = to_date 1201 best_workcenter = workcenter 1202 vals = { 1203 'workcenter_id': workcenter.id, 1204 'duration_expected': duration_expected, 1205 } 1206 1207 # If none of the workcenter are available, raise 1208 if best_finished_date == datetime.datetime.max: 1209 raise UserError(_('Impossible to plan the workorder. Please check the workcenter availabilities.')) 1210 1211 # Instantiate start_date for the next workorder planning 1212 if workorder.next_work_order_id: 1213 start_date = best_finished_date 1214 1215 # Create leave on chosen workcenter calendar 1216 leave = self.env['resource.calendar.leaves'].create({ 1217 'name': workorder.display_name, 1218 'calendar_id': best_workcenter.resource_calendar_id.id, 1219 'date_from': best_start_date, 1220 'date_to': best_finished_date, 1221 'resource_id': best_workcenter.resource_id.id, 1222 'time_type': 'other' 1223 }) 1224 vals['leave_id'] = leave.id 1225 workorder.write(vals) 1226 self.with_context(force_date=True).write({ 1227 'date_planned_start': self.workorder_ids[0].date_planned_start, 1228 'date_planned_finished': self.workorder_ids[-1].date_planned_finished 1229 }) 1230 1231 def button_unplan(self): 1232 if any(wo.state == 'done' for wo in self.workorder_ids): 1233 raise UserError(_("Some work orders are already done, you cannot unplan this manufacturing order.")) 1234 elif any(wo.state == 'progress' for wo in self.workorder_ids): 1235 raise UserError(_("Some work orders have already started, you cannot unplan this manufacturing order.")) 1236 1237 self.workorder_ids.leave_id.unlink() 1238 self.workorder_ids.write({ 1239 'date_planned_start': False, 1240 'date_planned_finished': False, 1241 }) 1242 1243 def _get_consumption_issues(self): 1244 """Compare the quantity consumed of the components, the expected quantity 1245 on the BoM and the consumption parameter on the order. 1246 1247 :return: list of tuples (order_id, product_id, consumed_qty, expected_qty) where the 1248 consumption isn't honored. order_id and product_id are recordset of mrp.production 1249 and product.product respectively 1250 :rtype: list 1251 """ 1252 issues = [] 1253 if self.env.context.get('skip_consumption', False) or self.env.context.get('skip_immediate', False): 1254 return issues 1255 for order in self: 1256 if order.consumption == 'flexible' or not order.bom_id or not order.bom_id.bom_line_ids: 1257 continue 1258 expected_move_values = order._get_moves_raw_values() 1259 expected_qty_by_product = defaultdict(float) 1260 for move_values in expected_move_values: 1261 move_product = self.env['product.product'].browse(move_values['product_id']) 1262 move_uom = self.env['uom.uom'].browse(move_values['product_uom']) 1263 move_product_qty = move_uom._compute_quantity(move_values['product_uom_qty'], move_product.uom_id) 1264 expected_qty_by_product[move_product] += move_product_qty * order.qty_producing / order.product_qty 1265 1266 done_qty_by_product = defaultdict(float) 1267 for move in order.move_raw_ids: 1268 qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id) 1269 rounding = move.product_id.uom_id.rounding 1270 if not (move.product_id in expected_qty_by_product or float_is_zero(qty_done, precision_rounding=rounding)): 1271 issues.append((order, move.product_id, qty_done, 0.0)) 1272 continue 1273 done_qty_by_product[move.product_id] += qty_done 1274 1275 for product, qty_to_consume in expected_qty_by_product.items(): 1276 qty_done = done_qty_by_product.get(product, 0.0) 1277 if float_compare(qty_to_consume, qty_done, precision_rounding=product.uom_id.rounding) != 0: 1278 issues.append((order, product, qty_done, qty_to_consume)) 1279 1280 return issues 1281 1282 def _action_generate_consumption_wizard(self, consumption_issues): 1283 ctx = self.env.context.copy() 1284 lines = [] 1285 for order, product_id, consumed_qty, expected_qty in consumption_issues: 1286 lines.append((0, 0, { 1287 'mrp_production_id': order.id, 1288 'product_id': product_id.id, 1289 'consumption': order.consumption, 1290 'product_uom_id': product_id.uom_id.id, 1291 'product_consumed_qty_uom': consumed_qty, 1292 'product_expected_qty_uom': expected_qty 1293 })) 1294 ctx.update({'default_mrp_production_ids': self.ids, 'default_mrp_consumption_warning_line_ids': lines}) 1295 action = self.env["ir.actions.actions"]._for_xml_id("mrp.action_mrp_consumption_warning") 1296 action['context'] = ctx 1297 return action 1298 1299 def _get_quantity_produced_issues(self): 1300 quantity_issues = [] 1301 if self.env.context.get('skip_backorder', False): 1302 return quantity_issues 1303 for order in self: 1304 if not float_is_zero(order._get_quantity_to_backorder(), precision_rounding=order.product_uom_id.rounding): 1305 quantity_issues.append(order) 1306 return quantity_issues 1307 1308 def _action_generate_backorder_wizard(self, quantity_issues): 1309 ctx = self.env.context.copy() 1310 lines = [] 1311 for order in quantity_issues: 1312 lines.append((0, 0, { 1313 'mrp_production_id': order.id, 1314 'to_backorder': True 1315 })) 1316 ctx.update({'default_mrp_production_ids': self.ids, 'default_mrp_production_backorder_line_ids': lines}) 1317 action = self.env["ir.actions.actions"]._for_xml_id("mrp.action_mrp_production_backorder") 1318 action['context'] = ctx 1319 return action 1320 1321 def action_cancel(self): 1322 """ Cancels production order, unfinished stock moves and set procurement 1323 orders in exception """ 1324 if not self.move_raw_ids: 1325 self.state = 'cancel' 1326 return True 1327 self._action_cancel() 1328 return True 1329 1330 def _action_cancel(self): 1331 documents_by_production = {} 1332 for production in self: 1333 documents = defaultdict(list) 1334 for move_raw_id in self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')): 1335 iterate_key = self._get_document_iterate_key(move_raw_id) 1336 if iterate_key: 1337 document = self.env['stock.picking']._log_activity_get_documents({move_raw_id: (move_raw_id.product_uom_qty, 0)}, iterate_key, 'UP') 1338 for key, value in document.items(): 1339 documents[key] += [value] 1340 if documents: 1341 documents_by_production[production] = documents 1342 # log an activity on Parent MO if child MO is cancelled. 1343 finish_moves = production.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) 1344 if finish_moves: 1345 production._log_downside_manufactured_quantity({finish_move: (production.product_uom_qty, 0.0) for finish_move in finish_moves}, cancel=True) 1346 1347 self.workorder_ids.filtered(lambda x: x.state not in ['done', 'cancel']).action_cancel() 1348 finish_moves = self.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) 1349 raw_moves = self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) 1350 1351 (finish_moves | raw_moves)._action_cancel() 1352 picking_ids = self.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel')) 1353 picking_ids.action_cancel() 1354 1355 for production, documents in documents_by_production.items(): 1356 filtered_documents = {} 1357 for (parent, responsible), rendering_context in documents.items(): 1358 if not parent or parent._name == 'stock.picking' and parent.state == 'cancel' or parent == production: 1359 continue 1360 filtered_documents[(parent, responsible)] = rendering_context 1361 production._log_manufacture_exception(filtered_documents, cancel=True) 1362 1363 # In case of a flexible BOM, we don't know from the state of the moves if the MO should 1364 # remain in progress or done. Indeed, if all moves are done/cancel but the quantity produced 1365 # is lower than expected, it might mean: 1366 # - we have used all components but we still want to produce the quantity expected 1367 # - we have used all components and we won't be able to produce the last units 1368 # 1369 # However, if the user clicks on 'Cancel', it is expected that the MO is either done or 1370 # canceled. If the MO is still in progress at this point, it means that the move raws 1371 # are either all done or a mix of done / canceled => the MO should be done. 1372 self.filtered(lambda p: p.state not in ['done', 'cancel'] and p.bom_id.consumption == 'flexible').write({'state': 'done'}) 1373 1374 return True 1375 1376 def _get_document_iterate_key(self, move_raw_id): 1377 return move_raw_id.move_orig_ids and 'move_orig_ids' or False 1378 1379 def _cal_price(self, consumed_moves): 1380 self.ensure_one() 1381 return True 1382 1383 def _post_inventory(self, cancel_backorder=False): 1384 for order in self: 1385 moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') 1386 moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) 1387 for move in moves_to_do.filtered(lambda m: m.product_qty == 0.0 and m.quantity_done > 0): 1388 move.product_uom_qty = move.quantity_done 1389 # MRP do not merge move, catch the result of _action_done in order 1390 # to get extra moves. 1391 moves_to_do = moves_to_do._action_done() 1392 moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do 1393 1394 finish_moves = order.move_finished_ids.filtered(lambda m: m.product_id == order.product_id and m.state not in ('done', 'cancel')) 1395 # the finish move can already be completed by the workorder. 1396 if not finish_moves.quantity_done: 1397 finish_moves.quantity_done = float_round(order.qty_producing - order.qty_produced, precision_rounding=order.product_uom_id.rounding, rounding_method='HALF-UP') 1398 finish_moves.move_line_ids.lot_id = order.lot_producing_id 1399 order._cal_price(moves_to_do) 1400 1401 moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) 1402 moves_to_finish = moves_to_finish._action_done(cancel_backorder=cancel_backorder) 1403 order.action_assign() 1404 consume_move_lines = moves_to_do.mapped('move_line_ids') 1405 order.move_finished_ids.move_line_ids.consume_line_ids = [(6, 0, consume_move_lines.ids)] 1406 return True 1407 1408 @api.model 1409 def _get_name_backorder(self, name, sequence): 1410 if not sequence: 1411 return name 1412 seq_back = "-" + "0" * (SIZE_BACK_ORDER_NUMERING - 1 - int(math.log10(sequence))) + str(sequence) 1413 regex = re.compile(r"-\d+$") 1414 if regex.search(name) and sequence > 1: 1415 return regex.sub(seq_back, name) 1416 return name + seq_back 1417 1418 def _get_backorder_mo_vals(self): 1419 self.ensure_one() 1420 next_seq = max(self.procurement_group_id.mrp_production_ids.mapped("backorder_sequence")) 1421 return { 1422 'name': self._get_name_backorder(self.name, next_seq + 1), 1423 'backorder_sequence': next_seq + 1, 1424 'procurement_group_id': self.procurement_group_id.id, 1425 'move_raw_ids': None, 1426 'move_finished_ids': None, 1427 'product_qty': self._get_quantity_to_backorder(), 1428 'lot_producing_id': False, 1429 'origin': self.origin 1430 } 1431 1432 def _generate_backorder_productions(self, close_mo=True): 1433 backorders = self.env['mrp.production'] 1434 for production in self: 1435 if production.backorder_sequence == 0: # Activate backorder naming 1436 production.backorder_sequence = 1 1437 production.name = self._get_name_backorder(production.name, production.backorder_sequence) 1438 backorder_mo = production.copy(default=production._get_backorder_mo_vals()) 1439 if close_mo: 1440 production.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({ 1441 'raw_material_production_id': backorder_mo.id, 1442 }) 1443 production.move_finished_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({ 1444 'production_id': backorder_mo.id, 1445 }) 1446 else: 1447 new_moves_vals = [] 1448 for move in production.move_raw_ids | production.move_finished_ids: 1449 if not move.additional: 1450 qty_to_split = move.product_uom_qty - move.unit_factor * production.qty_producing 1451 qty_to_split = move.product_uom._compute_quantity(qty_to_split, move.product_id.uom_id, rounding_method='HALF-UP') 1452 move_vals = move._split(qty_to_split) 1453 if not move_vals: 1454 continue 1455 if move.raw_material_production_id: 1456 move_vals[0]['raw_material_production_id'] = backorder_mo.id 1457 else: 1458 move_vals[0]['production_id'] = backorder_mo.id 1459 new_moves_vals.append(move_vals[0]) 1460 new_moves = self.env['stock.move'].create(new_moves_vals) 1461 backorders |= backorder_mo 1462 for old_wo, wo in zip(production.workorder_ids, backorder_mo.workorder_ids): 1463 wo.qty_produced = max(old_wo.qty_produced - old_wo.qty_producing, 0) 1464 if wo.product_tracking == 'serial': 1465 wo.qty_producing = 1 1466 else: 1467 wo.qty_producing = wo.qty_remaining 1468 if wo.qty_producing == 0: 1469 wo.action_cancel() 1470 1471 # We need to adapt `duration_expected` on both the original workorders and their 1472 # backordered workorders. To do that, we use the original `duration_expected` and the 1473 # ratio of the quantity really produced and the quantity to produce. 1474 ratio = production.qty_producing / production.product_qty 1475 for workorder in production.workorder_ids: 1476 workorder.duration_expected = workorder.duration_expected * ratio 1477 for workorder in backorder_mo.workorder_ids: 1478 workorder.duration_expected = workorder.duration_expected * (1 - ratio) 1479 1480 # As we have split the moves before validating them, we need to 'remove' the excess reservation 1481 if not close_mo: 1482 self.move_raw_ids.filtered(lambda m: not m.additional)._do_unreserve() 1483 self.move_raw_ids.filtered(lambda m: not m.additional)._action_assign() 1484 # Confirm only productions with remaining components 1485 backorders.filtered(lambda mo: mo.move_raw_ids).action_confirm() 1486 backorders.filtered(lambda mo: mo.move_raw_ids).action_assign() 1487 1488 # Remove the serial move line without reserved quantity. Post inventory will assigned all the non done moves 1489 # So those move lines are duplicated. 1490 backorders.move_raw_ids.move_line_ids.filtered(lambda ml: ml.product_id.tracking == 'serial' and ml.product_qty == 0).unlink() 1491 backorders.move_raw_ids._recompute_state() 1492 1493 return backorders 1494 1495 def button_mark_done(self): 1496 self._button_mark_done_sanity_checks() 1497 1498 if not self.env.context.get('button_mark_done_production_ids'): 1499 self = self.with_context(button_mark_done_production_ids=self.ids) 1500 res = self._pre_button_mark_done() 1501 if res is not True: 1502 return res 1503 1504 if self.env.context.get('mo_ids_to_backorder'): 1505 productions_to_backorder = self.browse(self.env.context['mo_ids_to_backorder']) 1506 productions_not_to_backorder = self - productions_to_backorder 1507 else: 1508 productions_not_to_backorder = self 1509 productions_to_backorder = self.env['mrp.production'] 1510 1511 self.workorder_ids.button_finish() 1512 1513 productions_not_to_backorder._post_inventory(cancel_backorder=True) 1514 productions_to_backorder._post_inventory(cancel_backorder=False) 1515 backorders = productions_to_backorder._generate_backorder_productions() 1516 1517 # if completed products make other confirmed/partially_available moves available, assign them 1518 done_move_finished_ids = (productions_to_backorder.move_finished_ids | productions_not_to_backorder.move_finished_ids).filtered(lambda m: m.state == 'done') 1519 done_move_finished_ids._trigger_assign() 1520 1521 # Moves without quantity done are not posted => set them as done instead of canceling. In 1522 # case the user edits the MO later on and sets some consumed quantity on those, we do not 1523 # want the move lines to be canceled. 1524 (productions_not_to_backorder.move_raw_ids | productions_not_to_backorder.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')).write({ 1525 'state': 'done', 1526 'product_uom_qty': 0.0, 1527 }) 1528 1529 for production in self: 1530 production.write({ 1531 'date_finished': fields.Datetime.now(), 1532 'product_qty': production.qty_produced, 1533 'priority': '0', 1534 'is_locked': True, 1535 }) 1536 1537 for workorder in self.workorder_ids.filtered(lambda w: w.state not in ('done', 'cancel')): 1538 workorder.duration_expected = workorder._get_duration_expected() 1539 1540 if not backorders: 1541 if self.env.context.get('from_workorder'): 1542 return { 1543 'type': 'ir.actions.act_window', 1544 'res_model': 'mrp.production', 1545 'views': [[self.env.ref('mrp.mrp_production_form_view').id, 'form']], 1546 'res_id': self.id, 1547 'target': 'main', 1548 } 1549 return True 1550 context = self.env.context.copy() 1551 context = {k: v for k, v in context.items() if not k.startswith('default_')} 1552 for k, v in context.items(): 1553 if k.startswith('skip_'): 1554 context[k] = False 1555 action = { 1556 'res_model': 'mrp.production', 1557 'type': 'ir.actions.act_window', 1558 'context': dict(context, mo_ids_to_backorder=None) 1559 } 1560 if len(backorders) == 1: 1561 action.update({ 1562 'view_mode': 'form', 1563 'res_id': backorders[0].id, 1564 }) 1565 else: 1566 action.update({ 1567 'name': _("Backorder MO"), 1568 'domain': [('id', 'in', backorders.ids)], 1569 'view_mode': 'tree,form', 1570 }) 1571 return action 1572 1573 def _pre_button_mark_done(self): 1574 productions_to_immediate = self._check_immediate() 1575 if productions_to_immediate: 1576 return productions_to_immediate._action_generate_immediate_wizard() 1577 1578 for production in self: 1579 if float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding): 1580 raise UserError(_('The quantity to produce must be positive!')) 1581 if not any(production.move_raw_ids.mapped('quantity_done')): 1582 raise UserError(_("You must indicate a non-zero amount consumed for at least one of your components")) 1583 1584 consumption_issues = self._get_consumption_issues() 1585 if consumption_issues: 1586 return self._action_generate_consumption_wizard(consumption_issues) 1587 1588 quantity_issues = self._get_quantity_produced_issues() 1589 if quantity_issues: 1590 return self._action_generate_backorder_wizard(quantity_issues) 1591 return True 1592 1593 def _button_mark_done_sanity_checks(self): 1594 self._check_company() 1595 for order in self: 1596 order._check_sn_uniqueness() 1597 1598 def do_unreserve(self): 1599 self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))._do_unreserve() 1600 return True 1601 1602 def button_unreserve(self): 1603 self.ensure_one() 1604 self.do_unreserve() 1605 return True 1606 1607 def button_scrap(self): 1608 self.ensure_one() 1609 return { 1610 'name': _('Scrap'), 1611 'view_mode': 'form', 1612 'res_model': 'stock.scrap', 1613 'view_id': self.env.ref('stock.stock_scrap_form_view2').id, 1614 'type': 'ir.actions.act_window', 1615 'context': {'default_production_id': self.id, 1616 'product_ids': (self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) | self.move_finished_ids.filtered(lambda x: x.state == 'done')).mapped('product_id').ids, 1617 'default_company_id': self.company_id.id 1618 }, 1619 'target': 'new', 1620 } 1621 1622 def action_see_move_scrap(self): 1623 self.ensure_one() 1624 action = self.env["ir.actions.actions"]._for_xml_id("stock.action_stock_scrap") 1625 action['domain'] = [('production_id', '=', self.id)] 1626 action['context'] = dict(self._context, default_origin=self.name) 1627 return action 1628 1629 @api.model 1630 def get_empty_list_help(self, help): 1631 self = self.with_context( 1632 empty_list_help_document_name=_("manufacturing order"), 1633 ) 1634 return super(MrpProduction, self).get_empty_list_help(help) 1635 1636 def _log_downside_manufactured_quantity(self, moves_modification, cancel=False): 1637 1638 def _keys_in_sorted(move): 1639 """ sort by picking and the responsible for the product the 1640 move. 1641 """ 1642 return (move.picking_id.id, move.product_id.responsible_id.id) 1643 1644 def _keys_in_groupby(move): 1645 """ group by picking and the responsible for the product the 1646 move. 1647 """ 1648 return (move.picking_id, move.product_id.responsible_id) 1649 1650 def _render_note_exception_quantity_mo(rendering_context): 1651 values = { 1652 'production_order': self, 1653 'order_exceptions': rendering_context, 1654 'impacted_pickings': False, 1655 'cancel': cancel 1656 } 1657 return self.env.ref('mrp.exception_on_mo')._render(values=values) 1658 1659 documents = self.env['stock.picking']._log_activity_get_documents(moves_modification, 'move_dest_ids', 'DOWN', _keys_in_sorted, _keys_in_groupby) 1660 documents = self.env['stock.picking']._less_quantities_than_expected_add_documents(moves_modification, documents) 1661 self.env['stock.picking']._log_activity(_render_note_exception_quantity_mo, documents) 1662 1663 def _log_manufacture_exception(self, documents, cancel=False): 1664 1665 def _render_note_exception_quantity_mo(rendering_context): 1666 visited_objects = [] 1667 order_exceptions = {} 1668 for exception in rendering_context: 1669 order_exception, visited = exception 1670 order_exceptions.update(order_exception) 1671 visited_objects += visited 1672 visited_objects = self.env[visited_objects[0]._name].concat(*visited_objects) 1673 impacted_object = [] 1674 if visited_objects and visited_objects._name == 'stock.move': 1675 visited_objects |= visited_objects.mapped('move_orig_ids') 1676 impacted_object = visited_objects.filtered(lambda m: m.state not in ('done', 'cancel')).mapped('picking_id') 1677 values = { 1678 'production_order': self, 1679 'order_exceptions': order_exceptions, 1680 'impacted_object': impacted_object, 1681 'cancel': cancel 1682 } 1683 return self.env.ref('mrp.exception_on_mo')._render(values=values) 1684 1685 self.env['stock.picking']._log_activity(_render_note_exception_quantity_mo, documents) 1686 1687 def button_unbuild(self): 1688 self.ensure_one() 1689 return { 1690 'name': _('Unbuild: %s', self.product_id.display_name), 1691 'view_mode': 'form', 1692 'res_model': 'mrp.unbuild', 1693 'view_id': self.env.ref('mrp.mrp_unbuild_form_view_simplified').id, 1694 'type': 'ir.actions.act_window', 1695 'context': {'default_product_id': self.product_id.id, 1696 'default_mo_id': self.id, 1697 'default_company_id': self.company_id.id, 1698 'default_location_id': self.location_dest_id.id, 1699 'default_location_dest_id': self.location_src_id.id, 1700 'create': False, 'edit': False}, 1701 'target': 'new', 1702 } 1703 1704 @api.model 1705 def _prepare_procurement_group_vals(self, values): 1706 return {'name': values['name']} 1707 1708 def _get_quantity_to_backorder(self): 1709 self.ensure_one() 1710 return max(self.product_qty - self.qty_producing, 0) 1711 1712 def _check_sn_uniqueness(self): 1713 """ Alert the user if the serial number as already been consumed/produced """ 1714 if self.product_tracking == 'serial' and self.lot_producing_id: 1715 sml = self.env['stock.move.line'].search_count([ 1716 ('lot_id', '=', self.lot_producing_id.id), 1717 ('location_id.usage', '=', 'production'), 1718 ('qty_done', '=', 1), 1719 ('state', '=', 'done') 1720 ]) 1721 if sml: 1722 raise UserError(_('This serial number for product %s has already been produced', self.product_id.name)) 1723 1724 for move in self.move_finished_ids: 1725 if move.has_tracking != 'serial' or move.product_id == self.product_id: 1726 continue 1727 for move_line in move.move_line_ids: 1728 domain = [ 1729 ('lot_id', '=', move_line.lot_id.id), 1730 ('qty_done', '=', 1), 1731 ('state', '=', 'done') 1732 ] 1733 message = _('The serial number %(number)s used for byproduct %(product_name)s has already been produced', 1734 number=move_line.lot_id.name, 1735 product_name=move_line.product_id.name) 1736 co_prod_move_lines = self.move_finished_ids.move_line_ids.filtered(lambda ml: ml.product_id != self.product_id) 1737 domain_unbuild = domain + [ 1738 ('production_id', '=', False), 1739 ('location_dest_id.usage', '=', 'production') 1740 ] 1741 1742 # Check presence of same sn in previous productions 1743 duplicates = self.env['stock.move.line'].search_count(domain + [ 1744 ('location_id.usage', '=', 'production') 1745 ]) 1746 if duplicates: 1747 # Maybe some move lines have been compensated by unbuild 1748 duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [ 1749 ('move_id.unbuild_id', '!=', False) 1750 ]) 1751 if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0): 1752 raise UserError(message) 1753 # Check presence of same sn in current production 1754 duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line 1755 if duplicates: 1756 raise UserError(message) 1757 1758 for move in self.move_raw_ids: 1759 if move.has_tracking != 'serial': 1760 continue 1761 for move_line in move.move_line_ids: 1762 if float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding): 1763 continue 1764 domain = [ 1765 ('lot_id', '=', move_line.lot_id.id), 1766 ('qty_done', '=', 1), 1767 ('state', '=', 'done') 1768 ] 1769 message = _('The serial number %(number)s used for component %(component)s has already been consumed', 1770 number=move_line.lot_id.name, 1771 component=move_line.product_id.name) 1772 co_prod_move_lines = self.move_raw_ids.move_line_ids 1773 domain_unbuild = domain + [ 1774 ('production_id', '=', False), 1775 ('location_id.usage', '=', 'production') 1776 ] 1777 1778 # Check presence of same sn in previous productions 1779 duplicates = self.env['stock.move.line'].search_count(domain + [ 1780 ('location_dest_id.usage', '=', 'production') 1781 ]) 1782 if duplicates: 1783 # Maybe some move lines have been compensated by unbuild 1784 duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [ 1785 ('move_id.unbuild_id', '!=', False) 1786 ]) 1787 if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0): 1788 raise UserError(message) 1789 # Check presence of same sn in current production 1790 duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line 1791 if duplicates: 1792 raise UserError(message) 1793 1794 def _check_immediate(self): 1795 immediate_productions = self.browse() 1796 if self.env.context.get('skip_immediate'): 1797 return immediate_productions 1798 pd = self.env['decimal.precision'].precision_get('Product Unit of Measure') 1799 for production in self: 1800 if all(float_is_zero(ml.qty_done, precision_digits=pd) for 1801 ml in production.move_raw_ids.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel')) 1802 ) and float_is_zero(production.qty_producing, precision_digits=pd): 1803 immediate_productions |= production 1804 return immediate_productions 1805