mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-24 13:32:01 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
123
odoo-bringout-oca-ocb-repair/repair/models/sale_order.py
Normal file
123
odoo-bringout-oca-ocb-repair/repair/models/sale_order.py
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
227
odoo-bringout-oca-ocb-repair/repair/models/stock_move.py
Normal file
227
odoo-bringout-oca-ocb-repair/repair/models/stock_move.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
100
odoo-bringout-oca-ocb-repair/repair/models/stock_warehouse.py
Normal file
100
odoo-bringout-oca-ocb-repair/repair/models/stock_warehouse.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue