200 lines
7.1 KiB
Python
200 lines
7.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
# Part of the Fusion Plating product family.
|
|
#
|
|
# Sub 12 Phase C — trigger-based quality points.
|
|
#
|
|
# Replaces the old "customer.x_fc_requires_qc + customer.x_fc_qc_template_id"
|
|
# direct binding. Now an admin defines fp.quality.point rules with filters
|
|
# (partner / part / coating / step kind) and a trigger event; matching
|
|
# records spawn fusion.plating.quality.check rows from the chosen template.
|
|
|
|
import logging
|
|
|
|
from odoo import _, api, fields, models
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
TRIGGER_TYPES = [
|
|
('manual', 'Manual'),
|
|
('so_confirmed', 'Sale Order Confirmed'),
|
|
('receiving_done', 'Receiving Closed'),
|
|
('job_confirmed', 'Job Confirmed'),
|
|
('job_step_done', 'Job Step Finished'),
|
|
('job_done', 'Job Completed'),
|
|
]
|
|
|
|
|
|
STEP_KINDS = [
|
|
('wet', 'Wet Process'),
|
|
('bake', 'Bake / Cure'),
|
|
('inspect', 'Inspection'),
|
|
('mask', 'Masking'),
|
|
('post', 'Post-Treatment'),
|
|
('other', 'Other'),
|
|
]
|
|
|
|
|
|
class FpQualityPoint(models.Model):
|
|
_name = 'fp.quality.point'
|
|
_description = 'Fusion Plating — Quality Point'
|
|
_inherit = ['mail.thread']
|
|
_order = 'sequence, name'
|
|
|
|
name = fields.Char(required=True, translate=True, tracking=True)
|
|
sequence = fields.Integer(default=10)
|
|
active = fields.Boolean(default=True, tracking=True)
|
|
description = fields.Text(translate=True)
|
|
|
|
trigger_type = fields.Selection(
|
|
TRIGGER_TYPES, string='Trigger', required=True,
|
|
default='job_confirmed', tracking=True,
|
|
help='When this point fires. "manual" never auto-fires.',
|
|
)
|
|
|
|
# ----- Filters (all optional; empty == match all) -----
|
|
partner_ids = fields.Many2many(
|
|
'res.partner', 'fp_quality_point_partner_rel',
|
|
'point_id', 'partner_id', string='Customers',
|
|
)
|
|
part_catalog_ids = fields.Many2many(
|
|
'fp.part.catalog', 'fp_quality_point_part_rel',
|
|
'point_id', 'part_id', string='Parts',
|
|
)
|
|
coating_config_ids = fields.Many2many(
|
|
'fp.coating.config', 'fp_quality_point_coating_rel',
|
|
'point_id', 'coating_id', string='Coatings',
|
|
)
|
|
step_kind = fields.Selection(STEP_KINDS, string='Step Kind')
|
|
|
|
template_id = fields.Many2one(
|
|
'fp.qc.checklist.template', string='Checklist Template',
|
|
required=True, ondelete='restrict',
|
|
)
|
|
assignee_user_id = fields.Many2one(
|
|
'res.users', string='Default Inspector',
|
|
help='If set, the auto-spawned QC check is pre-assigned here.',
|
|
)
|
|
team_id = fields.Many2one('fp.quality.team', string='Quality Team')
|
|
tag_ids = fields.Many2many(
|
|
'fp.quality.tag', 'fp_quality_point_tag_rel',
|
|
'point_id', 'tag_id', string='Tags',
|
|
)
|
|
|
|
# Stats
|
|
spawn_count = fields.Integer(
|
|
string='Checks Spawned', compute='_compute_spawn_count',
|
|
)
|
|
|
|
@api.depends('template_id')
|
|
def _compute_spawn_count(self):
|
|
Check = self.env['fusion.plating.quality.check']
|
|
for rec in self:
|
|
if not rec.template_id:
|
|
rec.spawn_count = 0
|
|
continue
|
|
rec.spawn_count = Check.search_count([
|
|
('template_id', '=', rec.template_id.id),
|
|
])
|
|
|
|
# ------------------------------------------------------------------
|
|
# Matching + spawning
|
|
# ------------------------------------------------------------------
|
|
def _matches(self, partner=None, part=None, coating=None, step=None):
|
|
"""Return True if this point's filters all pass against the supplied
|
|
context. Empty filter == match anything.
|
|
"""
|
|
self.ensure_one()
|
|
if self.partner_ids and (not partner or partner not in self.partner_ids):
|
|
return False
|
|
if self.part_catalog_ids and (
|
|
not part or part not in self.part_catalog_ids):
|
|
return False
|
|
if self.coating_config_ids and (
|
|
not coating or coating not in self.coating_config_ids):
|
|
return False
|
|
if self.step_kind and step and getattr(step, 'kind', None) \
|
|
and step.kind != self.step_kind:
|
|
return False
|
|
return True
|
|
|
|
@api.model
|
|
def _find_matching(self, trigger, partner=None, part=None, coating=None,
|
|
step=None):
|
|
"""Return active points whose trigger + filters match the context."""
|
|
candidates = self.search([
|
|
('active', '=', True),
|
|
('trigger_type', '=', trigger),
|
|
])
|
|
return candidates.filtered(lambda p: p._matches(
|
|
partner=partner, part=part, coating=coating, step=step,
|
|
))
|
|
|
|
def _spawn_check_for(self, source, partner=None, job=None, step=None):
|
|
"""Create a fusion.plating.quality.check from this point's template.
|
|
|
|
Idempotent per (point, source): if a check already exists with the
|
|
same template_id and the same job/step binding, no new one is
|
|
created (returns the existing one).
|
|
"""
|
|
self.ensure_one()
|
|
Check = self.env['fusion.plating.quality.check']
|
|
if not self.template_id:
|
|
_logger.warning(
|
|
'fp.quality.point %s: no template_id set, skipping spawn.',
|
|
self.name,
|
|
)
|
|
return False
|
|
|
|
domain = [('template_id', '=', self.template_id.id)]
|
|
if job:
|
|
domain.append(('job_id', '=', job.id))
|
|
if step and 'step_id' in Check._fields:
|
|
domain.append(('step_id', '=', step.id))
|
|
existing = Check.search(domain, limit=1)
|
|
if existing:
|
|
return existing
|
|
|
|
vals = {
|
|
'template_id': self.template_id.id,
|
|
}
|
|
# Best-effort field bindings — survives schema variations.
|
|
if 'partner_id' in Check._fields and partner:
|
|
vals['partner_id'] = partner.id
|
|
if 'job_id' in Check._fields and job:
|
|
vals['job_id'] = job.id
|
|
if 'step_id' in Check._fields and step:
|
|
vals['step_id'] = step.id
|
|
if 'state' in Check._fields:
|
|
vals['state'] = 'pending'
|
|
if 'inspector_id' in Check._fields and self.assignee_user_id:
|
|
vals['inspector_id'] = self.assignee_user_id.id
|
|
if 'team_id' in Check._fields and self.team_id:
|
|
vals['team_id'] = self.team_id.id
|
|
if 'tag_ids' in Check._fields and self.tag_ids:
|
|
vals['tag_ids'] = [(6, 0, self.tag_ids.ids)]
|
|
try:
|
|
return Check.create(vals)
|
|
except Exception as e:
|
|
_logger.warning(
|
|
'fp.quality.point %s: spawn failed for %s — %s',
|
|
self.name, source.display_name if source else '?', e,
|
|
)
|
|
return False
|
|
|
|
def action_spawn_manual(self):
|
|
"""Manual fire — present from the form view button. No source ctx."""
|
|
for rec in self:
|
|
rec._spawn_check_for(source=rec)
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'title': _('Quality Point fired'),
|
|
'message': _('Spawned %s check(s).') % len(self),
|
|
'type': 'success',
|
|
},
|
|
}
|