Purpose of this commit is to give description more "business oriented" because those descriptions appears in Odoo Studio which is supposed to be used by end users, not only by developers. Related Task ID : 37311
778 lines
38 KiB
Python
778 lines
38 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.addons import decimal_precision as dp
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import float_compare, float_round
|
|
|
|
class MrpProduction(models.Model):
|
|
""" Manufacturing Orders """
|
|
_name = 'mrp.production'
|
|
_description = 'Production Order'
|
|
_date_name = 'date_planned_start'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_order = 'date_planned_start asc,id'
|
|
|
|
@api.model
|
|
def _get_default_picking_type(self):
|
|
return self.env['stock.picking.type'].search([
|
|
('code', '=', 'mrp_operation'),
|
|
('warehouse_id.company_id', 'in', [self.env.context.get('company_id', self.env.user.company_id.id), False])],
|
|
limit=1).id
|
|
|
|
@api.model
|
|
def _get_default_location_src_id(self):
|
|
location = False
|
|
if self._context.get('default_picking_type_id'):
|
|
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_src_id
|
|
if not location:
|
|
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
|
return location and location.id or False
|
|
|
|
@api.model
|
|
def _get_default_location_dest_id(self):
|
|
location = False
|
|
if self._context.get('default_picking_type_id'):
|
|
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_dest_id
|
|
if not location:
|
|
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
|
return location and location.id or False
|
|
|
|
name = fields.Char(
|
|
'Reference', copy=False, readonly=True, default=lambda x: _('New'))
|
|
origin = fields.Char(
|
|
'Source', copy=False,
|
|
help="Reference of the document that generated this production order request.")
|
|
|
|
product_id = fields.Many2one(
|
|
'product.product', 'Product',
|
|
domain=[('type', 'in', ['product', 'consu'])],
|
|
readonly=True, required=True,
|
|
states={'confirmed': [('readonly', False)]})
|
|
product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id')
|
|
product_qty = fields.Float(
|
|
'Quantity To Produce',
|
|
default=1.0, digits=dp.get_precision('Product Unit of Measure'),
|
|
readonly=True, required=True, track_visibility='onchange',
|
|
states={'confirmed': [('readonly', False)]})
|
|
product_uom_id = fields.Many2one(
|
|
'uom.uom', 'Product Unit of Measure',
|
|
oldname='product_uom', readonly=True, required=True,
|
|
states={'confirmed': [('readonly', False)]})
|
|
product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True)
|
|
picking_type_id = fields.Many2one(
|
|
'stock.picking.type', 'Operation Type',
|
|
default=_get_default_picking_type, required=True)
|
|
location_src_id = fields.Many2one(
|
|
'stock.location', 'Raw Materials Location',
|
|
default=_get_default_location_src_id,
|
|
readonly=True, required=True,
|
|
states={'confirmed': [('readonly', False)]},
|
|
help="Location where the system will look for components.")
|
|
location_dest_id = fields.Many2one(
|
|
'stock.location', 'Finished Products Location',
|
|
default=_get_default_location_dest_id,
|
|
readonly=True, required=True,
|
|
states={'confirmed': [('readonly', False)]},
|
|
help="Location where the system will stock the finished products.")
|
|
date_planned_start = fields.Datetime(
|
|
'Deadline Start', copy=False, default=fields.Datetime.now,
|
|
index=True, required=True,
|
|
states={'confirmed': [('readonly', False)]}, oldname="date_planned")
|
|
date_planned_finished = fields.Datetime(
|
|
'Deadline End', copy=False, default=fields.Datetime.now,
|
|
index=True,
|
|
states={'confirmed': [('readonly', False)]})
|
|
date_start = fields.Datetime('Start Date', copy=False, index=True, readonly=True)
|
|
date_finished = fields.Datetime('End Date', copy=False, index=True, readonly=True)
|
|
bom_id = fields.Many2one(
|
|
'mrp.bom', 'Bill of Material',
|
|
readonly=True, states={'confirmed': [('readonly', False)]},
|
|
help="Bill of Materials allow you to define the list of required raw materials to make a finished product.")
|
|
routing_id = fields.Many2one(
|
|
'mrp.routing', 'Routing',
|
|
readonly=True, compute='_compute_routing', store=True,
|
|
help="The list of operations (list of work centers) to produce the finished product. The routing "
|
|
"is mainly used to compute work center costs during operations and to plan future loads on "
|
|
"work centers based on production planning.")
|
|
move_raw_ids = fields.One2many(
|
|
'stock.move', 'raw_material_production_id', 'Raw Materials', oldname='move_lines',
|
|
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
|
domain=[('scrapped', '=', False)])
|
|
move_finished_ids = fields.One2many(
|
|
'stock.move', 'production_id', 'Finished Products',
|
|
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
|
domain=[('scrapped', '=', False)])
|
|
finished_move_line_ids = fields.One2many(
|
|
'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product"
|
|
)
|
|
workorder_ids = fields.One2many(
|
|
'mrp.workorder', 'production_id', 'Work Orders',
|
|
copy=False, oldname='workcenter_lines', readonly=True)
|
|
workorder_count = fields.Integer('# Work Orders', compute='_compute_workorder_count')
|
|
workorder_done_count = fields.Integer('# Done Work Orders', compute='_compute_workorder_done_count')
|
|
move_dest_ids = fields.One2many('stock.move', 'created_production_id',
|
|
string="Stock Movements of Produced Goods")
|
|
|
|
state = fields.Selection([
|
|
('confirmed', 'Confirmed'),
|
|
('planned', 'Planned'),
|
|
('progress', 'In Progress'),
|
|
('done', 'Done'),
|
|
('cancel', 'Cancelled')], string='State',
|
|
copy=False, default='confirmed', track_visibility='onchange')
|
|
availability = fields.Selection([
|
|
('assigned', 'Available'),
|
|
('partially_available', 'Partially Available'),
|
|
('waiting', 'Waiting'),
|
|
('none', 'None')], string='Materials Availability',
|
|
compute='_compute_availability', store=True)
|
|
|
|
unreserve_visible = fields.Boolean(
|
|
'Allowed to Unreserve Inventory', compute='_compute_unreserve_visible',
|
|
help='Technical field to check when we can unreserve')
|
|
post_visible = fields.Boolean(
|
|
'Allowed to Post Inventory', compute='_compute_post_visible',
|
|
help='Technical field to check when we can post')
|
|
consumed_less_than_planned = fields.Boolean(
|
|
compute='_compute_consumed_less_than_planned',
|
|
help='Technical field used to see if we have to display a warning or not when confirming an order.')
|
|
|
|
user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self._uid)
|
|
company_id = fields.Many2one(
|
|
'res.company', 'Company',
|
|
default=lambda self: self.env['res.company']._company_default_get('mrp.production'),
|
|
required=True)
|
|
|
|
check_to_done = fields.Boolean(compute="_get_produced_qty", string="Check Produced Qty",
|
|
help="Technical Field to see if we can show 'Mark as Done' button")
|
|
qty_produced = fields.Float(compute="_get_produced_qty", string="Quantity Produced")
|
|
procurement_group_id = fields.Many2one(
|
|
'procurement.group', 'Procurement Group',
|
|
copy=False)
|
|
propagate = fields.Boolean(
|
|
'Propagate cancel and split',
|
|
help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too')
|
|
has_moves = fields.Boolean(compute='_has_moves')
|
|
scrap_ids = fields.One2many('stock.scrap', 'production_id', 'Scraps')
|
|
scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move')
|
|
priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
|
|
readonly=True, states={'confirmed': [('readonly', False)]}, default='1')
|
|
is_locked = fields.Boolean('Is Locked', default=True, copy=False)
|
|
show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots')
|
|
production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production')
|
|
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this manufacturing order')
|
|
delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids')
|
|
|
|
@api.depends('procurement_group_id')
|
|
def _compute_picking_ids(self):
|
|
for order in self:
|
|
order.picking_ids = self.env['stock.picking'].search([
|
|
('group_id', '=', order.procurement_group_id.id),
|
|
])
|
|
order.delivery_count = len(order.picking_ids)
|
|
|
|
def action_view_mo_delivery(self):
|
|
""" This function returns an action that display picking related to
|
|
manufacturing order orders. It can either be a in a list or in a form
|
|
view, if there is only one picking to show.
|
|
"""
|
|
self.ensure_one()
|
|
action = self.env.ref('stock.action_picking_tree_all').read()[0]
|
|
pickings = self.mapped('picking_ids')
|
|
if len(pickings) > 1:
|
|
action['domain'] = [('id', 'in', pickings.ids)]
|
|
elif pickings:
|
|
action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')]
|
|
action['res_id'] = pickings.id
|
|
return action
|
|
|
|
@api.depends('product_uom_id', 'product_qty', 'product_id.uom_id')
|
|
def _compute_product_uom_qty(self):
|
|
for production in self:
|
|
if production.product_id.uom_id != production.product_uom_id:
|
|
production.product_uom_qty = production.product_uom_id._compute_quantity(production.product_qty, production.product_id.uom_id)
|
|
else:
|
|
production.product_uom_qty = production.product_qty
|
|
|
|
@api.depends('product_id.tracking')
|
|
def _compute_show_lots(self):
|
|
for production in self:
|
|
production.show_final_lots = production.product_id.tracking != 'none'
|
|
|
|
def _inverse_lines(self):
|
|
""" Little hack to make sure that when you change something on these objects, it gets saved"""
|
|
pass
|
|
|
|
@api.depends('move_finished_ids.move_line_ids')
|
|
def _compute_lines(self):
|
|
for production in self:
|
|
production.finished_move_line_ids = production.move_finished_ids.mapped('move_line_ids')
|
|
|
|
@api.multi
|
|
@api.depends('bom_id.routing_id', 'bom_id.routing_id.operation_ids')
|
|
def _compute_routing(self):
|
|
for production in self:
|
|
if production.bom_id.routing_id.operation_ids:
|
|
production.routing_id = production.bom_id.routing_id.id
|
|
else:
|
|
production.routing_id = False
|
|
|
|
@api.multi
|
|
@api.depends('workorder_ids')
|
|
def _compute_workorder_count(self):
|
|
data = self.env['mrp.workorder'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id'])
|
|
count_data = dict((item['production_id'][0], item['production_id_count']) for item in data)
|
|
for production in self:
|
|
production.workorder_count = count_data.get(production.id, 0)
|
|
|
|
@api.multi
|
|
@api.depends('workorder_ids.state')
|
|
def _compute_workorder_done_count(self):
|
|
data = self.env['mrp.workorder'].read_group([
|
|
('production_id', 'in', self.ids),
|
|
('state', '=', 'done')], ['production_id'], ['production_id'])
|
|
count_data = dict((item['production_id'][0], item['production_id_count']) for item in data)
|
|
for production in self:
|
|
production.workorder_done_count = count_data.get(production.id, 0)
|
|
|
|
@api.multi
|
|
@api.depends('move_raw_ids.state', 'workorder_ids.move_raw_ids', 'bom_id.ready_to_produce')
|
|
def _compute_availability(self):
|
|
for order in self:
|
|
if not order.move_raw_ids:
|
|
order.availability = 'none'
|
|
continue
|
|
if order.bom_id.ready_to_produce == 'all_available':
|
|
order.availability = any(move.state not in ('assigned', 'done', 'cancel') for move in order.move_raw_ids) and 'waiting' or 'assigned'
|
|
else:
|
|
move_raw_ids = order.move_raw_ids.filtered(lambda m: m.product_qty)
|
|
partial_list = [x.state in ('partially_available', 'assigned') for x in move_raw_ids]
|
|
assigned_list = [x.state in ('assigned', 'done', 'cancel') for x in move_raw_ids]
|
|
order.availability = (all(assigned_list) and 'assigned') or (any(partial_list) and 'partially_available') or 'waiting'
|
|
|
|
@api.depends('move_raw_ids', 'is_locked', 'state', 'move_raw_ids.quantity_done')
|
|
def _compute_unreserve_visible(self):
|
|
for order in self:
|
|
already_reserved = order.is_locked and order.state not in ('done', 'cancel') and order.mapped('move_raw_ids.move_line_ids')
|
|
any_quantity_done = any([m.quantity_done > 0 for m in order.move_raw_ids])
|
|
order.unreserve_visible = not any_quantity_done and already_reserved
|
|
|
|
@api.multi
|
|
@api.depends('move_raw_ids.quantity_done', 'move_finished_ids.quantity_done', 'is_locked')
|
|
def _compute_post_visible(self):
|
|
for order in self:
|
|
if order.product_tmpl_id._is_cost_method_standard():
|
|
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids | order.move_finished_ids)
|
|
else:
|
|
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids)
|
|
|
|
@api.multi
|
|
@api.depends('move_raw_ids.quantity_done', 'move_raw_ids.product_uom_qty')
|
|
def _compute_consumed_less_than_planned(self):
|
|
for order in self:
|
|
order.consumed_less_than_planned = any(order.move_raw_ids.filtered(
|
|
lambda move: float_compare(move.quantity_done,
|
|
move.product_uom_qty,
|
|
precision_rounding=move.product_uom.rounding) == -1)
|
|
)
|
|
|
|
@api.multi
|
|
@api.depends('workorder_ids.state', 'move_finished_ids', 'is_locked')
|
|
def _get_produced_qty(self):
|
|
for production in self:
|
|
done_moves = production.move_finished_ids.filtered(lambda x: x.state != 'cancel' and x.product_id.id == production.product_id.id)
|
|
qty_produced = sum(done_moves.mapped('quantity_done'))
|
|
wo_done = True
|
|
if any([x.state not in ('done', 'cancel') for x in production.workorder_ids]):
|
|
wo_done = False
|
|
production.check_to_done = production.is_locked and done_moves and (qty_produced >= production.product_qty) and (production.state not in ('done', 'cancel')) and wo_done
|
|
production.qty_produced = qty_produced
|
|
return True
|
|
|
|
@api.multi
|
|
@api.depends('move_raw_ids')
|
|
def _has_moves(self):
|
|
for mo in self:
|
|
mo.has_moves = any(mo.move_raw_ids)
|
|
|
|
@api.multi
|
|
def _compute_scrap_move_count(self):
|
|
data = self.env['stock.scrap'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id'])
|
|
count_data = dict((item['production_id'][0], item['production_id_count']) for item in data)
|
|
for production in self:
|
|
production.scrap_count = count_data.get(production.id, 0)
|
|
|
|
|
|
_sql_constraints = [
|
|
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
|
|
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
|
|
]
|
|
|
|
@api.onchange('product_id', 'picking_type_id', 'company_id')
|
|
def onchange_product_id(self):
|
|
""" Finds UoM of changed product. """
|
|
if not self.product_id:
|
|
self.bom_id = False
|
|
else:
|
|
bom = self.env['mrp.bom']._bom_find(product=self.product_id, picking_type=self.picking_type_id, company_id=self.company_id.id)
|
|
if bom.type == 'normal':
|
|
self.bom_id = bom.id
|
|
self.product_qty = self.bom_id.product_qty
|
|
self.product_uom_id = self.bom_id.product_uom_id.id
|
|
else:
|
|
self.bom_id = False
|
|
self.product_uom_id = self.product_id.uom_id.id
|
|
return {'domain': {'product_uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}}
|
|
|
|
@api.onchange('bom_id')
|
|
def _onchange_bom_id(self):
|
|
self.product_qty = self.bom_id.product_qty
|
|
self.product_uom_id = self.bom_id.product_uom_id.id
|
|
|
|
@api.onchange('picking_type_id')
|
|
def onchange_picking_type(self):
|
|
location = self.env.ref('stock.stock_location_stock')
|
|
self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id
|
|
self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id
|
|
|
|
@api.multi
|
|
def write(self, vals):
|
|
res = super(MrpProduction, self).write(vals)
|
|
if 'date_planned_start' in vals:
|
|
moves = (self.mapped('move_raw_ids') + self.mapped('move_finished_ids')).filtered(
|
|
lambda r: r.state not in ['done', 'cancel'])
|
|
moves.write({
|
|
'date_expected': vals['date_planned_start'],
|
|
})
|
|
return res
|
|
|
|
@api.model
|
|
def create(self, values):
|
|
if not values.get('name', False) or values['name'] == _('New'):
|
|
picking_type_id = values.get('picking_type_id') or self._get_default_picking_type()
|
|
picking_type_id = self.env['stock.picking.type'].browse(picking_type_id)
|
|
if picking_type_id:
|
|
values['name'] = picking_type_id.sequence_id.next_by_id()
|
|
else:
|
|
values['name'] = self.env['ir.sequence'].next_by_code('mrp.production') or _('New')
|
|
if not values.get('procurement_group_id'):
|
|
values['procurement_group_id'] = self.env["procurement.group"].create({'name': values['name']}).id
|
|
production = super(MrpProduction, self).create(values)
|
|
production._generate_moves()
|
|
return production
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
if any(production.state != 'cancel' for production in self):
|
|
raise UserError(_('Cannot delete a manufacturing order not in cancel state'))
|
|
return super(MrpProduction, self).unlink()
|
|
|
|
def action_toggle_is_locked(self):
|
|
self.ensure_one()
|
|
self.is_locked = not self.is_locked
|
|
return True
|
|
|
|
@api.multi
|
|
def _generate_moves(self):
|
|
for production in self:
|
|
production._generate_finished_moves()
|
|
factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) / production.bom_id.product_qty
|
|
boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id)
|
|
production._generate_raw_moves(lines)
|
|
# Check for all draft moves whether they are mto or not
|
|
production._adjust_procure_method()
|
|
production.move_raw_ids._action_confirm()
|
|
return True
|
|
|
|
def _generate_finished_moves(self):
|
|
move = self.env['stock.move'].create({
|
|
'name': self.name,
|
|
'date': self.date_planned_start,
|
|
'date_expected': self.date_planned_start,
|
|
'picking_type_id': self.picking_type_id.id,
|
|
'product_id': self.product_id.id,
|
|
'product_uom': self.product_uom_id.id,
|
|
'product_uom_qty': self.product_qty,
|
|
'location_id': self.product_id.property_stock_production.id,
|
|
'location_dest_id': self.location_dest_id.id,
|
|
'company_id': self.company_id.id,
|
|
'production_id': self.id,
|
|
'warehouse_id': self.location_dest_id.get_warehouse().id,
|
|
'origin': self.name,
|
|
'group_id': self.procurement_group_id.id,
|
|
'propagate': self.propagate,
|
|
'move_dest_ids': [(4, x.id) for x in self.move_dest_ids],
|
|
})
|
|
move._action_confirm()
|
|
return move
|
|
|
|
def _generate_raw_moves(self, exploded_lines):
|
|
self.ensure_one()
|
|
moves = self.env['stock.move']
|
|
for bom_line, line_data in exploded_lines:
|
|
moves += self._generate_raw_move(bom_line, line_data)
|
|
return moves
|
|
|
|
def _generate_raw_move(self, bom_line, line_data):
|
|
quantity = line_data['qty']
|
|
# alt_op needed for the case when you explode phantom bom and all the lines will be consumed in the operation given by the parent bom line
|
|
alt_op = line_data['parent_line'] and line_data['parent_line'].operation_id.id or False
|
|
if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom':
|
|
return self.env['stock.move']
|
|
if bom_line.product_id.type not in ['product', 'consu']:
|
|
return self.env['stock.move']
|
|
if self.routing_id:
|
|
routing = self.routing_id
|
|
else:
|
|
routing = self.bom_id.routing_id
|
|
if routing and routing.location_id:
|
|
source_location = routing.location_id
|
|
else:
|
|
source_location = self.location_src_id
|
|
original_quantity = (self.product_qty - self.qty_produced) or 1.0
|
|
data = {
|
|
'sequence': bom_line.sequence,
|
|
'name': self.name,
|
|
'date': self.date_planned_start,
|
|
'date_expected': self.date_planned_start,
|
|
'bom_line_id': bom_line.id,
|
|
'picking_type_id': self.picking_type_id.id,
|
|
'product_id': bom_line.product_id.id,
|
|
'product_uom_qty': quantity,
|
|
'product_uom': bom_line.product_uom_id.id,
|
|
'location_id': source_location.id,
|
|
'location_dest_id': self.product_id.property_stock_production.id,
|
|
'raw_material_production_id': self.id,
|
|
'company_id': self.company_id.id,
|
|
'operation_id': bom_line.operation_id.id or alt_op,
|
|
'price_unit': bom_line.product_id.standard_price,
|
|
'procure_method': 'make_to_stock',
|
|
'origin': self.name,
|
|
'warehouse_id': source_location.get_warehouse().id,
|
|
'group_id': self.procurement_group_id.id,
|
|
'propagate': self.propagate,
|
|
'unit_factor': quantity / original_quantity,
|
|
}
|
|
return self.env['stock.move'].create(data)
|
|
|
|
@api.multi
|
|
def _adjust_procure_method(self):
|
|
try:
|
|
mto_route = self.env['stock.warehouse']._find_global_route('stock.route_warehouse0_mto', 'Make To Order')
|
|
except:
|
|
mto_route = False
|
|
for move in self.move_raw_ids:
|
|
product = move.product_id
|
|
routes = product.route_ids + product.route_from_categ_ids + move.warehouse_id.route_ids
|
|
# TODO: optimize with read_group?
|
|
pull = self.env['stock.rule'].search([('route_id', 'in', [x.id for x in routes]), ('location_src_id', '=', move.location_id.id),
|
|
('location_id', '=', move.location_dest_id.id), ('action', '!=', 'push')], limit=1)
|
|
if pull and (pull.procure_method == 'make_to_order'):
|
|
move.procure_method = pull.procure_method
|
|
elif not pull: # If there is no make_to_stock rule either
|
|
if mto_route and mto_route.id in [x.id for x in routes]:
|
|
move.procure_method = 'make_to_order'
|
|
|
|
@api.multi
|
|
def _update_raw_move(self, bom_line, line_data):
|
|
""" :returns update_move, old_quantity, new_quantity """
|
|
quantity = line_data['qty']
|
|
self.ensure_one()
|
|
move = self.move_raw_ids.filtered(lambda x: x.bom_line_id.id == bom_line.id and x.state not in ('done', 'cancel'))
|
|
if move:
|
|
old_qty = move[0].product_uom_qty
|
|
if quantity > 0:
|
|
move[0].write({'product_uom_qty': quantity})
|
|
elif quantity < 0: # Do not remove 0 lines
|
|
if move[0].quantity_done > 0:
|
|
raise UserError(_('Lines need to be deleted, but can not as you still have some quantities to consume in them. '))
|
|
move[0]._action_cancel()
|
|
move[0].unlink()
|
|
return move[0], old_qty, quantity
|
|
else:
|
|
move = self._generate_raw_move(bom_line, line_data)
|
|
return move, 0, quantity
|
|
|
|
@api.multi
|
|
def action_assign(self):
|
|
for production in self:
|
|
production.move_raw_ids._action_assign()
|
|
return True
|
|
|
|
@api.multi
|
|
def open_produce_product(self):
|
|
self.ensure_one()
|
|
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
|
|
return action
|
|
|
|
@api.multi
|
|
def button_plan(self):
|
|
""" Create work orders. And probably do stuff, like things. """
|
|
orders_to_plan = self.filtered(lambda order: order.routing_id and order.state == 'confirmed')
|
|
for order in orders_to_plan:
|
|
quantity = order.product_uom_id._compute_quantity(order.product_qty, order.bom_id.product_uom_id) / order.bom_id.product_qty
|
|
boms, lines = order.bom_id.explode(order.product_id, quantity, picking_type=order.bom_id.picking_type_id)
|
|
order._generate_workorders(boms)
|
|
return orders_to_plan.write({'state': 'planned'})
|
|
|
|
@api.multi
|
|
def _generate_workorders(self, exploded_boms):
|
|
workorders = self.env['mrp.workorder']
|
|
original_one = False
|
|
for bom, bom_data in exploded_boms:
|
|
# If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing
|
|
if bom.routing_id.id and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.routing_id.id != bom.routing_id.id):
|
|
temp_workorders = self._workorders_create(bom, bom_data)
|
|
workorders += temp_workorders
|
|
if temp_workorders: # In order to avoid two "ending work orders"
|
|
if original_one:
|
|
temp_workorders[-1].next_work_order_id = original_one
|
|
original_one = temp_workorders[0]
|
|
return workorders
|
|
|
|
def _workorders_create(self, bom, bom_data):
|
|
"""
|
|
:param bom: in case of recursive boms: we could create work orders for child
|
|
BoMs
|
|
"""
|
|
workorders = self.env['mrp.workorder']
|
|
bom_qty = bom_data['qty']
|
|
|
|
# Initial qty producing
|
|
if self.product_id.tracking == 'serial':
|
|
quantity = 1.0
|
|
else:
|
|
quantity = self.product_qty - sum(self.move_finished_ids.mapped('quantity_done'))
|
|
quantity = quantity if (quantity > 0) else 0
|
|
|
|
for operation in bom.routing_id.operation_ids:
|
|
# create workorder
|
|
cycle_number = float_round(bom_qty / operation.workcenter_id.capacity, precision_digits=0, rounding_method='UP')
|
|
duration_expected = (operation.workcenter_id.time_start +
|
|
operation.workcenter_id.time_stop +
|
|
cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency)
|
|
workorder = workorders.create({
|
|
'name': operation.name,
|
|
'production_id': self.id,
|
|
'workcenter_id': operation.workcenter_id.id,
|
|
'operation_id': operation.id,
|
|
'duration_expected': duration_expected,
|
|
'state': len(workorders) == 0 and 'ready' or 'pending',
|
|
'qty_producing': quantity,
|
|
'capacity': operation.workcenter_id.capacity,
|
|
})
|
|
if workorders:
|
|
workorders[-1].next_work_order_id = workorder.id
|
|
workorders += workorder
|
|
|
|
# assign moves; last operation receive all unassigned moves (which case ?)
|
|
moves_raw = self.move_raw_ids.filtered(lambda move: move.operation_id == operation)
|
|
if len(workorders) == len(bom.routing_id.operation_ids):
|
|
moves_raw |= self.move_raw_ids.filtered(lambda move: not move.operation_id)
|
|
moves_finished = self.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products?
|
|
moves_raw.mapped('move_line_ids').write({'workorder_id': workorder.id})
|
|
(moves_finished + moves_raw).write({'workorder_id': workorder.id})
|
|
|
|
workorder._generate_lot_ids()
|
|
return workorders
|
|
|
|
def _check_lots(self):
|
|
# Check that the raw materials were consumed for lots that we have produced.
|
|
if self.product_id.tracking != 'none':
|
|
finished_lots = set(self.finished_move_line_ids.mapped('lot_id'))
|
|
raw_finished_lots = set(self.move_raw_ids.mapped('move_line_ids.lot_produced_id'))
|
|
if not (raw_finished_lots <= finished_lots):
|
|
lots_short = raw_finished_lots - finished_lots
|
|
error_msg = _(
|
|
'Some raw materials have been consumed for a lot/serial number that has not been produced. '
|
|
'Unlock the MO and click on the components lines to correct it.\n'
|
|
'List of the components:\n'
|
|
)
|
|
move_lines = self.move_raw_ids.mapped('move_line_ids').filtered(lambda x: x.lot_produced_id in lots_short)
|
|
for ml in move_lines:
|
|
error_msg += ml.product_id.display_name + ' (' + ml.lot_produced_id.name +')\n'
|
|
raise UserError(error_msg)
|
|
|
|
@api.multi
|
|
def action_cancel(self):
|
|
""" Cancels production order, unfinished stock moves and set procurement
|
|
orders in exception """
|
|
if any(workorder.state == 'progress' for workorder in self.mapped('workorder_ids')):
|
|
raise UserError(_('You can not cancel production order, a work order is still in progress.'))
|
|
documents = {}
|
|
for production in self:
|
|
for move_raw_id in production.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')):
|
|
iterate_key = self._get_document_iterate_key(move_raw_id)
|
|
if iterate_key:
|
|
document = self.env['stock.picking']._log_activity_get_documents({move_raw_id: (move_raw_id.product_uom_qty, 0)}, iterate_key, 'UP')
|
|
for key, value in document.items():
|
|
if documents.get(key):
|
|
documents[key] += [value]
|
|
else:
|
|
documents[key] = [value]
|
|
production.workorder_ids.filtered(lambda x: x.state != 'cancel').action_cancel()
|
|
finish_moves = production.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
|
raw_moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
|
(finish_moves | raw_moves)._action_cancel()
|
|
picking_ids = production.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
|
picking_ids.action_cancel()
|
|
self.write({'state': 'cancel', 'is_locked': True})
|
|
if documents:
|
|
filtered_documents = {}
|
|
for (parent, responsible), rendering_context in documents.items():
|
|
if not parent or parent._name == 'stock.picking' and parent.state == 'cancel' or parent == self:
|
|
continue
|
|
filtered_documents[(parent, responsible)] = rendering_context
|
|
self._log_manufacture_exception(filtered_documents, cancel=True)
|
|
return True
|
|
|
|
def _get_document_iterate_key(self, move_raw_id):
|
|
return move_raw_id.move_orig_ids and 'move_orig_ids' or False
|
|
|
|
def _cal_price(self, consumed_moves):
|
|
self.ensure_one()
|
|
return True
|
|
|
|
@api.multi
|
|
def post_inventory(self):
|
|
for order in self:
|
|
moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done')
|
|
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
|
for move in moves_to_do.filtered(lambda m: m.product_qty == 0.0 and m.quantity_done > 0):
|
|
move.product_uom_qty = move.quantity_done
|
|
moves_to_do._action_done()
|
|
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do
|
|
order._cal_price(moves_to_do)
|
|
moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done','cancel'))
|
|
moves_to_finish._action_done()
|
|
order.action_assign()
|
|
consume_move_lines = moves_to_do.mapped('active_move_line_ids')
|
|
for moveline in moves_to_finish.mapped('active_move_line_ids'):
|
|
if moveline.product_id == order.product_id and moveline.move_id.has_tracking != 'none':
|
|
if any([not ml.lot_produced_id for ml in consume_move_lines]):
|
|
raise UserError(_('You can not consume without telling for which lot you consumed it'))
|
|
# Link all movelines in the consumed with same lot_produced_id false or the correct lot_produced_id
|
|
filtered_lines = consume_move_lines.filtered(lambda x: x.lot_produced_id == moveline.lot_id)
|
|
moveline.write({'consume_line_ids': [(6, 0, [x for x in filtered_lines.ids])]})
|
|
else:
|
|
# Link with everything
|
|
moveline.write({'consume_line_ids': [(6, 0, [x for x in consume_move_lines.ids])]})
|
|
return True
|
|
|
|
@api.multi
|
|
def button_mark_done(self):
|
|
self.ensure_one()
|
|
for wo in self.workorder_ids:
|
|
if wo.time_ids.filtered(lambda x: (not x.date_end) and (x.loss_type in ('productive', 'performance'))):
|
|
raise UserError(_('Work order %s is still running') % wo.name)
|
|
self._check_lots()
|
|
self.post_inventory()
|
|
moves_to_cancel = (self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel'))
|
|
moves_to_cancel._action_cancel()
|
|
self.write({'state': 'done', 'date_finished': fields.Datetime.now()})
|
|
return self.write({'state': 'done'})
|
|
|
|
@api.multi
|
|
def do_unreserve(self):
|
|
for production in self:
|
|
production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))._do_unreserve()
|
|
return True
|
|
|
|
@api.multi
|
|
def button_unreserve(self):
|
|
self.ensure_one()
|
|
self.do_unreserve()
|
|
return True
|
|
|
|
@api.multi
|
|
def button_scrap(self):
|
|
self.ensure_one()
|
|
return {
|
|
'name': _('Scrap'),
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'stock.scrap',
|
|
'view_id': self.env.ref('stock.stock_scrap_form_view2').id,
|
|
'type': 'ir.actions.act_window',
|
|
'context': {'default_production_id': self.id,
|
|
'product_ids': (self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) | self.move_finished_ids.filtered(lambda x: x.state == 'done')).mapped('product_id').ids,
|
|
},
|
|
'target': 'new',
|
|
}
|
|
|
|
@api.multi
|
|
def action_see_move_scrap(self):
|
|
self.ensure_one()
|
|
action = self.env.ref('stock.action_stock_scrap').read()[0]
|
|
action['domain'] = [('production_id', '=', self.id)]
|
|
return action
|
|
|
|
@api.model
|
|
def get_empty_list_help(self, help):
|
|
self = self.with_context(
|
|
empty_list_help_document_name=_("manufacturing order"),
|
|
)
|
|
return super(MrpProduction, self).get_empty_list_help(help)
|
|
|
|
def _log_downside_manufactured_quantity(self, moves_modification):
|
|
|
|
def _keys_in_sorted(move):
|
|
""" sort by picking and the responsible for the product the
|
|
move.
|
|
"""
|
|
return (move.picking_id.id, move.product_id.responsible_id.id)
|
|
|
|
def _keys_in_groupby(move):
|
|
""" group by picking and the responsible for the product the
|
|
move.
|
|
"""
|
|
return (move.picking_id, move.product_id.responsible_id)
|
|
|
|
def _render_note_exception_quantity_mo(rendering_context):
|
|
values = {
|
|
'production_order': self,
|
|
'order_exceptions': dict((key, d[key]) for d in rendering_context for key in d),
|
|
'impacted_pickings': False,
|
|
'cancel': False
|
|
}
|
|
return self.env.ref('mrp.exception_on_mo').render(values=values)
|
|
|
|
documents = {}
|
|
for move, (old_qty, new_qty) in moves_modification.items():
|
|
document = self.env['stock.picking']._log_activity_get_documents(
|
|
{move: (old_qty, new_qty)}, 'move_dest_ids', 'DOWN', _keys_in_sorted, _keys_in_groupby)
|
|
for key, value in document.items():
|
|
if documents.get(key):
|
|
documents[key] += [value]
|
|
else:
|
|
documents[key] = [value]
|
|
self.env['stock.picking']._log_activity(_render_note_exception_quantity_mo, documents)
|
|
|
|
def _log_manufacture_exception(self, documents, cancel=False):
|
|
|
|
def _render_note_exception_quantity_mo(rendering_context):
|
|
visited_objects = []
|
|
order_exceptions = {}
|
|
for exception in rendering_context:
|
|
order_exception, visited = exception
|
|
order_exceptions.update(order_exception)
|
|
visited_objects += visited
|
|
visited_objects = self.env[visited_objects[0]._name].concat(*visited_objects)
|
|
visited_objects |= visited_objects.mapped('move_orig_ids')
|
|
impacted_pickings = []
|
|
if visited_objects._name == 'stock.move':
|
|
impacted_pickings = visited_objects.filtered(lambda m: m.state not in ('done', 'cancel')).mapped('picking_id')
|
|
values = {
|
|
'production_order': self,
|
|
'order_exceptions': order_exceptions,
|
|
'impacted_pickings': impacted_pickings,
|
|
'cancel': cancel
|
|
}
|
|
return self.env.ref('mrp.exception_on_mo').render(values=values)
|
|
|
|
self.env['stock.picking']._log_activity(_render_note_exception_quantity_mo, documents)
|