1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
|
# -*- 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('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.quantity, 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._is_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()
self.move_raw_ids.picked = True
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')):
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.sudo()._update_finished_move() # Portal user may need sudo rights to update pickings
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_finished):
return super()._should_postpone_date_finished(date_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: not ml.picked and 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.picked and 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.quantity)
quantity -= quantity_to_process
# on which lot of finished product
if float_compare(quantity_to_process, ml.quantity, precision_rounding=rounding) >= 0:
ml.write({
'quantity': quantity_to_process,
'picked': True,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
})
else:
ml.write({
'quantity': quantity_to_process,
'picked': True,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
})
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,
'product_uom_id': self.product_uom_id.id,
'quantity': quantity,
'picked': True,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
})
if not self._get_quantity_to_backorder():
subcontract_move_id.move_line_ids.filtered(lambda ml: not ml.picked).unlink()
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
|