Files
Odoo-Modules/fusion_plating/fusion_plating_quality/models/fp_quality_point.py
gsinghpal f08f328688 changes
2026-04-27 00:11:18 -04:00

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',
},
}