[FIX] mrp_subcontracting: fix flow with tracked component

After the odoo/odoo#52949 , the registering of tracked component,
in case of subcontracting, was broken. Fix and try to mimic
the same flow than before. The produce wizard has been replaced
by a clean MO form to register component and use backorder mechanisme
of MO to manage tracked finished product + tracked component.

task-2278147

closes odoo/odoo#54994

Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
Rémy Voet (ryv)
2020-07-30 12:09:10 +00:00
parent 2458098f60
commit e7fd2ba5c8
14 changed files with 341 additions and 218 deletions
+3 -2
View File
@@ -108,7 +108,7 @@ class StockMove(models.Model):
store=True,
help='Technical Field to order moves')
needs_lots = fields.Boolean('Tracking', compute='_compute_needs_lots')
order_finished_lot_ids = fields.Many2many('stock.production.lot', compute='_compute_order_finished_lot_ids')
order_finished_lot_ids = fields.Many2many('stock.production.lot', string="Finished Lot/Serial Number", compute='_compute_order_finished_lot_ids')
finished_lots_exist = fields.Boolean('Finished Lots Exist', compute='_compute_order_finished_lot_ids')
should_consume_qty = fields.Float('Quantity To Consume', compute='_compute_should_consume_qty')
@@ -123,7 +123,8 @@ class StockMove(models.Model):
def _compute_order_finished_lot_ids(self):
for move in self:
if move.raw_material_production_id.move_finished_ids:
finished_lots_ids = move.raw_material_production_id.move_finished_ids.mapped('move_line_ids.lot_id').ids
# TODO ryv : to clean in master, finished_lots_exist is never used. order_finished_lot_ids can become one2many instead
finished_lots_ids = move.raw_material_production_id.lot_producing_id
if finished_lots_ids:
move.order_finished_lot_ids = finished_lots_ids
move.finished_lots_exist = True
+1 -1
View File
@@ -161,7 +161,7 @@
<field name="bom_id"
context="{'default_product_tmpl_id': product_tmpl_id}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
</group>
<group>
<group name="group_extra_info">
<field name="date_deadline"/>
<label for="date_planned_start"/>
<div class="o_row">
@@ -18,6 +18,7 @@
'views/stock_picking_views.xml',
'views/supplier_info_views.xml',
'views/product_views.xml',
'views/mrp_production_views.xml',
'wizard/stock_picking_return_views.xml',
],
'demo': [
+1 -1
View File
@@ -9,4 +9,4 @@ from . import stock_move_line
from . import stock_picking
from . import stock_rule
from . import stock_warehouse
from . import mrp_production
@@ -1,20 +1,64 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from collections import defaultdict
from odoo import fields, models, _, api
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare, float_is_zero
class MrpProduction(models.Model):
_inherit = 'mrp.production'
def write(self, vals):
res = super().write(vals)
if self.env.context.get('subcontract_move_id'):
for production in self:
if production.state not in ('cancel', 'done') and \
('lot_producing_id' in vals or 'qty_producing' in vals):
production._update_finished_move()
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'
)
@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 _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.qty_done, 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 subcontracting_record_component(self):
self.ensure_one()
assert self.env.context.get('subcontract_move_id')
if float_is_zero(self.qty_producing, precision_rounding=self.product_uom_id.rounding):
return {'type': 'ir.actions.act_window_close'}
for sml in self.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.name)
self._update_finished_move()
quantity_issues = self._get_quantity_produced_issues()
if quantity_issues:
backorder = self._generate_backorder_productions(close_mo=False)
# 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
subcontract_move_id = self.env['stock.move'].browse(self.env.context.get('subcontract_move_id'))
action = subcontract_move_id._action_record_components()
action.update({'res_id': backorder.id})
return action
return {'type': 'ir.actions.act_window_close'}
def _pre_button_mark_done(self):
if self.env.context.get('subcontract_move_id'):
@@ -23,8 +67,8 @@ class MrpProduction(models.Model):
def _update_finished_move(self):
""" After producing, set the move line on the subcontract picking. """
self.ensure_one()
subcontract_move_id = self.env.context.get('subcontract_move_id')
res = None
if subcontract_move_id:
subcontract_move_id = self.env['stock.move'].browse(subcontract_move_id)
quantity = self.qty_producing
@@ -73,8 +117,7 @@ class MrpProduction(models.Model):
'qty_done': quantity,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
})
if not self._get_todo():
if not self._get_quantity_to_backorder():
ml_reserved = subcontract_move_id.move_line_ids.filtered(lambda ml:
float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding) and
not float_is_zero(ml.product_uom_qty, precision_rounding=ml.product_uom_id.rounding))
@@ -82,11 +125,3 @@ class MrpProduction(models.Model):
for ml in subcontract_move_id.move_line_ids:
ml.product_uom_qty = ml.qty_done
subcontract_move_id._recompute_state()
return res
def _get_todo(self):
""" This method will return remaining todo quantity of production. """
main_product_moves = self.move_finished_ids.filtered(lambda x: x.product_id.id == self.product_id.id)
todo_quantity = self.product_qty - sum(main_product_moves.mapped('quantity_done'))
todo_quantity = todo_quantity if (todo_quantity > 0) else 0
return todo_quantity
+26 -39
View File
@@ -54,8 +54,7 @@ class StockMove(models.Model):
if 'product_uom_qty' in values:
if self.env.context.get('cancel_backorder') is False:
return super(StockMove, self).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'])
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(StockMove, self).write(values)
def action_show_details(self):
@@ -65,7 +64,7 @@ class StockMove(models.Model):
self.ensure_one()
if self.is_subcontract:
rounding = self.product_uom.rounding
production = self.move_orig_ids.production_id
production = self.move_orig_ids.production_id[-1:]
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:
@@ -84,6 +83,7 @@ class StockMove(models.Model):
moves = self.move_orig_ids.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')
ctx = dict(self._context, search_default_by_product=True, subcontract_move_id=self.id)
return {
'name': _('Raw Materials for %s') % (self.product_id.display_name),
'type': 'ir.actions.act_window',
@@ -91,6 +91,7 @@ class StockMove(models.Model):
'views': [(tree_view.id, 'list'), (form_view.id, 'form')],
'target': 'current',
'domain': [('id', 'in', moves.ids)],
'context': ctx
}
def _action_cancel(self):
@@ -127,8 +128,8 @@ class StockMove(models.Model):
def _action_record_components(self):
self.ensure_one()
production = self.move_orig_ids.production_id
view = self.env.ref('mrp.mrp_production_form_view')
production = self.move_orig_ids.production_id[-1:]
view = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_form_view')
return {
'name': _('Subcontract'),
'type': 'ir.actions.act_window',
@@ -141,32 +142,6 @@ class StockMove(models.Model):
'context': dict(self.env.context, subcontract_move_id=self.id),
}
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, move.move_orig_ids.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()
bom = self.env['mrp.bom'].sudo()._bom_subcontract_find(
@@ -192,6 +167,11 @@ operations.""") % ('\n'.join(overprocessed_moves.mapped('product_id.display_name
vals['location_id'] = self.location_id.id
return vals
def _should_bypass_set_qty_producing(self):
if self.env.context.get('subcontract_move_id'):
return False
return super()._should_bypass_set_qty_producing()
def _should_bypass_reservation(self):
""" If the move is subcontracted then ignore the reservation. """
should_bypass_reservation = super(StockMove, self)._should_bypass_reservation()
@@ -199,12 +179,19 @@ operations.""") % ('\n'.join(overprocessed_moves.mapped('product_id.display_name
return True
return should_bypass_reservation
def _update_subcontract_order_qty(self, quantity):
def _update_subcontract_order_qty(self, new_quantity):
for move in self:
quantity_change = quantity - move.product_uom_qty
production = move.move_orig_ids.production_id
if production:
self.env['change.production.qty'].with_context(skip_activity=True).create({
'mo_id': production.id,
'product_qty': production.product_uom_qty + quantity_change
}).change_prod_qty()
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.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()
@@ -1,23 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo import models, fields
class StockMoveLine(models.Model):
_inherit = 'stock.move.line'
@api.model_create_multi
def create(self, vals_list):
records = super(StockMoveLine, self).create(vals_list)
records.filtered(lambda ml: ml.move_id.is_subcontract).move_id._check_overprocessed_subcontract_qty()
return records
def write(self, values):
res = super(StockMoveLine, self).write(values)
self.filtered(lambda ml: ml.move_id.is_subcontract).move_id._check_overprocessed_subcontract_qty()
return res
def _should_bypass_reservation(self, location):
""" If the move line is subcontracted then ignore the reservation. """
should_bypass_reservation = super(StockMoveLine, self)._should_bypass_reservation(location)
@@ -4,7 +4,7 @@
from datetime import timedelta
from odoo import api, fields, models
from odoo.tools import float_is_zero
from odoo.tools.float_utils import float_is_zero
class StockPicking(models.Model):
@@ -24,12 +24,14 @@ class StockPicking(models.Model):
continue
# Hide if no components are track
subcontracted_productions = picking._get_subcontracted_productions()
subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
component_sub_moves = subcontracted_productions.mapped('move_raw_ids')
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in component_sub_moves):
picking.display_action_record_components = False
continue
# Hide if the production is to close
if not subcontracted_productions.filtered(lambda mo: mo.state not in ('to_close', 'done')):
# Hide if all tracked product move line have a lot and all productions are in rigth state
tracked_move_line = component_sub_moves.filtered(lambda sm: sm.has_tracking != 'none').move_line_ids
if all(sub_mo.state in ('to_close', 'done') for sub_mo in subcontracted_productions)\
and all(sub_sml.lot_id for sub_sml in tracked_move_line):
picking.display_action_record_components = False
continue
picking.display_action_record_components = True
@@ -39,29 +41,12 @@ class StockPicking(models.Model):
# -------------------------------------------------------------------------
def _action_done(self):
res = super(StockPicking, self)._action_done()
productions = self.env['mrp.production']
for picking in self:
for move in picking.move_lines:
if not move.is_subcontract:
continue
production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
if len(production) > 1:
production = production[-1]
if move._has_tracked_subcontract_components():
move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel')).move_line_ids.unlink()
move_finished_ids = move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
for ml in move.move_line_ids:
ml.copy({
'picking_id': False,
'production_id': move_finished_ids.production_id.id,
'move_id': move_finished_ids.id,
'qty_done': ml.qty_done,
'result_package_id': False,
'location_id': move_finished_ids.location_id.id,
'location_dest_id': move_finished_ids.location_dest_id.id,
})
else:
if not move.move_line_ids.lot_id:
# Auto set qty_producing if not tracked component
for move in picking.move_lines.filtered(lambda move: move.is_subcontract):
if not move._has_tracked_subcontract_components():
production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
if production:
qty_done_production_uom = move.product_uom._compute_quantity(move.quantity_done, production.product_uom_id)
production.qty_producing += qty_done_production_uom
for move in (production.move_raw_ids | production.move_finished_ids.filtered(lambda m: m.product_id != production.product_id)):
@@ -74,12 +59,15 @@ class StockPicking(models.Model):
rounding_method='HALF-UP')
move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
productions |= production
for subcontracted_production in productions:
productions_to_done = picking.move_lines.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
# TODO : to change in master with field on mrp
productions_to_done = productions_to_done.filtered(lambda pro: all(line.lot_id for line in pro.move_raw_ids.filtered(lambda sm: sm.has_tracking != 'none').move_line_ids))
for subcontracted_production in productions_to_done:
if subcontracted_production.state == 'progress':
subcontracted_production._post_inventory()
else:
subcontracted_production.button_mark_done()
subcontracted_production.with_context(subcontract_move_id=True).button_mark_done()
# For concistency, set the date on production move before the date
# on picking. (Tracability report + Product Moves menu item)
minimum_date = min(picking.move_line_ids.mapped('date'))
@@ -93,8 +81,8 @@ class StockPicking(models.Model):
for move in self.move_lines:
if not move._has_tracked_subcontract_components():
continue
production = move.move_orig_ids.production_id
if not production or production.state in ('done', 'to_close'):
production = move.move_orig_ids.production_id[-1:]
if not production:
continue
return move._action_record_components()
@@ -145,3 +133,7 @@ class StockPicking(models.Model):
finished_move = mo.move_finished_ids.filtered(lambda m: m.product_id == move.product_id)
finished_move.write({'move_dest_ids': [(4, move.id, False)]})
mo.action_assign()
if move._has_tracked_subcontract_components():
mo.qty_producing = move.product_uom_qty
mo.with_context(subcontract_move_id=True)._set_qty_producing()
@@ -402,87 +402,6 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
self.assertEqual(sum(move_finished.mapped('product_uom_qty')), 3.0)
self.assertEqual(sum(move_finished.mapped('quantity_done')), 3.0)
def test_flow_7(self):
""" Process a subcontracting receipt with tracked component and
finished product. Simulate the regiter components button.
Once the components are registered, try to do a correction on exisiting
move lines and check that the subcontracting document is updated.
"""
# Create a receipt picking from the subcontractor
(self.comp1 | self.comp2 | self.finished).write({'tracking': 'lot'})
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids_without_package.new() as move:
move.product_id = self.finished
move.product_uom_qty = 3 # FIXME sle: need to handle the backorder in subcontract
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
mo = picking_receipt.move_lines.move_orig_ids.production_id
move_comp1 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
move_comp2 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp2)
# move_finished is linked to receipt and not MO finished move.
move_finished = picking_receipt.move_lines
self.assertEqual(move_comp1.quantity_done, 0)
self.assertEqual(move_comp2.quantity_done, 0)
lot_c1 = self.env['stock.production.lot'].create({
'name': 'LOT C1',
'product_id': self.comp1.id,
'company_id': self.env.company.id,
})
lot_c2 = self.env['stock.production.lot'].create({
'name': 'LOT C2',
'product_id': self.comp2.id,
'company_id': self.env.company.id,
})
lot_f1 = self.env['stock.production.lot'].create({
'name': 'LOT F1',
'product_id': self.finished.id,
'company_id': self.env.company.id,
})
mo_form = Form(picking_receipt._get_subcontracted_productions().with_context(subcontract_move_id=picking_receipt.move_lines.id))
mo_form.qty_producing = 3
mo_form.lot_producing_id = lot_f1
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = lot_c1
ml.qty_done = 3
details_operation_form.save()
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = lot_c2
ml.qty_done = 3
details_operation_form.save()
self.assertEqual(move_comp1.quantity_done, 3)
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C1')
self.assertEqual(move_comp2.quantity_done, 3)
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C2')
self.assertEqual(move_finished.quantity_done, 3)
self.assertEqual(move_finished.move_line_ids.filtered(lambda ml: ml.product_uom_qty).lot_id.name, 'LOT F1')
corrected_final_lot = self.env['stock.production.lot'].create({
'name': 'LOT F2',
'product_id': self.finished.id,
'company_id': self.env.company.id,
})
details_operation_form = Form(picking_receipt.move_lines, view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.lot_id = corrected_final_lot
details_operation_form.save()
move_raw_comp_1 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
move_raw_comp_2 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp2)
picking_receipt.move_lines.move_orig_ids.production_id.lot_producing_id = corrected_final_lot
picking_receipt.move_lines.move_orig_ids.production_id.button_mark_done()
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).produce_line_ids.lot_id.name, 'LOT F2')
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).produce_line_ids.lot_id.name, 'LOT F2')
def test_flow_8(self):
resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
@@ -596,7 +515,7 @@ class TestSubcontractingTracking(TransactionCase):
})
# 2.2. Finished prodcut has tracking by serial number
self.finished_lot = self.env['product.product'].create({
self.finished_product = self.env['product.product'].create({
'name': 'finished',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
@@ -605,7 +524,7 @@ class TestSubcontractingTracking(TransactionCase):
bom_form = Form(self.env['mrp.bom'])
bom_form.type = 'subcontract'
bom_form.subcontractor_ids.add(self.subcontractor_partner1)
bom_form.product_tmpl_id = self.finished_lot.product_tmpl_id
bom_form.product_tmpl_id = self.finished_product.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp1_sn
bom_line.product_qty = 1
@@ -622,7 +541,7 @@ class TestSubcontractingTracking(TransactionCase):
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids_without_package.new() as move:
move.product_id = self.finished_lot
move.product_id = self.finished_product
move.product_uom_qty = 1
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
@@ -633,7 +552,6 @@ class TestSubcontractingTracking(TransactionCase):
# Check the created manufacturing order
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
self.assertEqual(len(mo), 1)
self.assertEqual(mo.state, 'confirmed')
self.assertEqual(len(mo.picking_ids), 0)
wh = picking_receipt.picking_type_id.warehouse_id
self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
@@ -658,7 +576,7 @@ class TestSubcontractingTracking(TransactionCase):
lot_id = self.env['stock.production.lot'].create({
'name': 'lot1',
'product_id': self.finished_lot.id,
'product_id': self.finished_product.id,
'company_id': self.env.company.id,
})
serial_id = self.env['stock.production.lot'].create({
@@ -667,15 +585,15 @@ class TestSubcontractingTracking(TransactionCase):
'company_id': self.env.company.id,
})
mo_form = Form(mo.with_context(subcontract_move_id=picking_receipt.move_lines.id))
action = picking_receipt.action_record_components()
mo = self.env['mrp.production'].browse(action['res_id'])
mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
mo_form.qty_producing = 1
mo_form.lot_producing_id = lot_id
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.qty_done = 1
with mo_form.move_line_raw_ids.edit(0) as ml:
ml.lot_id = serial_id
details_operation_form.save()
mo = mo_form.save()
mo.subcontracting_record_component()
# We should not be able to call the 'record_components' button
self.assertFalse(picking_receipt.display_action_record_components)
@@ -686,7 +604,84 @@ class TestSubcontractingTracking(TransactionCase):
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_lot, wh.lot_stock_id)
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
self.assertEqual(avail_qty_comp1, -1)
self.assertEqual(avail_qty_comp2, -1)
self.assertEqual(avail_qty_finished, 1)
def test_flow_tracked_backorder(self):
""" This test uses tracked (serial and lot) component and tracked (serial) finished product """
todo_nb = 4
self.comp2.tracking = 'lot'
self.finished_product.tracking = 'serial'
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids_without_package.new() as move:
move.product_id = self.finished_product
move.product_uom_qty = todo_nb
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
# We should be able to call the 'record_components' button
self.assertTrue(picking_receipt.display_action_record_components)
# Check the created manufacturing order
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
self.assertEqual(len(mo), 1)
self.assertEqual(len(mo.picking_ids), 0)
wh = picking_receipt.picking_type_id.warehouse_id
self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
self.assertFalse(mo.picking_type_id.active)
lot_comp2 = self.env['stock.production.lot'].create({
'name': 'lot_comp2',
'product_id': self.comp2.id,
'company_id': self.env.company.id,
})
serials_finished = []
serials_comp1 = []
for i in range(todo_nb):
serials_finished.append(self.env['stock.production.lot'].create({
'name': 'serial_fin_%s' % i,
'product_id': self.finished_product.id,
'company_id': self.env.company.id,
}))
serials_comp1.append(self.env['stock.production.lot'].create({
'name': 'serials_comp1_%s' % i,
'product_id': self.comp1_sn.id,
'company_id': self.env.company.id,
}))
for i in range(todo_nb):
action = picking_receipt.action_record_components()
mo = self.env['mrp.production'].browse(action['res_id'])
mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
mo_form.lot_producing_id = serials_finished[i]
with mo_form.move_line_raw_ids.edit(0) as ml:
self.assertEqual(ml.product_id, self.comp1_sn)
ml.lot_id = serials_comp1[i]
with mo_form.move_line_raw_ids.edit(1) as ml:
self.assertEqual(ml.product_id, self.comp2)
ml.lot_id = lot_comp2
mo = mo_form.save()
mo.subcontracting_record_component()
# We should not be able to call the 'record_components' button
self.assertFalse(picking_receipt.display_action_record_components)
picking_receipt.button_validate()
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("state"), ['done'] * todo_nb)
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), todo_nb)
self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("qty_produced"), [1] * todo_nb)
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
self.assertEqual(avail_qty_comp1, -todo_nb)
self.assertEqual(avail_qty_comp2, -todo_nb)
self.assertEqual(avail_qty_finished, todo_nb)
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mrp_production_subcontracting_form_view" model="ir.ui.view">
<field name="name">mrp.production.subcontracting.form.view</field>
<field name="model">mrp.production</field>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
<field name="arch" type="xml">
<xpath expr="//header" position="replace">
<field name="state" invisible="1"/>
</xpath>
<xpath expr="//div[@name='button_box']" position="replace"/>
<xpath expr="//group[@name='group_extra_info']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//page[@name='operations']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='lot_producing_id']" position="attributes">
<attribute name="attrs">{'invisible': [('product_tracking', 'in', ('none', False))], 'required': [('product_tracking', 'not in', ('none', False))]}</attribute>
</xpath>
<xpath expr="//page[@name='miscellaneous']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='bom_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//button[@name='action_generate_serial']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='move_raw_ids']" position="attributes">
<attribute name="invisible">1</attribute>
<attribute name="readonly">1</attribute>
</xpath>
<xpath expr="//field[@name='move_raw_ids']" position="after">
<field name="move_line_raw_ids" force_save="1"
context="{'tree_view_ref': 'mrp_subcontracting.mrp_subcontracting_stock_move_line_tree_view', 'default_company_id': company_id, 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id}"
/>
</xpath>
<xpath expr="//sheet" position="inside">
<footer>
<button name="subcontracting_record_component" attrs="{'invisible': ['|', ('state', '=', 'to_close'), ('qty_producing', '&lt;', 0.001)]}" string="Continue" type="object" class="oe_highlight"/>
<button name="subcontracting_record_component" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Record Production" type="object" class="oe_highlight"/>
<button string="Discard" special="cancel" />
</footer>
</xpath>
</field>
</record>
</odoo>
@@ -1,41 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mrp_subcontracting_stock_move_line_tree_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.stock.move.line.tree.view</field>
<field name="model">stock.move.line</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="company_id" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="owner_id" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="package_id" invisible="1"/>
<field name="result_package_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="state" invisible="1"/>
<!-- Don't put move_id here to avoid that the framework send falsy move_id -->
<field name="id" invisible="1"/>
<field name="product_id" required="1"/>
<field name="lot_id"
attrs="{'invisible': [('tracking', 'not in', ('serial', 'lot'))], 'required': [('tracking', 'in', ('serial', 'lot'))]}"
context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
<field name="product_uom_qty" readonly="1" force_save="1"/>
<field name="qty_done"/>
<field name="product_uom_id" groups="uom.group_uom"/>
</tree>
</field>
</record>
<record id="mrp_subcontracting_move_form_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.move.form.view</field>
<field name="model">stock.move</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<form create="0" delete="0">
<header>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<field name="product_uom_category_id" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="product_id" invisible="1"/>
<field name="finished_lots_exist" invisible="1"/>
<field name="state" invisible="1"/>
<field name="move_line_ids" context="{'default_product_id': product_id}" attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}">
<field name="sequence" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="picking_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="has_tracking" invisible="1"/>
<field name="product_uom_qty" invisible="1"/>
<group>
<field name="order_finished_lot_ids" widget="many2many_tags"/>
<field name="product_uom" groups="uom.group_uom"/>
<field name="quantity_done" string="Total Consumed" readonly="1"/>
</group>
<field name="move_line_ids"
attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}"
context="{'default_product_uom_id': product_uom, 'default_picking_id': picking_id, 'default_move_id': id, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_company_id': company_id}">
<tree editable="bottom" decoration-muted="state in ('done', 'cancel')">
<field name="company_id" invisible="1"/>
<field name="state" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="product_id" readonly="1"/>
<field name="product_uom_id" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="picking_id" invisible="1"/>
<field name="move_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="product_id" readonly="1" force_save="1"/>
<field name="qty_done"/>
<field name="lot_id" context="{'default_product_id': product_id}"/>
<field name="lot_id" attrs="{'column_invisible':[('parent.has_tracking', 'not in', ('serial', 'lot'))], 'required': [('tracking', 'in', ('serial', 'lot'))]}" context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
<!-- <record id="mrp_subcontracting_move_tree_view" model="ir.ui.view"> -->
<!-- <field name="name">mrp.subcontracting.move.tree.view</field> -->
<!-- <field name="model">stock.move</field> -->
<!-- <field name="priority">1000</field> -->
<!-- <field name="mode">primary</field> -->
<!-- <field name="inherit_id" ref="mrp.view_stock_move_raw_tree"/> -->
<!-- <field name="arch" type="xml"> -->
<!-- <xpath expr="//tree" position="attributes"> -->
<!-- <attribute name="create">0</attribute> -->
<!-- <attribute name="delete">0</attribute> -->
<!-- </xpath> -->
<!-- </field> -->
<!-- </record> -->
<record id="mrp_subcontracting_move_tree_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.move.tree.view</field>
<field name="model">stock.move</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<tree delete="0" create="0" decoration-muted="is_done" decoration-warning="quantity_done - product_uom_qty &gt; 0.0001" decoration-success="not is_done and quantity_done - product_uom_qty &lt; 0.0001">
<field name="company_id" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="name" invisible="1"/>
<field name="unit_factor" invisible="1"/>
<field name="date" invisible="1"/>
<field name="date_expected" invisible="1"/>
<field name="picking_type_id" invisible="1"/>
<field name="has_tracking" invisible="1"/>
<field name="operation_id" invisible="1"/>
<field name="is_done" invisible="1"/>
<field name="bom_line_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="warehouse_id" invisible="1"/>
<field name="product_uom_qty" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="state" invisible="1" force_save="1"/>
<field name="raw_material_production_id" invisible="1"/>
<field name="product_id" required="1"/>
<field name="order_finished_lot_ids" widget="many2many_tags"/>
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)]}" string="Reserved"/>
<field name="quantity_done" string="Consumed" readonly="1"/>
<field name="product_uom" groups="uom.group_uom"/>
</tree>
</field>
</record>
</odoo>
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_product_produce
from . import stock_picking_return
+1
View File
@@ -1093,6 +1093,7 @@ class StockMove(models.Model):
'location_id': self.location_id.id,
'location_dest_id': location_dest_id,
'picking_id': self.picking_id.id,
'company_id': self.company_id.id,
}
if quantity:
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
+1
View File
@@ -175,6 +175,7 @@
<field name="priority">1000</field>
<field name="arch" type="xml">
<form string="Move Detail">
<field name="sequence" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="state" invisible="1"/>
<field name="location_id" invisible="1"/>