From 850b8fece7551c88342ffffc33adb364dc31ed7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luc=C3=ADa=20Echeverr=C3=ADa?= Date: Tue, 7 Jul 2026 14:59:43 +0200 Subject: [PATCH] [18.0][ADD] ina_mp_mrp_repara: Repair management covering statuses, materials, times, and documentation --- ina_mp_mrp_repara/README.rst | 39 +++++ ina_mp_mrp_repara/__init__.py | 2 + ina_mp_mrp_repara/__manifest__.py | 26 +++ ina_mp_mrp_repara/models/__init__.py | 4 + ina_mp_mrp_repara/models/mrp_repair_docu.py | 53 ++++++ .../models/mrp_repair_tiempos.py | 48 ++++++ ina_mp_mrp_repara/models/repair_order.py | 156 ++++++++++++++++++ ina_mp_mrp_repara/models/stock_move.py | 62 +++++++ ina_mp_mrp_repara/pyproject.toml | 3 + .../security/ir.model.access.csv | 4 + .../views/mrp_repair_tiempos_views.xml | 65 ++++++++ .../views/repair_order_views.xml | 143 ++++++++++++++++ ina_mp_mrp_repara/wizard/__init__.py | 1 + ina_mp_mrp_repara/wizard/docu_rma_ver.py | 66 ++++++++ ina_mp_mrp_repara/wizard/docu_rma_ver.xml | 18 ++ 15 files changed, 690 insertions(+) create mode 100644 ina_mp_mrp_repara/README.rst create mode 100644 ina_mp_mrp_repara/__init__.py create mode 100644 ina_mp_mrp_repara/__manifest__.py create mode 100644 ina_mp_mrp_repara/models/__init__.py create mode 100644 ina_mp_mrp_repara/models/mrp_repair_docu.py create mode 100644 ina_mp_mrp_repara/models/mrp_repair_tiempos.py create mode 100644 ina_mp_mrp_repara/models/repair_order.py create mode 100644 ina_mp_mrp_repara/models/stock_move.py create mode 100644 ina_mp_mrp_repara/pyproject.toml create mode 100644 ina_mp_mrp_repara/security/ir.model.access.csv create mode 100644 ina_mp_mrp_repara/views/mrp_repair_tiempos_views.xml create mode 100644 ina_mp_mrp_repara/views/repair_order_views.xml create mode 100644 ina_mp_mrp_repara/wizard/__init__.py create mode 100644 ina_mp_mrp_repara/wizard/docu_rma_ver.py create mode 100644 ina_mp_mrp_repara/wizard/docu_rma_ver.xml diff --git a/ina_mp_mrp_repara/README.rst b/ina_mp_mrp_repara/README.rst new file mode 100644 index 00000000..5f569373 --- /dev/null +++ b/ina_mp_mrp_repara/README.rst @@ -0,0 +1,39 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg + :target: https://opensource.org/licenses/AGPL-3.0 + :alt: License: AGPL-3 + +================= +Ina MP MRP Repara +================= + +Gestión de Reparaciones. + +Extiende el módulo ``repair`` de Odoo con: + +- Estado intermedio *Pendiente Confirmacion* (``pteconfir``) en el flujo de reparaciones. +- Campos de clasificación: Tipo de Reparación, Motivo, Centro de Producción, OF vinculada. +- Creación de materiales de reparación a partir de los materiales definidos en el producto. +- Coste de la línea de reparación desde el precio estándar del producto. +- Bloqueo de productos fantasma como materiales en reparaciones. +- Registro de tiempos por empleado (``mrp.repair.tiempos``). +- Gestión de documentación adjunta desde el servidor de ficheros (``mrp.repair.docu``). + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, +please check there if your issue has already been reported. If you spotted +it first, help us smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Inael +* Ana Juaristi +* Lucía Echeverría + +Do not contact contributors directly about support or help with technical issues. diff --git a/ina_mp_mrp_repara/__init__.py b/ina_mp_mrp_repara/__init__.py new file mode 100644 index 00000000..9b429614 --- /dev/null +++ b/ina_mp_mrp_repara/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/ina_mp_mrp_repara/__manifest__.py b/ina_mp_mrp_repara/__manifest__.py new file mode 100644 index 00000000..cdd0a2a6 --- /dev/null +++ b/ina_mp_mrp_repara/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2026 Inael +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Ina MP MRP Repara", + "version": "18.0.1.0.0", + "summary": "Gestión de reparaciones: estados, materiales, tiempos, documentación", + "category": "Inael mp", + "license": "AGPL-3", + "author": "INAEL, jag", + "website": "https://github.com/avanzosc/custom-addons", + "depends": [ + "repair", + "crm_claim_links", + "mrp", + "hr", + "ina_mc_permisos", + "ina_mc_product_campos", + ], + "data": [ + "security/ir.model.access.csv", + "views/repair_order_views.xml", + "views/mrp_repair_tiempos_views.xml", + "wizard/docu_rma_ver.xml", + ], + "installable": True, +} diff --git a/ina_mp_mrp_repara/models/__init__.py b/ina_mp_mrp_repara/models/__init__.py new file mode 100644 index 00000000..fd51404d --- /dev/null +++ b/ina_mp_mrp_repara/models/__init__.py @@ -0,0 +1,4 @@ +from . import repair_order +from . import mrp_repair_tiempos +from . import mrp_repair_docu +from . import stock_move diff --git a/ina_mp_mrp_repara/models/mrp_repair_docu.py b/ina_mp_mrp_repara/models/mrp_repair_docu.py new file mode 100644 index 00000000..ff7b4294 --- /dev/null +++ b/ina_mp_mrp_repara/models/mrp_repair_docu.py @@ -0,0 +1,53 @@ +# Copyright 2026 Inael +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import os + +from odoo import api, fields, models + + +class MrpRepairDocu(models.Model): + _name = "mrp.repair.docu" + _description = "Documentos en Reparaciones" + + @api.depends("fichero") + def _compute_existe_fichero(self): + for r in self: + r.existe_fichero = bool(r.fichero and r._buscar_dto()) + + rma_id = fields.Many2one( + comodel_name="repair.order", + string="RMA de repara", + ondelete="cascade", + ) + carpeta = fields.Text(string="Carpeta del fichero", default="/in/Documentacion/RMA") + fichero = fields.Char(string="Documento") + existe_fichero = fields.Boolean( + string="Fichero Encontrado", + compute="_compute_existe_fichero", + ) + nota = fields.Text(copy=False) + + def action_ver_docu(self): + ver_obj = self.env["docu.rma.ver"] + id_obj1 = ver_obj.search([], limit=1) + if not id_obj1: + obj_id = ver_obj.create({}) + else: + obj_id = id_obj1 + obj_id.write({"docu": self.fichero, "carpeta": self.carpeta}) + return obj_id.ver_docu_rma() + + def _buscar_dto(self): + if not self.fichero: + return False + docu = self.fichero.strip() + if "." not in docu: + docu = docu + ".pdf" + directorio = "/media" + (self.carpeta or "").strip() + if not os.path.isdir(directorio): + return False + for root, _dirs, ficheros in os.walk(directorio): + for fichero in ficheros: + if docu == fichero: + return os.path.join(root, fichero) + return False diff --git a/ina_mp_mrp_repara/models/mrp_repair_tiempos.py b/ina_mp_mrp_repara/models/mrp_repair_tiempos.py new file mode 100644 index 00000000..c7e1f19b --- /dev/null +++ b/ina_mp_mrp_repara/models/mrp_repair_tiempos.py @@ -0,0 +1,48 @@ +# Copyright 2026 Inael +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models + + +class MrpRepairTiempos(models.Model): + _name = "mrp.repair.tiempos" + _description = "Tiempos en Reparaciones" + + @api.depends("entrada", "salida") + def _compute_tiempo(self): + for r in self: + if r.entrada and r.salida: + delta = r.salida - r.entrada + r.tiempo = delta.total_seconds() / 3600.0 + else: + r.tiempo = 0.0 + + repara_id = fields.Many2one( + comodel_name="repair.order", + string="Orden Reparacion", + ) + workcenter_id = fields.Many2one( + comodel_name="mrp.workcenter", + string="Centro de Produccion", + related="repara_id.workcenter_id", + store=True, + ) + employee_id = fields.Many2one(comodel_name="hr.employee", string="Empleado") + entrada = fields.Datetime(string="Inicio") + salida = fields.Datetime(string="Final") + tiempo = fields.Float( + string="Duracion HH:MM", + compute="_compute_tiempo", + digits=(12, 6), + store=True, + ) + estado = fields.Selection( + selection=[ + ("pendiente", "Pendiente"), + ("cerrada", "Cerrada"), + ("cancelada", "Cancelada"), + ("interrumpida", "Interrumpida"), + ("activa", "Activa"), + ], + default="pendiente", + required=True, + ) diff --git a/ina_mp_mrp_repara/models/repair_order.py b/ina_mp_mrp_repara/models/repair_order.py new file mode 100644 index 00000000..901a912b --- /dev/null +++ b/ina_mp_mrp_repara/models/repair_order.py @@ -0,0 +1,156 @@ +# Copyright 2026 Inael +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class RepairOrder(models.Model): + _inherit = "repair.order" + + @api.depends( + "product_id", "product_id.product_tmpl_id.product_reparacion_ids", "move_ids" + ) + def _compute_operacion_creada(self): + for repair in self: + material_lines = repair._get_materiales_reparacion() + add_moves = repair.move_ids.filtered( + lambda move: move.repair_line_type == "add" + ) + repair.operacion_creada = not material_lines or bool(add_moves) + + workcenter_id = fields.Many2one( + comodel_name="mrp.workcenter", string="Centro de Produccion" + ) + descripcion = fields.Text() + repara_id = fields.One2many( + comodel_name="mrp.repair.tiempos", + inverse_name="repara_id", + string="Tiempos Reparacion", + ) + tipo_repara = fields.Selection( + selection=[ + ("interna", "Interna Reprocesos Fabrica"), + ("internaespecial", "Interna Trabajos Especiales"), + ("internaotros", "Interna Otros Dptos."), + ("externa", "Externa"), + ("mto", "Mantenimiento"), + ("abono", "Abono"), + ], + string="Tipo de Reparacion", + ) + motivo_repara = fields.Selection( + selection=[ + ("cali", "Calidad"), + ("come", "Comercial"), + ("comp", "Compras"), + ("celd", "Fab/Celdas"), + ("lase", "Fab/Laser"), + ("enla", "Fab/Enlaces"), + ("nobl", "Fab/Noblejas"), + ("pren", "Fab/Prensas"), + ("resi", "Fab/Resina"), + ("trafo", "Fab/Trafos"), + ("tecn", "Of. Tecnica"), + ("gara", "Rep. en Garantia"), + ("fuer", "Rep. fuera Garantia"), + ("inve", "Sacar Inventario"), + ("stoc", "Stock Incorrecto"), + ("plan", "Planif. Fabrica"), + ("alma", "Almacen"), + ("labo", "Laboratorio"), + ("dire", "Direccion"), + ], + string="Motivo", + ) + production_id = fields.Many2one(comodel_name="mrp.production", string="O.F") + line_documen_ids = fields.One2many( + comodel_name="mrp.repair.docu", + inverse_name="rma_id", + string="Documentos", + copy=True, + ) + state = fields.Selection( + selection_add=[("pteconfir", "Pendiente Confirmacion")], + ondelete={"pteconfir": "set draft"}, + ) + operacion_creada = fields.Boolean( + string="Operaciones Creadas", + compute="_compute_operacion_creada", + ) + + def action_pte_confir(self): + if self.filtered(lambda r: r.state != "draft"): + raise UserError( + _("Solo se puede poner Pte. confirmar cuando este en estado Borrador.") + ) + return self.write({"state": "pteconfir"}) + + def action_validate(self): + self.ensure_one() + if self.state != "pteconfir": + raise UserError(_("Solo puede confirmar estando en Pte. Confirmar")) + return super().action_validate() + + def action_repair_start(self): + if self.filtered(lambda repair: repair.state != "confirmed"): + raise UserError(_("Solo se puede iniciar una reparación confirmada.")) + return super().action_repair_start() + + def _action_repair_confirm(self): + repairs_pteconfir = self.filtered(lambda r: r.state == "pteconfir") + remaining = self - repairs_pteconfir + if repairs_pteconfir: + repairs_pteconfir._check_company() + repairs_pteconfir.move_ids._check_company() + repairs_pteconfir.move_ids._adjust_procure_method( + picking_type_code="repair_operation" + ) + repairs_pteconfir.move_ids._action_confirm() + repairs_pteconfir.move_ids._trigger_scheduler() + repairs_pteconfir.write({"state": "confirmed"}) + if remaining: + return super()._action_repair_confirm() + return True + + def _get_materiales_reparacion(self): + self.ensure_one() + if not self.product_id: + return self.env["product.reparacion"] + return self.product_id.product_tmpl_id.product_reparacion_ids.filtered( + lambda line: line.material_id and line.cantidad + ) + + def action_crear_materiales(self): + Move = self.env["stock.move"] + for repair in self: + material_lines = repair._get_materiales_reparacion() + if not material_lines: + raise UserError( + _( + "El producto de la reparación no tiene materiales de" + " reparación definidos." + ) + ) + if repair.move_ids.filtered(lambda move: move.repair_line_type == "add"): + raise UserError(_("La reparación ya tiene materiales creados.")) + + vals_list = [] + repair_qty = repair.product_qty or 1.0 + for line in material_lines: + material = line.material_id + vals_list.append( + { + "repair_id": repair.id, + "repair_line_type": "add", + "product_id": material.id, + "product_uom_qty": line.cantidad * repair_qty, + "product_uom": material.uom_id.id, + "price_unit": material.standard_price, + "company_id": repair.company_id.id, + "date": repair.schedule_date, + "location_id": repair.location_id.id, + "location_dest_id": repair.location_dest_id.id, + } + ) + Move.create(vals_list) + return True diff --git a/ina_mp_mrp_repara/models/stock_move.py b/ina_mp_mrp_repara/models/stock_move.py new file mode 100644 index 00000000..9c552cfc --- /dev/null +++ b/ina_mp_mrp_repara/models/stock_move.py @@ -0,0 +1,62 @@ +# Copyright 2026 Inael +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class StockMove(models.Model): + _inherit = "stock.move" + + coste = fields.Float( + string="Precio Medio", + related="product_id.standard_price", + digits=(9, 2), + ) + + def _check_repair_product_fantasma(self): + for move in self: + if ( + move.repair_id + and move.repair_line_type + and move.product_id + and move.product_id.fantasma + ): + raise UserError(_("Es un fantasma y no se permiten.")) + + @api.constrains("repair_id", "repair_line_type", "product_id") + def _check_repair_product_fantasma_constrains(self): + self._check_repair_product_fantasma() + + @api.onchange("product_id", "repair_id", "repair_line_type") + def _onchange_repair_product_fantasma(self): + self._check_repair_product_fantasma() + for move in self: + if move.repair_id and move.repair_line_type and move.product_id: + move.price_unit = move.product_id.standard_price + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if ( + vals.get("repair_id") + and vals.get("repair_line_type") + and vals.get("product_id") + and "price_unit" not in vals + ): + product = self.env["product.product"].browse(vals["product_id"]) + vals["price_unit"] = product.standard_price + return super().create(vals_list) + + def write(self, vals): + if vals.get("product_id") and "price_unit" not in vals: + repair_moves = self.filtered(lambda m: m.repair_id and m.repair_line_type) + other_moves = self - repair_moves + if not repair_moves: + return super().write(vals) + product = self.env["product.product"].browse(vals["product_id"]) + res = True + if other_moves: + res = super(StockMove, other_moves).write(vals) + repair_vals = dict(vals, price_unit=product.standard_price) + return super(StockMove, repair_moves).write(repair_vals) and res + return super().write(vals) diff --git a/ina_mp_mrp_repara/pyproject.toml b/ina_mp_mrp_repara/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/ina_mp_mrp_repara/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/ina_mp_mrp_repara/security/ir.model.access.csv b/ina_mp_mrp_repara/security/ir.model.access.csv new file mode 100644 index 00000000..f478a919 --- /dev/null +++ b/ina_mp_mrp_repara/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mrp_repair_tiempos_user,mrp.repair.tiempos user,model_mrp_repair_tiempos,base.group_user,1,1,1,1 +access_mrp_repair_docu_user,mrp.repair.docu user,model_mrp_repair_docu,base.group_user,1,1,1,1 +access_docu_rma_ver_user,docu.rma.ver user,model_docu_rma_ver,base.group_user,1,1,1,1 diff --git a/ina_mp_mrp_repara/views/mrp_repair_tiempos_views.xml b/ina_mp_mrp_repara/views/mrp_repair_tiempos_views.xml new file mode 100644 index 00000000..7d417872 --- /dev/null +++ b/ina_mp_mrp_repara/views/mrp_repair_tiempos_views.xml @@ -0,0 +1,65 @@ + + + + mrp.repair.tiempos.tree + mrp.repair.tiempos + + + + + + + + + + + + + + + mrp.repair.tiempos.search + mrp.repair.tiempos + + + + + + + + + + + + + + + + Seguimiento Reparaciones + mrp.repair.tiempos + list + + + + diff --git a/ina_mp_mrp_repara/views/repair_order_views.xml b/ina_mp_mrp_repara/views/repair_order_views.xml new file mode 100644 index 00000000..5c6153de --- /dev/null +++ b/ina_mp_mrp_repara/views/repair_order_views.xml @@ -0,0 +1,143 @@ + + + + ina.repair.order.tree + repair.order + + + + RMA + + + + + + + + + + + + ina.repair.order.form + repair.order + + + + draft,pteconfir,confirmed,under_repair,done + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +