Files
odoo_source/addons/mrp_subcontracting/models/stock_move.py
T
Tiffany Chang (tic) 38b97971e9 [FIX] mrp_subcontracting{_purchase}: handle subcontract qty decrease
Steps to reproduce:
- create a subcontracted product (i.e. create subcontract BoM)
- create and confirm PO for subcontracted product (qty > 1)
- try to decrease PO qty for subcontracted product

Expected Result:
Since receipt is not yet validated, the qty in the receipt should
decrease (it will in the subcontract MO as well, but this doesn't matter
since no one should work directly with the MO)

Actual Result:
A validation error occurs saying the qty to produce must be non-negative

We allow neg demand qtys to be proprogated from SOs and POs since
https://github.com/odoo/odoo/pull/76752 . While we added in a check to
make sure MOs are not created when a neg qty change is proprogated, we
forgot to add a check for subcontracted created MOs, hence the
validation error (i.e. the neg qty change is trying to create a
subcontracted MO of a neg amount.)

Task: 2777571 (issue 2 of additional issues)
X-original-commit: 251f146b6ecb0e2d428089669199a6deb3a6c060
Part-of: odoo/odoo#97383
2022-08-03 15:09:57 +02:00

265 lines
13 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, AccessError
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_display_assign_serial(self):
super(StockMove, self)._compute_display_assign_serial()
for move in self:
if not move.is_subcontract:
continue
productions = move._get_subcontract_production()
if not productions or move.has_tracking != 'serial':
continue
if productions._has_tracked_component() or productions[:1].consumption != 'strict':
move.display_assign_serial = False
def _compute_show_subcontracting_details_visible(self):
""" Compute if the action button in order to see moves raw is visible """
self.show_subcontracting_details_visible = False
for move in self:
if not move.is_subcontract:
continue
if float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding):
continue
productions = move._get_subcontract_production()
if not productions or (productions[:1].consumption == 'strict' and not productions[:1]._has_tracked_component()):
continue
move.show_subcontracting_details_visible = True
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(StockMove, self)._compute_show_details_visible()
for move in self:
if not move.is_subcontract:
continue
if self.env.user.has_group('base.group_portal'):
move.show_details_visible = any(not p._has_been_recorded() for p in move._get_subcontract_production())
continue
productions = move._get_subcontract_production()
if not productions._has_tracked_component() and productions[:1].consumption == 'strict':
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(StockMove, self).copy(default=default)
if not default:
default = {}
default['location_id'] = self.picking_id.location_id.id
return super(StockMove, self).copy(default=default)
def write(self, values):
""" If the initial demand is updated then also update the linked
subcontract order to the new quantity.
"""
self._check_access_if_subcontractor(values)
if 'product_uom_qty' in values and self.env.context.get('cancel_backorder') is not False:
self.filtered(lambda m: m.is_subcontract and m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty(values['product_uom_qty'])
res = super().write(values)
if 'date' in values:
for move in self:
if move.state in ('done', 'cancel') or not move.is_subcontract:
continue
move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel')).write({
'date_planned_finished': move.date,
'date_planned_start': move.date,
})
return res
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self._check_access_if_subcontractor(vals)
return super().create(vals_list)
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.state != 'done' and (self._subcontrating_should_be_record() or self._subcontrating_can_be_record()):
return self._action_record_components()
action = super(StockMove, self).action_show_details()
if self.is_subcontract and all(p._has_been_recorded() for p in self._get_subcontract_production()):
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,
})
elif self.env.user.has_group('base.group_portal'):
if self.picking_type_id.show_reserved:
action['views'] = [(self.env.ref('mrp_subcontracting.mrp_subcontracting_view_stock_move_operations').id, 'form')]
else:
action['views'] = [(self.env.ref('mrp_subcontracting.mrp_subcontracting_view_stock_move_nosuggest_operations').id, 'form')]
return action
def action_show_subcontract_details(self):
""" Display moves raw for subcontracted product self. """
moves = self._get_subcontract_production().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')
ctx = dict(self._context, search_default_by_product=True)
if self.env.user.has_group('base.group_portal'):
form_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_portal_move_form_view')
ctx.update(no_breadcrumbs=False)
return {
'name': _('Raw Materials for %s') % (self.product_id.display_name),
'type': 'ir.actions.act_window',
'res_model': 'stock.move',
'views': [(tree_view.id, 'list'), (form_view.id, 'form')],
'target': 'current',
'domain': [('id', 'in', moves.ids)],
'context': ctx
}
def _set_quantities_to_reservation(self):
move_untouchable = self.filtered(lambda m: m.is_subcontract and m._get_subcontract_production()._has_tracked_component())
return super(StockMove, self - move_untouchable)._set_quantities_to_reservation()
def _action_cancel(self):
for move in self:
if move.is_subcontract:
active_production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
moves = self.env.context.get('moves_todo')
if not moves or active_production not in moves.move_orig_ids.production_id:
active_production.with_context(skip_activity=True).action_cancel()
return super()._action_cancel()
def _action_confirm(self, merge=True, merge_into=False):
subcontract_details_per_picking = defaultdict(list)
move_to_not_merge = self.env['stock.move']
for move in self:
if move.location_id.usage != 'supplier' or move.location_dest_id.usage == 'supplier':
continue
if move.move_orig_ids.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_company(move.company_id).property_stock_subcontractor.id
})
if float_compare(move.product_qty, 0, precision_rounding=move.product_uom.rounding) <= 0:
# If a subcontracted amount is decreased, don't create a MO that would be for a negative value.
# We don't care if the MO decreases even when done since everything is handled through picking
continue
move_to_not_merge |= move
for picking, subcontract_details in subcontract_details_per_picking.items():
picking._subcontracted_produce(subcontract_details)
# We avoid merging move due to complication with stock.rule.
res = super(StockMove, move_to_not_merge)._action_confirm(merge=False)
res |= super(StockMove, self - move_to_not_merge)._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_record_components(self):
self.ensure_one()
production = self._get_subcontract_production()[-1:]
view = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_form_view')
if self.env.user.has_group('base.group_portal'):
view = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_portal_form_view')
return {
'name': _('Subcontract'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mrp.production',
'views': [(view.id, 'form')],
'view_id': view.id,
'target': 'new',
'res_id': production.id,
'context': self.env.context,
}
def _get_subcontract_bom(self):
self.ensure_one()
bom = self.env['mrp.bom'].sudo()._bom_subcontract_find(
self.product_id,
picking_type=self.picking_type_id,
company_id=self.company_id.id,
bom_type='subcontract',
subcontractor=self.picking_id.partner_id,
)
return bom
def _subcontrating_should_be_record(self):
return self._get_subcontract_production().filtered(lambda p: not p._has_been_recorded() and p._has_tracked_component())
def _subcontrating_can_be_record(self):
return self._get_subcontract_production().filtered(lambda p: not p._has_been_recorded() and p.consumption != 'strict')
def _get_subcontract_production(self):
return self.filtered(lambda m: m.is_subcontract).move_orig_ids.production_id
# TODO: To be deleted, use self._get_subcontract_production()._has_tracked_component() instead
def _has_tracked_subcontract_components(self):
return any(m.has_tracking != 'none' for m in self._get_subcontract_production().move_raw_ids)
def _prepare_extra_move_vals(self, qty):
vals = super(StockMove, self)._prepare_extra_move_vals(qty)
vals['location_id'] = self.location_id.id
return vals
def _prepare_move_split_vals(self, qty):
vals = super(StockMove, self)._prepare_move_split_vals(qty)
vals['location_id'] = self.location_id.id
return vals
def _should_bypass_set_qty_producing(self):
if (self.production_id | self.raw_material_production_id)._get_subcontract_move():
return False
return super()._should_bypass_set_qty_producing()
def _should_bypass_reservation(self, forced_location=False):
""" If the move is subcontracted then ignore the reservation. """
should_bypass_reservation = super()._should_bypass_reservation(forced_location=forced_location)
if not should_bypass_reservation and self.is_subcontract:
return True
return should_bypass_reservation
def _update_subcontract_order_qty(self, new_quantity):
for move in self:
quantity_to_remove = move.product_uom_qty - new_quantity
productions = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))[::-1]
# Cancel productions until reach new_quantity
for production in productions:
if quantity_to_remove <= 0.0:
break
if quantity_to_remove >= production.product_qty:
quantity_to_remove -= production.product_qty
production.with_context(skip_activity=True).action_cancel()
else:
self.env['change.production.qty'].with_context(skip_activity=True).create({
'mo_id': production.id,
'product_qty': production.product_uom_qty - quantity_to_remove
}).change_prod_qty()
def _check_access_if_subcontractor(self, vals):
if self.env.user.has_group('base.group_portal') and not self.env.su:
if vals.get('state') == 'done':
raise AccessError(_("Portal users cannot create a stock move with a state 'Done' or change the current state to 'Done'."))