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