Skip to content
Snippets Groups Projects
Commit d1513739 authored by Arnold Moyaux's avatar Arnold Moyaux
Browse files

[REF] mrp_subcontracting: views v2

- nothing tracked
The "register components" button shouldn't appear, the receipt looks normal.
- finished products tracked
The "register components" button shouldn't appear, the receipt looks normal.
- finished products not tracked and components tracked
The "register components" button appear and the burger redirects to "Produce" wizard. The user should record all the productions before being able to access SML through the burger wizard. He cannot force any SML for subcontracted products with tracked components before having recorded all the productions.
- finished products and components tracked
The "register components" button appear and the burger redirects to "Produce" wizard. The user should record all the productions before being able to access SML through the burger wizard. He cannot force any SML for subcontracted products with tracked components before having recorded all the productions.
parent 18612003
Branches
Tags
No related merge requests found
......@@ -3,59 +3,98 @@
from collections import defaultdict
from odoo import fields, models
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(copy=False)
subcontract_components_ids = fields.One2many('stock.move.line',
compute='_compute_subcontract_move_line_ids',
inverse='_inverse_subcontract_move_line_ids',
string='Subcontracted Components', readonly=False)
is_subcontract = fields.Boolean('The move is a subcontract receipt', copy=False)
show_subcontracting_details_visible = fields.Boolean(
compute='_compute_show_subcontracting_details_visible'
)
def action_show_details(self):
action = super(StockMove, self).action_show_details()
action['context'].update({
'show_lots_m2o': True,
'show_lots_text': False,
})
return action
def _compute_subcontract_move_line_ids(self):
def _compute_show_subcontracting_details_visible(self):
""" Compute if the action button in order to see moves raw is visible """
for move in self:
if move.is_subcontract:
move.subcontract_components_ids = move.move_orig_ids.production_id.move_raw_ids.move_line_ids
if move.is_subcontract and move._has_tracked_subcontract_components() and\
not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding):
move.show_subcontracting_details_visible = True
else:
move.show_subcontracting_details_visible = False
def _inverse_subcontract_move_line_ids(self):
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 move.is_subcontract:
(move.move_orig_ids.production_id.move_raw_ids.move_line_ids - move.subcontract_components_ids).unlink()
if not move.is_subcontract:
continue
if not move._has_tracked_subcontract_components():
continue
move.show_details_visible = True
return res
def _get_subcontract_bom(self):
self.ensure_one()
bom = 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,
)
return bom
def write(self, values):
""" If the initial demand is updated then also update the linked
subcontract order to the new quantity.
"""
res = super(StockMove, self).write(values)
if 'product_uom_qty' in values:
self.filtered(lambda m: m.is_subcontract and
m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty()
return res
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:
production = self.move_orig_ids.production_id
rounding = self.product_uom.rounding
production = self.move_orig_ids.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:
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
action['context'] = dict(
active_id=production.id,
default_subcontract_move_id=self.id
)
return action
action = super(StockMove, self).action_show_details()
if self.is_subcontract:
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.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_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 not move.picking_id:
if not move.picking_id or self.env.context.get('do_not_create_subcontract_order'):
continue
bom = move._get_subcontract_bom()
if not bom:
......@@ -69,6 +108,46 @@ class StockMove(models.Model):
picking._subcontracted_produce(subcontract_details)
return super(StockMove, self)._action_confirm(merge=merge, merge_into=merge_into)
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(
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,
)
return bom
def _has_tracked_subcontract_components(self):
self.ensure_one()
return any(m.has_tracking != 'none' for m in self.move_orig_ids.production_id.move_raw_ids)
def _update_subcontract_order_qty(self):
for move in self:
production = move.move_orig_ids.production_id
......
......@@ -7,15 +7,12 @@ from odoo import models
class StockMoveLine(models.Model):
_inherit = 'stock.move.line'
def create(self, values):
records = super(StockMoveLine, self).create(values)
records.filtered(lambda ml: ml.move_id.is_subcontract).move_id._check_overprocessed_subcontract_qty()
return records
def write(self, values):
move_lines_subcontracted = self.filtered(lambda ml: ml.move_id.is_subcontract)
if not move_lines_subcontracted:
return super(StockMoveLine, self).write(values)
for ml in move_lines_subcontracted:
candidates = ml.move_id.move_orig_ids.move_line_ids
candidate = candidates.filtered(lambda c:
c.qty_done == ml.qty_done and ml.product_uom_qty == c.product_uom_qty and
ml.product_uom_id == c.product_uom_id and c.lot_id == ml.lot_id)
candidate = candidate and candidate[0] or self.env['stock.move.line']
candidate.write(values)
return super(StockMoveLine, self).write(values)
res = super(StockMoveLine, self).write(values)
self.filtered(lambda ml: ml.move_id.is_subcontract).move_id._check_overprocessed_subcontract_qty()
return res
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import api, fields, models
......@@ -17,10 +19,9 @@ class StockPicking(models.Model):
continue
if not picking._is_subcontract():
continue
# Hide if no move is tracked
# Hide if no components are track
subcontracted_productions = picking._get_subcontracted_productions()
subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
subcontracted_moves |= subcontracted_productions.mapped('move_finished_ids')
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
continue
# Hide if the production is to close
......@@ -32,11 +33,51 @@ class StockPicking(models.Model):
# Action methods
# -------------------------------------------------------------------------
def action_done(self):
res = super(StockPicking, self).action_done()
# action_done with an extra move will trigger two methods that
# create/modify a subcontracting order, _action_confirm and a write on
# product_uom_qty. This context will be used by _action_confirm and it
# will not create a subcontract order. Instead the existing order will
# be updated in the stock.move write during the merge move.
res = super(StockPicking, self.with_context(do_not_create_subcontract_order=True)).action_done()
productions = self.env['mrp.production']
for picking in self:
subcontracted_productions = picking._get_subcontracted_productions()
for subcontracted_production in subcontracted_productions:
for move in picking.move_lines:
if not move.is_subcontract:
continue
production = move.move_orig_ids.production_id
if move._has_tracked_subcontract_components():
move.move_orig_ids.move_line_ids.unlink()
move_finished_ids = move.move_orig_ids
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:
for move_line in move.move_line_ids:
produce = self.env['mrp.product.produce'].create({
'production_id': production.id,
'qty_producing': move_line.qty_done,
'product_uom_id': move_line.product_uom_id.id,
'finished_lot_id': move_line.lot_id.id,
'consumption': 'strict',
})
produce._generate_produce_lines()
produce._record_production()
productions |= production
for subcontracted_production in productions:
subcontracted_production.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'))
production_moves = subcontracted_production.move_raw_ids | subcontracted_production.move_finished_ids
production_moves.write({'date': minimum_date - timedelta(seconds=1)})
production_moves.move_line_ids.write({'date': minimum_date - timedelta(seconds=1)})
return res
def action_record_components(self):
......@@ -98,10 +139,3 @@ 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()
# Only skip the produce wizard if no moves is tracked
moves = mo.mapped('move_raw_ids') + mo.mapped('move_finished_ids')
if any(move.has_tracking != 'none' for move in moves):
continue
for m in moves:
m.quantity_done = m.product_uom_qty
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import fields, models, _
class StockWarehouse(models.Model):
......@@ -136,4 +136,3 @@ class StockWarehouse(models.Model):
def _get_subcontracting_location(self):
return self.company_id.subcontracting_location_id
......@@ -116,7 +116,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
# Pickings should directly be created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(len(mo.picking_ids), 1)
self.assertEquals(mo.state, 'to_close')
self.assertEquals(mo.state, 'confirmed')
self.assertEqual(len(mo.picking_ids.move_lines), 2)
picking = mo.picking_ids
......@@ -180,7 +180,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
# Pickings should directly be created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEquals(mo.state, 'to_close')
self.assertEquals(mo.state, 'confirmed')
picking_delivery = mo.picking_ids
self.assertEqual(len(picking_delivery), 1)
......@@ -244,7 +244,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
# Pickings should directly be created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEquals(mo.state, 'to_close')
self.assertEquals(mo.state, 'confirmed')
picking_delivery = mo.picking_ids
self.assertFalse(picking_delivery)
......@@ -462,24 +462,36 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
self.assertEqual(move_comp2.quantity_done, 5.0)
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, 5.0)
self.assertEqual(move_finished.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT F1')
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
})
details_operation_form = Form(picking_receipt.move_lines, view=self.env.ref('mrp_subcontracting.view_stock_move_operations_inherit_mrp_subcontracting'))
details_operation_form = Form(picking_receipt.move_lines, view=self.env.ref('stock.view_stock_move_operations'))
for i in range(len(details_operation_form._values['move_line_ids'])):
with details_operation_form.move_line_ids.edit(i) as ml:
if ml._values['qty_done']:
ml.lot_id = corrected_final_lot
for i in range(len(details_operation_form._values['subcontract_components_ids'])):
with details_operation_form.subcontract_components_ids.edit(i) as sc:
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)
details_subcontract_moves_form = Form(move_raw_comp_1, view=self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view'))
for i in range(len(details_subcontract_moves_form._values['move_line_ids'])):
with details_subcontract_moves_form.move_line_ids.edit(i) as sc:
if sc._values['qty_done']:
sc.lot_produced_ids.remove(index=0)
sc.lot_produced_ids.add(corrected_final_lot)
details_operation_form.save()
details_subcontract_moves_form.save()
details_subcontract_moves_form = Form(move_raw_comp_2, view=self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view'))
for i in range(len(details_subcontract_moves_form._values['move_line_ids'])):
with details_subcontract_moves_form.move_line_ids.edit(i) as sc:
if sc._values['qty_done']:
sc.lot_produced_ids.remove(index=0)
sc.lot_produced_ids.add(corrected_final_lot)
details_subcontract_moves_form.save()
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_produced_ids.name, 'LOT F2')
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_produced_ids.name, 'LOT F2')
......@@ -590,7 +602,7 @@ class TestSubcontractingTracking(TransactionCase):
self.assertFalse(picking_receipt.display_action_record_components)
picking_receipt.move_lines.quantity_done = 1
picking_receipt.move_lines.move_line_ids.lot_name = 'lot00001test'
picking_receipt.move_lines.move_line_ids.lot_id = lot_id.id
picking_receipt.button_validate()
self.assertEquals(mo.state, 'done')
......
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_stock_move_operations_inherit_mrp_subcontracting" model="ir.ui.view">
<field name="name">stock.move.operations.form.inherit.mrp.subcontracting</field>
<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="inherit_id" ref="stock.view_stock_move_operations"/>
<field name="priority">1000</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='move_line_ids']" position="after">
<field name="is_subcontract" invisible="1"/>
<h2 attrs="{'invisible': [('is_subcontract', '!=', True)]}">Components</h2>
<field name="subcontract_components_ids" attrs="{'invisible': [('is_subcontract', '!=', True)], 'readonly': ['|', ('state', '=', 'cancel'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}">
<tree editable="bottom">
<field name="tracking" invisible="1"/>
<field name="product_id" readonly="1"/>
<field name="lot_produced_ids" widget="many2many_tags" context="{'default_product_id': parent.product_id}"/>
<field name="qty_done"/>
<field name="lot_id"/>
</tree>
</field>
</xpath>
<xpath expr="//field[@name='move_line_ids']" position="before">
<h2 attrs="{'invisible': [('is_subcontract', '!=', True)]}">Subcontracted Product</h2>
<form create="0" delete="0">
<sheet>
<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'])]}">
<tree editable="bottom" decoration-muted="state in ('done', 'cancel')">
<field name="state" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="product_id" readonly="1"/>
<field name="lot_produced_ids"
widget="many2many_tags"
context="{'default_product_id': parent.product_id}"
attrs="{'column_invisible': [('parent.finished_lots_exist', '!=', True)]}"
/>
<field name="qty_done"/>
<field name="lot_id" context="{'default_product_id': product_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>
......
......@@ -9,6 +9,11 @@
<field name="display_action_record_components" invisible="1" />
<button name="action_record_components" class="oe_highlight" attrs="{'invisible': [('display_action_record_components', '=', False)]}" string="Record components" type="object"/>
</xpath>
<xpath expr="//field[@name='move_ids_without_package']//tree//button[@name='action_show_details']" position="after">
<field name="show_subcontracting_details_visible" invisible="1"/>
<button name="action_show_subcontract_details" string="Register components for subcontracted product" type="object" icon="fa-sitemap"
attrs="{'invisible': [('show_subcontracting_details_visible', '=', False)]}"/>
</xpath>
</field>
</record>
</odoo>
......
......@@ -14,6 +14,21 @@ class MrpProductProduce(models.TransientModel):
action['context'] = dict(action['context'], default_subcontract_move_id=self.subcontract_move_id.id)
return action
def _generate_produce_lines(self):
""" When the wizard is called in backend, the onchange that create the
produce lines is not trigger. This method generate them and is used with
_record_production to appropriately set the lot_produced_id and
appropriately create raw stock move lines.
"""
self.ensure_one()
moves = (self.move_raw_ids | self.move_finished_ids).filtered(
lambda move: move.state not in ('done', 'cancel')
)
for move in moves:
qty_to_consume = self._prepare_component_quantity(move, self.qty_producing)
line_values = self._generate_lines_values(move, qty_to_consume)
self.env['mrp.product.produce.line'].create(line_values)
def _update_finished_move(self):
""" After producing, set the move line on the subcontract picking. """
res = super(MrpProductProduce, self)._update_finished_move()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment