[IMP] repair,stock: allow creation/link of repairs from returns

This feature supports the use case of:
- a customer receives a delivery from you.
- there is a defective product in the delivery
- they schedule a repair (RO) + return (picking) for the product

We want to ease the creation/management of the repair order associated
with the return, therefore the following features are added:

- Create links between ROs and return pickings (smart button in return
  of all repairs associated with it + field in RO)
- Option (requires active setting) to create RO(s) directly from a
  return picking

Implementation notes:
- Because return picking type doesn't have its own "Type of Operation"
  (it is 'code'='incoming'), we default to only incoming picking types
  that have at least 1 other picking type it is the
  'return_picking_type_id' of.
- Originally a Wizard for choosing which/how many products should have
  ROs created for them was designed (i.e. to match return wizard + to
  multi-create ROs), but this was determined to be too complicated due
  to SN/lot selection. Instead simple approach of creating a single RO
  each time button is pushed was implemented to match the helpdesk >
  repair button workflow.
- ROs are purposely allowed for 'draft' and 'cancelled' returns/moves to
  support different types of workflows (e.g. RO is confirmed before
  return is)

closes odoo/odoo#85157

Task: 2732517
Signed-off-by: Arnold Moyaux <arm@odoo.com>
This commit is contained in:
Tiffany Chang (tic)
2022-03-28 18:59:57 +02:00
parent bee16a76ab
commit bee32f88d4
8 changed files with 151 additions and 3 deletions
+1
View File
@@ -30,6 +30,7 @@ The following topics are covered by this module:
'views/stock_move_views.xml',
'views/repair_views.xml',
'views/stock_lot_views.xml',
'views/stock_picking_views.xml',
'report/repair_reports.xml',
'report/repair_templates_repair_order.xml',
'data/ir_sequence_data.xml',
+1
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import repair
from . import stock_picking
from . import stock_traceability
from . import stock_lot
from . import account_move
+16 -1
View File
@@ -110,7 +110,16 @@ class Repair(models.Model):
'res.company', 'Company',
readonly=True, required=True, index=True,
default=lambda self: self.env.company)
sale_order_id = fields.Many2one('sale.order', 'Sale Order', copy=False, help="Sale Order from which the product to be repaired comes from.")
sale_order_id = fields.Many2one(
'sale.order', 'Sale Order', check_company=True,
copy=False, help="Sale Order from which the product to be repaired comes from.")
picking_id = fields.Many2one(
'stock.picking', 'Return', check_company=True,
domain="[('is_repairable', '!=', False), ('company_id', '=', company_id)]",
copy=False, help="Return Order from which the product to be repaired comes from.")
is_returned = fields.Boolean(
"Returned", compute='_compute_is_returned',
help="True if this repair is linked to a Return Order and the order is 'Done'. False otherwise.")
tag_ids = fields.Many2many('repair.tags', string="Tags")
invoiced = fields.Boolean('Invoiced', copy=False, readonly=True)
repaired = fields.Boolean('Repaired', copy=False, readonly=True)
@@ -127,6 +136,12 @@ class Repair(models.Model):
if order.partner_id:
order.default_address_id = order.partner_id.address_get(['contact'])['contact']
@api.depends('picking_id', 'picking_id.state')
def _compute_is_returned(self):
self.is_returned = False
returned = self.filtered(lambda r: r.picking_id and r.picking_id.state == 'done')
returned.is_returned = True
@api.depends('operations.price_subtotal', 'invoice_method', 'fees_lines.price_subtotal', 'pricelist_id.currency_id')
def _amount_untaxed(self):
for order in self:
+69
View File
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class PickingType(models.Model):
_inherit = 'stock.picking.type'
is_repairable = fields.Boolean(
'Create Repair Orders from Returns',
compute='_compute_is_repairable', store=True, readonly=False,
help="If ticked, you will be able to directly create repair orders from a return.")
return_type_of_ids = fields.One2many('stock.picking.type', 'return_picking_type_id')
@api.depends('return_type_of_ids', 'code')
def _compute_is_repairable(self):
for picking_type in self:
if not(picking_type.code == 'incoming' and picking_type.return_type_of_ids):
picking_type.is_repairable = False
class Picking(models.Model):
_inherit = 'stock.picking'
is_repairable = fields.Boolean(related='picking_type_id.is_repairable')
repair_ids = fields.One2many('repair.order', 'picking_id')
nbr_repairs = fields.Integer('Number of repairs linked to this picking', compute='_compute_nbr_repairs')
@api.depends('repair_ids')
def _compute_nbr_repairs(self):
for picking in self:
picking.nbr_repairs = len(picking.repair_ids)
def action_repair_return(self):
self.ensure_one()
ctx = self.env.context.copy()
ctx.update({
'default_location_id': self.location_dest_id.id,
'default_picking_id': self.id,
'default_partner_id': self.partner_id and self.partner_id.id or False,
})
return {
'name': _('Create Repair'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'repair.order',
'view_id': self.env.ref('repair.view_repair_order_form').id,
'context': ctx,
}
def action_view_repairs(self):
if self.repair_ids:
action = {
'res_model': 'repair.order',
'type': 'ir.actions.act_window',
}
if len(self.repair_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': self.repair_ids[0].id,
})
else:
action.update({
'name': _('Repair Orders'),
'view_mode': 'tree,form',
'domain': [('id', 'in', self.repair_ids.ids)],
})
return action
+22
View File
@@ -321,3 +321,25 @@ class TestRepair(AccountTestInvoicingCommon):
# tax02 should not be present since it belongs to the second company.
self.assertEqual(repair_order.operations.tax_id, tax01)
self.assertEqual(repair_order.fees_lines.tax_id, tax01)
def test_repair_return(self):
"""Tests functionality of creating a repair directly from a return picking,
i.e. repair can be made and defaults to appropriate return values. """
# test return
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.stock_warehouse.return_type_id
picking_form.partner_id = self.res_partner_1
picking_form.location_dest_id = self.stock_location_14
return_picking = picking_form.save()
# create repair
res_dict = return_picking.action_repair_return()
repair_form = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context']))
repair_form.product_id = self.product_product_3
repair = repair_form.save()
# test that the resulting repairs are correctly created
self.assertEqual(len(return_picking.repair_ids), 1, "A repair order should have been created and linked to original return.")
for repair in return_picking.repair_ids:
self.assertEqual(repair.location_id, return_picking.location_dest_id, "Repair location should have defaulted to return destination location")
self.assertEqual(repair.partner_id, return_picking.partner_id, "Repair customer should have defaulted to return customer")
+5 -1
View File
@@ -6,7 +6,7 @@
<field name="name">repair.tree</field>
<field name="model">repair.order</field>
<field name="arch" type="xml">
<tree string="Repairs order" multi_edit="1" sample="1">
<tree string="Repairs order" multi_edit="1" sample="1" decoration-info="state == 'draft'">
<field name="priority" optional="show" widget="priority" nolabel="1"/>
<field name="name"/>
<field name="schedule_date" optional="show" widget="remaining_days"/>
@@ -18,6 +18,8 @@
<field name="partner_id" readonly="1" optional="show"/>
<field name="address_id" optional="show"/>
<field name="guarantee_limit" optional="show"/>
<field name="picking_id" optional="hide"/>
<field name="is_returned" optional="hide"/>
<field name="sale_order_id" optional="show"/>
<field name="location_id" optional="hide"/>
<field name="company_id" groups="base.group_multi_company" readonly="1" optional="show"/>
@@ -84,6 +86,7 @@
<field name="partner_id" widget="res_partner_many2one" attrs="{'required':[('invoice_method','!=','none')]}" context="{'res_partner_search_mode': 'customer', 'show_vat': True}"/>
<field name="address_id" groups="account.group_delivery_invoice_address"/>
<field name="sale_order_id"/>
<field name="picking_id" options="{'no_create': True}"/>
<field name="user_id" domain="[('share', '=', False)]"/>
</group>
<group>
@@ -280,6 +283,7 @@
<filter string="Quotations" name="quotations" domain="[('state', '=', 'draft')]"/>
<filter string="Confirmed" domain="[('state', '=', 'confirmed')]" name="current" />
<filter string="Ready To Repair" name="ready_to_repair" domain="[('state', '=', 'ready')]"/>
<filter string="Returned" name="is_returned" domain="[('picking_id', '!=', False), ('picking_id.state', '=', 'done'), ('state', 'not in', ['cancel', 'done'])]"/>
<separator/>
<filter string="Invoiced" name="invoiced" domain="[('invoiced', '=', True)]"/>
<separator/>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="repair_view_picking_type_form" model="ir.ui.view">
<field name="name">stock.picking.type.inherit.repair</field>
<field name="model">stock.picking.type</field>
<field name="inherit_id" ref="stock.view_picking_type_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='locations']" position="after">
<field name="return_type_of_ids" invisible="1"/>
<group string="Repairs" attrs="{'invisible': ['|', ('code', '!=', 'incoming'), ('return_type_of_ids', '=', [])]}">
<field name="is_repairable"/>
</group>
</xpath>
</field>
</record>
<record id="repair_view_picking_form" model="ir.ui.view">
<field name="name">stock.picking.form.inherit.repair</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='%(stock.act_stock_return_picking)d']" position="after">
<field name="is_repairable" invisible="1"/>
<button string="Repair" name="action_repair_return" type="object" attrs="{'invisible': [('is_repairable', '=', False)]}" data-hotkey="shift+k"/>
</xpath>
<xpath expr="//button[@name='action_see_packages']" position="after">
<field name="repair_ids" invisible="1"/>
<button name="action_view_repairs" type="object"
class="oe_stat_button" icon="fa-wrench"
attrs="{'invisible': [('nbr_repairs', '=', 0)]}">
<field name="nbr_repairs" string="Repair Orders" widget="statinfo"/>
</button>
</xpath>
</field>
</record>
</odoo>
+1 -1
View File
@@ -71,7 +71,7 @@
<!-- As this group will be hidden without multi_loccation, you will not be able to create a
picking type with the code 'Internal', which make sense, but as the field 'code' on picking
types can't be partially hidden, you can still select the code internal in the form view -->
<group string="Locations" groups="stock.group_stock_multi_locations">
<group string="Locations" groups="stock.group_stock_multi_locations" name="locations">
<field name="default_location_src_id" options="{'no_create': True}" attrs="{'required': [('code', 'in', ('internal', 'outgoing'))]}"/>
<field name="default_location_dest_id" options="{'no_create': True}" attrs="{'required': [('code', 'in', ('internal', 'incoming'))]}"/>
</group>