187 lines
9.2 KiB
Python
187 lines
9.2 KiB
Python
# -*- 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.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()
|
|
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
|