mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
On v12 and with chained moves coming from sales, procurement, etc, move_orig_ids contains that full chain, so we get a singleton error. But as we want the subcontracting production, using mapped, we get that single record, so we apply this solution everywhere where it's needed.
236 lines
9.7 KiB
Python
236 lines
9.7 KiB
Python
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
|
# Copyright 2019 Odoo
|
|
# Copyright 2020 Tecnativa - Alexandre Díaz
|
|
# Copyright 2020 Tecnativa - Pedro M. Baeza
|
|
|
|
from collections import defaultdict
|
|
|
|
from odoo import fields, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools.float_utils import float_compare, float_is_zero
|
|
|
|
|
|
class StockMove(models.Model):
|
|
_inherit = 'stock.move'
|
|
|
|
is_subcontract = fields.Boolean('The move is a subcontract receipt')
|
|
show_subcontracting_details_visible = fields.Boolean(
|
|
compute='_compute_show_subcontracting_details_visible'
|
|
)
|
|
|
|
def _compute_show_subcontracting_details_visible(self):
|
|
"""Compute if the action button in order to see raw moves is visible"""
|
|
for move in self:
|
|
move.show_subcontracting_details_visible = (
|
|
move.is_subcontract and
|
|
move._has_tracked_subcontract_components() and
|
|
not float_is_zero(
|
|
move.quantity_done,
|
|
precision_rounding=move.product_uom.rounding
|
|
)
|
|
)
|
|
|
|
def _compute_show_details_visible(self):
|
|
""" If the move is subcontract and the components are tracked. Then the
|
|
show details button is visible.
|
|
"""
|
|
res = super()._compute_show_details_visible()
|
|
for move in self:
|
|
if not move.is_subcontract:
|
|
continue
|
|
if not move._has_tracked_subcontract_components():
|
|
continue
|
|
move.show_details_visible = True
|
|
return res
|
|
|
|
def copy(self, default=None):
|
|
self.ensure_one()
|
|
if not self.is_subcontract or 'location_id' in default:
|
|
return super().copy(default=default)
|
|
if not default:
|
|
default = {}
|
|
default['location_id'] = self.picking_id.location_id.id
|
|
return super().copy(default=default)
|
|
|
|
def write(self, values):
|
|
""" If the initial demand is updated then also update the linked
|
|
subcontract order to the new quantity.
|
|
"""
|
|
if 'product_uom_qty' in values:
|
|
if self.env.context.get('cancel_backorder') is False:
|
|
return super().write(values)
|
|
self.filtered(lambda m: (
|
|
m.is_subcontract and
|
|
m.state not in ['draft', 'cancel', 'done']
|
|
))._update_subcontract_order_qty(values['product_uom_qty'])
|
|
return super().write(values)
|
|
|
|
def action_show_details(self):
|
|
""" Open the produce wizard in order to register tracked components for
|
|
subcontracted product. Otherwise use standard behavior.
|
|
"""
|
|
self.ensure_one()
|
|
if self.is_subcontract:
|
|
rounding = self.product_uom.rounding
|
|
production = self.move_orig_ids.mapped("production_id")
|
|
if self._has_tracked_subcontract_components() and\
|
|
float_compare(production.qty_produced,
|
|
production.product_uom_qty,
|
|
precision_rounding=rounding) < 0 and\
|
|
float_compare(self.quantity_done, self.product_uom_qty,
|
|
precision_rounding=rounding) < 0:
|
|
return self._action_record_components()
|
|
action = super().action_show_details()
|
|
if self.is_subcontract and self._has_tracked_subcontract_components():
|
|
action['views'] = [
|
|
(self.env.ref('stock.view_stock_move_operations').id, 'form'),
|
|
]
|
|
action['context'].update({
|
|
'show_lots_m2o': self.has_tracking != 'none',
|
|
'show_lots_text': False,
|
|
})
|
|
return action
|
|
|
|
def action_show_subcontract_details(self):
|
|
""" Display moves raw for subcontracted product self. """
|
|
moves = self.move_orig_ids.mapped("production_id").move_raw_ids
|
|
tree_view = self.env.ref(
|
|
'mrp_subcontracting.mrp_subcontracting_move_tree_view')
|
|
form_view = self.env.ref(
|
|
'mrp_subcontracting.mrp_subcontracting_move_form_view')
|
|
return {
|
|
'name': _('Raw Materials for %s') % (self.product_id.display_name),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'stock.move',
|
|
'views': [(tree_view.id, 'tree'), (form_view.id, 'form')],
|
|
'target': 'current',
|
|
'domain': [('id', 'in', moves.ids)],
|
|
}
|
|
|
|
def _action_cancel(self):
|
|
for move in self:
|
|
if move.is_subcontract:
|
|
move.move_orig_ids.mapped("production_id").action_cancel()
|
|
return super()._action_cancel()
|
|
|
|
def _action_confirm(self, merge=True, merge_into=False):
|
|
subcontract_details_per_picking = defaultdict(list)
|
|
for move in self:
|
|
if move.location_id.usage != 'supplier' \
|
|
or move.location_dest_id.usage == 'supplier':
|
|
continue
|
|
if move.move_orig_ids.mapped("production_id"):
|
|
continue
|
|
bom = move._get_subcontract_bom()
|
|
if not bom:
|
|
continue
|
|
if float_is_zero(move.product_qty,
|
|
precision_rounding=move.product_uom.rounding) and\
|
|
move.picking_id.immediate_transfer is True:
|
|
raise UserError(_("To subcontract, use a planned transfer."))
|
|
subcontract_details_per_picking[move.picking_id].append(
|
|
(move, bom))
|
|
move.write({
|
|
'is_subcontract': True,
|
|
'location_id': move.picking_id.partner_id.with_context(
|
|
force_company=move.company_id.id)
|
|
.property_stock_subcontractor.id
|
|
})
|
|
for picking, subcontract_details in\
|
|
subcontract_details_per_picking.items():
|
|
picking._subcontracted_produce(subcontract_details)
|
|
|
|
res = super(StockMove, self)._action_confirm(merge=merge,
|
|
merge_into=merge_into)
|
|
if subcontract_details_per_picking:
|
|
self.env['stock.picking'].concat(
|
|
*list(subcontract_details_per_picking.keys())).action_assign()
|
|
return res
|
|
|
|
def _action_assign(self):
|
|
"""As we don't have the bypass reservation method in v12 at stock.move
|
|
level, we have to trick this method for splitting the assign in
|
|
2 steps, classifying previously the subcontract moves and then
|
|
faking location_id.should_bypass_reservation method through
|
|
context.
|
|
"""
|
|
subcontract_moves = self.filtered('is_subcontract')
|
|
res = super(StockMove, self - subcontract_moves)._action_assign()
|
|
super(StockMove, subcontract_moves.with_context(
|
|
mrp_subcontracting_bypass_reservation=True))._action_assign()
|
|
return res
|
|
|
|
def _action_record_components(self):
|
|
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
|
|
action['context'] = dict(
|
|
active_id=self.move_orig_ids.mapped("production_id").id,
|
|
default_subcontract_move_id=self.id
|
|
)
|
|
return action
|
|
|
|
def _check_overprocessed_subcontract_qty(self):
|
|
""" If a subcontracted move use tracked components. Do not allow to add
|
|
quantity without the produce wizard. Instead update the initial demand
|
|
and use the register component button. Split or correct a lot/sn is
|
|
possible.
|
|
"""
|
|
overprocessed_moves = self.env['stock.move']
|
|
for move in self:
|
|
if not move.is_subcontract:
|
|
continue
|
|
# Extra quantity is allowed when components do not need to be
|
|
# register
|
|
if not move._has_tracked_subcontract_components():
|
|
continue
|
|
rounding = move.product_uom.rounding
|
|
if float_compare(
|
|
move.quantity_done,
|
|
sum(move.move_orig_ids.mapped('production_id.qty_produced')),
|
|
precision_rounding=rounding
|
|
) > 0:
|
|
overprocessed_moves |= move
|
|
if overprocessed_moves:
|
|
raise UserError(_("""
|
|
You have to use 'Records Components' button in order to register quantity for a
|
|
subcontracted product(s) with tracked component(s):
|
|
%s.
|
|
If you want to process more than initially planned, you
|
|
can use the edit + unlock buttons in order to adapt the initial demand on the
|
|
operations.""") % ('\n'.join(overprocessed_moves.mapped(
|
|
'product_id.display_name'))))
|
|
|
|
def _get_subcontract_bom(self):
|
|
self.ensure_one()
|
|
return self.env['mrp.bom'].sudo()._bom_subcontract_find(
|
|
product=self.product_id,
|
|
picking_type=self.picking_type_id,
|
|
company_id=self.company_id.id,
|
|
bom_type='subcontract',
|
|
subcontractor=self.picking_id.partner_id,
|
|
)
|
|
|
|
def _has_tracked_subcontract_components(self):
|
|
self.ensure_one()
|
|
return any(m.has_tracking != 'none' for m in
|
|
self.move_orig_ids.mapped('production_id.move_raw_ids'))
|
|
|
|
def _prepare_extra_move_vals(self, qty):
|
|
vals = super()._prepare_extra_move_vals(qty)
|
|
vals['location_id'] = self.location_id.id
|
|
return vals
|
|
|
|
def _prepare_move_split_vals(self, qty):
|
|
vals = super()._prepare_move_split_vals(qty)
|
|
vals['location_id'] = self.location_id.id
|
|
return vals
|
|
|
|
def _update_subcontract_order_qty(self, quantity):
|
|
for move in self:
|
|
quantity_change = quantity - move.product_uom_qty
|
|
production = move.move_orig_ids.mapped("production_id")
|
|
if production:
|
|
self.env['change.production.qty'].create({
|
|
'mo_id': production.id,
|
|
'product_qty': production.product_uom_qty + quantity_change
|
|
}).change_prod_qty()
|