mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
237 lines
9.7 KiB
Python
237 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 api, 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
|
|
|
|
@api.multi
|
|
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()
|