Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions ina_mp_mrp_repara/README.rst
Original file line number Diff line number Diff line change
@@ -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
<https://github.com/avanzosc/custom-addons/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 <anajuaristi@avanzosc.es>
* Lucía Echeverría <luciaecheverria@avanzosc.es>

Do not contact contributors directly about support or help with technical issues.
2 changes: 2 additions & 0 deletions ina_mp_mrp_repara/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizard
26 changes: 26 additions & 0 deletions ina_mp_mrp_repara/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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,
}
4 changes: 4 additions & 0 deletions ina_mp_mrp_repara/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import repair_order
from . import mrp_repair_tiempos
from . import mrp_repair_docu
from . import stock_move
53 changes: 53 additions & 0 deletions ina_mp_mrp_repara/models/mrp_repair_docu.py
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions ina_mp_mrp_repara/models/mrp_repair_tiempos.py
Original file line number Diff line number Diff line change
@@ -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,
)
156 changes: 156 additions & 0 deletions ina_mp_mrp_repara/models/repair_order.py
Original file line number Diff line number Diff line change
@@ -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
62 changes: 62 additions & 0 deletions ina_mp_mrp_repara/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading