# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 from markupsafe import Markup from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.fields import Command from odoo.tools import float_compare, float_round, is_html_empty class QualityPointTest_Type(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): # The ORM will optimize the domain leaf before calling the search method: # ('field', '=', True) and ('field', '!=', False) become ('field', 'in', OrderedSet([True])) # ('field', '=', False) and ('field', '!=', True) become ('field', ' not in', OrderedSet([True])) if operator not in ('in', 'not in'): return NotImplemented if operator == 'in': return [] else: return [('technical_name', 'not in', ['register_production', 'register_byproducts', 'register_consumed_materials', 'print_label'])] class MrpRoutingWorkcenter(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') employee_ratio = fields.Float("Employee Capacity", default=1, help="Number of employees needed to complete operation.") default_picking_type_ids = fields.One2many(comodel_name='stock.picking.type', compute='_compute_default_picking_type_ids') @api.depends('employee_ratio') def _compute_cost(self): super()._compute_cost() for operation in self: operation.cost += (operation.time_total / 60.0) * operation.workcenter_id.employee_costs_hour * (operation.employee_ratio or 1) @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'], ['__count'] ) data = {operation.id: count for operation, count in read_group_res} for operation in self: operation.quality_point_count = data.get(operation.id, 0) def _compute_default_picking_type_ids(self): self.default_picking_type_ids = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')]) def write(self, vals): if 'active' in vals: self.with_context(active_test=False).quality_point_ids.write({'active': vals['active']}) 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): new_workcenters = super().copy(default) if default and "bom_id" in default: for new_workcenter in new_workcenters: new_workcenter.quality_point_ids._change_product_ids_for_bom(new_workcenter.bom_id) return new_workcenters 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, index='btree_not_null') 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', '=', '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) worksheet_document = fields.Binary('Image/PDF') # 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 & 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 point.test_type == 'register_byproducts': point.component_ids = point.bom_id.byproduct_ids.product_id elif point.test_type == 'register_consumed_materials': 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) @api.depends('operation_id') def _compute_show_failure_location(self): super()._compute_show_failure_location() for point in self: point.show_failure_location = point.show_failure_location and not point.operation_id 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)] @api.onchange('operation_id') def _onchange_operation_id(self): if self.operation_id: self._change_product_ids_for_bom(self.bom_id) @api.onchange('test_type_id') def _onchange_test_type_id(self): if self.test_type_id.technical_name not in ('register_byproducts', 'register_consumed_materials'): self.component_id = False def write(self, vals): res = super().write(vals) if 'picking_type_ids' in vals: self.filtered(lambda p: not p.is_workorder_step).operation_id = False return res def action_view_worksheet_document(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': _("Worksheet Preview"), 'res_model': 'quality.point', 'res_id': self.id, 'views': [(self.env.ref('mrp_workorder.quality_point_worksheet_document_preview_form').id, 'form')], 'target': 'new', } class QualityAlert(models.Model): _inherit = "quality.alert" workorder_id = fields.Many2one('mrp.workorder', 'Operation', check_company=True, index='btree_not_null') workcenter_id = fields.Many2one('mrp.workcenter', 'Work Center', check_company=True) production_id = fields.Many2one('mrp.production', "Production Order", check_company=True, index='btree_not_null') class QualityCheck(models.Model): _inherit = "quality.check" workorder_id = fields.Many2one( 'mrp.workorder', 'Operation', check_company=True, index='btree_not_null') workcenter_id = fields.Many2one('mrp.workcenter', related='workorder_id.workcenter_id') production_id = fields.Many2one( 'mrp.production', 'Production Order', check_company=True, index='btree_not_null') product_tracking = fields.Selection(related='production_id.product_tracking') # 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, index='btree_not_null') component_id = fields.Many2one( 'product.product', 'Component', check_company=True) component_barcode = fields.Char(related='component_id.barcode') component_uom_id = fields.Many2one('uom.uom', related='move_id.product_uom', string='Component Unit', readonly=True) finished_lot_ids = fields.Many2many('stock.lot', 'Finished Lot/Serial', related='production_id.lot_producing_ids') component_tracking = fields.Selection(related='component_id.tracking', string="Is Component Tracked") # Workorder specific fields 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') # TODO DELETE in MASTER # 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') # Employees employee_id = fields.Many2one('hr.employee', string="Employee") @api.model_create_multi def create(self, vals_list): quality_points = {value['point_id'] for value in vals_list if value.get('point_id')} quality_points_component_mapping = { point.id: point.component_id.id for point in self.env['quality.point'].browse(list(quality_points)) if point.component_id } for value in vals_list: if value.get('component_id') or not value.get('point_id'): continue component = quality_points_component_mapping.get(value['point_id']) if component: value['component_id'] = component return super().create(vals_list) @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.title and not check.point_id and 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_ids') def _compute_result(self): for check in self: if check.quality_state == 'none': check.result = '' else: check.result = check._get_check_result() def copy(self, default=None): default = dict(default or {}) new_checks = super().copy(default) for old_check, new_check in zip(self, new_checks): # only insert into chain if the quality check is linked with another quality check if old_check.previous_check_id or old_check.next_check_id: new_check._insert_in_chain('after', old_check) return new_checks def _get_check_result(self): if self.test_type in ('register_consumed_materials', 'register_byproducts'): if len(self.lot_ids) == 1: return f'{self.component_id.name} - {self.lot_ids[0].name}, {self.component_uom_id.name}' return f'{self.component_id.name}, {self.component_uom_id.name}' return '' def action_print(self): quality_point_id = self.point_id report_type = quality_point_id.test_report_type if self.product_id.tracking == 'none': res = self._get_product_label_action(report_type) else: if self.workorder_id.finished_lot_ids: res = self._get_lot_label_action(report_type) else: raise UserError(_('You did not set a lot/serial number for ' 'the final product')) # The button goes immediately to the next step res['next_check_id'] = self._next() return res def _get_print_qty(self): uom_unit = self.env.ref('uom.product_uom_unit') if self.product_tracking != 'serial' and self.product_id.uom_id._has_common_reference(uom_unit): qty = int(self.workorder_id.qty_producing) or int(self.workorder_id.qty_production) else: qty = 1 return qty def _get_product_label_action(self, report_type): self.ensure_one() 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' wizard_action['id'] = self.env.ref(xml_id).id return wizard_action def _get_lot_label_action(self, report_type): qty = self._get_print_qty() 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_ids.ids * qty) res['id'] = self.env.ref(xml_id).id return res def action_next(self): self.ensure_one() return {'next_check_id': self._next()} def add_check_in_chain(self, notify_bom=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 notify_bom and self.workorder_id.production_id.bom_id: body = _('BoM feedback (%(production)s - %(operation)s)', production=self.workorder_id.production_id.name, operation=self.workorder_id.operation_id.name) body += Markup("
%s") % _("New Step suggested by %(user_name)s", user_name=self.env.user.name) if self.title: body += Markup("
%s %s") % (_("Title:"), self.title) if self.note and not is_html_empty(self.note): body += Markup("
%s%s") % (_("Instruction:"), self.note) attachments = [] if self.worksheet_document: attachments = [('document', base64.b64decode(self.worksheet_document))] self.workorder_id.production_id.bom_id.message_post(body=body, attachments=attachments) def _next(self): """ 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() self.workorder_id.current_quality_check_id = self.id if self.test_type == 'picture' and not self.picture: raise UserError(_('Please upload a picture.')) if self.quality_state == 'none': self.do_pass() return self.workorder_id._change_quality_check(position='next') @api.ondelete(at_uninstall=False) def _unlink_repair_chain(self): for check in self: prev = check.previous_check_id nxt = check.next_check_id if prev and nxt: prev.next_check_id = nxt nxt.previous_check_id = prev elif prev: prev.next_check_id = False elif nxt: nxt.previous_check_id = False 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 def do_pass(self): res = super().do_pass() for check in self: if check.move_id: check.move_id.picked = True if check.workorder_id: if check.workorder_id.employee_id: check.employee_id = self.workorder_id.employee_id if check.workorder_id.state == 'ready': check.workorder_id.button_start(bypass=True) return res