19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -2,9 +2,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import repair
from . import stock_move
from . import stock_move_line
from . import stock_picking
from . import stock_traceability
from . import stock_lot
from . import account_move
from . import product
from . import mail_compose_message
from . import sale_order
from . import stock_warehouse

View file

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class AccountMove(models.Model):
_inherit = 'account.move'
repair_ids = fields.One2many('repair.order', 'invoice_id', readonly=True, copy=False)
def unlink(self):
repairs = self.sudo().repair_ids.filtered(lambda repair: repair.state != 'cancel')
if repairs:
repairs.sudo(False).state = '2binvoiced'
return super().unlink()
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
repair_line_ids = fields.One2many('repair.line', 'invoice_line_id', readonly=True, copy=False)
repair_fee_ids = fields.One2many('repair.fee', 'invoice_line_id', readonly=True, copy=False)
def _stock_account_get_anglo_saxon_price_unit(self):
price_unit = super()._stock_account_get_anglo_saxon_price_unit()
ro_line = self.sudo().repair_line_ids
if ro_line:
am = ro_line.invoice_line_id.move_id.sudo(False)
sm = ro_line.move_id.sudo(False)
price_unit = self._deduce_anglo_saxon_unit_price(am, sm)
return price_unit

View file

@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class MailComposeMessage(models.TransientModel):
_inherit = 'mail.compose.message'
def _action_send_mail(self, auto_commit=False):
if self.model == 'repair.order':
self = self.with_context(mail_notify_author=self.env.user.partner_id in self.partner_ids)
return super(MailComposeMessage, self)._action_send_mail(auto_commit=auto_commit)

View file

@ -1,25 +1,57 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class Product(models.Model):
class ProductProduct(models.Model):
_inherit = "product.product"
def _count_returned_sn_products(self, sn_lot):
remove_count = self.env['repair.line'].search_count([
('type', '=', 'remove'),
('product_uom_qty', '=', 1),
('lot_id', '=', sn_lot.id),
('state', '=', 'done'),
('location_dest_id.usage', '=', 'internal'),
product_catalog_product_is_in_repair = fields.Boolean(
compute='_compute_product_is_in_repair',
search='_search_product_is_in_repair',
)
def _compute_product_is_in_repair(self):
# Just to enable the _search method
self.product_catalog_product_is_in_repair = False
def _search_product_is_in_repair(self, operator, value):
if operator != 'in':
return NotImplemented
product_ids = self.env['repair.order'].search([
('id', 'in', [self.env.context.get('order_id', '')]),
]).move_ids.product_id.ids
return [('id', 'in', product_ids)]
def _count_returned_sn_products_domain(self, sn_lot, or_domains):
or_domains.append([
('move_id.repair_line_type', 'in', ['remove', 'recycle']),
('location_dest_usage', '=', 'internal'),
])
add_count = self.env['repair.line'].search_count([
('type', '=', 'add'),
('product_uom_qty', '=', 1),
('lot_id', '=', sn_lot.id),
('state', '=', 'done'),
('location_dest_id.usage', '=', 'production'),
])
return super()._count_returned_sn_products(sn_lot) + (remove_count - add_count)
return super()._count_returned_sn_products_domain(sn_lot, or_domains)
def _update_uom(self, to_uom_id):
for uom, product, repairs in self.env['repair.order']._read_group(
[('product_id', 'in', self.ids)],
['product_uom', 'product_id'],
['id:recordset'],
):
if uom != product.product_tmpl_id.uom_id:
raise UserError(_(
'As other units of measure (ex : %(problem_uom)s) '
'than %(uom)s have already been used for this product, the change of unit of measure can not be done.'
'If you want to change it, please archive the product and create a new one.',
problem_uom=uom.display_name, uom=product.product_tmpl_id.uom_id.display_name))
repairs.product_uom = to_uom_id
return super()._update_uom(to_uom_id)
class ProductTemplate(models.Model):
_inherit = "product.template"
service_tracking = fields.Selection(selection_add=[('repair', 'Repair Order')],
ondelete={'repair': 'set default'})
@api.model
def _get_saleable_tracking_types(self):
return super()._get_saleable_tracking_types() + ['repair']

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,123 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import api, fields, models, _
class SaleOrder(models.Model):
_inherit = 'sale.order'
repair_order_ids = fields.One2many(
comodel_name='repair.order', inverse_name='sale_order_id',
string='Repair Order', groups='stock.group_stock_user')
repair_count = fields.Integer(
"Repair Order(s)", compute='_compute_repair_count', groups='stock.group_stock_user')
@api.depends('repair_order_ids')
def _compute_repair_count(self):
for order in self:
order.repair_count = len(order.repair_order_ids)
def _action_cancel(self):
res = super()._action_cancel()
self.order_line._cancel_repair_order()
return res
def _action_confirm(self):
res = super()._action_confirm()
self.order_line._create_repair_order()
return res
def action_show_repair(self):
self.ensure_one()
if self.repair_count == 1:
return {
"type": "ir.actions.act_window",
"res_model": "repair.order",
"views": [[False, "form"]],
"res_id": self.repair_order_ids.id,
}
elif self.repair_count > 1:
return {
"name": _("Repair Orders"),
"type": "ir.actions.act_window",
"res_model": "repair.order",
"view_mode": "list,form",
"domain": [('sale_order_id', '=', self.id)],
}
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
def _prepare_qty_delivered(self):
repair_delivered_qties = defaultdict(float)
remaining_so_lines = self
for so_line in self:
move = so_line.move_ids.sudo().filtered(lambda m: m.repair_id and m.state == 'done')
if len(move) != 1:
continue
remaining_so_lines -= so_line
repair_delivered_qties[so_line] = move.quantity
delivered_qties = super(SaleOrderLine, remaining_so_lines)._prepare_qty_delivered()
delivered_qties.update(repair_delivered_qties)
return delivered_qties
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
res.filtered(lambda line: line.state in ('sale', 'done'))._create_repair_order()
return res
def write(self, vals):
if 'product_uom_qty' in vals:
old_product_uom_qty = {line.id: line.product_uom_qty for line in self}
res = super().write(vals)
for line in self:
if line.state in ('sale', 'done') and line.product_id:
if line.product_uom_id.compare(old_product_uom_qty[line.id], 0) <= 0 and line.product_uom_id.compare(line.product_uom_qty, 0) > 0:
self._create_repair_order()
if line.product_uom_id.compare(old_product_uom_qty[line.id], 0) > 0 and line.product_uom_id.compare(line.product_uom_qty, 0) <= 0:
self._cancel_repair_order()
return res
return super().write(vals)
def _action_launch_stock_rule(self, **kwargs):
# Picking must be generated for products created from the SO but not for parts added from the RO, as they're already handled there
lines_without_repair_move = self.filtered(lambda line: not line.move_ids.sudo().repair_id)
return super(SaleOrderLine, lines_without_repair_move)._action_launch_stock_rule(**kwargs)
def _create_repair_order(self):
new_repair_vals = []
for line in self:
# One RO for each line with at least a quantity of 1, quantities > 1 don't create multiple ROs
if any(line.id == ro.sale_order_line_id.id for ro in line.order_id.sudo().repair_order_ids) and line.product_uom_id.compare(line.product_uom_qty, 0) > 0:
binded_ro_ids = line.order_id.sudo().repair_order_ids.filtered(lambda ro: ro.sale_order_line_id.id == line.id and ro.state == 'cancel')
binded_ro_ids.action_repair_cancel_draft()
binded_ro_ids._action_repair_confirm()
continue
if line.product_template_id.sudo().service_tracking != 'repair' or line.move_ids.sudo().repair_id or line.product_uom_id.compare(line.product_uom_qty, 0) <= 0:
continue
order = line.order_id
new_repair_vals.append({
'state': 'confirmed',
'partner_id': order.partner_id.id,
'sale_order_id': order.id,
'sale_order_line_id': line.id,
'picking_type_id': order.warehouse_id.repair_type_id.id,
})
if new_repair_vals:
self.env['repair.order'].sudo().create(new_repair_vals)
def _cancel_repair_order(self):
# Each RO binded to a SO line with Qty set to 0 or cancelled is set to 'Cancelled'
binded_ro_ids = self.env['repair.order']
for line in self:
binded_ro_ids |= line.order_id.sudo().repair_order_ids.filtered(lambda ro: ro.sale_order_line_id.id == line.id and ro.state != 'done')
binded_ro_ids.action_repair_cancel()
def has_valued_move_ids(self):
res = super().has_valued_move_ids()
return res and not self.move_ids.repair_id

View file

@ -2,21 +2,55 @@
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class StockLot(models.Model):
_inherit = 'stock.lot'
repair_order_ids = fields.Many2many('repair.order', string="Repair Orders", compute="_compute_repair_order_ids")
repair_order_count = fields.Integer('Repair order count', compute="_compute_repair_order_ids")
repair_line_ids = fields.Many2many('repair.order', string="Repair Orders", compute="_compute_repair_line_ids")
repair_part_count = fields.Integer('Repair part count', compute="_compute_repair_line_ids")
in_repair_count = fields.Integer('In repair count', compute="_compute_in_repair_count")
repaired_count = fields.Integer('Repaired count', compute='_compute_repaired_count')
@api.depends('name')
def _compute_repair_order_ids(self):
def _compute_repair_line_ids(self):
repair_orders = defaultdict(lambda: self.env['repair.order'])
for repair_line in self.env['repair.line'].search([('lot_id', 'in', self.ids), ('state', '=', 'done')]):
repair_orders[repair_line.lot_id.id] |= repair_line.repair_id
repair_moves = self.env['stock.move'].search([
('repair_id', '!=', False),
('repair_line_type', '!=', False),
('move_line_ids.lot_id', 'in', self.ids),
('state', '=', 'done')])
for repair_line in repair_moves:
for rl_id in repair_line.lot_ids.ids:
repair_orders[rl_id] |= repair_line.repair_id
for lot in self:
lot.repair_order_ids = repair_orders[lot.id]
lot.repair_order_count = len(lot.repair_order_ids)
lot.repair_line_ids = repair_orders[lot.id]
lot.repair_part_count = len(lot.repair_line_ids)
def _compute_in_repair_count(self):
lot_data = self.env['repair.order']._read_group([('lot_id', 'in', self.ids), ('state', 'not in', ('done', 'cancel'))], ['lot_id'], ['__count'])
result = {lot.id: count for lot, count in lot_data}
for lot in self:
lot.in_repair_count = result.get(lot.id, 0)
def _compute_repaired_count(self):
lot_data = self.env['repair.order']._read_group([('lot_id', 'in', self.ids), ('state', '=', 'done')], ['lot_id'], ['__count'])
result = {lot.id: count for lot, count in lot_data}
for lot in self:
lot.repaired_count = result.get(lot.id, 0)
def action_lot_open_repairs(self):
action = self.env["ir.actions.actions"]._for_xml_id("repair.action_repair_order_tree")
action.update({
'domain': [('lot_id', '=', self.id)],
'context': {
'default_product_id': self.product_id.id,
'default_repair_lot_id': self.id,
'default_company_id': self.company_id.id or self.env.company.id,
},
})
return action
def action_view_ro(self):
self.ensure_one()
@ -25,15 +59,23 @@ class StockLot(models.Model):
'res_model': 'repair.order',
'type': 'ir.actions.act_window'
}
if len(self.repair_order_ids) == 1:
if len(self.repair_line_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': self.repair_order_ids[0].id
'res_id': self.repair_line_ids[0].id
})
else:
action.update({
'name': _("Repair orders of %s", self.name),
'domain': [('id', 'in', self.repair_order_ids.ids)],
'view_mode': 'tree,form'
'domain': [('id', 'in', self.repair_line_ids.ids)],
'view_mode': 'list,form'
})
return action
def _check_create(self):
active_repair_id = self.env.context.get('active_repair_id')
if active_repair_id:
active_repair = self.env['repair.order'].browse(active_repair_id)
if active_repair and not active_repair.picking_type_id.use_create_lots:
raise UserError(_('You are not allowed to create a lot or serial number with this operation type. To change this, go on the operation type and tick the box "Create New Lots/Serial Numbers".'))
return super()._check_create()

View file

@ -0,0 +1,227 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, Command, fields, models
from odoo.tools.misc import groupby
MAP_REPAIR_LINE_TYPE_TO_MOVE_LOCATIONS_FROM_REPAIR = {
'add': {'location_id': 'location_id', 'location_dest_id': 'location_dest_id'},
'remove': {'location_id': 'location_dest_id', 'location_dest_id': 'parts_location_id'},
'recycle': {'location_id': 'location_dest_id', 'location_dest_id': 'recycle_location_id'},
}
class StockMove(models.Model):
_inherit = 'stock.move'
repair_id = fields.Many2one('repair.order', check_company=True, index='btree_not_null', copy=False, ondelete='cascade')
repair_line_type = fields.Selection([
('add', 'Add'),
('remove', 'Remove'),
('recycle', 'Recycle')
], 'Type', store=True, index=True)
@api.depends('repair_line_type')
def _compute_forecast_information(self):
moves_to_compute = self.filtered(lambda move: not move.repair_line_type or move.repair_line_type == 'add')
for move in (self - moves_to_compute):
move.forecast_availability = move.product_qty
move.forecast_expected_date = False
return super(StockMove, moves_to_compute)._compute_forecast_information()
@api.depends('repair_id.picking_type_id')
def _compute_picking_type_id(self):
remaining_moves = self
for move in self:
if move.repair_id:
move.picking_type_id = move.repair_id.picking_type_id
remaining_moves -= move
return super(StockMove, remaining_moves)._compute_picking_type_id()
@api.depends('repair_id.location_id', 'repair_line_type')
def _compute_location_id(self):
ids_to_super = set()
for move in self:
if move.repair_id and move.repair_line_type:
move.location_id = move.repair_id[
MAP_REPAIR_LINE_TYPE_TO_MOVE_LOCATIONS_FROM_REPAIR[move.repair_line_type]['location_id']
]
else:
ids_to_super.add(move.id)
return super(StockMove, self.browse(ids_to_super))._compute_location_id()
@api.depends('repair_id.location_dest_id', 'repair_line_type')
def _compute_location_dest_id(self):
ids_to_super = set()
for move in self:
if move.repair_id and move.repair_line_type:
move.location_dest_id = move.repair_id[
MAP_REPAIR_LINE_TYPE_TO_MOVE_LOCATIONS_FROM_REPAIR[move.repair_line_type]['location_dest_id']
]
else:
ids_to_super.add(move.id)
return super(StockMove, self.browse(ids_to_super))._compute_location_dest_id()
@api.depends('repair_id.name')
def _compute_reference(self):
moves_with_reference = set()
for move in self:
if move.repair_id and move.repair_id.name:
move.reference = move.repair_id.name
moves_with_reference.add(move.id)
super(StockMove, self - self.env['stock.move'].browse(moves_with_reference))._compute_reference()
def copy_data(self, default=None):
default = dict(default or {})
vals_list = super().copy_data(default=default)
for move, vals in zip(self, vals_list):
if 'repair_id' in default or move.repair_id:
vals['sale_line_id'] = False
return vals_list
@api.ondelete(at_uninstall=False)
def _unlink_if_draft_or_cancel(self):
self.filtered('repair_id')._action_cancel()
return super()._unlink_if_draft_or_cancel()
def unlink(self):
self._clean_repair_sale_order_line()
return super().unlink()
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('repair_id') or 'repair_line_type' not in vals:
continue
repair_id = self.env['repair.order'].browse([vals['repair_id']])
vals['origin'] = repair_id.name
moves = super().create(vals_list)
repair_moves = self.env['stock.move']
for move in moves:
if not move.repair_id:
continue
move.reference_ids = [Command.link(r.id) for r in move.repair_id.reference_ids]
move.picking_type_id = move.repair_id.picking_type_id.id
repair_moves |= move
no_repair_moves = moves - repair_moves
draft_repair_moves = repair_moves.filtered(lambda m: m.state == 'draft' and m.repair_id.state in ('confirmed', 'under_repair'))
other_repair_moves = repair_moves - draft_repair_moves
draft_repair_moves._check_company()
draft_repair_moves._adjust_procure_method(picking_type_code='repair_operation')
res = draft_repair_moves._action_confirm()
res._trigger_scheduler()
confirmed_repair_moves = (res | other_repair_moves)
confirmed_repair_moves._create_repair_sale_order_line()
return (confirmed_repair_moves | no_repair_moves)
def write(self, vals):
res = super().write(vals)
repair_moves = self.env['stock.move']
moves_to_create_so_line = self.env['stock.move']
for move in self:
if not move.repair_id:
continue
# checks vals update
if not move.sale_line_id and 'sale_line_id' not in vals and move.repair_line_type == 'add':
moves_to_create_so_line |= move
if move.sale_line_id and ('repair_line_type' in vals or 'product_uom_qty' in vals):
repair_moves |= move
repair_moves._update_repair_sale_order_line()
moves_to_create_so_line._create_repair_sale_order_line()
return res
def action_add_from_catalog_repair(self):
repair_order = self.env['repair.order'].browse(self.env.context.get('order_id'))
return repair_order.action_add_from_catalog()
# Needed to also cancel the lastly added part
def _action_cancel(self):
self._clean_repair_sale_order_line()
return super()._action_cancel()
def _create_repair_sale_order_line(self):
if not self:
return
so_line_vals = []
for move in self:
if move.sale_line_id or move.repair_line_type != 'add' or not move.repair_id.sale_order_id:
continue
product_qty = move.product_uom_qty if move.repair_id.state != 'done' else move.quantity
so_line_vals.append({
'order_id': move.repair_id.sale_order_id.id,
'product_id': move.product_id.id,
'product_uom_qty': product_qty, # When relying only on so_line compute method, the sol quantity is only updated on next sol creation
'product_uom_id': move.product_uom.id,
'move_ids': [Command.link(move.id)],
'qty_delivered': move.quantity if move.state == 'done' else 0.0,
})
if move.repair_id.under_warranty:
so_line_vals[-1]['price_unit'] = 0.0
elif move.price_unit:
so_line_vals[-1]['price_unit'] = move.price_unit
self.env['sale.order.line'].create(so_line_vals)
def _clean_repair_sale_order_line(self):
self.filtered(
lambda m: m.repair_id and m.sale_line_id
).mapped('sale_line_id').write({'product_uom_qty': 0.0})
def _update_repair_sale_order_line(self):
if not self:
return
moves_to_clean = self.env['stock.move']
moves_to_update = self.env['stock.move']
for move in self:
if not move.repair_id:
continue
if move.sale_line_id and move.repair_line_type != 'add':
moves_to_clean |= move
if move.sale_line_id and move.repair_line_type == 'add':
moves_to_update |= move
moves_to_clean._clean_repair_sale_order_line()
for sale_line, _ in groupby(moves_to_update, lambda m: m.sale_line_id):
sale_line.product_uom_qty = sum(sale_line.move_ids.mapped('product_uom_qty'))
def _is_consuming(self):
return super()._is_consuming() or (self.repair_id and self.repair_line_type == 'add')
def _get_repair_locations(self, repair_line_type, repair_id=False):
location_map = MAP_REPAIR_LINE_TYPE_TO_MOVE_LOCATIONS_FROM_REPAIR.get(repair_line_type)
if location_map:
if not repair_id:
self.repair_id.ensure_one()
repair_id = self.repair_id
location_id, location_dest_id = [repair_id[field] for field in location_map.values()]
else:
location_id, location_dest_id = False, False
return location_id, location_dest_id
def _get_source_document(self):
return self.repair_id or super()._get_source_document()
def _set_repair_locations(self):
moves_per_repair = self.filtered(lambda m: (m.repair_id and m.repair_line_type) is not False).grouped('repair_id')
if not moves_per_repair:
return
for moves in moves_per_repair.values():
grouped_moves = moves.grouped('repair_line_type')
for line_type, m in grouped_moves.items():
m.location_id, m.location_dest_id = m._get_repair_locations(line_type)
def _should_be_assigned(self):
if self.repair_id:
return False
return super()._should_be_assigned()
def _split(self, qty, restrict_partner_id=False):
# When setting the Repair Order as done with partially done moves, do not split these moves
if self.repair_id:
return []
return super(StockMove, self)._split(qty, restrict_partner_id)
def action_show_details(self):
action = super().action_show_details()
if self.repair_line_type == 'recycle':
action['context'].update({'show_quant': False, 'show_destination_location': True})
return action

View file

@ -0,0 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class StockMoveLine(models.Model):
_inherit = 'stock.move.line'
def _should_show_lot_in_invoice(self):
return super()._should_show_lot_in_invoice() or self.move_id.repair_line_type

View file

@ -1,29 +1,182 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from odoo import _, api, fields, models
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools.misc import clean_context
class PickingType(models.Model):
class StockPickingType(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')
code = fields.Selection(selection_add=[
('repair_operation', 'Repair')
], ondelete={'repair_operation': 'cascade'})
@api.depends('return_type_of_ids', 'code')
def _compute_is_repairable(self):
count_repair_confirmed = fields.Integer(
string="Number of Repair Orders Confirmed", compute='_compute_count_repair')
count_repair_under_repair = fields.Integer(
string="Number of Repair Orders Under Repair", compute='_compute_count_repair')
count_repair_ready = fields.Integer(
string="Number of Repair Orders to Process", compute='_compute_count_repair')
count_repair_late = fields.Integer(
string="Number of Late Repair Orders", compute='_compute_count_repair')
default_product_location_src_id = fields.Many2one(
'stock.location', 'Product Source Location', compute='_compute_default_product_location_id',
check_company=True, store=True, readonly=False, precompute=True,
help="This is the default source location for the product to be repaired in repair orders with this operation type.")
default_product_location_dest_id = fields.Many2one(
'stock.location', 'Product Destination Location', compute='_compute_default_product_location_id',
check_company=True, store=True, readonly=False, precompute=True,
help="This is the default destination location for the product to be repaired in repair orders with this operation type.")
default_remove_location_dest_id = fields.Many2one(
'stock.location', 'Remove Destination Location', compute='_compute_default_remove_location_dest_id',
check_company=True, store=True, readonly=False, precompute=True,
help="This is the default remove destination location when you create a repair order with this operation type.")
default_recycle_location_dest_id = fields.Many2one(
'stock.location', 'Recycle Destination Location', compute='_compute_default_recycle_location_dest_id',
check_company=True, store=True, readonly=False, precompute=True,
help="This is the default recycle destination location when you create a repair order with this operation type.")
repair_properties_definition = fields.PropertiesDefinition('Repair Properties')
def _compute_count_repair(self):
repair_picking_types = self.filtered(lambda picking: picking.code == 'repair_operation')
# By default, set count_repair_xxx to False
self.count_repair_ready = False
self.count_repair_confirmed = False
self.count_repair_under_repair = False
self.count_repair_late = False
# shortcut
if not repair_picking_types:
return
picking_types = self.env['repair.order']._read_group(
[
('picking_type_id', 'in', repair_picking_types.ids),
('state', 'in', ('confirmed', 'under_repair')),
],
groupby=['picking_type_id', 'is_parts_available', 'state'],
aggregates=['id:count']
)
late_repairs = self.env['repair.order']._read_group(
[
('picking_type_id', 'in', repair_picking_types.ids),
('state', '=', 'confirmed'),
'|',
('schedule_date', '<', fields.Date.today()),
('is_parts_late', '=', True),
],
groupby=['picking_type_id'],
aggregates=['__count']
)
late_repairs = {pt.id: late_count for pt, late_count in late_repairs}
counts = {}
for pt in picking_types:
pt_count = counts.setdefault(pt[0].id, {})
# Only confirmed repairs (not "under repair" ones) are considered as ready
if pt[1] and pt[2] == 'confirmed':
pt_count.setdefault('ready', 0)
pt_count['ready'] += pt[3]
pt_count.setdefault(pt[2], 0)
pt_count[pt[2]] += pt[3]
for pt in repair_picking_types:
if pt.id not in counts:
continue
pt.count_repair_ready = counts[pt.id].get('ready')
pt.count_repair_confirmed = counts[pt.id].get('confirmed')
pt.count_repair_under_repair = counts[pt.id].get('under_repair')
pt.count_repair_late = late_repairs.get(pt.id, 0)
def _compute_default_location_src_id(self):
remaining_picking_type = self.env['stock.picking.type']
for picking_type in self:
if not(picking_type.code == 'incoming' and picking_type.return_type_of_ids):
picking_type.is_repairable = False
if picking_type.code != 'repair_operation':
remaining_picking_type |= picking_type
continue
stock_location = picking_type.warehouse_id.lot_stock_id
picking_type.default_location_src_id = stock_location.id
super(StockPickingType, remaining_picking_type)._compute_default_location_src_id()
def _compute_default_location_dest_id(self):
repair_picking_type = self.filtered(lambda pt: pt.code == 'repair_operation')
prod_locations = self.env['stock.location']._read_group(
[('usage', '=', 'production'), ('company_id', 'in', repair_picking_type.company_id.ids)],
['company_id'],
['id:min'],
)
prod_locations = {l[0].id: l[1] for l in prod_locations}
for picking_type in repair_picking_type:
picking_type.default_location_dest_id = prod_locations.get(picking_type.company_id.id)
super(StockPickingType, (self - repair_picking_type))._compute_default_location_dest_id()
@api.depends('code')
def _compute_default_product_location_id(self):
for picking_type in self:
if picking_type.code == 'repair_operation':
stock_location = picking_type.warehouse_id.lot_stock_id
picking_type.default_product_location_src_id = stock_location.id
picking_type.default_product_location_dest_id = stock_location.id
@api.depends('code')
def _compute_default_remove_location_dest_id(self):
repair_picking_type = self.filtered(lambda pt: pt.code == 'repair_operation')
company_ids = repair_picking_type.company_id.ids
company_ids.append(False)
scrap_locations = self.env['stock.location']._read_group(
[('usage', '=', 'inventory'), ('company_id', 'in', company_ids)],
['company_id'],
['id:min'],
)
scrap_locations = {l[0].id: l[1] for l in scrap_locations}
for picking_type in repair_picking_type:
picking_type.default_remove_location_dest_id = scrap_locations.get(picking_type.company_id.id)
@api.depends('code')
def _compute_default_recycle_location_dest_id(self):
for picking_type in self:
if picking_type.code == 'repair_operation':
stock_location = picking_type.warehouse_id.lot_stock_id
picking_type.default_recycle_location_dest_id = stock_location.id
def get_repair_stock_picking_action_picking_type(self):
action = self.env["ir.actions.actions"]._for_xml_id('repair.action_picking_repair')
if self:
action['display_name'] = self.display_name
return action
def _get_aggregated_records_by_date(self):
repair_picking_types = self.filtered(lambda picking: picking.code == 'repair_operation')
other_picking_types = (self - repair_picking_types)
records = super(StockPickingType, other_picking_types)._get_aggregated_records_by_date()
repair_records = self.env['repair.order']._read_group(
[
('picking_type_id', 'in', repair_picking_types.ids),
('state', '=', 'confirmed')
],
['picking_type_id'],
['schedule_date' + ':array_agg'],
)
# Make sure that all picking type IDs are represented, even if empty
picking_type_id_to_dates = {i: [] for i in repair_picking_types.ids}
picking_type_id_to_dates.update({r[0].id: r[1] for r in repair_records})
label = self.env._('Confirmed')
repair_records = [(i, d, label) for i, d in picking_type_id_to_dates.items()]
return records + repair_records
class Picking(models.Model):
class StockPicking(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')
@ -34,10 +187,11 @@ class Picking(models.Model):
def action_repair_return(self):
self.ensure_one()
ctx = self.env.context.copy()
ctx = clean_context(self.env.context.copy())
warehouse = self.picking_type_id.warehouse_id or self.env.user._get_default_warehouse_id()
ctx.update({
'default_location_id': self.location_dest_id.id,
'default_picking_id': self.id,
'default_repair_picking_id': self.id,
'default_picking_type_id': warehouse.repair_type_id.id,
'default_partner_id': self.partner_id and self.partner_id.id or False,
})
return {
@ -63,7 +217,24 @@ class Picking(models.Model):
else:
action.update({
'name': _('Repair Orders'),
'view_mode': 'tree,form',
'view_mode': 'list,form',
'domain': [('id', 'in', self.repair_ids.ids)],
})
return action
@api.model
def get_action_click_graph(self):
picking_type_code = self.env["stock.picking.type"].browse(
self.env.context["picking_type_id"]
).code
if picking_type_code == "repair_operation":
action = self._get_action("repair.action_picking_repair_graph")
if self:
action["context"].update({
"default_picking_type_id": self.picking_type_id,
"picking_type_id": self.picking_type_id,
})
return action
return super().get_action_click_graph()

View file

@ -3,12 +3,12 @@
from odoo import models, api
class MrpStockReport(models.TransientModel):
class StockTraceabilityReport(models.TransientModel):
_inherit = 'stock.traceability.report'
@api.model
def _get_reference(self, move_line):
res_model, res_id, ref = super(MrpStockReport, self)._get_reference(move_line)
res_model, res_id, ref = super()._get_reference(move_line)
if move_line.move_id.repair_id:
res_model = 'repair.order'
res_id = move_line.move_id.repair_id.id
@ -17,7 +17,7 @@ class MrpStockReport(models.TransientModel):
@api.model
def _get_linked_move_lines(self, move_line):
move_lines, is_used = super(MrpStockReport, self)._get_linked_move_lines(move_line)
move_lines, is_used = super()._get_linked_move_lines(move_line)
if not move_lines:
move_lines = move_line.move_id.repair_id and move_line.consume_line_ids
if not is_used:

View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import UserError
class StockWarehouse(models.Model):
_inherit = 'stock.warehouse'
repair_type_id = fields.Many2one('stock.picking.type', 'Repair Operation Type', check_company=True, copy=False)
repair_mto_pull_id = fields.Many2one(
'stock.rule', 'Repair MTO Rule', copy=False)
def _get_sequence_values(self, name=False, code=False):
values = super(StockWarehouse, self)._get_sequence_values(name=name, code=code)
values.update({
'repair_type_id': {
'name': _('%(name)s Sequence repair', name=self.name),
'prefix': self.code + '/' + (self.repair_type_id.sequence_code or 'RO') + '/',
'padding': 5,
'company_id': self.company_id.id
},
})
return values
def _get_picking_type_create_values(self, max_sequence):
data, next_sequence = super(StockWarehouse, self)._get_picking_type_create_values(max_sequence)
prod_location = self._get_production_location()
scrap_location = self.env['stock.location'].search([
('usage', '=', 'inventory'),
('company_id', 'in', [self.company_id.id, False]),
], limit=1)
if not scrap_location:
raise UserError(_("No location of type Inventory Loss found"))
data.update({
'repair_type_id': {
'name': _('Repairs'),
'code': 'repair_operation',
'default_location_src_id': self.lot_stock_id.id,
'default_location_dest_id': prod_location.id,
'default_remove_location_dest_id': scrap_location.id,
'default_recycle_location_dest_id': self.lot_stock_id.id,
'sequence': next_sequence + 1,
'sequence_code': 'RO',
'company_id': self.company_id.id,
'use_create_lots': True,
'use_existing_lots': True,
},
})
return data, max_sequence + 2
def _get_picking_type_update_values(self):
data = super(StockWarehouse, self)._get_picking_type_update_values()
data.update({
'repair_type_id': {
'active': self.active,
'barcode': self.code.replace(" ", "").upper() + "RO",
},
})
return data
@api.model
def _get_production_location(self):
location = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.company_id.id)], limit=1)
if not location:
raise UserError(_("Can't find any production location."))
return location
def _create_missing_locations(self, vals):
super()._create_missing_locations(vals)
for company_id in self.company_id:
location = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', company_id.id)], limit=1)
if not location:
company_id._create_production_location()
def _generate_global_route_rules_values(self):
rules = super()._generate_global_route_rules_values()
production_location = self._get_production_location()
rules.update({
'repair_mto_pull_id': {
'depends': ['repair_type_id'],
'create_values': {
'procure_method': 'make_to_order',
'company_id': self.company_id.id,
'action': 'pull',
'auto': 'manual',
'route_id': self._find_or_create_global_route('stock.route_warehouse0_mto', _('Replenish on Order (MTO)')).id,
'location_dest_id': self.repair_type_id.default_location_dest_id.id,
'location_src_id': self.repair_type_id.default_location_src_id.id,
'picking_type_id': self.repair_type_id.id
},
'update_values': {
'name': self._format_rulename(self.lot_stock_id, production_location, 'MTO'),
'active': True,
},
},
})
return rules