Merge PR #1287 into 14.0

Signed-off-by LoisRForgeFlow
This commit is contained in:
OCA-git-bot
2024-06-20 11:50:40 +00:00
19 changed files with 1487 additions and 0 deletions

105
repair_picking/README.rst Normal file
View File

@@ -0,0 +1,105 @@
==============
Repair Picking
==============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:7a770249391cf216da563d0a64febf0abd30c9fdae7325803988570d96d8d2f3
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github
:target: https://github.com/OCA/manufacture/tree/14.0/repair_picking
:alt: OCA/manufacture
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/manufacture-14-0/manufacture-14-0-repair_picking
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/manufacture&target_branch=14.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module extends the repair management functionality in Odoo, providing additional
options for handling the repair process in a more customizable and efficient way.
It allows the configuration of repair steps based on specific business requirements
and adds new picking types for managing component addition and removal in repair orders.
The main features are:
- Customize repair steps: Choose between a 1-step, 2-step, or 3-step repair process.
- Add and remove components during the repair process using separate picking types.
- Associate repair orders with pickings for improved traceability.
- Automatic creation of pickings and procurement routes based on the selected repair steps.
- Manage repair locations and routes more efficiently with warehouse settings.
**Table of contents**
.. contents::
:local:
Configuration
=============
#. Navigate to Inventory > Configuration > Warehouses, and select a warehouse.
#. In the "Repair Steps" field, choose between "Repair", "Pick component, repair", or "Pick component, repair, store removed component" to define the repair process.
#. Define the "Repair Location", "Add Component to Repair" picking type, "Remove component from Repair" picking type, and "Repair Route" as needed.
Usage
=====
#. Navigate to Repair > Repair Orders and create a new repair order.
#. In the Operations tab, add components to be added or removed during the repair process.
#. Confirm the repair order. This will automatically generate the necessary
pickings based on the configured repair steps.
#. Process the pickings as required during the repair process.
#. If the repair order needs to be canceled, all associated pickings that are not
in "cancel" or "done" state will also be canceled automatically.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/manufacture/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/manufacture/issues/new?body=module:%20repair_picking%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* ForgeFlow
Contributors
~~~~~~~~~~~~
* Joan Sisquella <joan.sisquella@forgeflow.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/manufacture <https://github.com/OCA/manufacture/tree/14.0/repair_picking>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1,2 @@
from . import models
from . import tests

View File

@@ -0,0 +1,20 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Repair Picking",
"version": "14.0.1.0.0",
"author": "ForgeFlow, Odoo Community Association (OCA)",
"category": "Repair",
"website": "https://github.com/OCA/manufacture",
"summary": "Enhanced repair order management with pickings "
"for adding and removing components",
"depends": ["repair", "stock_move_forced_lot", "repair_stock_move"],
"data": [
"views/stock_warehouse_views.xml",
"views/repair_order_view.xml",
],
"license": "LGPL-3",
"installable": True,
"auto_install": False,
}

View File

@@ -0,0 +1,3 @@
from . import stock_warehouse
from . import repair
from . import stock_rule

View File

@@ -0,0 +1,231 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com)
# 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.model
def _get_default_location_id(self):
warehouse = self.env["stock.warehouse"].search(
[("company_id", "=", self.env.company.id)], limit=1
)
return (
warehouse.repair_location_id.id
if warehouse and warehouse.repair_location_id
else False
)
# Changing default value on existing field
location_id = fields.Many2one(
default=_get_default_location_id,
)
picking_ids = fields.Many2many(
comodel_name="stock.picking",
compute="_compute_picking_ids",
copy=False,
string="Pickings associated to this repair order",
)
picking_count = fields.Integer(
string="Transfers", copy=False, compute="_compute_picking_ids"
)
procurement_group_id = fields.Many2one(
"procurement.group", "Procurement Group", copy=False
)
def action_view_pickings(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"stock.action_picking_tree_all"
)
action["domain"] = [("id", "in", self.picking_ids.ids)]
return action
def _compute_picking_ids(self):
for order in self:
moves = self.env["stock.move"].search(
[("repair_line_id", "in", order.operations.ids)]
)
order.picking_ids = moves.mapped("picking_id")
order.picking_count = len(moves.mapped("picking_id"))
def action_repair_cancel(self):
res = super().action_repair_cancel()
for picking in self.picking_ids:
if picking.state not in ["cancel", "done"]:
picking.action_cancel()
return res
def _action_launch_stock_rule(self, repair_lines):
for line in repair_lines:
self._run_procurement_repair(line)
return True
def _run_procurement_repair(self, line):
procurements = []
errors = []
procurement = self._prepare_procurement_repair(line)
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 True
@api.model
def _get_procurement_data_repair(self, line):
warehouse = self.location_id.get_warehouse()
if not self.procurement_group_id:
group_id = self.env["procurement.group"].create({"name": self.name})
self.procurement_group_id = group_id
procurement_data = {
"name": self.name,
"group_id": self.procurement_group_id,
"origin": self.name,
"date_planned": fields.Datetime.now(),
"product_id": line.product_id.id,
"product_qty": line.product_uom_qty,
"product_uom": line.product_uom.id,
"company_id": self.company_id,
"warehouse_id": warehouse,
"repair_line_id": line.id,
}
if line.lot_id:
procurement_data["lot_id"] = line.lot_id.id
if line.type == "remove":
procurement_data[
"source_repair_location_id"
] = line.repair_id.location_id.id
return procurement_data
@api.model
def _prepare_procurement_repair(self, line):
values = self._get_procurement_data_repair(line)
warehouse = self.location_id.get_warehouse()
location = (
self.location_id
if line.type == "add"
else warehouse.remove_c_type_id.default_location_dest_id
)
procurement = self.env["procurement.group"].Procurement(
line.product_id,
line.product_uom_qty,
line.product_uom,
location,
values.get("origin"),
values.get("origin"),
self.company_id,
values,
)
return procurement
def _update_stock_moves_and_picking_state(self):
for repair in self:
for picking in repair.picking_ids:
if picking.location_dest_id.id == self.location_id.id:
for move_line in picking.move_ids_without_package:
stock_moves = repair.stock_move_ids.filtered(
lambda m: m.product_id.id
== repair.operations.filtered(
lambda l: l.type == "add"
and l.product_id.id == m.product_id.id
).product_id.id
and m.location_id.id == self.location_id.id
)
if stock_moves:
stock_moves[0].write(
{
"move_orig_ids": [(4, move_line.id)],
"state": "waiting",
}
)
if picking.location_id.id == self.location_id.id:
for move_line in picking.move_ids_without_package:
stock_moves = repair.stock_move_ids.filtered(
lambda m: m.product_id.id
== repair.operations.filtered(
lambda l: l.type == "remove"
and l.product_id.id == m.product_id.id
).product_id.id
and m.location_dest_id.id == self.location_id.id
)
if stock_moves:
move_line.write(
{
"move_orig_ids": [(4, stock_moves[0].id)],
"state": "waiting",
}
)
# We are using write here because
# the repair_stock_move module does not use stock rules.
# As a result, we manually link the stock moves
# and then recompute the state of the picking.
picking._compute_state()
def action_repair_confirm(self):
res = super().action_repair_confirm()
for repair in self:
warehouse = repair.location_id.get_warehouse()
if warehouse.repair_steps in ["2_steps", "3_steps"]:
repair._action_launch_stock_rule(
repair.operations.filtered(lambda l: l.type == "add"),
)
if warehouse.repair_steps == "3_steps":
repair._action_launch_stock_rule(
repair.operations.filtered(lambda l: l.type == "remove"),
)
repair._update_stock_moves_and_picking_state()
return res
@api.onchange("location_id")
def _onchange_location_id(self):
warehouse = self.location_id.get_warehouse()
for line in self.operations:
if line.type == "add":
line.location_id = self.location_id
elif line.type == "remove" and warehouse.repair_steps == "3_steps":
line.location_dest_id = self.location_id
class RepairLine(models.Model):
_inherit = "repair.line"
@api.onchange("type", "product_id")
def onchange_operation_type(self):
super().onchange_operation_type()
production_location = self.env["stock.location"].search(
[("usage", "=", "production")], limit=1
)
warehouse = self.repair_id.location_id.get_warehouse()
if self.type == "add":
self.write(
{
"location_id": self.repair_id.location_id.id,
"location_dest_id": production_location.id,
}
)
elif self.type == "remove":
self.write({"location_id": production_location.id})
if warehouse.repair_steps in ["1_step", "2_steps"]:
scrap_location = self.env["stock.location"].search(
[
("scrap_location", "=", True),
("company_id", "=", warehouse.company_id.id),
],
limit=1,
)
self.write({"location_dest_id": scrap_location.id})
else:
self.write({"location_dest_id": self.repair_id.location_id.id})
def create(self, vals):
line = super().create(vals)
if line.repair_id.state in ["confirmed", "under_repair", "ready"]:
line.repair_id._action_launch_stock_rule(line)
return line

View File

@@ -0,0 +1,27 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, models
class StockRule(models.Model):
_inherit = "stock.rule"
def _get_custom_move_fields(self):
fields = super(StockRule, self)._get_custom_move_fields()
# Fields is added on `repair_stock_move` module.
fields += ["repair_line_id"]
return fields
class ProcurementGroup(models.Model):
_inherit = "procurement.group"
@api.model
def _get_rule_domain(self, location, values):
domain = super(ProcurementGroup, self)._get_rule_domain(location, values)
if values.get("source_repair_location_id"):
domain.append(
("location_src_id", "=", values.get("source_repair_location_id"))
)
return domain

View File

@@ -0,0 +1,184 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class StockWarehouse(models.Model):
_inherit = "stock.warehouse"
repair_steps = fields.Selection(
[
("1_step", "Repair"),
("2_steps", "Pick component, repair"),
("3_steps", "Pick component, repair, store removed component"),
],
string="Repair Steps",
default="1_step",
)
add_c_type_id = fields.Many2one(
"stock.picking.type", string="Add Component to Repair"
)
remove_c_type_id = fields.Many2one(
"stock.picking.type", string="Remove component from Repair"
)
repair_route_id = fields.Many2one("stock.location.route", string="Repair Route")
repair_location_id = fields.Many2one("stock.location", string="Repair Location")
def update_picking_types(self, repair_steps, repair_location_id):
if repair_steps in ["2_steps", "3_steps"]:
self.add_c_type_id.active = True
if repair_steps == "1_step":
self.add_c_type_id.active = False
if repair_steps == "3_steps":
self.remove_c_type_id.active = True
if repair_steps in ["1_step", "2_steps"]:
self.remove_c_type_id.active = False
if repair_location_id:
self.add_c_type_id.write({"default_location_dest_id": repair_location_id})
self.remove_c_type_id.write({"default_location_src_id": repair_location_id})
def update_repair_routes(self, repair_steps, repair_location_id):
if repair_steps == "2_steps" or repair_steps == "3_steps":
self.repair_route_id.active = True
existing_rule = (
self.env["stock.rule"]
.with_context(active_test=False)
.search(
[
("picking_type_id", "=", self.add_c_type_id.id),
("route_id", "=", self.repair_route_id.id),
],
limit=1,
)
)
existing_rule.active = True
if repair_steps == "1_step":
for rule in self.repair_route_id.rule_ids:
rule.active = False
self.repair_route_id.active = False
if repair_location_id:
self.repair_route_id.rule_ids.filtered(
lambda r: r.picking_type_id == self.add_c_type_id
).write({"location_id": repair_location_id})
self.repair_route_id.rule_ids.filtered(
lambda r: r.picking_type_id == self.remove_c_type_id
).write({"location_src_id": repair_location_id})
if repair_steps in ["1_step", "2_steps"]:
self.repair_route_id.rule_ids.filtered(
lambda r: r.picking_type_id == self.remove_c_type_id
).active = False
def write(self, vals):
res = super(StockWarehouse, self).write(vals)
for warehouse in self:
repair_steps = vals.get("repair_steps")
repair_location_id = vals.get("repair_location_id")
if repair_steps:
if repair_steps in ["3_steps", "2_steps"]:
warehouse._create_repair_picking_types()
warehouse._create_repair_route()
if repair_steps == "3_steps":
warehouse._create_remove_rule()
if repair_steps or repair_location_id:
warehouse.update_picking_types(repair_steps, repair_location_id)
warehouse.update_repair_routes(repair_steps, repair_location_id)
return res
def _create_repair_picking_types(self):
for warehouse in self:
repair_location_id = (
warehouse.repair_location_id.id or warehouse.lot_stock_id.id
)
if not warehouse.add_c_type_id:
pbr_type = self.env["stock.picking.type"].create(
{
"name": "Add Component to Repair",
"code": "internal",
"sequence_code": "ACR",
"warehouse_id": warehouse.id,
"default_location_src_id": warehouse.lot_stock_id.id,
"default_location_dest_id": repair_location_id,
"company_id": warehouse.company_id.id,
}
)
warehouse.add_c_type_id = pbr_type.id
else:
warehouse.add_c_type_id.write(
{"default_location_dest_id": repair_location_id}
)
if not warehouse.remove_c_type_id:
par_type = self.env["stock.picking.type"].create(
{
"name": "Remove component from Repair",
"code": "internal",
"sequence_code": "RCR",
"warehouse_id": warehouse.id,
"default_location_src_id": repair_location_id,
"default_location_dest_id": warehouse.view_location_id.id,
"company_id": warehouse.company_id.id,
}
)
warehouse.remove_c_type_id = par_type.id
else:
warehouse.remove_c_type_id.write(
{"default_location_src_id": repair_location_id}
)
def _create_repair_route(self):
for warehouse in self:
if not warehouse.repair_route_id:
route = self.env["stock.location.route"].create(
{
"name": "Repair Route for %s" % warehouse.name,
"warehouse_selectable": True,
"product_selectable": False,
"warehouse_ids": [(6, 0, warehouse.ids)],
"company_id": warehouse.company_id.id,
}
)
warehouse.repair_route_id = route.id
self.env["stock.rule"].create(
{
"name": "Add Component to Repair",
"picking_type_id": warehouse.add_c_type_id.id,
"route_id": route.id,
"location_src_id": warehouse.lot_stock_id.id,
"location_id": warehouse.repair_location_id.id
or warehouse.view_location_id.id,
"action": "pull",
"company_id": warehouse.company_id.id,
"warehouse_id": warehouse.id,
}
)
def _create_remove_rule(self):
for warehouse in self:
existing_rule = (
self.env["stock.rule"]
.with_context(active_test=False)
.search(
[
("picking_type_id", "=", warehouse.remove_c_type_id.id),
("route_id", "=", warehouse.repair_route_id.id),
],
limit=1,
)
)
if not existing_rule:
self.env["stock.rule"].create(
{
"name": "Remove component from Repair",
"picking_type_id": warehouse.remove_c_type_id.id,
"route_id": warehouse.repair_route_id.id,
"location_src_id": warehouse.repair_location_id.id
or warehouse.view_location_id.id,
"location_id": warehouse.view_location_id.id,
"action": "pull",
"company_id": warehouse.company_id.id,
"warehouse_id": warehouse.id,
"active": True,
}
)
else:
existing_rule.active = True

View File

@@ -0,0 +1,3 @@
#. Navigate to Inventory > Configuration > Warehouses, and select a warehouse.
#. In the "Repair Steps" field, choose between "Repair", "Pick component, repair", or "Pick component, repair, store removed component" to define the repair process.
#. Define the "Repair Location", "Add Component to Repair" picking type, "Remove component from Repair" picking type, and "Repair Route" as needed.

View File

@@ -0,0 +1 @@
* Joan Sisquella <joan.sisquella@forgeflow.com>

View File

@@ -0,0 +1,12 @@
This module extends the repair management functionality in Odoo, providing additional
options for handling the repair process in a more customizable and efficient way.
It allows the configuration of repair steps based on specific business requirements
and adds new picking types for managing component addition and removal in repair orders.
The main features are:
- Customize repair steps: Choose between a 1-step, 2-step, or 3-step repair process.
- Add and remove components during the repair process using separate picking types.
- Associate repair orders with pickings for improved traceability.
- Automatic creation of pickings and procurement routes based on the selected repair steps.
- Manage repair locations and routes more efficiently with warehouse settings.

View File

@@ -0,0 +1,7 @@
#. Navigate to Repair > Repair Orders and create a new repair order.
#. In the Operations tab, add components to be added or removed during the repair process.
#. Confirm the repair order. This will automatically generate the necessary
pickings based on the configured repair steps.
#. Process the pickings as required during the repair process.
#. If the repair order needs to be canceled, all associated pickings that are not
in "cancel" or "done" state will also be canceled automatically.

View File

@@ -0,0 +1,453 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Repair Picking</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="repair-picking">
<h1 class="title">Repair Picking</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:7a770249391cf216da563d0a64febf0abd30c9fdae7325803988570d96d8d2f3
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/manufacture/tree/14.0/repair_picking"><img alt="OCA/manufacture" src="https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/manufacture-14-0/manufacture-14-0-repair_picking"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/manufacture&amp;target_branch=14.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module extends the repair management functionality in Odoo, providing additional
options for handling the repair process in a more customizable and efficient way.
It allows the configuration of repair steps based on specific business requirements
and adds new picking types for managing component addition and removal in repair orders.</p>
<p>The main features are:</p>
<ul class="simple">
<li>Customize repair steps: Choose between a 1-step, 2-step, or 3-step repair process.</li>
<li>Add and remove components during the repair process using separate picking types.</li>
<li>Associate repair orders with pickings for improved traceability.</li>
<li>Automatic creation of pickings and procurement routes based on the selected repair steps.</li>
<li>Manage repair locations and routes more efficiently with warehouse settings.</li>
</ul>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<ol class="arabic simple">
<li>Navigate to Inventory &gt; Configuration &gt; Warehouses, and select a warehouse.</li>
<li>In the “Repair Steps” field, choose between “Repair”, “Pick component, repair”, or “Pick component, repair, store removed component” to define the repair process.</li>
<li>Define the “Repair Location”, “Add Component to Repair” picking type, “Remove component from Repair” picking type, and “Repair Route” as needed.</li>
</ol>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<ol class="arabic simple">
<li>Navigate to Repair &gt; Repair Orders and create a new repair order.</li>
<li>In the Operations tab, add components to be added or removed during the repair process.</li>
<li>Confirm the repair order. This will automatically generate the necessary
pickings based on the configured repair steps.</li>
<li>Process the pickings as required during the repair process.</li>
<li>If the repair order needs to be canceled, all associated pickings that are not
in “cancel” or “done” state will also be canceled automatically.</li>
</ol>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/manufacture/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/manufacture/issues/new?body=module:%20repair_picking%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>ForgeFlow</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul class="simple">
<li>Joan Sisquella &lt;<a class="reference external" href="mailto:joan.sisquella&#64;forgeflow.com">joan.sisquella&#64;forgeflow.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/manufacture/tree/14.0/repair_picking">OCA/manufacture</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,2 @@
from . import test_stock_repair_warehouse
from . import test_stock_repair_order

View File

@@ -0,0 +1,266 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.tests import common
class TestStockRepairOrder(common.SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.repair_model = cls.env["repair.order"]
cls.repair_line_model = cls.env["repair.line"]
cls.product_model = cls.env["product.product"]
cls.stock_location_model = cls.env["stock.location"]
cls.warehouse_model = cls.env["stock.warehouse"]
cls.company = cls.env.ref("base.main_company")
cls.warehouse = cls.warehouse_model.create(
{
"name": "Test Warehouse",
"code": "TW",
"company_id": cls.company.id,
}
)
cls.product1 = cls.product_model.create(
{
"name": "Product 1",
"type": "product",
"company_id": cls.company.id,
}
)
cls.product2 = cls.product_model.create(
{
"name": "Product 2",
"type": "product",
"company_id": cls.company.id,
}
)
cls.repair_location = cls.stock_location_model.create(
{
"name": "Repair Location",
"usage": "internal",
"location_id": cls.warehouse.view_location_id.id,
"company_id": cls.company.id,
}
)
cls.production_location = cls.stock_location_model.create(
{
"name": "Production Location",
"usage": "production",
"company_id": cls.company.id,
}
)
cls.env["stock.quant"].create(
{
"product_id": cls.product1.id,
"location_id": cls.repair_location.id,
"quantity": 10,
}
)
cls.env["stock.quant"].create(
{
"product_id": cls.product2.id,
"location_id": cls.warehouse.lot_stock_id.id,
"quantity": 10,
}
)
def test_1step_repair_order_flow(self):
self.warehouse.write(
{
"repair_steps": "1_step",
"repair_location_id": self.repair_location.id,
}
)
repair_order = self.repair_model.create(
{
"product_id": self.product1.id,
"product_uom": self.product1.uom_id.id,
"location_id": self.repair_location.id,
"company_id": self.company.id,
}
)
self.repair_line_model.create(
{
"name": "Repair Line 1",
"repair_id": repair_order.id,
"product_id": self.product2.id,
"type": "add",
"product_uom_qty": 1,
"product_uom": self.product2.uom_id.id,
"price_unit": 1,
"location_id": self.repair_location.id,
"location_dest_id": self.production_location.id,
}
)
repair_order.action_repair_confirm()
self.assertEqual(repair_order.state, "confirmed")
repair_order.action_repair_ready()
self.assertEqual(repair_order.state, "ready")
def test_2steps_repair_order_flow(self):
self.warehouse.write(
{
"repair_steps": "2_steps",
"repair_location_id": self.repair_location.id,
}
)
self.product2.write(
{"route_ids": [(6, 0, [self.warehouse.repair_route_id.id])]}
)
repair_order = self.repair_model.create(
{
"product_id": self.product1.id,
"product_uom": self.product1.uom_id.id,
"location_id": self.repair_location.id,
"company_id": self.company.id,
}
)
self.repair_line_model.create(
{
"name": "Repair Line 2",
"repair_id": repair_order.id,
"product_id": self.product2.id,
"type": "add",
"product_uom_qty": 1,
"product_uom": self.product2.uom_id.id,
"price_unit": 1,
"location_id": self.repair_location.id,
"location_dest_id": self.production_location.id,
}
)
repair_order.action_repair_confirm()
repair_order._compute_picking_ids()
self.assertEqual(repair_order.state, "confirmed")
self.assertTrue(repair_order.picking_ids)
self.assertEqual(len(repair_order.picking_ids), 1)
def test_3steps_repair_order_flow(self):
self.warehouse.write(
{
"repair_steps": "3_steps",
"repair_location_id": self.repair_location.id,
}
)
self.product2.write(
{"route_ids": [(6, 0, [self.warehouse.repair_route_id.id])]}
)
repair_order = self.repair_model.create(
{
"product_id": self.product1.id,
"product_uom": self.product1.uom_id.id,
"location_id": self.repair_location.id,
"company_id": self.company.id,
}
)
self.repair_line_model.create(
{
"name": "Repair Line 3",
"repair_id": repair_order.id,
"product_id": self.product2.id,
"type": "add",
"product_uom_qty": 1,
"product_uom": self.product2.uom_id.id,
"price_unit": 1,
"location_id": self.repair_location.id,
"location_dest_id": self.production_location.id,
}
)
self.repair_line_model.create(
{
"name": "Repair Line 4",
"repair_id": repair_order.id,
"product_id": self.product2.id,
"type": "remove",
"product_uom_qty": 1,
"product_uom": self.product2.uom_id.id,
"price_unit": 1,
"location_id": self.production_location.id,
"location_dest_id": self.repair_location.id,
}
)
repair_order.action_repair_confirm()
repair_order._compute_picking_ids()
self.assertEqual(repair_order.state, "confirmed")
self.assertTrue(repair_order.picking_ids)
self.assertEqual(len(repair_order.picking_ids), 2)
repair_order.action_repair_cancel()
self.assertEqual(repair_order.state, "cancel")
for picking in repair_order.picking_ids:
self.assertEqual(picking.state, "cancel")
def test_update_related_pickings(self):
self.warehouse.write(
{
"repair_steps": "3_steps",
"repair_location_id": self.repair_location.id,
}
)
self.product2.write(
{"route_ids": [(6, 0, [self.warehouse.repair_route_id.id])]}
)
repair_order = self.repair_model.create(
{
"product_id": self.product1.id,
"product_uom": self.product1.uom_id.id,
"location_id": self.repair_location.id,
"company_id": self.company.id,
}
)
self.repair_line_model.create(
{
"name": "Repair Line 3",
"repair_id": repair_order.id,
"product_id": self.product2.id,
"type": "add",
"product_uom_qty": 1,
"product_uom": self.product2.uom_id.id,
"price_unit": 1,
"location_id": self.repair_location.id,
"location_dest_id": self.production_location.id,
}
)
repair_order.action_repair_confirm()
repair_order._compute_picking_ids()
self.assertEqual(repair_order.state, "confirmed")
self.assertTrue(repair_order.picking_ids)
self.assertEqual(len(repair_order.picking_ids), 1)
self.assertEqual(len(repair_order.picking_ids.move_ids_without_package), 1)
self.assertEqual(
repair_order.picking_ids.move_ids_without_package.product_uom_qty, 1
)
self.repair_line_model.create(
{
"name": "Repair Line Add",
"repair_id": repair_order.id,
"product_id": self.product2.id,
"type": "add",
"product_uom_qty": 1,
"product_uom": self.product2.uom_id.id,
"price_unit": 1,
"location_id": self.repair_location.id,
"location_dest_id": self.production_location.id,
}
)
self.assertEqual(len(repair_order.picking_ids), 1)
self.assertEqual(len(repair_order.picking_ids.move_ids_without_package), 1)
self.assertEqual(
repair_order.picking_ids.move_ids_without_package.product_uom_qty, 2
)
self.repair_line_model.create(
{
"name": "Repair Line Remove",
"repair_id": repair_order.id,
"product_id": self.product2.id,
"type": "remove",
"product_uom_qty": 1,
"product_uom": self.product2.uom_id.id,
"price_unit": 1,
"location_id": self.production_location.id,
"location_dest_id": self.repair_location.id,
}
)
repair_order._compute_picking_ids()
self.assertEqual(len(repair_order.picking_ids), 2)

View File

@@ -0,0 +1,116 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.tests import common
class TestStockRepairWarehouse(common.SavepointCase):
@classmethod
def setUpClass(cls):
super(TestStockRepairWarehouse, cls).setUpClass()
cls.warehouse_obj = cls.env["stock.warehouse"]
cls.product_obj = cls.env["product.product"]
cls.product_1 = cls.product_obj.create(
{
"name": "Product 1",
"type": "product",
}
)
cls.warehouse_1 = cls.warehouse_obj.create(
{
"name": "Test Warehouse",
"code": "TWH",
"repair_steps": "1_step",
}
)
def test_01_warehouse_creation(self):
self.assertEqual(self.warehouse_1.repair_steps, "1_step")
def test_02_update_repair_steps(self):
self.warehouse_1.repair_steps = "2_steps"
self.assertEqual(self.warehouse_1.repair_steps, "2_steps")
self.assertTrue(self.warehouse_1.add_c_type_id.active)
self.assertFalse(self.warehouse_1.remove_c_type_id.active)
self.assertTrue(self.warehouse_1.repair_route_id.active)
def test_03_update_repair_steps_to_3_steps(self):
self.warehouse_1.repair_steps = "3_steps"
self.assertEqual(self.warehouse_1.repair_steps, "3_steps")
self.assertTrue(self.warehouse_1.add_c_type_id.active)
self.assertTrue(self.warehouse_1.remove_c_type_id.active)
self.assertTrue(self.warehouse_1.repair_route_id.active)
def test_04_reverse_and_update_repair_steps(self):
self.warehouse_1.repair_steps = "1_step"
self.warehouse_1.repair_steps = "2_steps"
self.assertEqual(self.warehouse_1.repair_steps, "2_steps")
self.assertTrue(self.warehouse_1.add_c_type_id.active)
self.assertFalse(self.warehouse_1.remove_c_type_id.active)
self.assertTrue(self.warehouse_1.repair_route_id.active)
add_rule = self.env["stock.rule"].search(
[
("picking_type_id", "=", self.warehouse_1.add_c_type_id.id),
("route_id", "=", self.warehouse_1.repair_route_id.id),
]
)
self.assertTrue(add_rule.active)
remove_rule = self.env["stock.rule"].search(
[
("picking_type_id", "=", self.warehouse_1.remove_c_type_id.id),
("route_id", "=", self.warehouse_1.repair_route_id.id),
]
)
self.assertFalse(remove_rule)
self.warehouse_1.repair_steps = "3_steps"
self.assertEqual(self.warehouse_1.repair_steps, "3_steps")
self.assertTrue(self.warehouse_1.add_c_type_id.active)
self.assertTrue(self.warehouse_1.remove_c_type_id.active)
self.assertTrue(self.warehouse_1.repair_route_id.active)
add_rule = self.env["stock.rule"].search(
[
("picking_type_id", "=", self.warehouse_1.add_c_type_id.id),
("route_id", "=", self.warehouse_1.repair_route_id.id),
]
)
self.assertTrue(add_rule.active)
remove_rule = self.env["stock.rule"].search(
[
("picking_type_id", "=", self.warehouse_1.remove_c_type_id.id),
("route_id", "=", self.warehouse_1.repair_route_id.id),
]
)
self.assertTrue(remove_rule.active)
self.warehouse_1.repair_steps = "2_steps"
self.assertEqual(self.warehouse_1.repair_steps, "2_steps")
self.assertTrue(self.warehouse_1.add_c_type_id.active)
self.assertFalse(self.warehouse_1.remove_c_type_id.active)
self.assertTrue(self.warehouse_1.repair_route_id.active)
add_rule = self.env["stock.rule"].search(
[
("picking_type_id", "=", self.warehouse_1.add_c_type_id.id),
("route_id", "=", self.warehouse_1.repair_route_id.id),
]
)
self.assertTrue(add_rule.active)
remove_rule = self.env["stock.rule"].search(
[
("picking_type_id", "=", self.warehouse_1.remove_c_type_id.id),
("route_id", "=", self.warehouse_1.repair_route_id.id),
]
)
self.assertFalse(remove_rule)
self.warehouse_1.repair_steps = "1_step"
self.assertFalse(self.warehouse_1.add_c_type_id.active)
self.assertFalse(self.warehouse_1.remove_c_type_id.active)
self.assertFalse(self.warehouse_1.repair_route_id.active)

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_repair_order_form_repair_stock_move_inherited" model="ir.ui.view">
<field name="name">repair.order.form - repair_stock_move - custom</field>
<field name="model">repair.order</field>
<field name="inherit_id" ref="repair_stock_move.view_repair_order_form" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button
name="action_view_pickings"
type="object"
class="oe_stat_button"
icon="fa-truck"
attrs="{'invisible': [('picking_count', '=', 0)]}"
help="Transfers associated with this repair order"
>
<field name="picking_count" widget="statinfo" string="Transfers" />
</button>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_warehouse_inherit_repair" model="ir.ui.view">
<field name="name">Stock Warehouse Inherit Repair</field>
<field name="model">stock.warehouse</field>
<field name="inherit_id" ref="stock.view_warehouse" />
<field name="arch" type="xml">
<xpath expr="//field[@name='resupply_wh_ids']" position="before">
<field name="repair_steps" widget="radio" />
</xpath>
<xpath expr="//field[@name='wh_output_stock_loc_id']" position="after">
<field name="repair_location_id" />
</xpath>
<xpath expr="//field[@name='out_type_id']" position="after">
<field
name="add_c_type_id"
attrs="{'invisible': [('repair_steps', 'not in', ['2_steps', '3_steps'])]}"
/>
<field
name="remove_c_type_id"
attrs="{'invisible': [('repair_steps', '!=', '3_steps')]}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
../../../../repair_picking

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)