mrp_production.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from collections import defaultdict
  4. from odoo import fields, models, _, api
  5. from odoo.exceptions import UserError, ValidationError, AccessError
  6. from odoo.tools.float_utils import float_compare, float_is_zero
  7. class MrpProduction(models.Model):
  8. _inherit = 'mrp.production'
  9. _rec_names_search = ['name', 'incoming_picking.name']
  10. move_line_raw_ids = fields.One2many(
  11. 'stock.move.line', string="Detail Component", readonly=False,
  12. inverse='_inverse_move_line_raw_ids', compute='_compute_move_line_raw_ids'
  13. )
  14. subcontracting_has_been_recorded = fields.Boolean("Has been recorded?", copy=False)
  15. subcontractor_id = fields.Many2one('res.partner', string="Subcontractor", help="Used to restrict access to the portal user through Record Rules")
  16. 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")
  17. incoming_picking = fields.Many2one(related='move_finished_ids.move_dest_ids.picking_id')
  18. @api.depends('name')
  19. def name_get(self):
  20. return [
  21. (record.id, "%s (%s)" % (record.incoming_picking.name, record.name)) if record.bom_id.type == 'subcontract'
  22. else (record.id, record.name) for record in self
  23. ]
  24. @api.depends('move_raw_ids.move_line_ids')
  25. def _compute_move_line_raw_ids(self):
  26. for production in self:
  27. production.move_line_raw_ids = production.move_raw_ids.move_line_ids
  28. def _compute_bom_product_ids(self):
  29. for production in self:
  30. production.bom_product_ids = production.bom_id.bom_line_ids.product_id
  31. def _inverse_move_line_raw_ids(self):
  32. for production in self:
  33. line_by_product = defaultdict(lambda: self.env['stock.move.line'])
  34. for line in production.move_line_raw_ids:
  35. line_by_product[line.product_id] |= line
  36. for move in production.move_raw_ids:
  37. move.move_line_ids = line_by_product.pop(move.product_id, self.env['stock.move.line'])
  38. for product_id, lines in line_by_product.items():
  39. qty = sum(line.product_uom_id._compute_quantity(line.qty_done, product_id.uom_id) for line in lines)
  40. move = production._get_move_raw_values(product_id, qty, product_id.uom_id)
  41. move['additional'] = True
  42. production.move_raw_ids = [(0, 0, move)]
  43. production.move_raw_ids.filtered(lambda m: m.product_id == product_id)[:1].move_line_ids = lines
  44. def write(self, vals):
  45. if self.env.user.has_group('base.group_portal') and not self.env.su:
  46. unauthorized_fields = set(vals.keys()) - set(self._get_writeable_fields_portal_user())
  47. if unauthorized_fields:
  48. raise AccessError(_("You cannot write on fields %s in mrp.production.", ', '.join(unauthorized_fields)))
  49. return super().write(vals)
  50. def action_merge(self):
  51. if any(production._get_subcontract_move() for production in self):
  52. raise ValidationError(_("Subcontracted manufacturing orders cannot be merged."))
  53. return super().action_merge()
  54. def subcontracting_record_component(self):
  55. self.ensure_one()
  56. if not self._get_subcontract_move():
  57. raise UserError(_("This MO isn't related to a subcontracted move"))
  58. if float_is_zero(self.qty_producing, precision_rounding=self.product_uom_id.rounding):
  59. return {'type': 'ir.actions.act_window_close'}
  60. if self.move_raw_ids and not any(self.move_raw_ids.mapped('quantity_done')):
  61. raise UserError(_("You must indicate a non-zero amount consumed for at least one of your components"))
  62. consumption_issues = self._get_consumption_issues()
  63. if consumption_issues:
  64. return self._action_generate_consumption_wizard(consumption_issues)
  65. self._update_finished_move()
  66. self.subcontracting_has_been_recorded = True
  67. quantity_issues = self._get_quantity_produced_issues()
  68. if quantity_issues:
  69. backorder = self.sudo()._split_productions()[1:]
  70. # No qty to consume to avoid propagate additional move
  71. # TODO avoid : stock move created in backorder with 0 as qty
  72. backorder.move_raw_ids.filtered(lambda m: m.additional).product_uom_qty = 0.0
  73. backorder.qty_producing = backorder.product_qty
  74. backorder._set_qty_producing()
  75. self.product_qty = self.qty_producing
  76. action = self._get_subcontract_move().filtered(lambda m: m.state not in ('done', 'cancel'))._action_record_components()
  77. action['res_id'] = backorder.id
  78. return action
  79. return {'type': 'ir.actions.act_window_close'}
  80. def _pre_button_mark_done(self):
  81. if self._get_subcontract_move():
  82. return True
  83. return super()._pre_button_mark_done()
  84. def _should_postpone_date_finished(self, date_planned_finished):
  85. return super()._should_postpone_date_finished(date_planned_finished) and not self._get_subcontract_move()
  86. def _update_finished_move(self):
  87. """ After producing, set the move line on the subcontract picking. """
  88. self.ensure_one()
  89. subcontract_move_id = self._get_subcontract_move().filtered(lambda m: m.state not in ('done', 'cancel'))
  90. if subcontract_move_id:
  91. quantity = self.qty_producing
  92. if self.lot_producing_id:
  93. move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_producing_id or not ml.lot_id)
  94. else:
  95. move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: not ml.lot_id)
  96. # Update reservation and quantity done
  97. for ml in move_lines:
  98. rounding = ml.product_uom_id.rounding
  99. if float_compare(quantity, 0, precision_rounding=rounding) <= 0:
  100. break
  101. quantity_to_process = min(quantity, ml.reserved_uom_qty - ml.qty_done)
  102. quantity -= quantity_to_process
  103. new_quantity_done = (ml.qty_done + quantity_to_process)
  104. # on which lot of finished product
  105. if float_compare(new_quantity_done, ml.reserved_uom_qty, precision_rounding=rounding) >= 0:
  106. ml.write({
  107. 'qty_done': new_quantity_done,
  108. 'lot_id': self.lot_producing_id and self.lot_producing_id.id,
  109. })
  110. else:
  111. new_qty_reserved = ml.reserved_uom_qty - new_quantity_done
  112. default = {
  113. 'reserved_uom_qty': new_quantity_done,
  114. 'qty_done': new_quantity_done,
  115. 'lot_id': self.lot_producing_id and self.lot_producing_id.id,
  116. }
  117. ml.copy(default=default)
  118. ml.with_context(bypass_reservation_update=True).write({
  119. 'reserved_uom_qty': new_qty_reserved,
  120. 'qty_done': 0
  121. })
  122. if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) > 0:
  123. self.env['stock.move.line'].create({
  124. 'move_id': subcontract_move_id.id,
  125. 'picking_id': subcontract_move_id.picking_id.id,
  126. 'product_id': self.product_id.id,
  127. 'location_id': subcontract_move_id.location_id.id,
  128. 'location_dest_id': subcontract_move_id.location_dest_id.id,
  129. 'reserved_uom_qty': 0,
  130. 'product_uom_id': self.product_uom_id.id,
  131. 'qty_done': quantity,
  132. 'lot_id': self.lot_producing_id and self.lot_producing_id.id,
  133. })
  134. if not self._get_quantity_to_backorder():
  135. ml_reserved = subcontract_move_id.move_line_ids.filtered(lambda ml:
  136. float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding) and
  137. not float_is_zero(ml.reserved_uom_qty, precision_rounding=ml.product_uom_id.rounding))
  138. ml_reserved.unlink()
  139. for ml in subcontract_move_id.move_line_ids:
  140. ml.reserved_uom_qty = ml.qty_done
  141. subcontract_move_id._recompute_state()
  142. def _subcontracting_filter_to_done(self):
  143. """ Filter subcontracting production where composant is already recorded and should be consider to be validate """
  144. def filter_in(mo):
  145. if mo.state in ('done', 'cancel'):
  146. return False
  147. if not mo.subcontracting_has_been_recorded:
  148. return False
  149. return True
  150. return self.filtered(filter_in)
  151. def _has_been_recorded(self):
  152. self.ensure_one()
  153. if self.state in ('cancel', 'done'):
  154. return True
  155. return self.subcontracting_has_been_recorded
  156. def _has_tracked_component(self):
  157. return any(m.has_tracking != 'none' for m in self.move_raw_ids)
  158. def _has_workorders(self):
  159. if self.subcontractor_id:
  160. return False
  161. else:
  162. return super()._has_workorders()
  163. def _get_subcontract_move(self):
  164. return self.move_finished_ids.move_dest_ids.filtered(lambda m: m.is_subcontract)
  165. def _get_writeable_fields_portal_user(self):
  166. return ['move_line_raw_ids', 'lot_producing_id', 'subcontracting_has_been_recorded', 'qty_producing', 'product_qty']
  167. def _subcontract_sanity_check(self):
  168. for production in self:
  169. if production.product_tracking != 'none' and not self.lot_producing_id:
  170. raise UserError(_('You must enter a serial number for %s') % production.product_id.name)
  171. for sml in production.move_raw_ids.move_line_ids:
  172. if sml.tracking != 'none' and not sml.lot_id:
  173. raise UserError(_('You must enter a serial number for each line of %s') % sml.product_id.display_name)
  174. return True