Files
odoo_source/addons/mrp/models/mrp_unbuild.py
T
Swapnesh Shah acb9e5fe49 [FIX] mrp: do now allow to select cancelled mo
Before this commit, User can select Cancelled MO before selecting Product.
but it is already blocking since we already have constrains to not allow ubnuilding
Cancelled MO.

With this commit, We are always showing Done MO.

closes odoo/odoo#70316

X-original-commit: 48c0efcc97e35e00ec5803235a89a2c18e821990
Signed-off-by: Rémy Voet <ryv-odoo@users.noreply.github.com>
2021-05-04 10:31:16 +00:00

305 lines
15 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import AccessError, UserError
from odoo.tools import float_compare
from odoo.osv import expression
class MrpUnbuild(models.Model):
_name = "mrp.unbuild"
_description = "Unbuild Order"
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'id desc'
name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New'))
product_id = fields.Many2one(
'product.product', 'Product', check_company=True,
domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
required=True, states={'done': [('readonly', True)]})
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda s: s.env.company,
required=True, index=True, states={'done': [('readonly', True)]})
product_qty = fields.Float(
'Quantity', default=1.0,
required=True, states={'done': [('readonly', True)]})
product_uom_id = fields.Many2one(
'uom.uom', 'Unit of Measure',
required=True, states={'done': [('readonly', True)]})
bom_id = fields.Many2one(
'mrp.bom', 'Bill of Material',
domain="""[
'|',
('product_id', '=', product_id),
'&',
('product_tmpl_id.product_variant_ids', '=', product_id),
('product_id','=',False),
('type', '=', 'normal'),
'|',
('company_id', '=', company_id),
('company_id', '=', False)
]
""",
states={'done': [('readonly', True)]}, check_company=True)
mo_id = fields.Many2one(
'mrp.production', 'Manufacturing Order',
domain="[('id', 'in', allowed_mo_ids)]",
states={'done': [('readonly', True)]}, check_company=True)
mo_bom_id = fields.Many2one('mrp.bom', 'Bill of Material used on the Production Order', related='mo_id.bom_id')
lot_id = fields.Many2one(
'stock.production.lot', 'Lot/Serial Number',
domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True,
states={'done': [('readonly', True)]}, help="Lot/Serial Number of the product to unbuild.")
has_tracking=fields.Selection(related='product_id.tracking', readonly=True)
location_id = fields.Many2one(
'stock.location', 'Source Location',
domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
check_company=True,
required=True, states={'done': [('readonly', True)]}, help="Location where the product you want to unbuild is.")
location_dest_id = fields.Many2one(
'stock.location', 'Destination Location',
domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
check_company=True,
required=True, states={'done': [('readonly', True)]}, help="Location where you want to send the components resulting from the unbuild order.")
consume_line_ids = fields.One2many(
'stock.move', 'consume_unbuild_id', readonly=True,
string='Consumed Disassembly Lines')
produce_line_ids = fields.One2many(
'stock.move', 'unbuild_id', readonly=True,
string='Processed Disassembly Lines')
state = fields.Selection([
('draft', 'Draft'),
('done', 'Done')], string='Status', default='draft')
allowed_mo_ids = fields.One2many('mrp.production', compute='_compute_allowed_mo_ids')
@api.depends('company_id', 'product_id')
def _compute_allowed_mo_ids(self):
for unbuild in self:
domain = [
('state', '=', 'done'),
('company_id', '=', unbuild.company_id.id)
]
if unbuild.product_id:
domain = expression.AND([domain, [('product_id', '=', unbuild.product_id.id)]])
allowed_mos = self.env['mrp.production'].search_read(domain, ['id'])
if allowed_mos:
unbuild.allowed_mo_ids = [mo['id'] for mo in allowed_mos]
else:
unbuild.allowed_mo_ids = False
@api.onchange('company_id')
def _onchange_company_id(self):
if self.company_id:
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.company_id.id)], limit=1)
if self.location_id.company_id != self.company_id:
self.location_id = warehouse.lot_stock_id
if self.location_dest_id.company_id != self.company_id:
self.location_dest_id = warehouse.lot_stock_id
else:
self.location_id = False
self.location_dest_id = False
@api.onchange('mo_id')
def _onchange_mo_id(self):
if self.mo_id:
self.product_id = self.mo_id.product_id.id
self.bom_id = self.mo_id.bom_id
self.product_uom_id = self.mo_id.product_uom_id
if self.has_tracking == 'serial':
self.product_qty = 1
else:
self.product_qty = self.mo_id.product_qty
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
self.bom_id = self.env['mrp.bom']._bom_find(self.product_id, company_id=self.company_id.id)[self.product_id]
self.product_uom_id = self.product_id.uom_id.id
@api.constrains('product_qty')
def _check_qty(self):
for unbuild in self:
if unbuild.product_qty <= 0:
raise ValueError(_('Unbuild Order product quantity has to be strictly positive.'))
@api.model
def create(self, vals):
if not vals.get('name') or vals['name'] == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('mrp.unbuild') or _('New')
return super(MrpUnbuild, self).create(vals)
@api.ondelete(at_uninstall=False)
def _unlink_except_done(self):
if 'done' in self.mapped('state'):
raise UserError(_("You cannot delete an unbuild order if the state is 'Done'."))
def action_unbuild(self):
self.ensure_one()
self._check_company()
if self.product_id.tracking != 'none' and not self.lot_id.id:
raise UserError(_('You should provide a lot number for the final product.'))
if self.mo_id:
if self.mo_id.state != 'done':
raise UserError(_('You cannot unbuild a undone manufacturing order.'))
consume_moves = self._generate_consume_moves()
consume_moves._action_confirm()
produce_moves = self._generate_produce_moves()
produce_moves._action_confirm()
finished_moves = consume_moves.filtered(lambda m: m.product_id == self.product_id)
consume_moves -= finished_moves
if any(produce_move.has_tracking != 'none' and not self.mo_id for produce_move in produce_moves):
raise UserError(_('Some of your components are tracked, you have to specify a manufacturing order in order to retrieve the correct components.'))
if any(consume_move.has_tracking != 'none' and not self.mo_id for consume_move in consume_moves):
raise UserError(_('Some of your byproducts are tracked, you have to specify a manufacturing order in order to retrieve the correct byproducts.'))
for finished_move in finished_moves:
if finished_move.has_tracking != 'none':
self.env['stock.move.line'].create({
'move_id': finished_move.id,
'lot_id': self.lot_id.id,
'qty_done': finished_move.product_uom_qty,
'product_id': finished_move.product_id.id,
'product_uom_id': finished_move.product_uom.id,
'location_id': finished_move.location_id.id,
'location_dest_id': finished_move.location_dest_id.id,
})
else:
finished_move.quantity_done = finished_move.product_uom_qty
# TODO: Will fail if user do more than one unbuild with lot on the same MO. Need to check what other unbuild has aready took
for move in produce_moves | consume_moves:
if move.has_tracking != 'none':
original_move = move in produce_moves and self.mo_id.move_raw_ids or self.mo_id.move_finished_ids
original_move = original_move.filtered(lambda m: m.product_id == move.product_id)
needed_quantity = move.product_uom_qty
moves_lines = original_move.mapped('move_line_ids')
if move in produce_moves and self.lot_id:
moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.produce_line_ids.lot_id) # FIXME sle: double check with arm
for move_line in moves_lines:
# Iterate over all move_lines until we unbuilded the correct quantity.
taken_quantity = min(needed_quantity, move_line.qty_done)
if taken_quantity:
self.env['stock.move.line'].create({
'move_id': move.id,
'lot_id': move_line.lot_id.id,
'qty_done': taken_quantity,
'product_id': move.product_id.id,
'product_uom_id': move_line.product_uom_id.id,
'location_id': move.location_id.id,
'location_dest_id': move.location_dest_id.id,
})
needed_quantity -= taken_quantity
else:
move.quantity_done = move.product_uom_qty
finished_moves._action_done()
consume_moves._action_done()
produce_moves._action_done()
produced_move_line_ids = produce_moves.mapped('move_line_ids').filtered(lambda ml: ml.qty_done > 0)
consume_moves.mapped('move_line_ids').write({'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]})
if self.mo_id:
unbuild_msg = _(
"%s %s unbuilt in", self.product_qty, self.product_uom_id.name) + " <a href=# data-oe-model=mrp.unbuild data-oe-id=%d>%s</a>" % (self.id, self.display_name)
self.mo_id.message_post(
body=unbuild_msg,
subtype_id=self.env.ref('mail.mt_note').id)
return self.write({'state': 'done'})
def _generate_consume_moves(self):
moves = self.env['stock.move']
for unbuild in self:
if unbuild.mo_id:
finished_moves = unbuild.mo_id.move_finished_ids.filtered(lambda move: move.state == 'done')
factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.product_qty, unbuild.product_uom_id)
for finished_move in finished_moves:
moves += unbuild._generate_move_from_existing_move(finished_move, factor, finished_move.location_dest_id, finished_move.location_id)
else:
factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
moves += unbuild._generate_move_from_bom_line(self.product_id, self.product_uom_id, unbuild.product_qty)
for byproduct in unbuild.bom_id.byproduct_ids:
quantity = byproduct.product_qty * factor
moves += unbuild._generate_move_from_bom_line(byproduct.product_id, byproduct.product_uom_id, quantity, byproduct_id=byproduct.id)
return moves
def _generate_produce_moves(self):
moves = self.env['stock.move']
for unbuild in self:
if unbuild.mo_id:
raw_moves = unbuild.mo_id.move_raw_ids.filtered(lambda move: move.state == 'done')
factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.product_qty, unbuild.product_uom_id)
for raw_move in raw_moves:
moves += unbuild._generate_move_from_existing_move(raw_move, factor, raw_move.location_dest_id, self.location_dest_id)
else:
factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
boms, lines = unbuild.bom_id.explode(unbuild.product_id, factor, picking_type=unbuild.bom_id.picking_type_id)
for line, line_data in lines:
moves += unbuild._generate_move_from_bom_line(line.product_id, line.product_uom_id, line_data['qty'], bom_line_id=line.id)
return moves
def _generate_move_from_existing_move(self, move, factor, location_id, location_dest_id):
return self.env['stock.move'].create({
'name': self.name,
'date': self.create_date,
'product_id': move.product_id.id,
'product_uom_qty': move.product_uom_qty * factor,
'product_uom': move.product_uom.id,
'procure_method': 'make_to_stock',
'location_dest_id': location_dest_id.id,
'location_id': location_id.id,
'warehouse_id': location_dest_id.warehouse_id.id,
'unbuild_id': self.id,
'company_id': move.company_id.id,
})
def _generate_move_from_bom_line(self, product, product_uom, quantity, bom_line_id=False, byproduct_id=False):
product_prod_location = product.with_company(self.company_id).property_stock_production
location_id = bom_line_id and product_prod_location or self.location_id
location_dest_id = bom_line_id and self.location_dest_id or product_prod_location
warehouse = location_dest_id.warehouse_id
return self.env['stock.move'].create({
'name': self.name,
'date': self.create_date,
'bom_line_id': bom_line_id,
'byproduct_id': byproduct_id,
'product_id': product.id,
'product_uom_qty': quantity,
'product_uom': product_uom.id,
'procure_method': 'make_to_stock',
'location_dest_id': location_dest_id.id,
'location_id': location_id.id,
'warehouse_id': warehouse.id,
'unbuild_id': self.id,
'company_id': self.company_id.id,
})
def action_validate(self):
self.ensure_one()
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
available_qty = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True)
unbuild_qty = self.product_uom_id._compute_quantity(self.product_qty, self.product_id.uom_id)
if float_compare(available_qty, unbuild_qty, precision_digits=precision) >= 0:
return self.action_unbuild()
else:
return {
'name': self.product_id.display_name + _(': Insufficient Quantity To Unbuild'),
'view_mode': 'form',
'res_model': 'stock.warn.insufficient.qty.unbuild',
'view_id': self.env.ref('mrp.stock_warn_insufficient_qty_unbuild_form_view').id,
'type': 'ir.actions.act_window',
'context': {
'default_product_id': self.product_id.id,
'default_location_id': self.location_id.id,
'default_unbuild_id': self.id,
'default_quantity': unbuild_qty,
'default_product_uom_name': self.product_id.uom_name
},
'target': 'new'
}