mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
[14.0][ADD] mrp_component_operation
This commit is contained in:
0
mrp_component_operation/README.rst
Normal file
0
mrp_component_operation/README.rst
Normal file
4
mrp_component_operation/__init__.py
Normal file
4
mrp_component_operation/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com)
|
||||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
||||||
|
from . import models
|
||||||
|
from . import wizards
|
||||||
21
mrp_component_operation/__manifest__.py
Normal file
21
mrp_component_operation/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com)
|
||||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "MRP Components Operations",
|
||||||
|
"version": "14.0.1.0.0",
|
||||||
|
"author": "ForgeFlow, Odoo Community Association (OCA)",
|
||||||
|
"summary": "Allows to operate the components from a MO",
|
||||||
|
"website": "https://github.com/OCA/manufacture",
|
||||||
|
"category": "Manufacturing",
|
||||||
|
"depends": ["mrp", "stock_move_forced_lot"],
|
||||||
|
"data": [
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"views/mrp_component_operation_views.xml",
|
||||||
|
"views/mrp_production_views.xml",
|
||||||
|
"views/stock_view.xml",
|
||||||
|
"wizards/mrp_component_operate_wizard.xml",
|
||||||
|
],
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"installable": True,
|
||||||
|
}
|
||||||
3
mrp_component_operation/models/__init__.py
Normal file
3
mrp_component_operation/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from . import mrp_component_operation
|
||||||
|
from . import mrp_production
|
||||||
|
from . import stock_location_route
|
||||||
60
mrp_component_operation/models/mrp_component_operation.py
Normal file
60
mrp_component_operation/models/mrp_component_operation.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com)
|
||||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class MrpComponentOperation(models.Model):
|
||||||
|
_name = "mrp.component.operation"
|
||||||
|
_description = "Component Operation"
|
||||||
|
|
||||||
|
name = fields.Char(help="Component Operation Reference", required=True)
|
||||||
|
|
||||||
|
source_location_id = fields.Many2one(
|
||||||
|
"stock.location",
|
||||||
|
"Source Location",
|
||||||
|
help="The Location where the components are.",
|
||||||
|
)
|
||||||
|
|
||||||
|
source_route_id = fields.Many2one(
|
||||||
|
comodel_name="stock.location.route",
|
||||||
|
string="Source Route",
|
||||||
|
help="The Route used to pick the components.",
|
||||||
|
domain=[("mo_component_selectable", "=", True)],
|
||||||
|
)
|
||||||
|
|
||||||
|
destination_location_id = fields.Many2one(
|
||||||
|
"stock.location",
|
||||||
|
"Destination Location",
|
||||||
|
help="The Location where the components are going to be transferred.",
|
||||||
|
)
|
||||||
|
|
||||||
|
destination_route_id = fields.Many2one(
|
||||||
|
comodel_name="stock.location.route",
|
||||||
|
string="Destination Route",
|
||||||
|
help="The Route used to transfer the components to the destination location.",
|
||||||
|
domain=[("mo_component_selectable", "=", True)],
|
||||||
|
)
|
||||||
|
|
||||||
|
scrap_location_id = fields.Many2one(
|
||||||
|
"stock.location",
|
||||||
|
"Scrap Location",
|
||||||
|
)
|
||||||
|
|
||||||
|
incoming_operation = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("no", "No"),
|
||||||
|
("replace", "Pick Component from Source Route"),
|
||||||
|
],
|
||||||
|
default="no",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
outgoing_operation = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("no", "No"),
|
||||||
|
("move", "Move to Destination Location"),
|
||||||
|
("scrap", "Make a Scrap"),
|
||||||
|
],
|
||||||
|
default="no",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
25
mrp_component_operation/models/mrp_production.py
Normal file
25
mrp_component_operation/models/mrp_production.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com)
|
||||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
||||||
|
from odoo import _, models
|
||||||
|
|
||||||
|
|
||||||
|
class MrpProduction(models.Model):
|
||||||
|
|
||||||
|
_inherit = "mrp.production"
|
||||||
|
|
||||||
|
def button_operate_components(self):
|
||||||
|
return {
|
||||||
|
"name": _("Operate Component"),
|
||||||
|
"view_mode": "form",
|
||||||
|
"res_model": "mrp.component.operate",
|
||||||
|
"view_id": self.env.ref(
|
||||||
|
"mrp_component_operation.view_mrp_component_operate_form"
|
||||||
|
).id,
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"context": {
|
||||||
|
"default_mo_id": self.id,
|
||||||
|
"product_ids": self.move_raw_ids.move_line_ids.product_id.mapped("id"),
|
||||||
|
"lot_ids": self.move_raw_ids.move_line_ids.lot_id.mapped("id"),
|
||||||
|
},
|
||||||
|
"target": "new",
|
||||||
|
}
|
||||||
9
mrp_component_operation/models/stock_location_route.py
Normal file
9
mrp_component_operation/models/stock_location_route.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com)
|
||||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class StockLocationRoute(models.Model):
|
||||||
|
_inherit = "stock.location.route"
|
||||||
|
|
||||||
|
mo_component_selectable = fields.Boolean(string="Selectable on MO Components")
|
||||||
1
mrp_component_operation/readme/CONTRIBUTORS.rst
Normal file
1
mrp_component_operation/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* David Jiménez <david.jimenez@forgeflow.com>
|
||||||
2
mrp_component_operation/readme/DESCRIPTION.rst
Normal file
2
mrp_component_operation/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
This module allows to operate the components from a MO, being able to move it to another location or making an scrap
|
||||||
|
and take another from a desired location and link it to the current MO.
|
||||||
6
mrp_component_operation/readme/USAGE.rst
Normal file
6
mrp_component_operation/readme/USAGE.rst
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Go to Manufacture -> Configuration -> Settings -> Component Operations.
|
||||||
|
Create a new Operation, select the desired outgoing operation and the desired incoming operation.
|
||||||
|
Then fill the Locations and Routes (check in the routes that are selectable for mo operations in the route page).
|
||||||
|
Once saved, go to the Manufacture Order and click on the button "Operate Component".
|
||||||
|
Select the desired product (lot if needed) and the operation wanted.
|
||||||
|
When Confirmed, in the MO will be linked the pickings/scraps done.
|
||||||
5
mrp_component_operation/security/ir.model.access.csv
Normal file
5
mrp_component_operation/security/ir.model.access.csv
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_mrp_component_operation_user,mrp.component.operation.user,model_mrp_component_operation,mrp.group_mrp_user,1,0,0,0
|
||||||
|
access_mrp_component_operation_manager,mrp.component.operation.manager,model_mrp_component_operation,mrp.group_mrp_manager,1,1,1,1
|
||||||
|
access_mrp_component_operate_user,mrp.component.operate.user,model_mrp_component_operate,mrp.group_mrp_user,1,0,0,0
|
||||||
|
access_mrp_component_operate_manager,mrp.component.operate.manager,model_mrp_component_operate,mrp.group_mrp_manager,1,1,1,1
|
||||||
|
1
mrp_component_operation/tests/__init__.py
Normal file
1
mrp_component_operation/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_component_operate
|
||||||
322
mrp_component_operation/tests/test_component_operate.py
Normal file
322
mrp_component_operation/tests/test_component_operate.py
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
from odoo import api
|
||||||
|
from odoo.tests import common
|
||||||
|
|
||||||
|
|
||||||
|
class TestComponentOperation(common.SavepointCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestComponentOperation, cls).setUpClass()
|
||||||
|
cls.user_admin = cls.env.ref("base.user_admin")
|
||||||
|
cls.env = api.Environment(cls.cr, cls.user_admin.id, {})
|
||||||
|
cls.ProcurementGroup = cls.env["procurement.group"]
|
||||||
|
cls.MrpProduction = cls.env["mrp.production"]
|
||||||
|
cls.env.user.company_id.manufacturing_lead = 0
|
||||||
|
cls.env.user.tz = False # Make sure there's no timezone in user
|
||||||
|
|
||||||
|
cls.picking_type = cls.env["stock.picking.type"].search(
|
||||||
|
[
|
||||||
|
("code", "=", "mrp_operation"),
|
||||||
|
("sequence_id.company_id", "=", cls.env.user.company_id.id),
|
||||||
|
],
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
cls.product1 = cls.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "TEST Parent",
|
||||||
|
"route_ids": [
|
||||||
|
(6, 0, [cls.env.ref("mrp.route_warehouse0_manufacture").id])
|
||||||
|
],
|
||||||
|
"type": "product",
|
||||||
|
"produce_delay": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cls.product2 = cls.env["product.product"].create(
|
||||||
|
{"name": "TEST Child", "type": "product"}
|
||||||
|
)
|
||||||
|
cls.product3 = cls.env["product.product"].create(
|
||||||
|
{"name": "TEST Child Serial", "type": "product", "tracking": "serial"}
|
||||||
|
)
|
||||||
|
cls.bom = cls.env["mrp.bom"].create(
|
||||||
|
{
|
||||||
|
"product_id": cls.product1.id,
|
||||||
|
"product_tmpl_id": cls.product1.product_tmpl_id.id,
|
||||||
|
"type": "normal",
|
||||||
|
"bom_line_ids": [
|
||||||
|
(0, 0, {"product_id": cls.product2.id, "product_qty": 2}),
|
||||||
|
(0, 0, {"product_id": cls.product3.id, "product_qty": 1}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cls.stock_picking_type = cls.env.ref("stock.picking_type_out")
|
||||||
|
cls.warehouse = cls.env["stock.warehouse"].search(
|
||||||
|
[("company_id", "=", cls.env.user.company_id.id)], limit=1
|
||||||
|
)
|
||||||
|
cls.warehouse.manufacture_steps = "pbm"
|
||||||
|
cls.ressuply_loc1 = cls.warehouse.lot_stock_id
|
||||||
|
cls.source_location = cls.env.ref("stock.stock_location_stock")
|
||||||
|
cls.destination_location = cls.env.ref("stock.stock_location_output")
|
||||||
|
stock_location_locations_virtual = cls.env["stock.location"].create(
|
||||||
|
{"name": "Virtual Locations", "usage": "view", "posz": 1}
|
||||||
|
)
|
||||||
|
cls.scrapped_location = cls.env["stock.location"].create(
|
||||||
|
{
|
||||||
|
"name": "Scrapped",
|
||||||
|
"location_id": stock_location_locations_virtual.id,
|
||||||
|
"scrap_location": True,
|
||||||
|
"usage": "inventory",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cls.source_route = cls.env["stock.location.route"].create(
|
||||||
|
{
|
||||||
|
"name": "Source Route",
|
||||||
|
"mo_component_selectable": True,
|
||||||
|
"sequence": 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.destination_route = cls.env["stock.location.route"].create(
|
||||||
|
{
|
||||||
|
"name": "Destination Route",
|
||||||
|
"mo_component_selectable": True,
|
||||||
|
"sequence": 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.env["stock.rule"].create(
|
||||||
|
{
|
||||||
|
"name": "Transfer",
|
||||||
|
"route_id": cls.source_route.id,
|
||||||
|
"location_src_id": cls.ressuply_loc1.id,
|
||||||
|
"location_id": cls.source_location.id,
|
||||||
|
"action": "pull",
|
||||||
|
"picking_type_id": cls.warehouse.int_type_id.id,
|
||||||
|
"procure_method": "make_to_stock",
|
||||||
|
"warehouse_id": cls.warehouse.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.env["stock.rule"].create(
|
||||||
|
{
|
||||||
|
"name": "Transfer 2",
|
||||||
|
"route_id": cls.destination_route.id,
|
||||||
|
"location_src_id": cls.source_location.id,
|
||||||
|
"location_id": cls.destination_location.id,
|
||||||
|
"action": "pull",
|
||||||
|
"picking_type_id": cls.warehouse.int_type_id.id,
|
||||||
|
"procure_method": "make_to_stock",
|
||||||
|
"warehouse_id": cls.warehouse.id,
|
||||||
|
"propagate_warehouse_id": cls.warehouse.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.operation_scrap_replace = cls.env["mrp.component.operation"].create(
|
||||||
|
{
|
||||||
|
"name": "Operation Scrap and Replace",
|
||||||
|
"incoming_operation": "replace",
|
||||||
|
"outgoing_operation": "scrap",
|
||||||
|
"source_location_id": cls.source_location.id,
|
||||||
|
"source_route_id": cls.source_route.id,
|
||||||
|
"scrap_location_id": cls.scrapped_location.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.operation_no = cls.env["mrp.component.operation"].create(
|
||||||
|
{
|
||||||
|
"name": "Operation Scrap and Replace",
|
||||||
|
"incoming_operation": "no",
|
||||||
|
"outgoing_operation": "no",
|
||||||
|
"source_location_id": cls.source_location.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.operation_move_replace = cls.env["mrp.component.operation"].create(
|
||||||
|
{
|
||||||
|
"name": "Operation Move",
|
||||||
|
"incoming_operation": "replace",
|
||||||
|
"outgoing_operation": "move",
|
||||||
|
"source_location_id": cls.source_location.id,
|
||||||
|
"source_route_id": cls.source_route.id,
|
||||||
|
"destination_location_id": cls.destination_location.id,
|
||||||
|
"destination_route_id": cls.destination_route.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_01_scrap_and_replace(self):
|
||||||
|
nb_product_todo = 5
|
||||||
|
serials_p2 = []
|
||||||
|
for i in range(nb_product_todo):
|
||||||
|
serials_p2.append(
|
||||||
|
self.env["stock.production.lot"].create(
|
||||||
|
{
|
||||||
|
"name": f"lot_consumed_2_{i}",
|
||||||
|
"product_id": self.product3.id,
|
||||||
|
"company_id": self.env.company.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.env["stock.quant"]._update_available_quantity(
|
||||||
|
self.product3, self.ressuply_loc1, 1, lot_id=serials_p2[-1]
|
||||||
|
)
|
||||||
|
self.env["stock.quant"]._update_available_quantity(
|
||||||
|
self.product2, self.ressuply_loc1, 10
|
||||||
|
)
|
||||||
|
mo = self.MrpProduction.create(
|
||||||
|
{
|
||||||
|
"bom_id": self.bom.id,
|
||||||
|
"product_id": self.product1.id,
|
||||||
|
"product_qty": 2,
|
||||||
|
"product_uom_id": self.product1.uom_id.id,
|
||||||
|
"date_deadline": "2023-01-01 15:00:00",
|
||||||
|
"date_planned_start": "2023-01-01 15:00:00",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mo._onchange_move_raw()
|
||||||
|
mo._onchange_move_finished()
|
||||||
|
mo.action_confirm()
|
||||||
|
mo.action_assign()
|
||||||
|
self.assertEqual(mo.move_raw_ids[0].move_line_ids.product_uom_qty, 4)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 2)
|
||||||
|
wizard = self.env["mrp.component.operate"].create(
|
||||||
|
{
|
||||||
|
"product_id": mo.move_raw_ids[1].product_id.id,
|
||||||
|
"lot_id": mo.move_raw_ids[1].move_line_ids[0].lot_id.id,
|
||||||
|
"operation_id": self.operation_scrap_replace.id,
|
||||||
|
"mo_id": mo.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(wizard.product_qty, 1)
|
||||||
|
self.assertEqual(wizard.product_id, self.product3)
|
||||||
|
lot = wizard.lot_id
|
||||||
|
wizard.action_operate_component()
|
||||||
|
self.assertEqual(len(mo.picking_ids), 1)
|
||||||
|
self.assertEqual(mo.scrap_ids.product_id, self.product3)
|
||||||
|
self.assertEqual(mo.scrap_ids.lot_id, lot)
|
||||||
|
self.assertEqual(mo.scrap_ids.state, "done")
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 1)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 0)
|
||||||
|
self.assertEqual(mo.picking_ids.product_id, self.product3)
|
||||||
|
mo.picking_ids.action_assign()
|
||||||
|
mo.picking_ids.button_validate()
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 1)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 1)
|
||||||
|
|
||||||
|
def test_02_move_and_replace(self):
|
||||||
|
nb_product_todo = 5
|
||||||
|
serials_p2 = []
|
||||||
|
for i in range(nb_product_todo):
|
||||||
|
serials_p2.append(
|
||||||
|
self.env["stock.production.lot"].create(
|
||||||
|
{
|
||||||
|
"name": f"lot_consumed_2_{i}",
|
||||||
|
"product_id": self.product3.id,
|
||||||
|
"company_id": self.env.company.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.env["stock.quant"]._update_available_quantity(
|
||||||
|
self.product3, self.ressuply_loc1, 1, lot_id=serials_p2[-1]
|
||||||
|
)
|
||||||
|
self.env["stock.quant"]._update_available_quantity(
|
||||||
|
self.product2, self.ressuply_loc1, 10
|
||||||
|
)
|
||||||
|
mo = self.MrpProduction.create(
|
||||||
|
{
|
||||||
|
"bom_id": self.bom.id,
|
||||||
|
"product_id": self.product1.id,
|
||||||
|
"product_qty": 1,
|
||||||
|
"product_uom_id": self.product1.uom_id.id,
|
||||||
|
"date_deadline": "2023-01-01 15:00:00",
|
||||||
|
"date_planned_start": "2023-01-01 15:00:00",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mo._onchange_move_raw()
|
||||||
|
mo._onchange_move_finished()
|
||||||
|
mo.action_confirm()
|
||||||
|
mo.action_assign()
|
||||||
|
self.assertEqual(mo.move_raw_ids[0].move_line_ids.product_uom_qty, 2)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 1)
|
||||||
|
wizard = self.env["mrp.component.operate"].create(
|
||||||
|
{
|
||||||
|
"product_id": mo.move_raw_ids[1].product_id.id,
|
||||||
|
"lot_id": mo.move_raw_ids[1].move_line_ids[0].lot_id.id,
|
||||||
|
"operation_id": self.operation_move_replace.id,
|
||||||
|
"mo_id": mo.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(wizard.product_qty, 1)
|
||||||
|
self.assertEqual(wizard.product_id, self.product3)
|
||||||
|
lot = wizard.lot_id
|
||||||
|
wizard.action_operate_component()
|
||||||
|
self.assertEqual(len(mo.picking_ids), 2)
|
||||||
|
self.assertEqual(mo.picking_ids[1].location_dest_id, self.destination_location)
|
||||||
|
self.assertEqual(mo.picking_ids[1].move_lines.product_id, self.product3)
|
||||||
|
self.assertEqual(mo.picking_ids[0].location_dest_id, self.source_location)
|
||||||
|
self.assertEqual(mo.picking_ids[0].move_lines.product_id, self.product3)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 0)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 0)
|
||||||
|
mo.picking_ids[1].action_assign()
|
||||||
|
mo.picking_ids[1].button_validate()
|
||||||
|
self.assertEqual(
|
||||||
|
mo.picking_ids[1].move_line_ids.location_dest_id, self.destination_location
|
||||||
|
)
|
||||||
|
self.assertEqual(mo.picking_ids[1].move_line_ids.lot_id, lot)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 0)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 0)
|
||||||
|
mo.picking_ids[0].action_assign()
|
||||||
|
mo.picking_ids[0].button_validate()
|
||||||
|
self.assertEqual(
|
||||||
|
mo.picking_ids[0].move_line_ids.location_dest_id, self.source_location
|
||||||
|
)
|
||||||
|
self.assertEqual(mo.picking_ids[0].move_line_ids.product_id, self.product3)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 0)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 1)
|
||||||
|
|
||||||
|
def test_03_nothing_and_nothing(self):
|
||||||
|
nb_product_todo = 5
|
||||||
|
serials_p2 = []
|
||||||
|
for i in range(nb_product_todo):
|
||||||
|
serials_p2.append(
|
||||||
|
self.env["stock.production.lot"].create(
|
||||||
|
{
|
||||||
|
"name": f"lot_consumed_2_{i}",
|
||||||
|
"product_id": self.product3.id,
|
||||||
|
"company_id": self.env.company.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.env["stock.quant"]._update_available_quantity(
|
||||||
|
self.product3, self.ressuply_loc1, 1, lot_id=serials_p2[-1]
|
||||||
|
)
|
||||||
|
self.env["stock.quant"]._update_available_quantity(
|
||||||
|
self.product2, self.ressuply_loc1, 10
|
||||||
|
)
|
||||||
|
mo = self.MrpProduction.create(
|
||||||
|
{
|
||||||
|
"bom_id": self.bom.id,
|
||||||
|
"product_id": self.product1.id,
|
||||||
|
"product_qty": 2,
|
||||||
|
"product_uom_id": self.product1.uom_id.id,
|
||||||
|
"date_deadline": "2023-01-01 15:00:00",
|
||||||
|
"date_planned_start": "2023-01-01 15:00:00",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mo._onchange_move_raw()
|
||||||
|
mo._onchange_move_finished()
|
||||||
|
mo.action_confirm()
|
||||||
|
mo.action_assign()
|
||||||
|
self.assertEqual(mo.move_raw_ids[0].move_line_ids.product_uom_qty, 4)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 2)
|
||||||
|
wizard = self.env["mrp.component.operate"].create(
|
||||||
|
{
|
||||||
|
"product_id": mo.move_raw_ids[1].product_id.id,
|
||||||
|
"lot_id": mo.move_raw_ids[1].move_line_ids[0].lot_id.id,
|
||||||
|
"operation_id": self.operation_no.id,
|
||||||
|
"mo_id": mo.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(wizard.product_qty, 1)
|
||||||
|
self.assertEqual(wizard.product_id, self.product3)
|
||||||
|
wizard.action_operate_component()
|
||||||
|
self.assertEqual(len(mo.picking_ids), 0)
|
||||||
|
self.assertEqual(mo.move_raw_ids[0].move_line_ids.product_uom_qty, 4)
|
||||||
|
self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 2)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_mrp_component_operation_form" model="ir.ui.view">
|
||||||
|
<field name="name">view_mrp_component_operation_form</field>
|
||||||
|
<field name="model">mrp.component.operation</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Component Operation">
|
||||||
|
<sheet>
|
||||||
|
<h1>
|
||||||
|
<field name="name" string="Reference" />
|
||||||
|
</h1>
|
||||||
|
<group name="operations" string="Operations">
|
||||||
|
<field name="outgoing_operation" />
|
||||||
|
<field name="incoming_operation" />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
name="locations"
|
||||||
|
string="Locations/Routes"
|
||||||
|
attrs="{'invisible': [('outgoing_operation', '=', 'no'),('incoming_operation', '=', 'no')]}"
|
||||||
|
>
|
||||||
|
<field
|
||||||
|
name="source_location_id"
|
||||||
|
attrs="{'invisible': [('incoming_operation', '!=', 'replace'),('outgoing_operation', '!=', 'scrap')], 'required': ['|',('incoming_operation', '=', 'replace'),('outgoing_operation', '=', 'scrap')]}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="source_route_id"
|
||||||
|
attrs="{'invisible': [('incoming_operation', '!=', 'replace')], 'required': [('incoming_operation', '=', 'replace')]}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="destination_location_id"
|
||||||
|
attrs="{'invisible': [('outgoing_operation', '!=', 'move')], 'required': [('outgoing_operation', '=', 'move')]}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="destination_route_id"
|
||||||
|
attrs="{'invisible': [('outgoing_operation', '!=', 'move')], 'required': [('outgoing_operation', '=', 'move')]}"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="scrap_location_id"
|
||||||
|
attrs="{'invisible': [('outgoing_operation', '!=', 'scrap')], 'required': [('outgoing_operation', '=', 'scrap')]}"
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_menu_mrp_component_operation" model="ir.actions.act_window">
|
||||||
|
<field name="name">Component Operation</field>
|
||||||
|
<field name="res_model">mrp.component.operation</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="menu_mrp_component_operation"
|
||||||
|
name="Component Operations"
|
||||||
|
action="action_menu_mrp_component_operation"
|
||||||
|
parent="mrp.menu_mrp_configuration"
|
||||||
|
sequence="6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</odoo>
|
||||||
18
mrp_component_operation/views/mrp_production_views.xml
Normal file
18
mrp_component_operation/views/mrp_production_views.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="mrp_production_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">mrp.production.form</field>
|
||||||
|
<field name="model">mrp.production</field>
|
||||||
|
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<button name="button_scrap" position="after">
|
||||||
|
<button
|
||||||
|
name="button_operate_components"
|
||||||
|
type="object"
|
||||||
|
string="Operate Components"
|
||||||
|
attrs="{'invisible': [('state', 'in', ('cancel', 'draft', 'done'))]}"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
16
mrp_component_operation/views/stock_view.xml
Normal file
16
mrp_component_operation/views/stock_view.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record
|
||||||
|
id="stock_location_route_form_view_inherit_mrp_component"
|
||||||
|
model="ir.ui.view"
|
||||||
|
>
|
||||||
|
<field name="name">stock.location.route.form - mrp.component</field>
|
||||||
|
<field name="inherit_id" ref="stock.stock_location_route_form_view" />
|
||||||
|
<field name="model">stock.location.route</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='warehouse_selectable']" position="before">
|
||||||
|
<field name="mo_component_selectable" string="Component Operations" />
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1
mrp_component_operation/wizards/__init__.py
Normal file
1
mrp_component_operation/wizards/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import mrp_component_operate
|
||||||
160
mrp_component_operation/wizards/mrp_component_operate.py
Normal file
160
mrp_component_operation/wizards/mrp_component_operate.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com)
|
||||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class MrpComponentOperate(models.Model):
|
||||||
|
_name = "mrp.component.operate"
|
||||||
|
_description = "Component Operate"
|
||||||
|
|
||||||
|
product_id = fields.Many2one("product.product", required=True)
|
||||||
|
|
||||||
|
tracking = fields.Selection(
|
||||||
|
string="Product Tracking", readonly=True, related="product_id.tracking"
|
||||||
|
)
|
||||||
|
|
||||||
|
product_qty = fields.Float(
|
||||||
|
"Quantity", default=1.0, required=True, digits="Product Unit of Measure"
|
||||||
|
)
|
||||||
|
|
||||||
|
lot_id = fields.Many2one("stock.production.lot")
|
||||||
|
|
||||||
|
mo_id = fields.Many2one("mrp.production", ondelete="cascade", required=True)
|
||||||
|
|
||||||
|
operation_id = fields.Many2one("mrp.component.operation", required=True)
|
||||||
|
|
||||||
|
incoming_operation = fields.Selection(
|
||||||
|
related="operation_id.incoming_operation",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
outgoing_operation = fields.Selection(
|
||||||
|
related="operation_id.outgoing_operation",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.onchange("operation_id")
|
||||||
|
def _onchange_operation_id(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.incoming_operation = rec.operation_id.incoming_operation
|
||||||
|
rec.outgoing_operation = rec.operation_id.outgoing_operation
|
||||||
|
|
||||||
|
def _run_incoming_operations(self):
|
||||||
|
res = []
|
||||||
|
if self.incoming_operation == "replace":
|
||||||
|
res = self._run_procurement(
|
||||||
|
self.operation_id.source_route_id, self.operation_id.source_location_id
|
||||||
|
)
|
||||||
|
move = self.mo_id.move_raw_ids.filtered(
|
||||||
|
lambda x: x.product_id == self.product_id
|
||||||
|
)
|
||||||
|
filtered_pickings = self.mo_id.picking_ids.filtered(
|
||||||
|
lambda x: x.location_dest_id == self.operation_id.source_location_id
|
||||||
|
)
|
||||||
|
move.move_orig_ids |= filtered_pickings[
|
||||||
|
len(filtered_pickings) - 1
|
||||||
|
].move_ids_without_package
|
||||||
|
elif self.incoming_operation == "no":
|
||||||
|
res = []
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _run_outgoing_operations(self):
|
||||||
|
res = []
|
||||||
|
if self.outgoing_operation == "scrap":
|
||||||
|
res = self._create_scrap()
|
||||||
|
elif self.outgoing_operation == "move":
|
||||||
|
res = self._run_procurement(
|
||||||
|
self.operation_id.destination_route_id,
|
||||||
|
self.operation_id.destination_location_id,
|
||||||
|
)
|
||||||
|
move = self.mo_id.move_raw_ids.move_line_ids.filtered(
|
||||||
|
lambda x: x.product_id == self.product_id
|
||||||
|
and (x.lot_id == self.lot_id or self.lot_id is False)
|
||||||
|
)
|
||||||
|
if move.product_uom_qty == self.product_qty:
|
||||||
|
move.unlink()
|
||||||
|
else:
|
||||||
|
move.write(
|
||||||
|
{
|
||||||
|
"product_uom_qty": (move.product_uom_qty - self.product_qty),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
move.move_id._recompute_state()
|
||||||
|
elif self.outgoing_operation == "no":
|
||||||
|
res = []
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _create_scrap(self):
|
||||||
|
scrap = self.env["stock.scrap"].create(
|
||||||
|
{
|
||||||
|
"origin": self.mo_id.name,
|
||||||
|
"product_id": self.product_id.id,
|
||||||
|
"lot_id": self.lot_id.id,
|
||||||
|
"scrap_qty": self.product_qty,
|
||||||
|
"product_uom_id": self.product_id.product_tmpl_id.uom_id.id,
|
||||||
|
"location_id": self.operation_id.source_location_id.id,
|
||||||
|
"scrap_location_id": self.operation_id.scrap_location_id.id,
|
||||||
|
"create_date": fields.Datetime.now(),
|
||||||
|
"company_id": self.env.company.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.mo_id.scrap_ids |= scrap
|
||||||
|
scrap.action_validate()
|
||||||
|
return scrap
|
||||||
|
|
||||||
|
def _run_procurement(self, route, dest_location):
|
||||||
|
"""Method called when the user clicks on create picking"""
|
||||||
|
procurements = []
|
||||||
|
errors = []
|
||||||
|
procurement = self._prepare_procurement(route, dest_location)
|
||||||
|
procurements.append(procurement)
|
||||||
|
try:
|
||||||
|
self.env["procurement.group"].run(procurements)
|
||||||
|
except UserError as error:
|
||||||
|
errors.append(error.args[0])
|
||||||
|
if errors:
|
||||||
|
raise UserError("\n".join(errors))
|
||||||
|
return procurements
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_procurement_data(self, route, dest_location):
|
||||||
|
if not route:
|
||||||
|
raise ValidationError(_("No route specified"))
|
||||||
|
procurement_data = {
|
||||||
|
"name": self.mo_id and self.mo_id.name,
|
||||||
|
"group_id": self.mo_id.procurement_group_id,
|
||||||
|
"origin": self.mo_id.name,
|
||||||
|
"date_planned": fields.Datetime.now(),
|
||||||
|
"product_id": self.product_id.id,
|
||||||
|
"product_qty": self.product_qty,
|
||||||
|
"product_uom": self.product_id.product_tmpl_id.uom_id.id,
|
||||||
|
"location_id": dest_location.id,
|
||||||
|
"route_ids": route,
|
||||||
|
"company_id": self.env.company.id,
|
||||||
|
"mrp_production_ids": self.mo_id.id,
|
||||||
|
}
|
||||||
|
if self.lot_id and route != self.operation_id.source_route_id:
|
||||||
|
procurement_data["lot_id"] = self.lot_id.id
|
||||||
|
return procurement_data
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _prepare_procurement(self, route, dest_location):
|
||||||
|
values = self._get_procurement_data(route, dest_location)
|
||||||
|
procurement = self.env["procurement.group"].Procurement(
|
||||||
|
self.product_id,
|
||||||
|
self.product_qty,
|
||||||
|
self.product_id.product_tmpl_id.uom_id,
|
||||||
|
dest_location,
|
||||||
|
values.get("origin"),
|
||||||
|
values.get("origin"),
|
||||||
|
self.env.company,
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
return procurement
|
||||||
|
|
||||||
|
def action_operate_component(self):
|
||||||
|
self._run_outgoing_operations()
|
||||||
|
self._run_incoming_operations()
|
||||||
|
return
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_mrp_component_operate_form" model="ir.ui.view">
|
||||||
|
<field name="name">view_mrp_component_operate_form</field>
|
||||||
|
<field name="model">mrp.component.operate</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Component Operation">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field
|
||||||
|
name="product_id"
|
||||||
|
options="{'no_create': True}"
|
||||||
|
domain="[('id', 'in', context.get('product_ids', []))]"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lot_id"
|
||||||
|
string="Lot/Serial Number"
|
||||||
|
attrs="{'required': [('tracking', '!=', 'none')]}"
|
||||||
|
domain="[('product_id', '=', product_id), ('id', 'in', context.get('lot_ids', []))]"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="product_qty"
|
||||||
|
attrs="{'readonly': ['|',('tracking', '=', 'serial'),('product_id', '=', False)]}"
|
||||||
|
/>
|
||||||
|
<field name="operation_id" />
|
||||||
|
<field name="tracking" invisible="1" />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
name="operations"
|
||||||
|
string="Operations"
|
||||||
|
attrs="{'invisible': [('operation_id', '=', False)]}"
|
||||||
|
>
|
||||||
|
<field name="outgoing_operation" />
|
||||||
|
<field name="incoming_operation" />
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
<footer>
|
||||||
|
<button
|
||||||
|
name="action_operate_component"
|
||||||
|
string="Done"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
/>
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../mrp_component_operation
|
||||||
6
setup/mrp_component_operation/setup.py
Normal file
6
setup/mrp_component_operation/setup.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import setuptools
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
setup_requires=['setuptools-odoo'],
|
||||||
|
odoo_addon=True,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user