· 4 years ago · Jul 15, 2021, 08:08 AM
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=True, 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=True, 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)
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)
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.qty_producing >= production.product_qty:
480 production.state = 'to_close'
481 elif any(wo_state in ('progress', 'done') for wo_state in production.workorder_ids.mapped('state')):
482 production.state = 'progress'
483 elif not float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding):
484 production.state = 'progress'
485 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):
486 production.state = 'progress'
487 else:
488 production.state = 'confirmed'
489
490 # Compute reservation state
491 # State where the reservation does not matter.
492 production.reservation_state = False
493 # Compute reservation state according to its component's moves.
494 if production.state not in ('draft', 'done', 'cancel'):
495 relevant_move_state = production.move_raw_ids._get_relevant_state_among_moves()
496 if relevant_move_state == 'partially_available':
497 if production.bom_id.operation_ids and production.bom_id.ready_to_produce == 'asap':
498 production.reservation_state = production._get_ready_to_produce_state()
499 else:
500 production.reservation_state = 'confirmed'
501 elif relevant_move_state != 'draft':
502 production.reservation_state = relevant_move_state
503
504 @api.depends('move_raw_ids', 'state', 'move_raw_ids.product_uom_qty')
505 def _compute_unreserve_visible(self):
506 for order in self:
507 already_reserved = order.state not in ('done', 'cancel') and order.mapped('move_raw_ids.move_line_ids')
508 any_quantity_done = any(m.quantity_done > 0 for m in order.move_raw_ids)
509
510 order.unreserve_visible = not any_quantity_done and already_reserved
511 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)
512
513 @api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done')
514 def _get_produced_qty(self):
515 for production in self:
516 done_moves = production.move_finished_ids.filtered(lambda x: x.state != 'cancel' and x.product_id.id == production.product_id.id)
517 qty_produced = sum(done_moves.mapped('quantity_done'))
518 production.qty_produced = qty_produced
519 return True
520
521 def _compute_scrap_move_count(self):
522 data = self.env['stock.scrap'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id'])
523 count_data = dict((item['production_id'][0], item['production_id_count']) for item in data)
524 for production in self:
525 production.scrap_count = count_data.get(production.id, 0)
526
527 @api.depends('move_finished_ids')
528 def _compute_move_byproduct_ids(self):
529 for order in self:
530 order.move_byproduct_ids = order.move_finished_ids.filtered(lambda m: m.product_id != order.product_id)
531
532 def _set_move_byproduct_ids(self):
533 move_finished_ids = self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id)
534 self.move_finished_ids = move_finished_ids | self.move_byproduct_ids
535
536 @api.depends('state')
537 def _compute_show_lock(self):
538 for order in self:
539 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'}
540
541 @api.depends('state','move_raw_ids')
542 def _compute_show_lot_ids(self):
543 for order in self:
544 order.show_lot_ids = order.state != 'draft' and any(m.product_id.tracking == 'serial' for m in order.move_raw_ids)
545
546 _sql_constraints = [
547 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
548 ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
549 ]
550
551 @api.model
552 def _search_delay_alert_date(self, operator, value):
553 late_stock_moves = self.env['stock.move'].search([('delay_alert_date', operator, value)])
554 return ['|', ('move_raw_ids', 'in', late_stock_moves.ids), ('move_finished_ids', 'in', late_stock_moves.ids)]
555
556 @api.onchange('company_id')
557 def onchange_company_id(self):
558 if self.company_id:
559 if self.move_raw_ids:
560 self.move_raw_ids.update({'company_id': self.company_id})
561 if self.picking_type_id and self.picking_type_id.company_id != self.company_id:
562 self.picking_type_id = self.env['stock.picking.type'].search([
563 ('code', '=', 'mrp_operation'),
564 ('warehouse_id.company_id', '=', self.company_id.id),
565 ], limit=1).id
566
567 @api.onchange('product_id', 'picking_type_id', 'company_id')
568 def onchange_product_id(self):
569 """ Finds UoM of changed product. """
570 if not self.product_id:
571 self.bom_id = False
572 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):
573 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')
574 if bom:
575 self.bom_id = bom.id
576 self.product_qty = self.bom_id.product_qty
577 self.product_uom_id = self.bom_id.product_uom_id.id
578 else:
579 self.bom_id = False
580 self.product_uom_id = self.product_id.uom_id.id
581
582 @api.onchange('product_qty', 'product_uom_id')
583 def _onchange_product_qty(self):
584 for workorder in self.workorder_ids:
585 workorder.product_uom_id = self.product_uom_id
586 if self._origin.product_qty:
587 workorder.duration_expected = workorder._get_duration_expected(ratio=self.product_qty / self._origin.product_qty)
588 else:
589 workorder.duration_expected = workorder._get_duration_expected()
590 if workorder.date_planned_start and workorder.duration_expected:
591 workorder.date_planned_finished = workorder.date_planned_start + relativedelta(minutes=workorder.duration_expected)
592
593 @api.onchange('bom_id')
594 def _onchange_bom_id(self):
595 if not self.product_id and self.bom_id:
596 self.product_id = self.bom_id.product_id or self.bom_id.product_tmpl_id.product_variant_ids[0]
597 self.product_qty = self.bom_id.product_qty or 1.0
598 self.product_uom_id = self.bom_id and self.bom_id.product_uom_id.id or self.product_id.uom_id.id
599 self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)]
600 self.move_finished_ids = [(2, move.id) for move in self.move_finished_ids]
601 self.picking_type_id = self.bom_id.picking_type_id or self.picking_type_id
602
603 @api.onchange('date_planned_start', 'product_id')
604 def _onchange_date_planned_start(self):
605 if self.date_planned_start and not self.is_planned:
606 date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay)
607 date_planned_finished = date_planned_finished + relativedelta(days=self.company_id.manufacturing_lead)
608 if date_planned_finished == self.date_planned_start:
609 date_planned_finished = date_planned_finished + relativedelta(hours=1)
610 self.date_planned_finished = date_planned_finished
611 self.move_raw_ids = [(1, m.id, {'date': self.date_planned_start}) for m in self.move_raw_ids]
612 self.move_finished_ids = [(1, m.id, {'date': date_planned_finished}) for m in self.move_finished_ids]
613
614 @api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id')
615 def _onchange_move_raw(self):
616 if not self.bom_id and not self._origin.product_id:
617 return
618 # Clear move raws if we are changing the product. In case of creation (self._origin is empty),
619 # we need to avoid keeping incorrect lines, so clearing is necessary too.
620 if self.product_id != self._origin.product_id:
621 self.move_raw_ids = [(5,)]
622 if self.bom_id and self.product_qty > 0:
623 # keep manual entries
624 list_move_raw = [(4, move.id) for move in self.move_raw_ids.filtered(lambda m: not m.bom_line_id)]
625 moves_raw_values = self._get_moves_raw_values()
626 move_raw_dict = {move.bom_line_id.id: move for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)}
627 for move_raw_values in moves_raw_values:
628 if move_raw_values['bom_line_id'] in move_raw_dict:
629 # update existing entries
630 list_move_raw += [(1, move_raw_dict[move_raw_values['bom_line_id']].id, move_raw_values)]
631 else:
632 # add new entries
633 list_move_raw += [(0, 0, move_raw_values)]
634 self.move_raw_ids = list_move_raw
635 else:
636 self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)]
637
638 @api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id')
639 def _onchange_move_finished(self):
640 if self.product_id and self.product_qty > 0:
641 # keep manual entries
642 list_move_finished = [(4, move.id) for move in self.move_finished_ids.filtered(
643 lambda m: not m.byproduct_id and m.product_id != self.product_id)]
644 moves_finished_values = self._get_moves_finished_values()
645 moves_byproduct_dict = {move.byproduct_id.id: move for move in self.move_finished_ids.filtered(lambda m: m.byproduct_id)}
646 move_finished = self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id)
647 for move_finished_values in moves_finished_values:
648 if move_finished_values.get('byproduct_id') in moves_byproduct_dict:
649 # update existing entries
650 list_move_finished += [(1, moves_byproduct_dict[move_finished_values['byproduct_id']].id, move_finished_values)]
651 elif move_finished_values.get('product_id') == self.product_id.id and move_finished:
652 list_move_finished += [(1, move_finished.id, move_finished_values)]
653 else:
654 # add new entries
655 list_move_finished += [(0, 0, move_finished_values)]
656 self.move_finished_ids = list_move_finished
657 else:
658 self.move_finished_ids = [(2, move.id) for move in self.move_finished_ids.filtered(lambda m: m.bom_line_id)]
659
660 @api.onchange('location_src_id', 'move_raw_ids', 'bom_id')
661 def _onchange_location(self):
662 source_location = self.location_src_id
663 self.move_raw_ids.update({
664 'warehouse_id': source_location.get_warehouse().id,
665 'location_id': source_location.id,
666 })
667
668 @api.onchange('location_dest_id', 'move_finished_ids', 'bom_id')
669 def _onchange_location_dest(self):
670 destination_location = self.location_dest_id
671 update_value_list = []
672 for move in self.move_finished_ids:
673 update_value_list += [(1, move.id, ({
674 'warehouse_id': destination_location.get_warehouse().id,
675 'location_dest_id': destination_location.id,
676 }))]
677 self.move_finished_ids = update_value_list
678
679 @api.onchange('picking_type_id')
680 def onchange_picking_type(self):
681 location = self.env.ref('stock.stock_location_stock')
682 try:
683 location.check_access_rule('read')
684 except (AttributeError, AccessError):
685 location = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).lot_stock_id
686 self.move_raw_ids.update({'picking_type_id': self.picking_type_id})
687 self.move_finished_ids.update({'picking_type_id': self.picking_type_id})
688 self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id
689 self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id
690
691 @api.onchange('qty_producing', 'lot_producing_id')
692 def _onchange_producing(self):
693 self._set_qty_producing()
694
695 @api.onchange('lot_producing_id')
696 def _onchange_lot_producing(self):
697 if self.product_id.tracking == 'serial':
698 if self.env['stock.move.line'].search_count([
699 ('company_id', '=', self.company_id.id),
700 ('product_id', '=', self.product_id.id),
701 ('lot_id', '=', self.lot_producing_id.id),
702 ('state', '!=', 'cancel')
703 ]):
704 return {
705 'warning': {
706 'title': _('Warning'),
707 'message': _('Existing Serial number (%s). Please correct the serial numbers encoded.') % self.lot_producing_id.name
708 }
709 }
710
711 @api.onchange('bom_id')
712 def _onchange_workorder_ids(self):
713 if self.bom_id:
714 self._create_workorder()
715
716 def write(self, vals):
717 if 'workorder_ids' in self:
718 production_to_replan = self.filtered(lambda p: p.is_planned)
719 res = super(MrpProduction, self).write(vals)
720
721 for production in self:
722 if 'date_planned_start' in vals and not self.env.context.get('force_date', False):
723 if production.state in ['done', 'cancel']:
724 raise UserError(_('You cannot move a manufacturing order once it is cancelled or done.'))
725 if production.is_planned:
726 production.button_unplan()
727 move_vals = self._get_move_finished_values(self.product_id, self.product_uom_qty, self.product_uom_id)
728 production.move_finished_ids.write({'date': move_vals['date']})
729 if vals.get('date_planned_start'):
730 production.move_raw_ids.write({'date': production.date_planned_start, 'date_deadline': production.date_planned_start})
731 if vals.get('date_planned_finished'):
732 production.move_finished_ids.write({'date': production.date_planned_finished})
733 if any(field in ['move_raw_ids', 'move_finished_ids', 'workorder_ids'] for field in vals) and production.state != 'draft':
734 if production.state == 'done':
735 # for some reason moves added after state = 'done' won't save group_id, reference if added in
736 # "stock_move.default_get()"
737 production.move_raw_ids.filtered(lambda move: move.additional and move.date > production.date_planned_start).write({
738 'group_id': production.procurement_group_id.id,
739 'reference': production.name,
740 'date': production.date_planned_start,
741 'date_deadline': production.date_planned_start
742 })
743 production.move_finished_ids.filtered(lambda move: move.additional and move.date > production.date_planned_finished).write({
744 'reference': production.name,
745 'date': production.date_planned_finished,
746 'date_deadline': production.date_deadline
747 })
748 production._autoconfirm_production()
749 if production in production_to_replan:
750 production._plan_workorders(replan=True)
751 if production.state == 'done' and ('lot_producing_id' in vals or 'qty_producing' in vals):
752 finished_move_lines = production.move_finished_ids.filtered(
753 lambda move: move.product_id == self.product_id and move.state == 'done').mapped('move_line_ids')
754 if 'lot_producing_id' in vals:
755 finished_move_lines.write({'lot_id': vals.get('lot_producing_id')})
756 if 'qty_producing' in vals:
757 finished_move_lines.write({'qty_done': vals.get('qty_producing')})
758
759 if not production.bom_id.operation_ids and vals.get('date_planned_start') and not vals.get('date_planned_finished'):
760 new_date_planned_start = fields.Datetime.to_datetime(vals.get('date_planned_start'))
761 if not production.date_planned_finished or new_date_planned_start >= production.date_planned_finished:
762 production.date_planned_finished = new_date_planned_start + datetime.timedelta(hours=1)
763 return res
764
765 @api.model
766 def create(self, values):
767 # Remove from `move_finished_ids` the by-product moves and then move `move_byproduct_ids`
768 # into `move_finished_ids` to avoid duplicate and inconsistency.
769 if values.get('move_finished_ids', False):
770 values['move_finished_ids'] = list(filter(lambda move: move[2]['byproduct_id'] is False, values['move_finished_ids']))
771 if values.get('move_byproduct_ids', False):
772 values['move_finished_ids'] = values.get('move_finished_ids', []) + values['move_byproduct_ids']
773 del values['move_byproduct_ids']
774 if not values.get('name', False) or values['name'] == _('New'):
775 picking_type_id = values.get('picking_type_id') or self._get_default_picking_type()
776 picking_type_id = self.env['stock.picking.type'].browse(picking_type_id)
777 if picking_type_id:
778 values['name'] = picking_type_id.sequence_id.next_by_id()
779 else:
780 values['name'] = self.env['ir.sequence'].next_by_code('mrp.production') or _('New')
781 if not values.get('procurement_group_id'):
782 procurement_group_vals = self._prepare_procurement_group_vals(values)
783 values['procurement_group_id'] = self.env["procurement.group"].create(procurement_group_vals).id
784 production = super(MrpProduction, self).create(values)
785 (production.move_raw_ids | production.move_finished_ids).write({
786 'group_id': production.procurement_group_id.id,
787 'origin': production.name
788 })
789 production.move_raw_ids.write({'date': production.date_planned_start})
790 production.move_finished_ids.write({'date': production.date_planned_finished})
791 # Trigger move_raw creation when importing a file
792 if 'import_file' in self.env.context:
793 production._onchange_move_raw()
794 production._onchange_move_finished()
795 return production
796
797 def unlink(self):
798 if any(production.state == 'done' for production in self):
799 raise UserError(_('Cannot delete a manufacturing order in done state.'))
800 self.action_cancel()
801 not_cancel = self.filtered(lambda m: m.state != 'cancel')
802 if not_cancel:
803 productions_name = ', '.join([prod.display_name for prod in not_cancel])
804 raise UserError(_('%s cannot be deleted. Try to cancel them before.', productions_name))
805
806 workorders_to_delete = self.workorder_ids.filtered(lambda wo: wo.state != 'done')
807 if workorders_to_delete:
808 workorders_to_delete.unlink()
809 return super(MrpProduction, self).unlink()
810
811 def action_toggle_is_locked(self):
812 self.ensure_one()
813 self.is_locked = not self.is_locked
814 return True
815
816 def _create_workorder(self):
817 for production in self:
818 if not production.bom_id:
819 continue
820 workorders_values = []
821
822 product_qty = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id)
823 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)
824
825 for bom, bom_data in exploded_boms:
826 # If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders.
827 if not (bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids)):
828 continue
829 for operation in bom.operation_ids:
830 workorders_values += [{
831 'name': operation.name,
832 'production_id': production.id,
833 'workcenter_id': operation.workcenter_id.id,
834 'product_uom_id': production.product_uom_id.id,
835 'operation_id': operation.id,
836 'state': 'pending',
837 'consumption': production.consumption,
838 }]
839 production.workorder_ids = [(5, 0)] + [(0, 0, value) for value in workorders_values]
840 for workorder in production.workorder_ids:
841 workorder.duration_expected = workorder._get_duration_expected()
842
843 def _get_move_finished_values(self, product_id, product_uom_qty, product_uom, operation_id=False, byproduct_id=False):
844 group_orders = self.procurement_group_id.mrp_production_ids
845 move_dest_ids = self.move_dest_ids
846 if len(group_orders) > 1:
847 move_dest_ids |= group_orders[0].move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_dest_ids
848 date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay)
849 date_planned_finished = date_planned_finished + relativedelta(days=self.company_id.manufacturing_lead)
850 if date_planned_finished == self.date_planned_start:
851 date_planned_finished = date_planned_finished + relativedelta(hours=1)
852 return {
853 'product_id': product_id,
854 'product_uom_qty': product_uom_qty,
855 'product_uom': product_uom,
856 'operation_id': operation_id,
857 'byproduct_id': byproduct_id,
858 'name': self.name,
859 'date': date_planned_finished,
860 'date_deadline': self.date_deadline,
861 'picking_type_id': self.picking_type_id.id,
862 'location_id': self.product_id.with_company(self.company_id).property_stock_production.id,
863 'location_dest_id': self.location_dest_id.id,
864 'company_id': self.company_id.id,
865 'production_id': self.id,
866 'warehouse_id': self.location_dest_id.get_warehouse().id,
867 'origin': self.name,
868 'group_id': self.procurement_group_id.id,
869 'propagate_cancel': self.propagate_cancel,
870 'move_dest_ids': [(4, x.id) for x in move_dest_ids],
871 }
872
873 def _get_moves_finished_values(self):
874 moves = []
875 for production in self:
876 if production.product_id in production.bom_id.byproduct_ids.mapped('product_id'):
877 raise UserError(_("You cannot have %s as the finished product and in the Byproducts", self.product_id.name))
878 moves.append(production._get_move_finished_values(production.product_id.id, production.product_qty, production.product_uom_id.id))
879 for byproduct in production.bom_id.byproduct_ids:
880 product_uom_factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id)
881 qty = byproduct.product_qty * (product_uom_factor / production.bom_id.product_qty)
882 moves.append(production._get_move_finished_values(
883 byproduct.product_id.id, qty, byproduct.product_uom_id.id,
884 byproduct.operation_id.id, byproduct.id))
885 return moves
886
887 def _get_moves_raw_values(self):
888 moves = []
889 for production in self:
890 factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) / production.bom_id.product_qty
891 boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id)
892 for bom_line, line_data in lines:
893 if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom' or\
894 bom_line.product_id.type not in ['product', 'consu']:
895 continue
896 operation = bom_line.operation_id.id or line_data['parent_line'] and line_data['parent_line'].operation_id.id
897 moves.append(production._get_move_raw_values(
898 bom_line.product_id,
899 line_data['qty'],
900 bom_line.product_uom_id,
901 operation,
902 bom_line
903 ))
904 return moves
905
906 def _get_move_raw_values(self, product_id, product_uom_qty, product_uom, operation_id=False, bom_line=False):
907 source_location = self.location_src_id
908 data = {
909 'sequence': bom_line.sequence if bom_line else 10,
910 'name': self.name,
911 'date': self.date_planned_start,
912 'date_deadline': self.date_planned_start,
913 'bom_line_id': bom_line.id if bom_line else False,
914 'picking_type_id': self.picking_type_id.id,
915 'product_id': product_id.id,
916 'product_uom_qty': product_uom_qty,
917 'product_uom': product_uom.id,
918 'location_id': source_location.id,
919 'location_dest_id': self.product_id.with_company(self.company_id).property_stock_production.id,
920 'raw_material_production_id': self.id,
921 'company_id': self.company_id.id,
922 'operation_id': operation_id,
923 'price_unit': product_id.standard_price,
924 'procure_method': 'make_to_stock',
925 'origin': self.name,
926 'state': 'draft',
927 'warehouse_id': source_location.get_warehouse().id,
928 'group_id': self.procurement_group_id.id,
929 'propagate_cancel': self.propagate_cancel,
930 }
931 return data
932
933 def _set_qty_producing(self):
934 if self.product_id.tracking == 'serial':
935 qty_producing_uom = self.product_uom_id._compute_quantity(self.qty_producing, self.product_id.uom_id, rounding_method='HALF-UP')
936 if qty_producing_uom != 1:
937 self.qty_producing = self.product_id.uom_id._compute_quantity(1, self.product_uom_id, rounding_method='HALF-UP')
938
939 for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)):
940 if move._should_bypass_set_qty_producing():
941 continue
942 new_qty = self.product_uom_id._compute_quantity((self.qty_producing - self.qty_produced) * move.unit_factor, self.product_uom_id, rounding_method='HALF-UP')
943 move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
944 move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
945
946 def _update_raw_moves(self, factor):
947 self.ensure_one()
948 update_info = []
949 move_to_unlink = self.env['stock.move']
950 for move in self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')):
951 old_qty = move.product_uom_qty
952 new_qty = old_qty * factor
953 if new_qty > 0:
954 move.write({'product_uom_qty': new_qty})
955 move._action_assign()
956 update_info.append((move, old_qty, new_qty))
957 else:
958 if move.quantity_done > 0:
959 raise UserError(_('Lines need to be deleted, but can not as you still have some quantities to consume in them. '))
960 move._action_cancel()
961 move_to_unlink |= move
962 move_to_unlink.unlink()
963 return update_info
964
965 def _get_ready_to_produce_state(self):
966 """ returns 'assigned' if enough components are reserved in order to complete
967 the first operation of the bom. If not returns 'waiting'
968 """
969 self.ensure_one()
970 first_operation = self.bom_id.operation_ids[0]
971 if len(self.bom_id.operation_ids) == 1:
972 moves_in_first_operation = self.move_raw_ids
973 else:
974 moves_in_first_operation = self.move_raw_ids.filtered(lambda move: move.operation_id == first_operation)
975 moves_in_first_operation = moves_in_first_operation.filtered(
976 lambda move: move.bom_line_id and
977 not move.bom_line_id._skip_bom_line(self.product_id)
978 )
979
980 if all(move.state == 'assigned' for move in moves_in_first_operation):
981 return 'assigned'
982 return 'confirmed'
983
984 def _autoconfirm_production(self):
985 """Automatically run `action_confirm` on `self`.
986
987 If the production has one of its move was added after the initial call
988 to `action_confirm`.
989 """
990 moves_to_confirm = self.env['stock.move']
991 for production in self:
992 if production.state in ('done', 'cancel'):
993 continue
994 additional_moves = production.move_raw_ids.filtered(
995 lambda move: move.state == 'draft' and move.additional
996 )
997 additional_moves.write({
998 'group_id': production.procurement_group_id.id,
999 })
1000 additional_moves._adjust_procure_method()
1001 moves_to_confirm |= additional_moves
1002 additional_byproducts = production.move_finished_ids.filtered(
1003 lambda move: move.state == 'draft' and move.additional
1004 )
1005 moves_to_confirm |= additional_byproducts
1006
1007 if moves_to_confirm:
1008 moves_to_confirm._action_confirm()
1009 # run scheduler for moves forecasted to not have enough in stock
1010 moves_to_confirm._trigger_scheduler()
1011
1012 self.workorder_ids.filtered(lambda w: w.state not in ['done', 'cancel'])._action_confirm()
1013
1014 def action_view_mrp_production_childs(self):
1015 self.ensure_one()
1016 mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids
1017 action = {
1018 'res_model': 'mrp.production',
1019 'type': 'ir.actions.act_window',
1020 }
1021 if len(mrp_production_ids) == 1:
1022 action.update({
1023 'view_mode': 'form',
1024 'res_id': mrp_production_ids[0],
1025 })
1026 else:
1027 action.update({
1028 'name': _("%s Child MO's") % self.name,
1029 'domain': [('id', 'in', mrp_production_ids)],
1030 'view_mode': 'tree,form',
1031 })
1032 return action
1033
1034 def action_view_mrp_production_sources(self):
1035 self.ensure_one()
1036 mrp_production_ids = self.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids.ids
1037 action = {
1038 'res_model': 'mrp.production',
1039 'type': 'ir.actions.act_window',
1040 }
1041 if len(mrp_production_ids) == 1:
1042 action.update({
1043 'view_mode': 'form',
1044 'res_id': mrp_production_ids[0],
1045 })
1046 else:
1047 action.update({
1048 'name': _("MO Generated by %s") % self.name,
1049 'domain': [('id', 'in', mrp_production_ids)],
1050 'view_mode': 'tree,form',
1051 })
1052 return action
1053
1054 def action_view_mrp_production_backorders(self):
1055 backorder_ids = self.procurement_group_id.mrp_production_ids.ids
1056 return {
1057 'res_model': 'mrp.production',
1058 'type': 'ir.actions.act_window',
1059 'name': _("Backorder MO's"),
1060 'domain': [('id', 'in', backorder_ids)],
1061 'view_mode': 'tree,form',
1062 }
1063
1064 def action_generate_serial(self):
1065 self.ensure_one()
1066 self.lot_producing_id = self.env['stock.production.lot'].create({
1067 'product_id': self.product_id.id,
1068 'company_id': self.company_id.id
1069 })
1070 if self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_line_ids:
1071 self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_line_ids.lot_id = self.lot_producing_id
1072 if self.product_id.tracking == 'serial':
1073 self._set_qty_producing()
1074
1075 def _action_generate_immediate_wizard(self):
1076 view = self.env.ref('mrp.view_immediate_production')
1077 return {
1078 'name': _('Immediate Production?'),
1079 'type': 'ir.actions.act_window',
1080 'view_mode': 'form',
1081 'res_model': 'mrp.immediate.production',
1082 'views': [(view.id, 'form')],
1083 'view_id': view.id,
1084 'target': 'new',
1085 'context': dict(self.env.context, default_mo_ids=[(4, mo.id) for mo in self]),
1086 }
1087
1088 def action_confirm(self):
1089 self._check_company()
1090 for production in self:
1091 if production.bom_id:
1092 production.consumption = production.bom_id.consumption
1093 if not production.move_raw_ids:
1094 raise UserError(_("Add some materials to consume before marking this MO as to do."))
1095 # In case of Serial number tracking, force the UoM to the UoM of product
1096 if production.product_tracking == 'serial' and production.product_uom_id != production.product_id.uom_id:
1097 production.write({
1098 'product_qty': production.product_uom_id._compute_quantity(production.product_qty, production.product_id.uom_id),
1099 'product_uom_id': production.product_id.uom_id
1100 })
1101 for move_finish in production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id):
1102 move_finish.write({
1103 'product_uom_qty': move_finish.product_uom._compute_quantity(move_finish.product_uom_qty, move_finish.product_id.uom_id),
1104 'product_uom': move_finish.product_id.uom_id
1105 })
1106 production.move_raw_ids._adjust_procure_method()
1107 (production.move_raw_ids | production.move_finished_ids)._action_confirm()
1108 production.workorder_ids._action_confirm()
1109 # run scheduler for moves forecasted to not have enough in stock
1110 production.move_raw_ids._trigger_scheduler()
1111 return True
1112
1113 def action_assign(self):
1114 for production in self:
1115 production.move_raw_ids._action_assign()
1116 return True
1117
1118 def button_plan(self):
1119 """ Create work orders. And probably do stuff, like things. """
1120 orders_to_plan = self.filtered(lambda order: not order.is_planned)
1121 for order in orders_to_plan:
1122 (order.move_raw_ids | order.move_finished_ids).filtered(lambda m: m.state == 'draft')._action_confirm()
1123 order._plan_workorders()
1124 return True
1125
1126 def _plan_workorders(self, replan=False):
1127 """ Plan all the production's workorders depending on the workcenters
1128 work schedule.
1129
1130 :param replan: If it is a replan, only ready and pending workorder will be take in account
1131 :type replan: bool.
1132 """
1133 self.ensure_one()
1134
1135 if not self.workorder_ids:
1136 return
1137 # Schedule all work orders (new ones and those already created)
1138 qty_to_produce = max(self.product_qty - self.qty_produced, 0)
1139 qty_to_produce = self.product_uom_id._compute_quantity(qty_to_produce, self.product_id.uom_id)
1140 start_date = max(self.date_planned_start, datetime.datetime.now())
1141 if replan:
1142 workorder_ids = self.workorder_ids.filtered(lambda wo: wo.state in ['ready', 'pending'])
1143 # We plan the manufacturing order according to its `date_planned_start`, but if
1144 # `date_planned_start` is in the past, we plan it as soon as possible.
1145 workorder_ids.leave_id.unlink()
1146 else:
1147 workorder_ids = self.workorder_ids.filtered(lambda wo: not wo.date_planned_start)
1148 for workorder in workorder_ids:
1149 workcenters = workorder.workcenter_id | workorder.workcenter_id.alternative_workcenter_ids
1150
1151 best_finished_date = datetime.datetime.max
1152 vals = {}
1153 for workcenter in workcenters:
1154 # compute theoretical duration
1155 if workorder.workcenter_id == workcenter:
1156 duration_expected = workorder.duration_expected
1157 else:
1158 duration_expected = workorder._get_duration_expected(alternative_workcenter=workcenter)
1159
1160 from_date, to_date = workcenter._get_first_available_slot(start_date, duration_expected)
1161 # If the workcenter is unavailable, try planning on the next one
1162 if not from_date:
1163 continue
1164 # Check if this workcenter is better than the previous ones
1165 if to_date and to_date < best_finished_date:
1166 best_start_date = from_date
1167 best_finished_date = to_date
1168 best_workcenter = workcenter
1169 vals = {
1170 'workcenter_id': workcenter.id,
1171 'duration_expected': duration_expected,
1172 }
1173
1174 # If none of the workcenter are available, raise
1175 if best_finished_date == datetime.datetime.max:
1176 raise UserError(_('Impossible to plan the workorder. Please check the workcenter availabilities.'))
1177
1178 # Instantiate start_date for the next workorder planning
1179 if workorder.next_work_order_id:
1180 start_date = best_finished_date
1181
1182 # Create leave on chosen workcenter calendar
1183 leave = self.env['resource.calendar.leaves'].create({
1184 'name': workorder.display_name,
1185 'calendar_id': best_workcenter.resource_calendar_id.id,
1186 'date_from': best_start_date,
1187 'date_to': best_finished_date,
1188 'resource_id': best_workcenter.resource_id.id,
1189 'time_type': 'other'
1190 })
1191 vals['leave_id'] = leave.id
1192 workorder.write(vals)
1193 self.with_context(force_date=True).write({
1194 'date_planned_start': self.workorder_ids[0].date_planned_start,
1195 'date_planned_finished': self.workorder_ids[-1].date_planned_finished
1196 })
1197
1198 def button_unplan(self):
1199 if any(wo.state == 'done' for wo in self.workorder_ids):
1200 raise UserError(_("Some work orders are already done, you cannot unplan this manufacturing order."))
1201 elif any(wo.state == 'progress' for wo in self.workorder_ids):
1202 raise UserError(_("Some work orders have already started, you cannot unplan this manufacturing order."))
1203
1204 self.workorder_ids.leave_id.unlink()
1205 self.workorder_ids.write({
1206 'date_planned_start': False,
1207 'date_planned_finished': False,
1208 })
1209
1210 def _get_consumption_issues(self):
1211 """Compare the quantity consumed of the components, the expected quantity
1212 on the BoM and the consumption parameter on the order.
1213
1214 :return: list of tuples (order_id, product_id, consumed_qty, expected_qty) where the
1215 consumption isn't honored. order_id and product_id are recordset of mrp.production
1216 and product.product respectively
1217 :rtype: list
1218 """
1219 issues = []
1220 if self.env.context.get('skip_consumption', False) or self.env.context.get('skip_immediate', False):
1221 return issues
1222 for order in self:
1223 if order.consumption == 'flexible' or not order.bom_id or not order.bom_id.bom_line_ids:
1224 continue
1225 expected_move_values = order._get_moves_raw_values()
1226 expected_qty_by_product = defaultdict(float)
1227 for move_values in expected_move_values:
1228 move_product = self.env['product.product'].browse(move_values['product_id'])
1229 move_uom = self.env['uom.uom'].browse(move_values['product_uom'])
1230 move_product_qty = move_uom._compute_quantity(move_values['product_uom_qty'], move_product.uom_id)
1231 expected_qty_by_product[move_product] += move_product_qty * order.qty_producing / order.product_qty
1232
1233 done_qty_by_product = defaultdict(float)
1234 for move in order.move_raw_ids:
1235 qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
1236 rounding = move.product_id.uom_id.rounding
1237 if not (move.product_id in expected_qty_by_product or float_is_zero(qty_done, precision_rounding=rounding)):
1238 issues.append((order, move.product_id, qty_done, 0.0))
1239 continue
1240 done_qty_by_product[move.product_id] += qty_done
1241
1242 for product, qty_to_consume in expected_qty_by_product.items():
1243 qty_done = done_qty_by_product.get(product, 0.0)
1244 if float_compare(qty_to_consume, qty_done, precision_rounding=product.uom_id.rounding) != 0:
1245 issues.append((order, product, qty_done, qty_to_consume))
1246
1247 return issues
1248
1249 def _action_generate_consumption_wizard(self, consumption_issues):
1250 ctx = self.env.context.copy()
1251 lines = []
1252 for order, product_id, consumed_qty, expected_qty in consumption_issues:
1253 lines.append((0, 0, {
1254 'mrp_production_id': order.id,
1255 'product_id': product_id.id,
1256 'consumption': order.consumption,
1257 'product_uom_id': product_id.uom_id.id,
1258 'product_consumed_qty_uom': consumed_qty,
1259 'product_expected_qty_uom': expected_qty
1260 }))
1261 ctx.update({'default_mrp_production_ids': self.ids, 'default_mrp_consumption_warning_line_ids': lines})
1262 action = self.env["ir.actions.actions"]._for_xml_id("mrp.action_mrp_consumption_warning")
1263 action['context'] = ctx
1264 return action
1265
1266 def _get_quantity_produced_issues(self):
1267 quantity_issues = []
1268 if self.env.context.get('skip_backorder', False):
1269 return quantity_issues
1270 for order in self:
1271 if not float_is_zero(order._get_quantity_to_backorder(), precision_rounding=order.product_uom_id.rounding):
1272 quantity_issues.append(order)
1273 return quantity_issues
1274
1275 def _action_generate_backorder_wizard(self, quantity_issues):
1276 ctx = self.env.context.copy()
1277 lines = []
1278 for order in quantity_issues:
1279 lines.append((0, 0, {
1280 'mrp_production_id': order.id,
1281 'to_backorder': True
1282 }))
1283 ctx.update({'default_mrp_production_ids': self.ids, 'default_mrp_production_backorder_line_ids': lines})
1284 action = self.env["ir.actions.actions"]._for_xml_id("mrp.action_mrp_production_backorder")
1285 action['context'] = ctx
1286 return action
1287
1288 def action_cancel(self):
1289 """ Cancels production order, unfinished stock moves and set procurement
1290 orders in exception """
1291 if not self.move_raw_ids:
1292 self.state = 'cancel'
1293 return True
1294 self._action_cancel()
1295 return True
1296
1297 def _action_cancel(self):
1298 documents_by_production = {}
1299 for production in self:
1300 documents = defaultdict(list)
1301 for move_raw_id in self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')):
1302 iterate_key = self._get_document_iterate_key(move_raw_id)
1303 if iterate_key:
1304 document = self.env['stock.picking']._log_activity_get_documents({move_raw_id: (move_raw_id.product_uom_qty, 0)}, iterate_key, 'UP')
1305 for key, value in document.items():
1306 documents[key] += [value]
1307 if documents:
1308 documents_by_production[production] = documents
1309 # log an activity on Parent MO if child MO is cancelled.
1310 finish_moves = production.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
1311 if finish_moves:
1312 production._log_downside_manufactured_quantity({finish_move: (production.product_uom_qty, 0.0) for finish_move in finish_moves}, cancel=True)
1313
1314 self.workorder_ids.filtered(lambda x: x.state not in ['done', 'cancel']).action_cancel()
1315 finish_moves = self.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
1316 raw_moves = self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
1317
1318 (finish_moves | raw_moves)._action_cancel()
1319 picking_ids = self.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
1320 picking_ids.action_cancel()
1321
1322 for production, documents in documents_by_production.items():
1323 filtered_documents = {}
1324 for (parent, responsible), rendering_context in documents.items():
1325 if not parent or parent._name == 'stock.picking' and parent.state == 'cancel' or parent == production:
1326 continue
1327 filtered_documents[(parent, responsible)] = rendering_context
1328 production._log_manufacture_exception(filtered_documents, cancel=True)
1329
1330 # In case of a flexible BOM, we don't know from the state of the moves if the MO should
1331 # remain in progress or done. Indeed, if all moves are done/cancel but the quantity produced
1332 # is lower than expected, it might mean:
1333 # - we have used all components but we still want to produce the quantity expected
1334 # - we have used all components and we won't be able to produce the last units
1335 #
1336 # However, if the user clicks on 'Cancel', it is expected that the MO is either done or
1337 # canceled. If the MO is still in progress at this point, it means that the move raws
1338 # are either all done or a mix of done / canceled => the MO should be done.
1339 self.filtered(lambda p: p.state not in ['done', 'cancel'] and p.bom_id.consumption == 'flexible').write({'state': 'done'})
1340
1341 return True
1342
1343 def _get_document_iterate_key(self, move_raw_id):
1344 return move_raw_id.move_orig_ids and 'move_orig_ids' or False
1345
1346 def _cal_price(self, consumed_moves):
1347 self.ensure_one()
1348 return True
1349
1350 def _post_inventory(self, cancel_backorder=False):
1351 for order in self:
1352 moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done')
1353 moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
1354 for move in moves_to_do.filtered(lambda m: m.product_qty == 0.0 and m.quantity_done > 0):
1355 move.product_uom_qty = move.quantity_done
1356 # MRP do not merge move, catch the result of _action_done in order
1357 # to get extra moves.
1358 moves_to_do = moves_to_do._action_done()
1359 moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do
1360
1361 finish_moves = order.move_finished_ids.filtered(lambda m: m.product_id == order.product_id and m.state not in ('done', 'cancel'))
1362 # the finish move can already be completed by the workorder.
1363 if not finish_moves.quantity_done:
1364 finish_moves.quantity_done = float_round(order.qty_producing - order.qty_produced, precision_rounding=order.product_uom_id.rounding, rounding_method='HALF-UP')
1365 finish_moves.move_line_ids.lot_id = order.lot_producing_id
1366 order._cal_price(moves_to_do)
1367
1368 moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
1369 moves_to_finish = moves_to_finish._action_done(cancel_backorder=cancel_backorder)
1370 order.action_assign()
1371 consume_move_lines = moves_to_do.mapped('move_line_ids')
1372 order.move_finished_ids.move_line_ids.consume_line_ids = [(6, 0, consume_move_lines.ids)]
1373 return True
1374
1375 @api.model
1376 def _get_name_backorder(self, name, sequence):
1377 if not sequence:
1378 return name
1379 seq_back = "-" + "0" * (SIZE_BACK_ORDER_NUMERING - 1 - int(math.log10(sequence))) + str(sequence)
1380 if re.search("-\\d{%d}$" % SIZE_BACK_ORDER_NUMERING, name):
1381 return name[:-SIZE_BACK_ORDER_NUMERING-1] + seq_back
1382 return name + seq_back
1383
1384 def _get_backorder_mo_vals(self):
1385 self.ensure_one()
1386 next_seq = max(self.procurement_group_id.mrp_production_ids.mapped("backorder_sequence"))
1387 return {
1388 'name': self._get_name_backorder(self.name, next_seq + 1),
1389 'backorder_sequence': next_seq + 1,
1390 'procurement_group_id': self.procurement_group_id.id,
1391 'move_raw_ids': None,
1392 'move_finished_ids': None,
1393 'product_qty': self._get_quantity_to_backorder(),
1394 'lot_producing_id': False,
1395 'origin': self.origin
1396 }
1397
1398 def _generate_backorder_productions(self, close_mo=True):
1399 backorders = self.env['mrp.production']
1400 for production in self:
1401 if production.backorder_sequence == 0: # Activate backorder naming
1402 production.backorder_sequence = 1
1403 backorder_mo = production.copy(default=production._get_backorder_mo_vals())
1404 if close_mo:
1405 production.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({
1406 'raw_material_production_id': backorder_mo.id,
1407 })
1408 production.move_finished_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({
1409 'production_id': backorder_mo.id,
1410 })
1411 else:
1412 new_moves_vals = []
1413 for move in production.move_raw_ids | production.move_finished_ids:
1414 if not move.additional:
1415 qty_to_split = move.product_uom_qty - move.unit_factor * production.qty_producing
1416 qty_to_split = move.product_uom._compute_quantity(qty_to_split, move.product_id.uom_id, rounding_method='HALF-UP')
1417 move_vals = move._split(qty_to_split)
1418 if not move_vals:
1419 continue
1420 if move.raw_material_production_id:
1421 move_vals[0]['raw_material_production_id'] = backorder_mo.id
1422 else:
1423 move_vals[0]['production_id'] = backorder_mo.id
1424 new_moves_vals.append(move_vals[0])
1425 new_moves = self.env['stock.move'].create(new_moves_vals)
1426 backorders |= backorder_mo
1427 for old_wo, wo in zip(production.workorder_ids, backorder_mo.workorder_ids):
1428 wo.qty_produced = max(old_wo.qty_produced - old_wo.qty_producing, 0)
1429 if wo.product_tracking == 'serial':
1430 wo.qty_producing = 1
1431 else:
1432 wo.qty_producing = wo.qty_remaining
1433 if wo.qty_producing == 0:
1434 wo.action_cancel()
1435
1436 production.name = self._get_name_backorder(production.name, production.backorder_sequence)
1437
1438 # We need to adapt `duration_expected` on both the original workorders and their
1439 # backordered workorders. To do that, we use the original `duration_expected` and the
1440 # ratio of the quantity really produced and the quantity to produce.
1441 ratio = production.qty_producing / production.product_qty
1442 for workorder in production.workorder_ids:
1443 workorder.duration_expected = workorder.duration_expected * ratio
1444 for workorder in backorder_mo.workorder_ids:
1445 workorder.duration_expected = workorder.duration_expected * (1 - ratio)
1446
1447 # As we have split the moves before validating them, we need to 'remove' the excess reservation
1448 if not close_mo:
1449 self.move_raw_ids.filtered(lambda m: not m.additional)._do_unreserve()
1450 self.move_raw_ids.filtered(lambda m: not m.additional)._action_assign()
1451 # Confirm only productions with remaining components
1452 backorders.filtered(lambda mo: mo.move_raw_ids).action_confirm()
1453 backorders.filtered(lambda mo: mo.move_raw_ids).action_assign()
1454
1455 # Remove the serial move line without reserved quantity. Post inventory will assigned all the non done moves
1456 # So those move lines are duplicated.
1457 backorders.move_raw_ids.move_line_ids.filtered(lambda ml: ml.product_id.tracking == 'serial' and ml.product_qty == 0).unlink()
1458 backorders.move_raw_ids._recompute_state()
1459
1460 return backorders
1461
1462 def button_mark_done(self):
1463 self._button_mark_done_sanity_checks()
1464
1465 if not self.env.context.get('button_mark_done_production_ids'):
1466 self = self.with_context(button_mark_done_production_ids=self.ids)
1467 res = self._pre_button_mark_done()
1468 if res is not True:
1469 return res
1470
1471 if self.env.context.get('mo_ids_to_backorder'):
1472 productions_to_backorder = self.browse(self.env.context['mo_ids_to_backorder'])
1473 productions_not_to_backorder = self - productions_to_backorder
1474 else:
1475 productions_not_to_backorder = self
1476 productions_to_backorder = self.env['mrp.production']
1477
1478 self.workorder_ids.button_finish()
1479
1480 productions_not_to_backorder._post_inventory(cancel_backorder=True)
1481 productions_to_backorder._post_inventory(cancel_backorder=False)
1482 backorders = productions_to_backorder._generate_backorder_productions()
1483
1484 # if completed products make other confirmed/partially_available moves available, assign them
1485 done_move_finished_ids = (productions_to_backorder.move_finished_ids | productions_not_to_backorder.move_finished_ids).filtered(lambda m: m.state == 'done')
1486 done_move_finished_ids._trigger_assign()
1487
1488 # Moves without quantity done are not posted => set them as done instead of canceling. In
1489 # case the user edits the MO later on and sets some consumed quantity on those, we do not
1490 # want the move lines to be canceled.
1491 (productions_not_to_backorder.move_raw_ids | productions_not_to_backorder.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')).write({
1492 'state': 'done',
1493 'product_uom_qty': 0.0,
1494 })
1495
1496 for production in self:
1497 production.write({
1498 'date_finished': fields.Datetime.now(),
1499 'product_qty': production.qty_produced,
1500 'priority': '0',
1501 'is_locked': True,
1502 })
1503
1504 for workorder in self.workorder_ids.filtered(lambda w: w.state not in ('done', 'cancel')):
1505 workorder.duration_expected = workorder._get_duration_expected()
1506
1507 if not backorders:
1508 if self.env.context.get('from_workorder'):
1509 return {
1510 'type': 'ir.actions.act_window',
1511 'res_model': 'mrp.production',
1512 'views': [[self.env.ref('mrp.mrp_production_form_view').id, 'form']],
1513 'res_id': self.id,
1514 'target': 'main',
1515 }
1516 return True
1517 context = self.env.context.copy()
1518 context = {k: v for k, v in context.items() if not k.startswith('default_')}
1519 for k, v in context.items():
1520 if k.startswith('skip_'):
1521 context[k] = False
1522 action = {
1523 'res_model': 'mrp.production',
1524 'type': 'ir.actions.act_window',
1525 'context': dict(context, mo_ids_to_backorder=None)
1526 }
1527 if len(backorders) == 1:
1528 action.update({
1529 'view_mode': 'form',
1530 'res_id': backorders[0].id,
1531 })
1532 else:
1533 action.update({
1534 'name': _("Backorder MO"),
1535 'domain': [('id', 'in', backorders.ids)],
1536 'view_mode': 'tree,form',
1537 })
1538 return action
1539
1540 def _pre_button_mark_done(self):
1541 productions_to_immediate = self._check_immediate()
1542 if productions_to_immediate:
1543 return productions_to_immediate._action_generate_immediate_wizard()
1544
1545 for production in self:
1546 if float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding):
1547 raise UserError(_('The quantity to produce must be positive!'))
1548
1549 consumption_issues = self._get_consumption_issues()
1550 if consumption_issues:
1551 return self._action_generate_consumption_wizard(consumption_issues)
1552
1553 quantity_issues = self._get_quantity_produced_issues()
1554 if quantity_issues:
1555 return self._action_generate_backorder_wizard(quantity_issues)
1556 return True
1557
1558 def _button_mark_done_sanity_checks(self):
1559 self._check_company()
1560 for order in self:
1561 order._check_sn_uniqueness()
1562
1563 def do_unreserve(self):
1564 self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))._do_unreserve()
1565 return True
1566
1567 def button_unreserve(self):
1568 self.ensure_one()
1569 self.do_unreserve()
1570 return True
1571
1572 def button_scrap(self):
1573 self.ensure_one()
1574 return {
1575 'name': _('Scrap'),
1576 'view_mode': 'form',
1577 'res_model': 'stock.scrap',
1578 'view_id': self.env.ref('stock.stock_scrap_form_view2').id,
1579 'type': 'ir.actions.act_window',
1580 'context': {'default_production_id': self.id,
1581 '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,
1582 'default_company_id': self.company_id.id
1583 },
1584 'target': 'new',
1585 }
1586
1587 def action_see_move_scrap(self):
1588 self.ensure_one()
1589 action = self.env["ir.actions.actions"]._for_xml_id("stock.action_stock_scrap")
1590 action['domain'] = [('production_id', '=', self.id)]
1591 action['context'] = dict(self._context, default_origin=self.name)
1592 return action
1593
1594 @api.model
1595 def get_empty_list_help(self, help):
1596 self = self.with_context(
1597 empty_list_help_document_name=_("manufacturing order"),
1598 )
1599 return super(MrpProduction, self).get_empty_list_help(help)
1600
1601 def _log_downside_manufactured_quantity(self, moves_modification, cancel=False):
1602
1603 def _keys_in_sorted(move):
1604 """ sort by picking and the responsible for the product the
1605 move.
1606 """
1607 return (move.picking_id.id, move.product_id.responsible_id.id)
1608
1609 def _keys_in_groupby(move):
1610 """ group by picking and the responsible for the product the
1611 move.
1612 """
1613 return (move.picking_id, move.product_id.responsible_id)
1614
1615 def _render_note_exception_quantity_mo(rendering_context):
1616 values = {
1617 'production_order': self,
1618 'order_exceptions': rendering_context,
1619 'impacted_pickings': False,
1620 'cancel': cancel
1621 }
1622 return self.env.ref('mrp.exception_on_mo')._render(values=values)
1623
1624 documents = self.env['stock.picking']._log_activity_get_documents(moves_modification, 'move_dest_ids', 'DOWN', _keys_in_sorted, _keys_in_groupby)
1625 documents = self.env['stock.picking']._less_quantities_than_expected_add_documents(moves_modification, documents)
1626 self.env['stock.picking']._log_activity(_render_note_exception_quantity_mo, documents)
1627
1628 def _log_manufacture_exception(self, documents, cancel=False):
1629
1630 def _render_note_exception_quantity_mo(rendering_context):
1631 visited_objects = []
1632 order_exceptions = {}
1633 for exception in rendering_context:
1634 order_exception, visited = exception
1635 order_exceptions.update(order_exception)
1636 visited_objects += visited
1637 visited_objects = self.env[visited_objects[0]._name].concat(*visited_objects)
1638 impacted_object = []
1639 if visited_objects and visited_objects._name == 'stock.move':
1640 visited_objects |= visited_objects.mapped('move_orig_ids')
1641 impacted_object = visited_objects.filtered(lambda m: m.state not in ('done', 'cancel')).mapped('picking_id')
1642 values = {
1643 'production_order': self,
1644 'order_exceptions': order_exceptions,
1645 'impacted_object': impacted_object,
1646 'cancel': cancel
1647 }
1648 return self.env.ref('mrp.exception_on_mo')._render(values=values)
1649
1650 self.env['stock.picking']._log_activity(_render_note_exception_quantity_mo, documents)
1651
1652 def button_unbuild(self):
1653 self.ensure_one()
1654 return {
1655 'name': _('Unbuild: %s', self.product_id.display_name),
1656 'view_mode': 'form',
1657 'res_model': 'mrp.unbuild',
1658 'view_id': self.env.ref('mrp.mrp_unbuild_form_view_simplified').id,
1659 'type': 'ir.actions.act_window',
1660 'context': {'default_mo_id': self.id,
1661 'default_company_id': self.company_id.id,
1662 'default_location_id': self.location_dest_id.id,
1663 'default_location_dest_id': self.location_src_id.id,
1664 'create': False, 'edit': False},
1665 'target': 'new',
1666 }
1667
1668 @api.model
1669 def _prepare_procurement_group_vals(self, values):
1670 return {'name': values['name']}
1671
1672 def _get_quantity_to_backorder(self):
1673 self.ensure_one()
1674 return max(self.product_qty - self.qty_producing, 0)
1675
1676 def _check_sn_uniqueness(self):
1677 """ Alert the user if the serial number as already been consumed/produced """
1678 if self.product_tracking == 'serial' and self.lot_producing_id:
1679 sml = self.env['stock.move.line'].search_count([
1680 ('lot_id', '=', self.lot_producing_id.id),
1681 ('location_id.usage', '=', 'production'),
1682 ('qty_done', '=', 1),
1683 ('state', '=', 'done')
1684 ])
1685 if sml:
1686 raise UserError(_('This serial number for product %s has already been produced', self.product_id.name))
1687
1688 for move in self.move_finished_ids:
1689 if move.has_tracking != 'serial' or move.product_id == self.product_id:
1690 continue
1691 for move_line in move.move_line_ids:
1692 domain = [
1693 ('lot_id', '=', move_line.lot_id.id),
1694 ('qty_done', '=', 1),
1695 ('state', '=', 'done')
1696 ]
1697 message = _('The serial number %(number)s used for byproduct %(product_name)s has already been produced',
1698 number=move_line.lot_id.name,
1699 product_name=move_line.product_id.name)
1700 co_prod_move_lines = self.move_finished_ids.move_line_ids.filtered(lambda ml: ml.product_id != self.product_id)
1701 domain_unbuild = domain + [
1702 ('production_id', '=', False),
1703 ('location_dest_id.usage', '=', 'production')
1704 ]
1705
1706 # Check presence of same sn in previous productions
1707 duplicates = self.env['stock.move.line'].search_count(domain + [
1708 ('location_id.usage', '=', 'production')
1709 ])
1710 if duplicates:
1711 # Maybe some move lines have been compensated by unbuild
1712 duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild)
1713 if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0):
1714 raise UserError(message)
1715 # Check presence of same sn in current production
1716 duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line
1717 if duplicates:
1718 raise UserError(message)
1719
1720 for move in self.move_raw_ids:
1721 if move.has_tracking != 'serial':
1722 continue
1723 for move_line in move.move_line_ids:
1724 if float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding):
1725 continue
1726 domain = [
1727 ('lot_id', '=', move_line.lot_id.id),
1728 ('qty_done', '=', 1),
1729 ('state', '=', 'done')
1730 ]
1731 message = _('The serial number %(number)s used for component %(component)s has already been consumed',
1732 number=move_line.lot_id.name,
1733 component=move_line.product_id.name)
1734 co_prod_move_lines = self.move_raw_ids.move_line_ids
1735 domain_unbuild = domain + [
1736 ('production_id', '=', False),
1737 ('location_id.usage', '=', 'production')
1738 ]
1739
1740 # Check presence of same sn in previous productions
1741 duplicates = self.env['stock.move.line'].search_count(domain + [
1742 ('location_dest_id.usage', '=', 'production')
1743 ])
1744 if duplicates:
1745 # Maybe some move lines have been compensated by unbuild
1746 duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild)
1747 if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0):
1748 raise UserError(message)
1749 # Check presence of same sn in current production
1750 duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line
1751 if duplicates:
1752 raise UserError(message)
1753
1754 def _check_immediate(self):
1755 immediate_productions = self.browse()
1756 if self.env.context.get('skip_immediate'):
1757 return immediate_productions
1758 pd = self.env['decimal.precision'].precision_get('Product Unit of Measure')
1759 for production in self:
1760 if all(float_is_zero(ml.qty_done, precision_digits=pd) for
1761 ml in production.move_raw_ids.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
1762 ) and float_is_zero(production.qty_producing, precision_digits=pd):
1763 immediate_productions |= production
1764 return immediate_productions
1765