[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:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user