601 lines
29 KiB
Python
601 lines
29 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from collections import defaultdict
|
||
|
import math
|
||
|
|
||
|
from odoo import api, fields, models, _
|
||
|
from odoo.addons import decimal_precision as dp
|
||
|
from odoo.exceptions import UserError
|
||
|
|
||
|
|
||
|
class MrpProduction(models.Model):
|
||
|
""" Manufacturing Orders """
|
||
|
_name = 'mrp.production'
|
||
|
_description = 'Manufacturing Order'
|
||
|
_date_name = 'date_planned_start'
|
||
|
_inherit = ['mail.thread', 'ir.needaction_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,
|
||
|
states={'confirmed': [('readonly', False)]})
|
||
|
product_uom_id = fields.Many2one(
|
||
|
'product.uom', 'Product Unit of Measure',
|
||
|
oldname='product_uom', readonly=True, required=True,
|
||
|
states={'confirmed': [('readonly', False)]})
|
||
|
picking_type_id = fields.Many2one(
|
||
|
'stock.picking.type', 'Picking 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)])
|
||
|
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')
|
||
|
|
||
|
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='Availability',
|
||
|
compute='_compute_availability', store=True)
|
||
|
|
||
|
unreserve_visible = fields.Boolean(
|
||
|
'Inventory Unreserve Visible', compute='_compute_unreserve_visible',
|
||
|
help='Technical field to check when we can unreserve')
|
||
|
post_visible = fields.Boolean(
|
||
|
'Inventory Post Visible', compute='_compute_post_visible',
|
||
|
help='Technical field to check when we can post')
|
||
|
|
||
|
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)
|
||
|
procurement_ids = fields.One2many('procurement.order', 'production_id', 'Related Procurements')
|
||
|
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')
|
||
|
|
||
|
@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', 'move_raw_ids.partially_available', '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:
|
||
|
partial_list = [x.partially_available and x.state in ('waiting', 'confirmed', 'assigned') for x in order.move_raw_ids]
|
||
|
assigned_list = [x.state in ('assigned', 'done', 'cancel') for x in order.move_raw_ids]
|
||
|
order.availability = (all(assigned_list) and 'assigned') or (any(partial_list) and 'partially_available') or 'waiting'
|
||
|
|
||
|
@api.depends('state', 'move_raw_ids.reserved_quant_ids')
|
||
|
def _compute_unreserve_visible(self):
|
||
|
for order in self:
|
||
|
if order.state in ['done', 'cancel'] or not order.move_raw_ids.mapped('reserved_quant_ids'):
|
||
|
order.unreserve_visible = False
|
||
|
else:
|
||
|
order.unreserve_visible = True
|
||
|
|
||
|
@api.multi
|
||
|
@api.depends('move_raw_ids.quantity_done', 'move_finished_ids.quantity_done')
|
||
|
def _compute_post_visible(self):
|
||
|
for order in self:
|
||
|
order.post_visible = any(order.move_raw_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel']))) or \
|
||
|
any(order.move_finished_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel'])))
|
||
|
|
||
|
@api.multi
|
||
|
@api.depends('workorder_ids.state', 'move_finished_ids')
|
||
|
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 = 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
|
||
|
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('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'):
|
||
|
if values.get('picking_type_id'):
|
||
|
values['name'] = self.env['stock.picking.type'].browse(values['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()
|
||
|
|
||
|
@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,
|
||
|
'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,
|
||
|
'move_dest_id': self.procurement_ids and self.procurement_ids[0].move_dest_id.id or False,
|
||
|
'procurement_id': self.procurement_ids and self.procurement_ids[0].id or False,
|
||
|
'company_id': self.company_id.id,
|
||
|
'production_id': self.id,
|
||
|
'origin': self.name,
|
||
|
'group_id': self.procurement_group_id.id,
|
||
|
'propagate': self.propagate,
|
||
|
})
|
||
|
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,
|
||
|
'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']._get_mto_route()
|
||
|
except:
|
||
|
mto_route = False
|
||
|
for move in self.move_raw_ids:
|
||
|
product = move.product_id
|
||
|
routes = product.route_ids + product.route_from_categ_ids
|
||
|
# TODO: optimize with read_group?
|
||
|
pull = self.env['procurement.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)], 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):
|
||
|
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:
|
||
|
if quantity > 0:
|
||
|
move[0].write({'product_uom_qty': quantity})
|
||
|
else:
|
||
|
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
|
||
|
else:
|
||
|
self._generate_raw_move(bom_line, line_data)
|
||
|
|
||
|
@api.multi
|
||
|
def action_assign(self):
|
||
|
for production in self:
|
||
|
move_to_assign = production.move_raw_ids.filtered(lambda x: x.state in ('confirmed', 'waiting', 'assigned'))
|
||
|
move_to_assign.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']
|
||
|
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):
|
||
|
workorders += self._workorders_create(bom, bom_data)
|
||
|
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 = math.ceil(bom_qty / operation.workcenter_id.capacity) # TODO: float_round 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_lot_ids').write({'workorder_id': workorder.id})
|
||
|
(moves_finished + moves_raw).write({'workorder_id': workorder.id})
|
||
|
|
||
|
workorder._generate_lot_ids()
|
||
|
return workorders
|
||
|
|
||
|
@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.'))
|
||
|
ProcurementOrder = self.env['procurement.order']
|
||
|
for production in self:
|
||
|
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()
|
||
|
|
||
|
procurements = ProcurementOrder.search([('move_dest_id', 'in', (finish_moves | raw_moves).ids)])
|
||
|
if procurements:
|
||
|
procurements.cancel()
|
||
|
|
||
|
# Put relatfinish_to_canceled procurements in exception -> I agree
|
||
|
ProcurementOrder.search([('production_id', 'in', self.ids)]).write({'state': 'exception'})
|
||
|
|
||
|
self.write({'state': 'cancel'})
|
||
|
return True
|
||
|
|
||
|
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'))
|
||
|
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()
|
||
|
|
||
|
for move in moves_to_finish:
|
||
|
#Group quants by lots
|
||
|
lot_quants = {}
|
||
|
raw_lot_quants = {}
|
||
|
quants = self.env['stock.quant']
|
||
|
if move.has_tracking != 'none':
|
||
|
for quant in move.quant_ids:
|
||
|
lot_quants.setdefault(quant.lot_id.id, self.env['stock.quant'])
|
||
|
raw_lot_quants.setdefault(quant.lot_id.id, self.env['stock.quant'])
|
||
|
lot_quants[quant.lot_id.id] |= quant
|
||
|
for move_raw in moves_to_do:
|
||
|
if (move.has_tracking != 'none') and (move_raw.has_tracking != 'none'):
|
||
|
for lot in lot_quants:
|
||
|
lots = move_raw.move_lot_ids.filtered(lambda x: x.lot_produced_id.id == lot).mapped('lot_id')
|
||
|
raw_lot_quants[lot] |= move_raw.quant_ids.filtered(lambda x: (x.lot_id in lots) and (x.qty > 0.0))
|
||
|
else:
|
||
|
quants |= move_raw.quant_ids.filtered(lambda x: x.qty > 0.0)
|
||
|
if move.has_tracking != 'none':
|
||
|
for lot in lot_quants:
|
||
|
lot_quants[lot].sudo().write({'consumed_quant_ids': [(6, 0, [x.id for x in raw_lot_quants[lot] | quants])]})
|
||
|
else:
|
||
|
move.quant_ids.sudo().write({'consumed_quant_ids': [(6, 0, [x.id for x in quants])]})
|
||
|
order.action_assign()
|
||
|
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.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()})
|
||
|
self.env["procurement.order"].search([('production_id', 'in', self.ids)]).check()
|
||
|
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
|