123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- # -*- coding: utf-8 -*-
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- from collections import defaultdict
- from odoo import fields, models, _, api
- from odoo.exceptions import UserError, ValidationError, AccessError
- from odoo.tools.float_utils import float_compare, float_is_zero
- class MrpProduction(models.Model):
- _inherit = 'mrp.production'
- _rec_names_search = ['name', 'incoming_picking.name']
- move_line_raw_ids = fields.One2many(
- 'stock.move.line', string="Detail Component", readonly=False,
- inverse='_inverse_move_line_raw_ids', compute='_compute_move_line_raw_ids'
- )
- subcontracting_has_been_recorded = fields.Boolean("Has been recorded?", copy=False)
- subcontractor_id = fields.Many2one('res.partner', string="Subcontractor", help="Used to restrict access to the portal user through Record Rules")
- bom_product_ids = fields.Many2many('product.product', compute="_compute_bom_product_ids", help="List of Products used in the BoM, used to filter the list of products in the subcontracting portal view")
- incoming_picking = fields.Many2one(related='move_finished_ids.move_dest_ids.picking_id')
- @api.depends('name')
- def name_get(self):
- return [
- (record.id, "%s (%s)" % (record.incoming_picking.name, record.name)) if record.bom_id.type == 'subcontract'
- else (record.id, record.name) for record in self
- ]
- @api.depends('move_raw_ids.move_line_ids')
- def _compute_move_line_raw_ids(self):
- for production in self:
- production.move_line_raw_ids = production.move_raw_ids.move_line_ids
- def _compute_bom_product_ids(self):
- for production in self:
- production.bom_product_ids = production.bom_id.bom_line_ids.product_id
- def _inverse_move_line_raw_ids(self):
- for production in self:
- line_by_product = defaultdict(lambda: self.env['stock.move.line'])
- for line in production.move_line_raw_ids:
- line_by_product[line.product_id] |= line
- for move in production.move_raw_ids:
- move.move_line_ids = line_by_product.pop(move.product_id, self.env['stock.move.line'])
- for product_id, lines in line_by_product.items():
- qty = sum(line.product_uom_id._compute_quantity(line.qty_done, product_id.uom_id) for line in lines)
- move = production._get_move_raw_values(product_id, qty, product_id.uom_id)
- move['additional'] = True
- production.move_raw_ids = [(0, 0, move)]
- production.move_raw_ids.filtered(lambda m: m.product_id == product_id)[:1].move_line_ids = lines
- def write(self, vals):
- if self.env.user.has_group('base.group_portal') and not self.env.su:
- unauthorized_fields = set(vals.keys()) - set(self._get_writeable_fields_portal_user())
- if unauthorized_fields:
- raise AccessError(_("You cannot write on fields %s in mrp.production.", ', '.join(unauthorized_fields)))
- return super().write(vals)
- def action_merge(self):
- if any(production._get_subcontract_move() for production in self):
- raise ValidationError(_("Subcontracted manufacturing orders cannot be merged."))
- return super().action_merge()
- def subcontracting_record_component(self):
- self.ensure_one()
- if not self._get_subcontract_move():
- raise UserError(_("This MO isn't related to a subcontracted move"))
- if float_is_zero(self.qty_producing, precision_rounding=self.product_uom_id.rounding):
- return {'type': 'ir.actions.act_window_close'}
- if self.move_raw_ids and not any(self.move_raw_ids.mapped('quantity_done')):
- raise UserError(_("You must indicate a non-zero amount consumed for at least one of your components"))
- consumption_issues = self._get_consumption_issues()
- if consumption_issues:
- return self._action_generate_consumption_wizard(consumption_issues)
- self._update_finished_move()
- self.subcontracting_has_been_recorded = True
- quantity_issues = self._get_quantity_produced_issues()
- if quantity_issues:
- backorder = self.sudo()._split_productions()[1:]
- # No qty to consume to avoid propagate additional move
- # TODO avoid : stock move created in backorder with 0 as qty
- backorder.move_raw_ids.filtered(lambda m: m.additional).product_uom_qty = 0.0
- backorder.qty_producing = backorder.product_qty
- backorder._set_qty_producing()
- self.product_qty = self.qty_producing
- action = self._get_subcontract_move().filtered(lambda m: m.state not in ('done', 'cancel'))._action_record_components()
- action['res_id'] = backorder.id
- return action
- return {'type': 'ir.actions.act_window_close'}
- def _pre_button_mark_done(self):
- if self._get_subcontract_move():
- return True
- return super()._pre_button_mark_done()
- def _should_postpone_date_finished(self, date_planned_finished):
- return super()._should_postpone_date_finished(date_planned_finished) and not self._get_subcontract_move()
- def _update_finished_move(self):
- """ After producing, set the move line on the subcontract picking. """
- self.ensure_one()
- subcontract_move_id = self._get_subcontract_move().filtered(lambda m: m.state not in ('done', 'cancel'))
- if subcontract_move_id:
- quantity = self.qty_producing
- if self.lot_producing_id:
- move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_producing_id or not ml.lot_id)
- else:
- move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: not ml.lot_id)
- # Update reservation and quantity done
- for ml in move_lines:
- rounding = ml.product_uom_id.rounding
- if float_compare(quantity, 0, precision_rounding=rounding) <= 0:
- break
- quantity_to_process = min(quantity, ml.reserved_uom_qty - ml.qty_done)
- quantity -= quantity_to_process
- new_quantity_done = (ml.qty_done + quantity_to_process)
- # on which lot of finished product
- if float_compare(new_quantity_done, ml.reserved_uom_qty, precision_rounding=rounding) >= 0:
- ml.write({
- 'qty_done': new_quantity_done,
- 'lot_id': self.lot_producing_id and self.lot_producing_id.id,
- })
- else:
- new_qty_reserved = ml.reserved_uom_qty - new_quantity_done
- default = {
- 'reserved_uom_qty': new_quantity_done,
- 'qty_done': new_quantity_done,
- 'lot_id': self.lot_producing_id and self.lot_producing_id.id,
- }
- ml.copy(default=default)
- ml.with_context(bypass_reservation_update=True).write({
- 'reserved_uom_qty': new_qty_reserved,
- 'qty_done': 0
- })
- if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) > 0:
- self.env['stock.move.line'].create({
- 'move_id': subcontract_move_id.id,
- 'picking_id': subcontract_move_id.picking_id.id,
- 'product_id': self.product_id.id,
- 'location_id': subcontract_move_id.location_id.id,
- 'location_dest_id': subcontract_move_id.location_dest_id.id,
- 'reserved_uom_qty': 0,
- 'product_uom_id': self.product_uom_id.id,
- 'qty_done': quantity,
- 'lot_id': self.lot_producing_id and self.lot_producing_id.id,
- })
- if not self._get_quantity_to_backorder():
- ml_reserved = subcontract_move_id.move_line_ids.filtered(lambda ml:
- float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding) and
- not float_is_zero(ml.reserved_uom_qty, precision_rounding=ml.product_uom_id.rounding))
- ml_reserved.unlink()
- for ml in subcontract_move_id.move_line_ids:
- ml.reserved_uom_qty = ml.qty_done
- subcontract_move_id._recompute_state()
- def _subcontracting_filter_to_done(self):
- """ Filter subcontracting production where composant is already recorded and should be consider to be validate """
- def filter_in(mo):
- if mo.state in ('done', 'cancel'):
- return False
- if not mo.subcontracting_has_been_recorded:
- return False
- return True
- return self.filtered(filter_in)
- def _has_been_recorded(self):
- self.ensure_one()
- if self.state in ('cancel', 'done'):
- return True
- return self.subcontracting_has_been_recorded
- def _has_tracked_component(self):
- return any(m.has_tracking != 'none' for m in self.move_raw_ids)
- def _has_workorders(self):
- if self.subcontractor_id:
- return False
- else:
- return super()._has_workorders()
- def _get_subcontract_move(self):
- return self.move_finished_ids.move_dest_ids.filtered(lambda m: m.is_subcontract)
- def _get_writeable_fields_portal_user(self):
- return ['move_line_raw_ids', 'lot_producing_id', 'subcontracting_has_been_recorded', 'qty_producing', 'product_qty']
- def _subcontract_sanity_check(self):
- for production in self:
- if production.product_tracking != 'none' and not self.lot_producing_id:
- raise UserError(_('You must enter a serial number for %s') % production.product_id.name)
- for sml in production.move_raw_ids.move_line_ids:
- if sml.tracking != 'none' and not sml.lot_id:
- raise UserError(_('You must enter a serial number for each line of %s') % sml.product_id.display_name)
- return True
|