合并企业版代码(未测试,先提交到测试分支)
This commit is contained in:
10
mrp_workorder/models/__init__.py
Normal file
10
mrp_workorder/models/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_production
|
||||
from . import mrp_workorder
|
||||
from . import quality
|
||||
from . import res_config_settings
|
||||
from . import stock_move
|
||||
from . import stock_move_line
|
||||
from . import mrp_bom
|
||||
15
mrp_workorder/models/mrp_bom.py
Normal file
15
mrp_workorder/models/mrp_bom.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class MrpBom(models.Model):
|
||||
_name = 'mrp.bom'
|
||||
_inherit = ['mail.activity.mixin', 'mrp.bom']
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if 'product_id' in vals or 'product_tmpl_id' in vals:
|
||||
self.operation_ids.quality_point_ids._change_product_ids_for_bom(self)
|
||||
return res
|
||||
22
mrp_workorder/models/mrp_production.py
Normal file
22
mrp_workorder/models/mrp_production.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = 'mrp.production'
|
||||
_start_name = "date_planned_start"
|
||||
_stop_name = "date_planned_finished"
|
||||
|
||||
check_ids = fields.One2many('quality.check', 'production_id', string="Checks")
|
||||
|
||||
def _split_productions(self, amounts=False, cancel_remaining_qty=False, set_consumed_qty=False):
|
||||
productions = super()._split_productions(amounts=amounts, cancel_remaining_qty=cancel_remaining_qty, set_consumed_qty=set_consumed_qty)
|
||||
backorders = productions[1:]
|
||||
if not backorders:
|
||||
return productions
|
||||
for wo in backorders.workorder_ids:
|
||||
if wo.current_quality_check_id.component_id:
|
||||
wo.current_quality_check_id._update_component_quantity()
|
||||
return productions
|
||||
631
mrp_workorder/models/mrp_workorder.py
Normal file
631
mrp_workorder/models/mrp_workorder.py
Normal file
@@ -0,0 +1,631 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from bisect import bisect_left
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pytz import utc
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.web.controllers.utils import clean_action
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import float_compare, float_is_zero, relativedelta
|
||||
from odoo.addons.resource.models.resource import Intervals, sum_intervals, string_to_datetime
|
||||
|
||||
|
||||
class MrpWorkcenter(models.Model):
|
||||
_name = 'mrp.workcenter'
|
||||
_inherit = 'mrp.workcenter'
|
||||
|
||||
def action_work_order(self):
|
||||
if not self.env.context.get('desktop_list_view', False):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.mrp_workorder_action_tablet")
|
||||
return action
|
||||
else:
|
||||
return super(MrpWorkcenter, self).action_work_order()
|
||||
|
||||
|
||||
class MrpProductionWorkcenterLine(models.Model):
|
||||
_name = 'mrp.workorder'
|
||||
_inherit = ['mrp.workorder', 'barcodes.barcode_events_mixin']
|
||||
|
||||
quality_point_ids = fields.Many2many('quality.point', compute='_compute_quality_point_ids', store=True)
|
||||
quality_point_count = fields.Integer('Steps', compute='_compute_quality_point_count')
|
||||
|
||||
check_ids = fields.One2many('quality.check', 'workorder_id')
|
||||
finished_product_check_ids = fields.Many2many('quality.check', compute='_compute_finished_product_check_ids')
|
||||
quality_check_todo = fields.Boolean(compute='_compute_check')
|
||||
quality_check_fail = fields.Boolean(compute='_compute_check')
|
||||
quality_alert_ids = fields.One2many('quality.alert', 'workorder_id')
|
||||
quality_alert_count = fields.Integer(compute="_compute_quality_alert_count")
|
||||
|
||||
current_quality_check_id = fields.Many2one(
|
||||
'quality.check', "Current Quality Check", check_company=True)
|
||||
|
||||
# QC-related fields
|
||||
allow_producing_quantity_change = fields.Boolean('Allow Changes to Producing Quantity', default=True)
|
||||
|
||||
is_last_lot = fields.Boolean('Is Last lot', compute='_compute_is_last_lot')
|
||||
is_first_started_wo = fields.Boolean('Is The first Work Order', compute='_compute_is_last_unfinished_wo')
|
||||
is_last_unfinished_wo = fields.Boolean('Is Last Work Order To Process', compute='_compute_is_last_unfinished_wo', store=False)
|
||||
lot_id = fields.Many2one(related='current_quality_check_id.lot_id', readonly=False)
|
||||
move_id = fields.Many2one(related='current_quality_check_id.move_id', readonly=False)
|
||||
move_line_id = fields.Many2one(related='current_quality_check_id.move_line_id', readonly=False)
|
||||
move_line_ids = fields.One2many(related='move_id.move_line_ids')
|
||||
quality_state = fields.Selection(related='current_quality_check_id.quality_state', string="Quality State", readonly=False)
|
||||
qty_done = fields.Float(related='current_quality_check_id.qty_done', readonly=False)
|
||||
test_type_id = fields.Many2one('quality.point.test_type', 'Test Type', related='current_quality_check_id.test_type_id')
|
||||
test_type = fields.Char(related='test_type_id.technical_name')
|
||||
user_id = fields.Many2one(related='current_quality_check_id.user_id', readonly=False)
|
||||
worksheet_page = fields.Integer('Worksheet page')
|
||||
picture = fields.Binary(related='current_quality_check_id.picture', readonly=False)
|
||||
additional = fields.Boolean(related='current_quality_check_id.additional')
|
||||
|
||||
@api.depends('operation_id')
|
||||
def _compute_quality_point_ids(self):
|
||||
for workorder in self:
|
||||
quality_points = workorder.operation_id.quality_point_ids
|
||||
quality_points = quality_points.filtered(lambda qp: not qp.product_ids or workorder.production_id.product_id in qp.product_ids)
|
||||
workorder.quality_point_ids = quality_points
|
||||
|
||||
@api.depends('operation_id')
|
||||
def _compute_quality_point_count(self):
|
||||
for workorder in self:
|
||||
quality_point = workorder.operation_id.quality_point_ids
|
||||
workorder.quality_point_count = len(quality_point)
|
||||
|
||||
@api.depends('qty_producing', 'qty_remaining')
|
||||
def _compute_is_last_lot(self):
|
||||
for wo in self:
|
||||
precision = wo.production_id.product_uom_id.rounding
|
||||
wo.is_last_lot = float_compare(wo.qty_producing, wo.qty_remaining, precision_rounding=precision) >= 0
|
||||
|
||||
@api.depends('production_id.workorder_ids')
|
||||
def _compute_is_last_unfinished_wo(self):
|
||||
for wo in self:
|
||||
wo.is_first_started_wo = all(wo.state != 'done' for wo in (wo.production_id.workorder_ids - wo))
|
||||
other_wos = wo.production_id.workorder_ids - wo
|
||||
other_states = other_wos.mapped(lambda w: w.state == 'done')
|
||||
wo.is_last_unfinished_wo = all(other_states)
|
||||
|
||||
@api.depends('check_ids')
|
||||
def _compute_finished_product_check_ids(self):
|
||||
for wo in self:
|
||||
wo.finished_product_check_ids = wo.check_ids.filtered(lambda c: c.finished_product_sequence == wo.qty_produced)
|
||||
|
||||
def write(self, values):
|
||||
res = super().write(values)
|
||||
if 'qty_producing' in values:
|
||||
for wo in self:
|
||||
if wo.current_quality_check_id.component_id:
|
||||
wo.current_quality_check_id._update_component_quantity()
|
||||
return res
|
||||
|
||||
def action_back(self):
|
||||
self.ensure_one()
|
||||
if self.is_user_working and self.working_state != 'blocked':
|
||||
self.button_pending()
|
||||
domain = [('state', 'not in', ['done', 'cancel', 'pending'])]
|
||||
if self.env.context.get('from_production_order'):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("mrp.action_mrp_workorder_production_specific")
|
||||
action['domain'] = domain
|
||||
action['target'] = 'main'
|
||||
action['view_id'] = 'mrp.mrp_production_workorder_tree_editable_view'
|
||||
action['context'] = {
|
||||
'no_breadcrumbs': True,
|
||||
}
|
||||
if self.env.context.get('from_manufacturing_order'):
|
||||
action['context'].update({
|
||||
'search_default_production_id': self.production_id.id
|
||||
})
|
||||
else:
|
||||
# workorder tablet view action should redirect to the same tablet view with same workcenter when WO mark as done.
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.mrp_workorder_action_tablet")
|
||||
action['domain'] = domain
|
||||
action['context'] = {
|
||||
'no_breadcrumbs': True,
|
||||
'search_default_workcenter_id': self.workcenter_id.id
|
||||
}
|
||||
|
||||
return clean_action(action, self.env)
|
||||
|
||||
def action_cancel(self):
|
||||
self.mapped('check_ids').filtered(lambda c: c.quality_state == 'none').sudo().unlink()
|
||||
return super(MrpProductionWorkcenterLine, self).action_cancel()
|
||||
|
||||
def action_generate_serial(self):
|
||||
self.ensure_one()
|
||||
self.finished_lot_id = self.env['stock.lot'].create({
|
||||
'product_id': self.product_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
'name': self.env['stock.lot']._get_next_serial(self.company_id, self.product_id) or self.env['ir.sequence'].next_by_code('stock.lot.serial'),
|
||||
})
|
||||
|
||||
def _create_subsequent_checks(self):
|
||||
""" When processing a step with regiter a consumed material
|
||||
that's a lot we will some times need to create a new
|
||||
intermediate check.
|
||||
e.g.: Register 2 product A tracked by SN. We will register one
|
||||
with the current checks but we need to generate a second step
|
||||
for the second SN. Same for lot if the user wants to use more
|
||||
than one lot.
|
||||
"""
|
||||
# Create another quality check if necessary
|
||||
next_check = self.current_quality_check_id.next_check_id
|
||||
if next_check.component_id != self.current_quality_check_id.product_id or\
|
||||
next_check.point_id != self.current_quality_check_id.point_id:
|
||||
# TODO: manage reservation here
|
||||
|
||||
# Creating quality checks
|
||||
quality_check_data = {
|
||||
'workorder_id': self.id,
|
||||
'product_id': self.product_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
'finished_product_sequence': self.qty_produced,
|
||||
}
|
||||
if self.current_quality_check_id.point_id:
|
||||
quality_check_data.update({
|
||||
'point_id': self.current_quality_check_id.point_id.id,
|
||||
'team_id': self.current_quality_check_id.point_id.team_id.id,
|
||||
})
|
||||
else:
|
||||
quality_check_data.update({
|
||||
'component_id': self.current_quality_check_id.component_id.id,
|
||||
'test_type_id': self.current_quality_check_id.test_type_id.id,
|
||||
'team_id': self.current_quality_check_id.team_id.id,
|
||||
})
|
||||
move = self.current_quality_check_id.move_id
|
||||
quality_check_data.update(self._defaults_from_move(move))
|
||||
new_check = self.env['quality.check'].create(quality_check_data)
|
||||
new_check._insert_in_chain('after', self.current_quality_check_id)
|
||||
|
||||
def _change_quality_check(self, position):
|
||||
"""Change the quality check currently set on the workorder `self`.
|
||||
|
||||
The workorder points to a check. A check belongs to a chain.
|
||||
This method allows to change the selected check by moving on the checks
|
||||
chain according to `position`.
|
||||
|
||||
:param position: Where we need to change the cursor on the check chain
|
||||
:type position: string
|
||||
"""
|
||||
self.ensure_one()
|
||||
assert position in ['first', 'next', 'previous', 'last']
|
||||
checks_to_consider = self.check_ids.filtered(lambda c: c.quality_state == 'none')
|
||||
if position == 'first':
|
||||
check = checks_to_consider.filtered(lambda check: not check.previous_check_id)
|
||||
elif position == 'next':
|
||||
check = self.current_quality_check_id.next_check_id
|
||||
if not check:
|
||||
check = checks_to_consider[:1]
|
||||
elif check.quality_state != 'none':
|
||||
self.current_quality_check_id = check
|
||||
return self._change_quality_check(position='next')
|
||||
if check.test_type in ('register_byproducts', 'register_consumed_materials'):
|
||||
check._update_component_quantity()
|
||||
elif position == 'previous':
|
||||
check = self.current_quality_check_id.previous_check_id
|
||||
else:
|
||||
check = checks_to_consider.filtered(lambda check: not check.next_check_id)
|
||||
self.write({
|
||||
'allow_producing_quantity_change':
|
||||
not check.previous_check_id.filtered(lambda c: c.quality_state != 'fail')
|
||||
and all(c.quality_state != 'fail' for c in checks_to_consider)
|
||||
and self.is_first_started_wo,
|
||||
'current_quality_check_id': check.id,
|
||||
'worksheet_page': check.point_id.worksheet_page,
|
||||
})
|
||||
|
||||
def action_menu(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mrp.workorder',
|
||||
'views': [[self.env.ref('mrp_workorder.mrp_workorder_view_form_tablet_menu').id, 'form']],
|
||||
'name': _('Menu'),
|
||||
'target': 'new',
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
def action_add_component(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mrp_workorder.additional.product',
|
||||
'views': [[self.env.ref('mrp_workorder.view_mrp_workorder_additional_product_wizard').id, 'form']],
|
||||
'name': _('Add Component'),
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_workorder_id': self.id,
|
||||
'default_type': 'component',
|
||||
'default_company_id': self.company_id.id,
|
||||
}
|
||||
}
|
||||
|
||||
def action_add_byproduct(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mrp_workorder.additional.product',
|
||||
'views': [[self.env.ref('mrp_workorder.view_mrp_workorder_additional_product_wizard').id, 'form']],
|
||||
'name': _('Add By-Product'),
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_workorder_id': self.id,
|
||||
'default_type': 'byproduct',
|
||||
}
|
||||
}
|
||||
|
||||
def button_start(self):
|
||||
res = super().button_start()
|
||||
for check in self.check_ids:
|
||||
if check.component_tracking == 'serial' and check.component_id:
|
||||
check._update_component_quantity()
|
||||
return res
|
||||
|
||||
def action_propose_change(self, change_type, title):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'propose.change',
|
||||
'views': [[self.env.ref('mrp_workorder.view_propose_change_wizard').id, 'form']],
|
||||
'name': title,
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_workorder_id': self.id,
|
||||
'default_step_id': self.current_quality_check_id.id,
|
||||
'default_change_type': change_type,
|
||||
}
|
||||
}
|
||||
|
||||
def action_add_step(self):
|
||||
self.ensure_one()
|
||||
if self.current_quality_check_id:
|
||||
team = self.current_quality_check_id.team_id
|
||||
else:
|
||||
team = self.env['quality.alert.team'].search(['|', ('company_id', '=', self.company_id.id), ('company_id', '=', False)], limit=1)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'quality.check',
|
||||
'views': [[self.env.ref('mrp_workorder.add_quality_check_from_tablet').id, 'form']],
|
||||
'name': _('Add a Step'),
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_test_type_id': self.env.ref('quality.test_type_instructions').id,
|
||||
'default_workorder_id': self.id,
|
||||
'default_product_id': self.product_id.id,
|
||||
'default_team_id': team.id,
|
||||
}
|
||||
}
|
||||
|
||||
def _compute_check(self):
|
||||
for workorder in self:
|
||||
todo = False
|
||||
fail = False
|
||||
for check in workorder.check_ids:
|
||||
if check.quality_state == 'none':
|
||||
todo = True
|
||||
elif check.quality_state == 'fail':
|
||||
fail = True
|
||||
if fail and todo:
|
||||
break
|
||||
workorder.quality_check_fail = fail
|
||||
workorder.quality_check_todo = todo
|
||||
|
||||
def _compute_quality_alert_count(self):
|
||||
for workorder in self:
|
||||
workorder.quality_alert_count = len(workorder.quality_alert_ids)
|
||||
|
||||
def _create_checks(self):
|
||||
for wo in self:
|
||||
# Track components which have a control point
|
||||
processed_move = self.env['stock.move']
|
||||
|
||||
production = wo.production_id
|
||||
|
||||
move_raw_ids = wo.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
|
||||
move_finished_ids = wo.move_finished_ids.filtered(lambda m: m.state not in ('done', 'cancel') and m.product_id != wo.production_id.product_id)
|
||||
previous_check = self.env['quality.check']
|
||||
for point in wo.quality_point_ids:
|
||||
# Check if we need a quality control for this point
|
||||
if point.check_execute_now():
|
||||
moves = self.env['stock.move']
|
||||
values = {
|
||||
'production_id': production.id,
|
||||
'workorder_id': wo.id,
|
||||
'point_id': point.id,
|
||||
'team_id': point.team_id.id,
|
||||
'company_id': wo.company_id.id,
|
||||
'product_id': production.product_id.id,
|
||||
# Two steps are from the same production
|
||||
# if and only if the produced quantities at the time they were created are equal.
|
||||
'finished_product_sequence': wo.qty_produced,
|
||||
'previous_check_id': previous_check.id,
|
||||
'worksheet_document': point.worksheet_document,
|
||||
}
|
||||
if point.test_type == 'register_byproducts':
|
||||
moves = move_finished_ids.filtered(lambda m: m.product_id == point.component_id)
|
||||
if not moves:
|
||||
moves = production.move_finished_ids.filtered(lambda m: not m.operation_id and m.product_id == point.component_id)
|
||||
elif point.test_type == 'register_consumed_materials':
|
||||
moves = move_raw_ids.filtered(lambda m: m.product_id == point.component_id)
|
||||
if not moves:
|
||||
moves = production.move_raw_ids.filtered(lambda m: not m.operation_id and m.product_id == point.component_id)
|
||||
else:
|
||||
check = self.env['quality.check'].create(values)
|
||||
previous_check.next_check_id = check
|
||||
previous_check = check
|
||||
# Create 'register ...' checks
|
||||
for move in moves:
|
||||
check_vals = values.copy()
|
||||
check_vals.update(wo._defaults_from_move(move))
|
||||
# Create quality check and link it to the chain
|
||||
check_vals.update({'previous_check_id': previous_check.id})
|
||||
check = self.env['quality.check'].create(check_vals)
|
||||
previous_check.next_check_id = check
|
||||
previous_check = check
|
||||
processed_move |= moves
|
||||
|
||||
# Generate quality checks associated with unreferenced components
|
||||
moves_without_check = ((move_raw_ids | move_finished_ids) - processed_move).filtered(lambda move: (move.has_tracking != 'none' and not move.raw_material_production_id.use_auto_consume_components_lots) or move.operation_id)
|
||||
quality_team_id = self.env['quality.alert.team'].search(['|', ('company_id', '=', wo.company_id.id), ('company_id', '=', False)], limit=1).id
|
||||
for move in moves_without_check:
|
||||
values = {
|
||||
'production_id': production.id,
|
||||
'workorder_id': wo.id,
|
||||
'product_id': production.product_id.id,
|
||||
'company_id': wo.company_id.id,
|
||||
'component_id': move.product_id.id,
|
||||
'team_id': quality_team_id,
|
||||
# Two steps are from the same production
|
||||
# if and only if the produced quantities at the time they were created are equal.
|
||||
'finished_product_sequence': wo.qty_produced,
|
||||
'previous_check_id': previous_check.id,
|
||||
}
|
||||
if move in move_raw_ids:
|
||||
test_type = self.env.ref('mrp_workorder.test_type_register_consumed_materials')
|
||||
if move in move_finished_ids:
|
||||
test_type = self.env.ref('mrp_workorder.test_type_register_byproducts')
|
||||
values.update({'test_type_id': test_type.id})
|
||||
values.update(wo._defaults_from_move(move))
|
||||
check = self.env['quality.check'].create(values)
|
||||
previous_check.next_check_id = check
|
||||
previous_check = check
|
||||
|
||||
# Set default quality_check
|
||||
wo._change_quality_check(position='first')
|
||||
|
||||
def _get_byproduct_move_to_update(self):
|
||||
moves = super(MrpProductionWorkcenterLine, self)._get_byproduct_move_to_update()
|
||||
return moves.filtered(lambda m: m.product_id.tracking == 'none')
|
||||
|
||||
def record_production(self):
|
||||
if not self:
|
||||
return True
|
||||
|
||||
self.ensure_one()
|
||||
self._check_sn_uniqueness()
|
||||
self._check_company()
|
||||
if any(x.quality_state == 'none' for x in self.check_ids if x.test_type != 'instructions'):
|
||||
raise UserError(_('You still need to do the quality checks!'))
|
||||
if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
|
||||
raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.'))
|
||||
|
||||
if self.production_id.product_id.tracking != 'none' and not self.finished_lot_id and self.move_raw_ids:
|
||||
raise UserError(_('You should provide a lot/serial number for the final product'))
|
||||
|
||||
backorder = False
|
||||
# Trigger the backorder process if we produce less than expected
|
||||
if float_compare(self.qty_producing, self.qty_remaining, precision_rounding=self.product_uom_id.rounding) == -1 and self.is_first_started_wo:
|
||||
backorder = self.production_id._split_productions()[1:]
|
||||
for workorder in backorder.workorder_ids:
|
||||
if workorder.product_tracking == 'serial':
|
||||
workorder.qty_producing = 1
|
||||
else:
|
||||
workorder.qty_producing = workorder.qty_remaining
|
||||
self.production_id.product_qty = self.qty_producing
|
||||
else:
|
||||
if self.operation_id:
|
||||
backorder = (self.production_id.procurement_group_id.mrp_production_ids - self.production_id).filtered(
|
||||
lambda p: p.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_id).state not in ('cancel', 'done')
|
||||
)[:1]
|
||||
else:
|
||||
index = list(self.production_id.workorder_ids).index(self)
|
||||
backorder = (self.production_id.procurement_group_id.mrp_production_ids - self.production_id).filtered(
|
||||
lambda p: index < len(p.workorder_ids) and p.workorder_ids[index].state not in ('cancel', 'done')
|
||||
)[:1]
|
||||
|
||||
self.button_finish()
|
||||
|
||||
if backorder:
|
||||
for wo in (self.production_id | backorder).workorder_ids:
|
||||
if wo.state in ('done', 'cancel'):
|
||||
continue
|
||||
wo.current_quality_check_id.update(wo._defaults_from_move(wo.move_id))
|
||||
if wo.move_id:
|
||||
wo.current_quality_check_id._update_component_quantity()
|
||||
if not self.env.context.get('no_start_next'):
|
||||
if self.operation_id:
|
||||
return backorder.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_id).open_tablet_view()
|
||||
else:
|
||||
index = list(self.production_id.workorder_ids).index(self)
|
||||
return backorder.workorder_ids[index].open_tablet_view()
|
||||
return True
|
||||
|
||||
def _defaults_from_move(self, move):
|
||||
self.ensure_one()
|
||||
vals = {'move_id': move.id}
|
||||
move_line_id = move.move_line_ids.filtered(lambda sml: sml._without_quality_checks())[:1]
|
||||
if move_line_id:
|
||||
vals.update({
|
||||
'move_line_id': move_line_id.id,
|
||||
'lot_id': move_line_id.lot_id.id,
|
||||
'qty_done': move_line_id.reserved_uom_qty or 1.0
|
||||
})
|
||||
return vals
|
||||
|
||||
# --------------------------
|
||||
# Buttons from quality.check
|
||||
# --------------------------
|
||||
|
||||
def open_tablet_view(self):
|
||||
self.ensure_one()
|
||||
if not self.is_user_working and self.working_state != 'blocked' and self.state in ('ready', 'waiting', 'progress', 'pending'):
|
||||
self.button_start()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.tablet_client_action")
|
||||
action['target'] = 'fullscreen'
|
||||
action['res_id'] = self.id
|
||||
action['context'] = {
|
||||
'active_id': self.id,
|
||||
'from_production_order': self.env.context.get('from_production_order'),
|
||||
'from_manufacturing_order': self.env.context.get('from_manufacturing_order')
|
||||
}
|
||||
return action
|
||||
|
||||
def action_open_manufacturing_order(self):
|
||||
action = self.with_context(no_start_next=True).do_finish()
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
res = self.production_id.button_mark_done()
|
||||
if res is not True:
|
||||
res['context'] = dict(res['context'], from_workorder=True)
|
||||
return res
|
||||
except (UserError, ValidationError) as e:
|
||||
# log next activity on MO with error message
|
||||
self.production_id.activity_schedule(
|
||||
'mail.mail_activity_data_warning',
|
||||
note=e.name,
|
||||
summary=('The %s could not be closed') % (self.production_id.name),
|
||||
user_id=self.env.user.id)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mrp.production',
|
||||
'views': [[self.env.ref('mrp.mrp_production_form_view').id, 'form']],
|
||||
'res_id': self.production_id.id,
|
||||
'target': 'main',
|
||||
}
|
||||
return action
|
||||
|
||||
def do_finish(self):
|
||||
action = True
|
||||
if self.state != 'done':
|
||||
action = self.record_production()
|
||||
if action is not True:
|
||||
return action
|
||||
# workorder tree view action should redirect to the same view instead of workorder kanban view when WO mark as done.
|
||||
return self.action_back()
|
||||
|
||||
def get_workorder_data(self):
|
||||
# order quality check chain
|
||||
ele = self.check_ids.filtered(lambda check: not check.previous_check_id)
|
||||
sorted_check_list = []
|
||||
while ele:
|
||||
sorted_check_list += ele.ids
|
||||
ele = ele.next_check_id
|
||||
data = {
|
||||
'mrp.workorder': self.read(self._get_fields_for_tablet(), load=False)[0],
|
||||
'quality.check': self.check_ids._get_fields_for_tablet(sorted_check_list),
|
||||
'operation': self.operation_id.read(self.operation_id._get_fields_for_tablet())[0] if self.operation_id else {},
|
||||
'working_state': self.workcenter_id.working_state,
|
||||
'views': {
|
||||
'workorder': self.env.ref('mrp_workorder.mrp_workorder_view_form_tablet').id,
|
||||
'check': self.env.ref('mrp_workorder.quality_check_view_form_tablet').id,
|
||||
},
|
||||
}
|
||||
return data
|
||||
|
||||
def get_summary_data(self):
|
||||
self.ensure_one()
|
||||
# show rainbow man only the first time
|
||||
show_rainbow = any(not t.date_end for t in self.time_ids)
|
||||
self.end_all()
|
||||
if any(step.quality_state == 'none' for step in self.check_ids):
|
||||
raise UserError(_('You still need to do the quality checks!'))
|
||||
last30op = self.env['mrp.workorder'].search_read([
|
||||
('operation_id', '=', self.operation_id.id),
|
||||
('date_finished', '>', fields.datetime.today() - relativedelta(days=30)),
|
||||
], ['duration'], order='duration')
|
||||
last30op = [item['duration'] for item in last30op]
|
||||
|
||||
passed_checks = len(list(check.quality_state == 'pass' for check in self.check_ids))
|
||||
if passed_checks:
|
||||
score = int(3.0 * len(self.check_ids) / passed_checks)
|
||||
elif not self.check_ids:
|
||||
score = 3
|
||||
else:
|
||||
score = 0
|
||||
|
||||
return {
|
||||
'duration': self.duration,
|
||||
'position': bisect_left(last30op, self.duration), # which position regarded other workorders ranked by duration
|
||||
'quality_score': score,
|
||||
'show_rainbow': show_rainbow,
|
||||
}
|
||||
|
||||
def _action_confirm(self):
|
||||
res = super()._action_confirm()
|
||||
self.filtered(lambda wo: not wo.check_ids)._create_checks()
|
||||
return res
|
||||
|
||||
def _update_qty_producing(self, quantity):
|
||||
if float_is_zero(quantity, precision_rounding=self.product_uom_id.rounding):
|
||||
self.check_ids.unlink()
|
||||
super()._update_qty_producing(quantity)
|
||||
|
||||
def _web_gantt_progress_bar_workcenter_id(self, res_ids, start, stop):
|
||||
self.env['mrp.workorder'].check_access_rights('read')
|
||||
workcenters = self.env['mrp.workcenter'].search([('id', 'in', res_ids)])
|
||||
workorders = self.env['mrp.workorder'].search([
|
||||
('workcenter_id', 'in', res_ids),
|
||||
('state', 'not in', ['done', 'cancel']),
|
||||
('date_planned_start', '<=', stop.replace(tzinfo=None)),
|
||||
('date_planned_finished', '>=', start.replace(tzinfo=None)),
|
||||
])
|
||||
planned_hours = defaultdict(float)
|
||||
workcenters_work_intervals, dummy = workcenters.resource_id._get_valid_work_intervals(start, stop)
|
||||
for workorder in workorders:
|
||||
max_start = max(start, utc.localize(workorder.date_planned_start))
|
||||
min_end = min(stop, utc.localize(workorder.date_planned_finished))
|
||||
interval = Intervals([(max_start, min_end, self.env['resource.calendar.attendance'])])
|
||||
work_intervals = interval & workcenters_work_intervals[workorder.workcenter_id.resource_id.id]
|
||||
planned_hours[workorder.workcenter_id] += sum_intervals(work_intervals)
|
||||
work_hours = {
|
||||
id: sum_intervals(work_intervals) for id, work_intervals in workcenters_work_intervals.items()
|
||||
}
|
||||
return {
|
||||
workcenter.id: {
|
||||
'value': planned_hours[workcenter],
|
||||
'max_value': work_hours.get(workcenter.resource_id.id, 0.0),
|
||||
}
|
||||
for workcenter in workcenters
|
||||
}
|
||||
|
||||
def _web_gantt_progress_bar(self, field, res_ids, start, stop):
|
||||
if field == 'workcenter_id':
|
||||
return dict(
|
||||
self._web_gantt_progress_bar_workcenter_id(res_ids, start, stop),
|
||||
warning=_("This workcenter isn't expected to have open workorders during this period. Work hours :"),
|
||||
)
|
||||
raise NotImplementedError("This Progress Bar is not implemented.")
|
||||
|
||||
@api.model
|
||||
def gantt_progress_bar(self, fields, res_ids, date_start_str, date_stop_str):
|
||||
start_utc, stop_utc = string_to_datetime(date_start_str), string_to_datetime(date_stop_str)
|
||||
today = datetime.now(utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
start_utc = max(start_utc, today)
|
||||
progress_bars = {}
|
||||
for field in fields:
|
||||
progress_bars[field] = self._web_gantt_progress_bar(field, res_ids[field], start_utc, stop_utc)
|
||||
return progress_bars
|
||||
|
||||
def _get_fields_for_tablet(self):
|
||||
""" List of fields on the workorder object that are needed by the tablet
|
||||
client action. The purpose of this function is to be overridden in order
|
||||
to inject new fields to the client action.
|
||||
"""
|
||||
return [
|
||||
'production_id',
|
||||
'name',
|
||||
'qty_producing',
|
||||
'state',
|
||||
'company_id',
|
||||
'workcenter_id',
|
||||
'current_quality_check_id',
|
||||
'operation_note',
|
||||
]
|
||||
543
mrp_workorder/models/quality.py
Normal file
543
mrp_workorder/models/quality.py
Normal file
@@ -0,0 +1,543 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from markupsafe import Markup
|
||||
from odoo import SUPERUSER_ID, api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.fields import Command
|
||||
from odoo.tools import float_compare, float_round
|
||||
|
||||
|
||||
class TestType(models.Model):
|
||||
_inherit = "quality.point.test_type"
|
||||
|
||||
allow_registration = fields.Boolean(search='_get_domain_from_allow_registration',
|
||||
store=False, default=False)
|
||||
|
||||
def _get_domain_from_allow_registration(self, operator, value):
|
||||
if value:
|
||||
return []
|
||||
else:
|
||||
return [('technical_name', 'not in', ['register_byproducts', 'register_consumed_materials', 'print_label'])]
|
||||
|
||||
|
||||
class MrpRouting(models.Model):
|
||||
_inherit = "mrp.routing.workcenter"
|
||||
|
||||
quality_point_ids = fields.One2many('quality.point', 'operation_id', copy=True)
|
||||
quality_point_count = fields.Integer('Instructions', compute='_compute_quality_point_count')
|
||||
|
||||
@api.depends('quality_point_ids')
|
||||
def _compute_quality_point_count(self):
|
||||
read_group_res = self.env['quality.point'].sudo().read_group(
|
||||
[('id', 'in', self.quality_point_ids.ids)],
|
||||
['operation_id'], 'operation_id'
|
||||
)
|
||||
data = dict((res['operation_id'][0], res['operation_id_count']) for res in read_group_res)
|
||||
for operation in self:
|
||||
operation.quality_point_count = data.get(operation.id, 0)
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if 'bom_id' in vals:
|
||||
self.quality_point_ids._change_product_ids_for_bom(self.bom_id)
|
||||
return res
|
||||
|
||||
def copy(self, default=None):
|
||||
res = super().copy(default)
|
||||
if default and "bom_id" in default:
|
||||
res.quality_point_ids._change_product_ids_for_bom(res.bom_id)
|
||||
return res
|
||||
|
||||
def toggle_active(self):
|
||||
self.with_context(active_test=False).quality_point_ids.toggle_active()
|
||||
return super().toggle_active()
|
||||
|
||||
def action_mrp_workorder_show_steps(self):
|
||||
self.ensure_one()
|
||||
if self.bom_id.picking_type_id:
|
||||
picking_type_ids = self.bom_id.picking_type_id.ids
|
||||
else:
|
||||
picking_type_ids = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')], limit=1).ids
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.action_mrp_workorder_show_steps")
|
||||
ctx = {
|
||||
'default_company_id': self.company_id.id,
|
||||
'default_operation_id': self.id,
|
||||
'default_picking_type_ids': picking_type_ids,
|
||||
}
|
||||
action.update({'context': ctx, 'domain': [('operation_id', '=', self.id)]})
|
||||
return action
|
||||
|
||||
def _get_fields_for_tablet(self):
|
||||
""" List of fields on the operation object that are needed by the tablet
|
||||
client action. The purpose of this function is to be overridden in order
|
||||
to inject new fields to the client action.
|
||||
"""
|
||||
return [
|
||||
'worksheet',
|
||||
'id',
|
||||
]
|
||||
|
||||
|
||||
class QualityPoint(models.Model):
|
||||
_inherit = "quality.point"
|
||||
|
||||
def _default_product_ids(self):
|
||||
# Determines a default product from the default operation's BOM.
|
||||
operation_id = self.env.context.get('default_operation_id')
|
||||
if operation_id:
|
||||
bom = self.env['mrp.routing.workcenter'].browse(operation_id).bom_id
|
||||
return bom.product_id.ids if bom.product_id else bom.product_tmpl_id.product_variant_id.ids
|
||||
|
||||
is_workorder_step = fields.Boolean(compute='_compute_is_workorder_step')
|
||||
operation_id = fields.Many2one(
|
||||
'mrp.routing.workcenter', 'Step', check_company=True)
|
||||
bom_id = fields.Many2one(related='operation_id.bom_id')
|
||||
bom_active = fields.Boolean('Related Bill of Material Active', related='bom_id.active')
|
||||
component_ids = fields.One2many('product.product', compute='_compute_component_ids')
|
||||
product_ids = fields.Many2many(
|
||||
default=_default_product_ids,
|
||||
domain="operation_id and [('id', 'in', bom_product_ids)] or [('type', 'in', ('product', 'consu')), '|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
bom_product_ids = fields.One2many('product.product', compute="_compute_bom_product_ids")
|
||||
test_type_id = fields.Many2one(
|
||||
'quality.point.test_type',
|
||||
domain="[('allow_registration', '=', operation_id and is_workorder_step)]")
|
||||
test_report_type = fields.Selection([('pdf', 'PDF'), ('zpl', 'ZPL')], string="Report Type", default="pdf", required=True)
|
||||
source_document = fields.Selection(
|
||||
selection=[('operation', 'Specific Page of Operation Worksheet'), ('step', 'Custom')],
|
||||
string="Step Document",
|
||||
default='operation')
|
||||
worksheet_page = fields.Integer('Worksheet Page', default=1)
|
||||
worksheet_document = fields.Binary('Image/PDF')
|
||||
worksheet_url = fields.Char('Google doc URL')
|
||||
# Used with type register_consumed_materials the product raw to encode.
|
||||
component_id = fields.Many2one('product.product', 'Product To Register', check_company=True)
|
||||
|
||||
@api.onchange('bom_product_ids', 'is_workorder_step')
|
||||
def _onchange_bom_product_ids(self):
|
||||
if self.is_workorder_step and self.bom_product_ids:
|
||||
self.product_ids = self.product_ids._origin & self.bom_product_ids
|
||||
self.product_category_ids = False
|
||||
|
||||
@api.depends('bom_id.product_id', 'bom_id.product_tmpl_id.product_variant_ids', 'is_workorder_step', 'bom_id')
|
||||
def _compute_bom_product_ids(self):
|
||||
self.bom_product_ids = False
|
||||
points_for_workorder_step = self.filtered(lambda p: p.operation_id and p.bom_id)
|
||||
for point in points_for_workorder_step:
|
||||
bom_product_ids = point.bom_id.product_id or point.bom_id.product_tmpl_id.product_variant_ids
|
||||
point.bom_product_ids = bom_product_ids.filtered(lambda p: not p.company_id or p.company_id == point.company_id._origin)
|
||||
|
||||
@api.depends('product_ids', 'test_type_id', 'is_workorder_step')
|
||||
def _compute_component_ids(self):
|
||||
self.component_ids = False
|
||||
for point in self:
|
||||
if not point.is_workorder_step or not self.bom_id or point.test_type not in ('register_consumed_materials', 'register_byproducts'):
|
||||
point.component_id = None
|
||||
continue
|
||||
if point.test_type == 'register_byproducts':
|
||||
point.component_ids = point.bom_id.byproduct_ids.product_id
|
||||
else:
|
||||
bom_products = point.bom_id.product_id or point.bom_id.product_tmpl_id.product_variant_ids
|
||||
# If product_ids is set the step will exist only for these product variant then we can filter out for the bom explode
|
||||
if point.product_ids:
|
||||
bom_products &= point.product_ids._origin
|
||||
|
||||
component_product_ids = set()
|
||||
for product in bom_products:
|
||||
dummy, lines_done = point.bom_id.explode(product, 1.0)
|
||||
component_product_ids |= {line[0].product_id.id for line in lines_done}
|
||||
point.component_ids = self.env['product.product'].browse(component_product_ids)
|
||||
|
||||
@api.depends('operation_id', 'picking_type_ids')
|
||||
def _compute_is_workorder_step(self):
|
||||
for quality_point in self:
|
||||
quality_point.is_workorder_step = quality_point.picking_type_ids and\
|
||||
all(pt.code == 'mrp_operation' for pt in quality_point.picking_type_ids)
|
||||
|
||||
def _change_product_ids_for_bom(self, bom_id):
|
||||
products = bom_id.product_id or bom_id.product_tmpl_id.product_variant_ids
|
||||
self.product_ids = [Command.set(products.ids)]
|
||||
|
||||
def _get_comparison_values(self):
|
||||
if not self:
|
||||
return False
|
||||
self.ensure_one()
|
||||
return tuple(self[key] for key in ('test_type_id', 'title', 'component_id', 'sequence'))
|
||||
|
||||
@api.onchange('operation_id')
|
||||
def _onchange_operation_id(self):
|
||||
if self.operation_id:
|
||||
self._change_product_ids_for_bom(self.bom_id)
|
||||
|
||||
|
||||
class QualityAlert(models.Model):
|
||||
_inherit = "quality.alert"
|
||||
|
||||
workorder_id = fields.Many2one('mrp.workorder', 'Operation', check_company=True)
|
||||
workcenter_id = fields.Many2one('mrp.workcenter', 'Work Center', check_company=True)
|
||||
production_id = fields.Many2one('mrp.production', "Production Order", check_company=True)
|
||||
|
||||
|
||||
class QualityCheck(models.Model):
|
||||
_inherit = "quality.check"
|
||||
|
||||
workorder_id = fields.Many2one(
|
||||
'mrp.workorder', 'Operation', check_company=True)
|
||||
workcenter_id = fields.Many2one('mrp.workcenter', related='workorder_id.workcenter_id', store=True, readonly=True) # TDE: necessary ?
|
||||
production_id = fields.Many2one(
|
||||
'mrp.production', 'Production Order', check_company=True)
|
||||
|
||||
# doubly linked chain for tablet view navigation
|
||||
next_check_id = fields.Many2one('quality.check')
|
||||
previous_check_id = fields.Many2one('quality.check')
|
||||
|
||||
# For components registration
|
||||
move_id = fields.Many2one(
|
||||
'stock.move', 'Stock Move', check_company=True)
|
||||
move_line_id = fields.Many2one(
|
||||
'stock.move.line', 'Stock Move Line', check_company=True)
|
||||
component_id = fields.Many2one(
|
||||
'product.product', 'Component', check_company=True)
|
||||
component_uom_id = fields.Many2one('uom.uom', related='move_id.product_uom', readonly=True)
|
||||
|
||||
qty_done = fields.Float('Done', digits='Product Unit of Measure')
|
||||
finished_lot_id = fields.Many2one('stock.lot', 'Finished Lot/Serial', related='production_id.lot_producing_id', store=True)
|
||||
additional = fields.Boolean('Register additional product', compute='_compute_additional')
|
||||
component_tracking = fields.Selection(related='component_id.tracking', string="Is Component Tracked")
|
||||
|
||||
# Workorder specific fields
|
||||
component_remaining_qty = fields.Float('Remaining Quantity for Component', compute='_compute_component_data', digits='Product Unit of Measure')
|
||||
component_qty_to_do = fields.Float(compute='_compute_component_qty_to_do')
|
||||
is_user_working = fields.Boolean(related="workorder_id.is_user_working")
|
||||
consumption = fields.Selection(related="workorder_id.consumption")
|
||||
working_state = fields.Selection(related="workorder_id.working_state")
|
||||
is_deleted = fields.Boolean('Deleted in production')
|
||||
|
||||
# Computed fields
|
||||
title = fields.Char('Title', compute='_compute_title')
|
||||
result = fields.Char('Result', compute='_compute_result')
|
||||
|
||||
# Used to group the steps belonging to the same production
|
||||
# We use a float because it is actually filled in by the produced quantity at the step creation.
|
||||
finished_product_sequence = fields.Float('Finished Product Sequence Number')
|
||||
worksheet_document = fields.Binary('Image/PDF')
|
||||
worksheet_page = fields.Integer(related='point_id.worksheet_page')
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values):
|
||||
points = self.env['quality.point'].search([
|
||||
('id', 'in', [value.get('point_id') for value in values]),
|
||||
('component_id', '!=', False)
|
||||
])
|
||||
for value in values:
|
||||
if not value.get('component_id') and value.get('point_id'):
|
||||
point = points.filtered(lambda p: p.id == value.get('point_id'))
|
||||
if point:
|
||||
value['component_id'] = point.component_id.id
|
||||
return super(QualityCheck, self).create(values)
|
||||
|
||||
@api.depends('test_type_id', 'component_id', 'component_id.name', 'workorder_id', 'workorder_id.name')
|
||||
def _compute_title(self):
|
||||
super()._compute_title()
|
||||
for check in self:
|
||||
if not check.point_id or check.component_id:
|
||||
check.title = '{} "{}"'.format(check.test_type_id.display_name, check.component_id.name or check.workorder_id.name)
|
||||
|
||||
@api.depends('point_id', 'quality_state', 'component_id', 'component_uom_id', 'lot_id', 'qty_done')
|
||||
def _compute_result(self):
|
||||
for check in self:
|
||||
if check.quality_state == 'none':
|
||||
check.result = ''
|
||||
else:
|
||||
check.result = check._get_check_result()
|
||||
|
||||
@api.depends('move_id')
|
||||
def _compute_additional(self):
|
||||
""" The stock_move is linked to additional workorder line only at
|
||||
record_production. So line without move during production are additionnal
|
||||
ones. """
|
||||
for check in self:
|
||||
check.additional = not check.move_id
|
||||
|
||||
@api.depends('qty_done', 'component_remaining_qty')
|
||||
def _compute_component_qty_to_do(self):
|
||||
for wo in self:
|
||||
wo.component_qty_to_do = wo.qty_done - wo.component_remaining_qty
|
||||
|
||||
def _get_check_result(self):
|
||||
if self.test_type in ('register_consumed_materials', 'register_byproducts') and self.lot_id:
|
||||
return '{} - {}, {} {}'.format(self.component_id.name, self.lot_id.name, self.qty_done, self.component_uom_id.name)
|
||||
elif self.test_type in ('register_consumed_materials', 'register_byproducts'):
|
||||
return '{}, {} {}'.format(self.component_id.name, self.qty_done, self.component_uom_id.name)
|
||||
else:
|
||||
return ''
|
||||
|
||||
@api.depends('workorder_id.state', 'quality_state', 'workorder_id.qty_producing',
|
||||
'component_tracking', 'test_type', 'component_id', 'move_line_id.lot_id'
|
||||
)
|
||||
def _compute_component_data(self):
|
||||
self.component_remaining_qty = False
|
||||
self.component_uom_id = False
|
||||
for check in self:
|
||||
if check.test_type in ('register_byproducts', 'register_consumed_materials'):
|
||||
if check.quality_state == 'none':
|
||||
completed_lines = check.workorder_id.move_line_ids.filtered(lambda l: l.lot_id) if check.component_id.tracking != 'none' else check.workorder_id.move_line_ids
|
||||
if check.move_id.additional:
|
||||
qty = check.workorder_id.qty_remaining
|
||||
else:
|
||||
qty = check.workorder_id.qty_producing
|
||||
check.component_remaining_qty = self._prepare_component_quantity(check.move_id, qty) - sum(completed_lines.mapped('qty_done'))
|
||||
check.component_uom_id = check.move_id.product_uom
|
||||
|
||||
def action_print(self):
|
||||
if self.product_id.uom_id.category_id == self.env.ref('uom.product_uom_categ_unit'):
|
||||
qty = int(self.workorder_id.qty_producing)
|
||||
else:
|
||||
qty = 1
|
||||
|
||||
quality_point_id = self.point_id
|
||||
report_type = quality_point_id.test_report_type
|
||||
|
||||
if self.product_id.tracking == 'none':
|
||||
xml_id = 'product.action_open_label_layout'
|
||||
wizard_action = self.env['ir.actions.act_window']._for_xml_id(xml_id)
|
||||
wizard_action['context'] = {'default_product_ids': self.product_id.ids}
|
||||
if report_type == 'zpl':
|
||||
wizard_action['context']['default_print_format'] = 'zpl'
|
||||
res = wizard_action
|
||||
else:
|
||||
if self.workorder_id.finished_lot_id:
|
||||
if report_type == 'zpl':
|
||||
xml_id = 'stock.label_lot_template'
|
||||
else:
|
||||
xml_id = 'stock.action_report_lot_label'
|
||||
res = self.env.ref(xml_id).report_action([self.workorder_id.finished_lot_id.id] * qty)
|
||||
else:
|
||||
raise UserError(_('You did not set a lot/serial number for '
|
||||
'the final product'))
|
||||
|
||||
res['id'] = self.env.ref(xml_id).id
|
||||
|
||||
# The button goes immediately to the next step
|
||||
self._next()
|
||||
return res
|
||||
|
||||
def action_next(self):
|
||||
self.ensure_one()
|
||||
return self._next()
|
||||
|
||||
def action_continue(self):
|
||||
self.ensure_one()
|
||||
self._next(continue_production=True)
|
||||
|
||||
def add_check_in_chain(self, activity=True):
|
||||
self.ensure_one()
|
||||
if self.workorder_id.current_quality_check_id:
|
||||
self._insert_in_chain('after', self.workorder_id.current_quality_check_id)
|
||||
else:
|
||||
self.workorder_id.current_quality_check_id = self
|
||||
if self.workorder_id.production_id.bom_id and activity:
|
||||
body = Markup(_("<b>New Step suggested by %s</b><br/>"
|
||||
"<b>Reason:</b>"
|
||||
"%s", self.env.user.name, self.additional_note
|
||||
))
|
||||
self.env['mail.activity'].sudo().create({
|
||||
'res_model_id': self.env.ref('mrp.model_mrp_bom').id,
|
||||
'res_id': self.workorder_id.production_id.bom_id.id,
|
||||
'user_id': self.workorder_id.product_id.responsible_id.id or SUPERUSER_ID,
|
||||
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
|
||||
'summary': _('BoM feedback %s (%s)', self.title, self.workorder_id.production_id.name),
|
||||
'note': body,
|
||||
})
|
||||
|
||||
@api.model
|
||||
def _prepare_component_quantity(self, move, qty_producing):
|
||||
""" helper that computes quantity to consume (or to create in case of byproduct)
|
||||
depending on the quantity producing and the move's unit factor"""
|
||||
if move.product_id.tracking == 'serial':
|
||||
uom = move.product_id.uom_id
|
||||
else:
|
||||
uom = move.product_uom
|
||||
return move.product_uom._compute_quantity(
|
||||
qty_producing * move.unit_factor,
|
||||
uom,
|
||||
round=False
|
||||
)
|
||||
|
||||
def _create_extra_move_lines(self):
|
||||
"""Create new sml if quantity produced is bigger than the reserved one"""
|
||||
vals_list = []
|
||||
# apply putaway
|
||||
location_dest_id = self.move_id.location_dest_id._get_putaway_strategy(self.move_id.product_id)
|
||||
quants = self.env['stock.quant']._gather(self.product_id, self.move_id.location_id, lot_id=self.lot_id, strict=False)
|
||||
# Search for a sub-locations where the product is available.
|
||||
# Loop on the quants to get the locations. If there is not enough
|
||||
# quantity into stock, we take the move location. Anyway, no
|
||||
# reservation is made, so it is still possible to change it afterwards.
|
||||
shared_vals = {
|
||||
'move_id': self.move_id.id,
|
||||
'product_id': self.move_id.product_id.id,
|
||||
'location_dest_id': location_dest_id.id,
|
||||
'reserved_uom_qty': 0,
|
||||
'product_uom_id': self.move_id.product_uom.id,
|
||||
'lot_id': self.lot_id.id,
|
||||
'company_id': self.move_id.company_id.id,
|
||||
}
|
||||
for quant in quants:
|
||||
vals = shared_vals.copy()
|
||||
quantity = quant.quantity - quant.reserved_quantity
|
||||
quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom_id, rounding_method='HALF-UP')
|
||||
rounding = quant.product_uom_id.rounding
|
||||
if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or
|
||||
float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0):
|
||||
continue
|
||||
vals.update({
|
||||
'location_id': quant.location_id.id,
|
||||
'qty_done': min(quantity, self.qty_done),
|
||||
})
|
||||
|
||||
vals_list.append(vals)
|
||||
self.qty_done -= vals['qty_done']
|
||||
# If all the qty_done is distributed, we can close the loop
|
||||
if float_compare(self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) <= 0:
|
||||
break
|
||||
|
||||
if float_compare(self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) > 0:
|
||||
vals = shared_vals.copy()
|
||||
vals.update({
|
||||
'location_id': self.move_id.location_id.id,
|
||||
'qty_done': self.qty_done,
|
||||
})
|
||||
|
||||
vals_list.append(vals)
|
||||
return vals_list
|
||||
|
||||
def _next(self, continue_production=False):
|
||||
""" This function:
|
||||
|
||||
- first: fullfill related move line with right lot and validated quantity.
|
||||
- second: Generate new quality check for remaining quantity and link them to the original check.
|
||||
- third: Pass to the next check or return a failure message.
|
||||
"""
|
||||
self.ensure_one()
|
||||
rounding = self.workorder_id.product_uom_id.rounding
|
||||
if float_compare(self.workorder_id.qty_producing, 0, precision_rounding=rounding) <= 0:
|
||||
raise UserError(_('Please ensure the quantity to produce is greater than 0.'))
|
||||
elif self.test_type in ('register_byproducts', 'register_consumed_materials'):
|
||||
# Form validation
|
||||
# in case we use continue production instead of validate button.
|
||||
# We would like to consume 0 and leave lot_id blank to close the consumption
|
||||
rounding = self.component_uom_id.rounding
|
||||
if self.component_tracking != 'none' and not self.lot_id and self.qty_done != 0:
|
||||
raise UserError(_('Please enter a Lot/SN.'))
|
||||
if float_compare(self.qty_done, 0, precision_rounding=rounding) < 0:
|
||||
raise UserError(_('Please enter a positive quantity.'))
|
||||
|
||||
# Write the lot and qty to the move line
|
||||
if self.move_line_id:
|
||||
rounding = self.move_line_id.product_uom_id.rounding
|
||||
if float_compare(self.qty_done, self.move_line_id.reserved_uom_qty, precision_rounding=rounding) >= 0:
|
||||
self.move_line_id.write({
|
||||
'qty_done': self.qty_done,
|
||||
'lot_id': self.lot_id.id,
|
||||
})
|
||||
else:
|
||||
new_qty_reserved = self.move_line_id.reserved_uom_qty - self.qty_done
|
||||
default = {
|
||||
'reserved_uom_qty': new_qty_reserved,
|
||||
'qty_done': 0,
|
||||
}
|
||||
self.move_line_id.copy(default=default)
|
||||
self.move_line_id.with_context(bypass_reservation_update=True).write({
|
||||
'reserved_uom_qty': self.qty_done,
|
||||
'qty_done': self.qty_done,
|
||||
})
|
||||
self.move_line_id.lot_id = self.lot_id
|
||||
else:
|
||||
line = self.env['stock.move.line'].create(self._create_extra_move_lines())
|
||||
self.move_line_id = line[:1]
|
||||
if continue_production:
|
||||
self.workorder_id._create_subsequent_checks()
|
||||
|
||||
if self.test_type == 'picture' and not self.picture:
|
||||
raise UserError(_('Please upload a picture.'))
|
||||
|
||||
if self.quality_state == 'none':
|
||||
self.do_pass()
|
||||
|
||||
self.workorder_id._change_quality_check(position='next')
|
||||
|
||||
def _update_component_quantity(self):
|
||||
if self.component_tracking == 'serial':
|
||||
self._origin.qty_done = self.component_id.uom_id._compute_quantity(1, self.component_uom_id, rounding_method='HALF-UP')
|
||||
return
|
||||
move = self.move_id
|
||||
# Compute the new quantity for the current component
|
||||
rounding = move.product_uom.rounding
|
||||
new_qty = self._prepare_component_quantity(move, self.workorder_id.qty_producing)
|
||||
qty_todo = float_round(new_qty, precision_rounding=rounding)
|
||||
qty_todo = qty_todo - move.quantity_done
|
||||
if self.move_line_id and self.move_line_id.lot_id:
|
||||
qty_todo = min(self.move_line_id.reserved_uom_qty, qty_todo)
|
||||
self.qty_done = qty_todo
|
||||
|
||||
|
||||
def _insert_in_chain(self, position, relative):
|
||||
"""Insert the quality check `self` in a chain of quality checks.
|
||||
|
||||
The chain of quality checks is implicitly given by the `relative` argument,
|
||||
i.e. by following its `previous_check_id` and `next_check_id` fields.
|
||||
|
||||
:param position: Where we need to insert `self` according to `relative`
|
||||
:type position: string
|
||||
:param relative: Where we need to insert `self` in the chain
|
||||
:type relative: A `quality.check` record.
|
||||
"""
|
||||
self.ensure_one()
|
||||
assert position in ['before', 'after']
|
||||
if position == 'before':
|
||||
new_previous = relative.previous_check_id
|
||||
self.next_check_id = relative
|
||||
self.previous_check_id = new_previous
|
||||
new_previous.next_check_id = self
|
||||
relative.previous_check_id = self
|
||||
else:
|
||||
new_next = relative.next_check_id
|
||||
self.next_check_id = new_next
|
||||
self.previous_check_id = relative
|
||||
new_next.previous_check_id = self
|
||||
relative.next_check_id = self
|
||||
|
||||
@api.model
|
||||
def _get_fields_list_for_tablet(self):
|
||||
return [
|
||||
'lot_id',
|
||||
'move_id',
|
||||
'move_line_id',
|
||||
'note',
|
||||
'additional_note',
|
||||
'title',
|
||||
'quality_state',
|
||||
'qty_done',
|
||||
'test_type_id',
|
||||
'test_type',
|
||||
'user_id',
|
||||
'picture',
|
||||
'additional',
|
||||
'worksheet_document',
|
||||
'worksheet_page',
|
||||
'is_deleted',
|
||||
'point_id',
|
||||
]
|
||||
|
||||
def _get_fields_for_tablet(self, sorted_check_list):
|
||||
""" List of fields on the quality check object that are needed by the tablet
|
||||
client action. The purpose of this function is to be overridden in order
|
||||
to inject new fields to the client action.
|
||||
"""
|
||||
if sorted_check_list:
|
||||
self = self.browse(sorted_check_list)
|
||||
values = self.read(self._get_fields_list_for_tablet(), load=False)
|
||||
for check in values:
|
||||
check['worksheet_url'] = self.env['quality.check'].browse(check['id']).point_id.worksheet_url
|
||||
return values
|
||||
18
mrp_workorder/models/res_config_settings.py
Normal file
18
mrp_workorder/models/res_config_settings.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
group_mrp_wo_tablet_timer = fields.Boolean("Timer", implied_group="mrp_workorder.group_mrp_wo_tablet_timer")
|
||||
|
||||
def set_values(self):
|
||||
super().set_values()
|
||||
if not self.user_has_groups('mrp.group_mrp_manager'):
|
||||
return
|
||||
register_byproducts = self.env.ref('mrp_workorder.test_type_register_byproducts').sudo()
|
||||
if register_byproducts.active != self.group_mrp_byproducts:
|
||||
register_byproducts.active = self.group_mrp_byproducts
|
||||
25
mrp_workorder/models/stock_move.py
Normal file
25
mrp_workorder/models/stock_move.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
def _should_bypass_set_qty_producing(self):
|
||||
production = self.raw_material_production_id or self.production_id
|
||||
if production and (self.has_tracking == 'none' or production.use_auto_consume_components_lots) and ((self.product_id in production.workorder_ids.quality_point_ids.component_id) or self.operation_id):
|
||||
return True
|
||||
return super()._should_bypass_set_qty_producing()
|
||||
|
||||
def _action_assign(self, force_qty=False):
|
||||
res = super()._action_assign(force_qty=force_qty)
|
||||
for workorder in self.raw_material_production_id.workorder_ids:
|
||||
for check in workorder.check_ids:
|
||||
if check.test_type not in ('register_consumed_materials', 'register_byproducts'):
|
||||
continue
|
||||
if check.move_line_id:
|
||||
continue
|
||||
check.write(workorder._defaults_from_move(check.move_id))
|
||||
return res
|
||||
14
mrp_workorder/models/stock_move_line.py
Normal file
14
mrp_workorder/models/stock_move_line.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = 'stock.move.line'
|
||||
|
||||
quality_check_ids = fields.One2many('quality.check', 'move_line_id', string='Check')
|
||||
|
||||
def _without_quality_checks(self):
|
||||
self.ensure_one()
|
||||
return not self.quality_check_ids
|
||||
Reference in New Issue
Block a user