folder rename
This commit is contained in:
6
fusion_plating/fusion_plating_kpi/__init__.py
Normal file
6
fusion_plating/fusion_plating_kpi/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import models
|
||||
34
fusion_plating/fusion_plating_kpi/__manifest__.py
Normal file
34
fusion_plating/fusion_plating_kpi/__manifest__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — KPI Dashboard',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Configurable KPI dashboards for plating operations.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'depends': [
|
||||
'fusion_plating',
|
||||
'fusion_plating_portal',
|
||||
'fusion_plating_quality',
|
||||
],
|
||||
'data': [
|
||||
'security/fp_kpi_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/fp_kpi_views.xml',
|
||||
'views/fp_kpi_value_views.xml',
|
||||
'views/fp_kpi_dashboard_views.xml',
|
||||
'views/fp_menu.xml',
|
||||
'data/fp_kpi_builtin_data.xml',
|
||||
'data/fp_kpi_cron_data.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
Built-in KPI definitions. These are created once on install;
|
||||
noupdate="1" ensures they are not overwritten on module upgrade.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="fp_kpi_otd" model="fusion.plating.kpi">
|
||||
<field name="name">On-Time Delivery %</field>
|
||||
<field name="code">otd_pct</field>
|
||||
<field name="kpi_type">otd</field>
|
||||
<field name="target_value">95</field>
|
||||
<field name="uom">%</field>
|
||||
<field name="compute_method">auto</field>
|
||||
</record>
|
||||
|
||||
<record id="fp_kpi_d2d" model="fusion.plating.kpi">
|
||||
<field name="name">Dock-to-Dock Lead Time</field>
|
||||
<field name="code">d2d_days</field>
|
||||
<field name="kpi_type">dock_to_dock</field>
|
||||
<field name="target_value">5</field>
|
||||
<field name="uom">days</field>
|
||||
<field name="compute_method">auto</field>
|
||||
</record>
|
||||
|
||||
<record id="fp_kpi_throughput" model="fusion.plating.kpi">
|
||||
<field name="name">Daily Throughput</field>
|
||||
<field name="code">throughput</field>
|
||||
<field name="kpi_type">throughput</field>
|
||||
<field name="target_value">100</field>
|
||||
<field name="uom">parts</field>
|
||||
<field name="compute_method">auto</field>
|
||||
</record>
|
||||
|
||||
<record id="fp_kpi_quality_yield" model="fusion.plating.kpi">
|
||||
<field name="name">Quality Yield %</field>
|
||||
<field name="code">quality_yield</field>
|
||||
<field name="kpi_type">quality_yield</field>
|
||||
<field name="target_value">99</field>
|
||||
<field name="uom">%</field>
|
||||
<field name="compute_method">auto</field>
|
||||
</record>
|
||||
|
||||
<record id="fp_kpi_bath_uptime" model="fusion.plating.kpi">
|
||||
<field name="name">Bath Uptime %</field>
|
||||
<field name="code">bath_uptime</field>
|
||||
<field name="kpi_type">bath_uptime</field>
|
||||
<field name="target_value">90</field>
|
||||
<field name="uom">%</field>
|
||||
<field name="compute_method">auto</field>
|
||||
</record>
|
||||
|
||||
<record id="fp_kpi_custom_1" model="fusion.plating.kpi">
|
||||
<field name="name">Custom KPI</field>
|
||||
<field name="code">custom_1</field>
|
||||
<field name="kpi_type">custom</field>
|
||||
<field name="compute_method">manual</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
18
fusion_plating/fusion_plating_kpi/data/fp_kpi_cron_data.xml
Normal file
18
fusion_plating/fusion_plating_kpi/data/fp_kpi_cron_data.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="ir_cron_fp_kpi_compute_daily" model="ir.cron">
|
||||
<field name="name">Fusion Plating KPI: Compute Daily Values</field>
|
||||
<field name="model_id" ref="model_fusion_plating_kpi"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_compute_daily()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
7
fusion_plating/fusion_plating_kpi/models/__init__.py
Normal file
7
fusion_plating/fusion_plating_kpi/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import fp_kpi
|
||||
from . import fp_kpi_value
|
||||
Binary file not shown.
Binary file not shown.
376
fusion_plating/fusion_plating_kpi/models/fp_kpi.py
Normal file
376
fusion_plating/fusion_plating_kpi/models/fp_kpi.py
Normal file
@@ -0,0 +1,376 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
KPI_TYPE_SELECTION = [
|
||||
('otd', 'On-Time Delivery'),
|
||||
('dock_to_dock', 'Dock-to-Dock Lead Time'),
|
||||
('throughput', 'Daily Throughput'),
|
||||
('quality_yield', 'Quality Yield'),
|
||||
('bath_uptime', 'Bath Uptime'),
|
||||
('rework_count', 'Rework Events by Month'),
|
||||
('failed_spec_count', 'Failed Spec Count by Month'),
|
||||
('processed_parts', 'Processed Parts by Month'),
|
||||
('shipped_parts', 'Shipped Parts by Month'),
|
||||
('cost_per_part', 'Average Cost Per Part'),
|
||||
('custom', 'Custom'),
|
||||
]
|
||||
|
||||
TREND_SELECTION = [
|
||||
('up', 'Up'),
|
||||
('down', 'Down'),
|
||||
('flat', 'Flat'),
|
||||
]
|
||||
|
||||
|
||||
class FpKpi(models.Model):
|
||||
"""Plating KPI Definition.
|
||||
|
||||
Each record represents a single measurable KPI. Auto-type KPIs have
|
||||
their daily value computed by the scheduled action; manual KPIs are
|
||||
updated by operators or supervisors through the value history.
|
||||
"""
|
||||
_name = 'fusion.plating.kpi'
|
||||
_description = 'Plating KPI Definition'
|
||||
_order = 'kpi_type, name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
help='Human-readable KPI label, e.g. "On-Time Delivery %".',
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
required=True,
|
||||
help='Unique machine identifier, e.g. "otd_pct".',
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.plating.facility',
|
||||
string='Facility',
|
||||
help='Leave blank for company-wide.',
|
||||
)
|
||||
kpi_type = fields.Selection(
|
||||
KPI_TYPE_SELECTION,
|
||||
string='Type',
|
||||
required=True,
|
||||
default='custom',
|
||||
)
|
||||
target_value = fields.Float(
|
||||
string='Target',
|
||||
)
|
||||
uom = fields.Char(
|
||||
string='Unit',
|
||||
help='Display unit, e.g. "%", "days", "parts".',
|
||||
)
|
||||
compute_method = fields.Selection(
|
||||
[
|
||||
('auto', 'Automatic'),
|
||||
('manual', 'Manual'),
|
||||
],
|
||||
string='Compute Method',
|
||||
default='manual',
|
||||
required=True,
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
value_ids = fields.One2many(
|
||||
'fusion.plating.kpi.value',
|
||||
'kpi_id',
|
||||
string='Value History',
|
||||
)
|
||||
current_value = fields.Float(
|
||||
string='Current Value',
|
||||
compute='_compute_current_value',
|
||||
store=False,
|
||||
)
|
||||
trend = fields.Selection(
|
||||
TREND_SELECTION,
|
||||
string='Trend',
|
||||
compute='_compute_current_value',
|
||||
store=False,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('code_uniq', 'unique(code, company_id)',
|
||||
'The KPI code must be unique per company.'),
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Computed fields
|
||||
# ------------------------------------------------------------------
|
||||
@api.depends('value_ids', 'value_ids.value', 'value_ids.date')
|
||||
def _compute_current_value(self):
|
||||
for kpi in self:
|
||||
values = kpi.value_ids.sorted('date', reverse=True)
|
||||
if not values:
|
||||
kpi.current_value = 0.0
|
||||
kpi.trend = 'flat'
|
||||
continue
|
||||
kpi.current_value = values[0].value
|
||||
if len(values) >= 2:
|
||||
diff = values[0].value - values[1].value
|
||||
if diff > 0.001:
|
||||
kpi.trend = 'up'
|
||||
elif diff < -0.001:
|
||||
kpi.trend = 'down'
|
||||
else:
|
||||
kpi.trend = 'flat'
|
||||
else:
|
||||
kpi.trend = 'flat'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Cron entry point
|
||||
# ------------------------------------------------------------------
|
||||
@api.model
|
||||
def _cron_compute_daily(self):
|
||||
"""Compute today's value for every active auto-KPI."""
|
||||
today = fields.Date.context_today(self)
|
||||
kpis = self.search([
|
||||
('compute_method', '=', 'auto'),
|
||||
('active', '=', True),
|
||||
])
|
||||
for kpi in kpis:
|
||||
try:
|
||||
value = kpi._compute_kpi_value(today)
|
||||
if value is not None:
|
||||
# Upsert: replace today's value if already present
|
||||
existing = self.env['fusion.plating.kpi.value'].search([
|
||||
('kpi_id', '=', kpi.id),
|
||||
('date', '=', today),
|
||||
], limit=1)
|
||||
if existing:
|
||||
existing.value = value
|
||||
else:
|
||||
self.env['fusion.plating.kpi.value'].create({
|
||||
'kpi_id': kpi.id,
|
||||
'date': today,
|
||||
'value': value,
|
||||
})
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
'Failed to compute KPI %s (%s)', kpi.name, kpi.code)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Per-type computation (raw SQL for speed)
|
||||
# ------------------------------------------------------------------
|
||||
def _compute_kpi_value(self, today):
|
||||
"""Dispatch to the correct SQL-based calculator."""
|
||||
self.ensure_one()
|
||||
method = getattr(self, '_kpi_%s' % self.kpi_type, None)
|
||||
if method:
|
||||
return method(today)
|
||||
return None
|
||||
|
||||
def _company_clause(self, alias='j'):
|
||||
"""Return a SQL WHERE fragment restricting to company."""
|
||||
return " AND %s.company_id = %s" % (alias, self.company_id.id)
|
||||
|
||||
# -- On-Time Delivery --------------------------------------------------
|
||||
def _kpi_otd(self, today):
|
||||
"""Portal jobs shipped on or before target_ship_date (last 30 d).
|
||||
|
||||
NOTE: fusion.plating.portal.job has no facility_id — the
|
||||
facility filter on this KPI type is therefore ignored.
|
||||
"""
|
||||
cutoff = today - timedelta(days=30)
|
||||
extra = self._company_clause('j')
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
COUNT(*) FILTER (
|
||||
WHERE j.actual_ship_date <= j.target_ship_date
|
||||
) AS on_time,
|
||||
COUNT(*) AS total
|
||||
FROM fusion_plating_portal_job j
|
||||
WHERE j.state IN ('shipped', 'complete')
|
||||
AND j.actual_ship_date IS NOT NULL
|
||||
AND j.target_ship_date IS NOT NULL
|
||||
AND j.actual_ship_date >= %s
|
||||
""" + extra, (cutoff,))
|
||||
row = self.env.cr.fetchone()
|
||||
if row and row[1]:
|
||||
return round(row[0] / row[1] * 100, 2)
|
||||
return None
|
||||
|
||||
# -- Dock-to-Dock Lead Time --------------------------------------------
|
||||
def _kpi_dock_to_dock(self, today):
|
||||
cutoff = today - timedelta(days=30)
|
||||
extra = self._company_clause('j')
|
||||
self.env.cr.execute("""
|
||||
SELECT AVG(j.actual_ship_date - j.received_date)
|
||||
FROM fusion_plating_portal_job j
|
||||
WHERE j.state IN ('shipped', 'complete')
|
||||
AND j.actual_ship_date IS NOT NULL
|
||||
AND j.received_date IS NOT NULL
|
||||
AND j.actual_ship_date >= %s
|
||||
""" + extra, (cutoff,))
|
||||
row = self.env.cr.fetchone()
|
||||
if row and row[0] is not None:
|
||||
return round(float(row[0]), 2)
|
||||
return None
|
||||
|
||||
# -- Daily Throughput ---------------------------------------------------
|
||||
def _kpi_throughput(self, today):
|
||||
extra = self._company_clause('j')
|
||||
self.env.cr.execute("""
|
||||
SELECT COALESCE(SUM(j.quantity), 0)
|
||||
FROM fusion_plating_portal_job j
|
||||
WHERE j.received_date = %s
|
||||
""" + extra, (today,))
|
||||
row = self.env.cr.fetchone()
|
||||
if row:
|
||||
return float(row[0])
|
||||
return None
|
||||
|
||||
# -- Quality Yield % ---------------------------------------------------
|
||||
def _kpi_quality_yield(self, today):
|
||||
cutoff = today - timedelta(days=30)
|
||||
extra_j = self._company_clause('j')
|
||||
# Total parts shipped in period
|
||||
self.env.cr.execute("""
|
||||
SELECT COALESCE(SUM(j.quantity), 0)
|
||||
FROM fusion_plating_portal_job j
|
||||
WHERE j.state IN ('shipped', 'complete')
|
||||
AND j.actual_ship_date >= %s
|
||||
""" + extra_j, (cutoff,))
|
||||
total_parts = self.env.cr.fetchone()[0]
|
||||
if not total_parts:
|
||||
return None
|
||||
|
||||
# Sum of NCR quantity_affected in same window
|
||||
# NCR model *does* have facility_id
|
||||
extra_n = self._company_clause('n')
|
||||
if self.facility_id:
|
||||
extra_n += " AND n.facility_id = %s" % self.facility_id.id
|
||||
self.env.cr.execute("""
|
||||
SELECT COALESCE(SUM(n.quantity_affected), 0)
|
||||
FROM fusion_plating_ncr n
|
||||
WHERE n.reported_date >= %s
|
||||
""" + extra_n, (cutoff,))
|
||||
ncr_qty = self.env.cr.fetchone()[0]
|
||||
|
||||
if total_parts > 0:
|
||||
return round((total_parts - ncr_qty) / total_parts * 100, 2)
|
||||
return None
|
||||
|
||||
# -- Bath Uptime % -----------------------------------------------------
|
||||
def _kpi_bath_uptime(self, today):
|
||||
"""Operational baths / active baths * 100.
|
||||
|
||||
Bath has facility via tank_id.facility_id — filter is supported.
|
||||
"""
|
||||
extra = self._company_clause('b')
|
||||
if self.facility_id:
|
||||
extra += """
|
||||
AND b.tank_id IN (
|
||||
SELECT t.id FROM fusion_plating_tank t
|
||||
WHERE t.facility_id = %s
|
||||
)""" % self.facility_id.id
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE b.state = 'operational') AS up,
|
||||
COUNT(*) AS total
|
||||
FROM fusion_plating_bath b
|
||||
WHERE b.active = TRUE
|
||||
""" + extra)
|
||||
row = self.env.cr.fetchone()
|
||||
if row and row[1]:
|
||||
return round(row[0] / row[1] * 100, 2)
|
||||
return None
|
||||
|
||||
# -- Rework Events (current month) -------------------------------------
|
||||
def _kpi_rework_count(self, today):
|
||||
"""Count NCRs flagged as rework in the current month."""
|
||||
month_start = today.replace(day=1)
|
||||
extra = self._company_clause('n')
|
||||
if self.facility_id:
|
||||
extra += " AND n.facility_id = %s" % self.facility_id.id
|
||||
self.env.cr.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM fusion_plating_ncr n
|
||||
WHERE n.reported_date >= %s
|
||||
AND n.reported_date <= %s
|
||||
AND n.disposition = 'rework'
|
||||
""" + extra, (month_start, today))
|
||||
row = self.env.cr.fetchone()
|
||||
return float(row[0]) if row else None
|
||||
|
||||
# -- Failed Spec Count (current month) ---------------------------------
|
||||
def _kpi_failed_spec_count(self, today):
|
||||
"""Count NCRs in the current month (any disposition)."""
|
||||
month_start = today.replace(day=1)
|
||||
extra = self._company_clause('n')
|
||||
if self.facility_id:
|
||||
extra += " AND n.facility_id = %s" % self.facility_id.id
|
||||
self.env.cr.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM fusion_plating_ncr n
|
||||
WHERE n.reported_date >= %s
|
||||
AND n.reported_date <= %s
|
||||
""" + extra, (month_start, today))
|
||||
row = self.env.cr.fetchone()
|
||||
return float(row[0]) if row else None
|
||||
|
||||
# -- Processed Parts (current month) -----------------------------------
|
||||
def _kpi_processed_parts(self, today):
|
||||
"""Sum of quantities received this month."""
|
||||
month_start = today.replace(day=1)
|
||||
extra = self._company_clause('j')
|
||||
self.env.cr.execute("""
|
||||
SELECT COALESCE(SUM(j.quantity), 0)
|
||||
FROM fusion_plating_portal_job j
|
||||
WHERE j.received_date >= %s
|
||||
AND j.received_date <= %s
|
||||
""" + extra, (month_start, today))
|
||||
row = self.env.cr.fetchone()
|
||||
return float(row[0]) if row else None
|
||||
|
||||
# -- Shipped Parts (current month) -------------------------------------
|
||||
def _kpi_shipped_parts(self, today):
|
||||
"""Sum of quantities shipped this month."""
|
||||
month_start = today.replace(day=1)
|
||||
extra = self._company_clause('j')
|
||||
self.env.cr.execute("""
|
||||
SELECT COALESCE(SUM(j.quantity), 0)
|
||||
FROM fusion_plating_portal_job j
|
||||
WHERE j.state IN ('shipped', 'complete')
|
||||
AND j.actual_ship_date >= %s
|
||||
AND j.actual_ship_date <= %s
|
||||
""" + extra, (month_start, today))
|
||||
row = self.env.cr.fetchone()
|
||||
return float(row[0]) if row else None
|
||||
|
||||
# -- Average Cost Per Part (last 30 days) ------------------------------
|
||||
def _kpi_cost_per_part(self, today):
|
||||
"""Average standard_price for products on shipped jobs (30 d)."""
|
||||
cutoff = today - timedelta(days=30)
|
||||
extra = self._company_clause('j')
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
COALESCE(SUM(j.quantity), 0),
|
||||
COUNT(DISTINCT j.id)
|
||||
FROM fusion_plating_portal_job j
|
||||
WHERE j.state IN ('shipped', 'complete')
|
||||
AND j.actual_ship_date >= %s
|
||||
""" + extra, (cutoff,))
|
||||
row = self.env.cr.fetchone()
|
||||
total_qty = row[0] if row else 0
|
||||
if not total_qty:
|
||||
return None
|
||||
# Use product standard prices from related MO raw materials
|
||||
# Fallback: return total shipped qty for now (will refine)
|
||||
return round(float(total_qty), 2)
|
||||
58
fusion_plating/fusion_plating_kpi/models/fp_kpi_value.py
Normal file
58
fusion_plating/fusion_plating_kpi/models/fp_kpi_value.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpKpiValue(models.Model):
|
||||
"""KPI Daily Value.
|
||||
|
||||
One row per KPI per day. Auto-computed KPIs are populated by the
|
||||
scheduled action; manual KPIs are entered by operators/supervisors.
|
||||
"""
|
||||
_name = 'fusion.plating.kpi.value'
|
||||
_description = 'KPI Daily Value'
|
||||
_order = 'date desc'
|
||||
|
||||
kpi_id = fields.Many2one(
|
||||
'fusion.plating.kpi',
|
||||
string='KPI',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
date = fields.Date(
|
||||
string='Date',
|
||||
required=True,
|
||||
default=fields.Date.today,
|
||||
index=True,
|
||||
)
|
||||
value = fields.Float(
|
||||
string='Value',
|
||||
required=True,
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.plating.facility',
|
||||
string='Facility',
|
||||
related='kpi_id.facility_id',
|
||||
store=True,
|
||||
)
|
||||
kpi_type = fields.Selection(
|
||||
related='kpi_id.kpi_type',
|
||||
string='KPI Type',
|
||||
store=True,
|
||||
index=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
related='kpi_id.company_id',
|
||||
store=True,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('kpi_date_uniq', 'unique(kpi_id, date)',
|
||||
'Only one value per KPI per day is allowed.'),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- RECORD RULES — Multi-company isolation -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="fp_kpi_company_rule" model="ir.rule">
|
||||
<field name="name">Fusion Plating KPI: KPI — multi-company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_kpi"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="fp_kpi_value_company_rule" model="ir.rule">
|
||||
<field name="name">Fusion Plating KPI: KPI Value — multi-company</field>
|
||||
<field name="model_id" ref="model_fusion_plating_kpi_value"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,7 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_fp_kpi_operator,fp.kpi.operator,model_fusion_plating_kpi,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_kpi_supervisor,fp.kpi.supervisor,model_fusion_plating_kpi,fusion_plating.group_fusion_plating_supervisor,1,1,0,0
|
||||
access_fp_kpi_manager,fp.kpi.manager,model_fusion_plating_kpi,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_kpi_value_operator,fp.kpi.value.operator,model_fusion_plating_kpi_value,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_kpi_value_supervisor,fp.kpi.value.supervisor,model_fusion_plating_kpi_value,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_kpi_value_manager,fp.kpi.value.manager,model_fusion_plating_kpi_value,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
|
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
Dashboard-style act_window actions for KPI categories.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Production KPIs -->
|
||||
<!-- OTD, Dock-to-Dock, Throughput, Processed Parts, Shipped Parts -->
|
||||
<!-- ================================================================= -->
|
||||
<record id="action_fp_kpi_dashboard_production" model="ir.actions.act_window">
|
||||
<field name="name">Production KPIs</field>
|
||||
<field name="res_model">fusion.plating.kpi.value</field>
|
||||
<field name="view_mode">graph,pivot,list</field>
|
||||
<field name="search_view_id" ref="fp_kpi_value_view_search"/>
|
||||
<field name="domain">[('kpi_type', 'in', ['otd', 'dock_to_dock', 'throughput', 'processed_parts', 'shipped_parts'])]</field>
|
||||
<field name="context">{
|
||||
'search_default_filter_quarter': 1,
|
||||
'search_default_group_kpi_type': 1,
|
||||
'search_default_group_date': 1,
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No production KPI data yet
|
||||
</p>
|
||||
<p>
|
||||
Production KPIs include On-Time Delivery, Dock-to-Dock Lead Time,
|
||||
Throughput, Processed Parts, and Shipped Parts.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Quality KPIs -->
|
||||
<!-- Quality Yield, Rework Events, Failed Spec Count -->
|
||||
<!-- ================================================================= -->
|
||||
<record id="action_fp_kpi_dashboard_quality" model="ir.actions.act_window">
|
||||
<field name="name">Quality KPIs</field>
|
||||
<field name="res_model">fusion.plating.kpi.value</field>
|
||||
<field name="view_mode">graph,pivot,list</field>
|
||||
<field name="search_view_id" ref="fp_kpi_value_view_search"/>
|
||||
<field name="domain">[('kpi_type', 'in', ['quality_yield', 'rework_count', 'failed_spec_count'])]</field>
|
||||
<field name="context">{
|
||||
'search_default_filter_quarter': 1,
|
||||
'search_default_group_kpi_type': 1,
|
||||
'search_default_group_date': 1,
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No quality KPI data yet
|
||||
</p>
|
||||
<p>
|
||||
Quality KPIs include Quality Yield, Rework Events, and Failed Spec Count.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Sales & Finance KPIs -->
|
||||
<!-- Bath Uptime, Cost Per Part, Custom (revenue, margin) -->
|
||||
<!-- ================================================================= -->
|
||||
<record id="action_fp_kpi_dashboard_finance" model="ir.actions.act_window">
|
||||
<field name="name">Sales & Finance KPIs</field>
|
||||
<field name="res_model">fusion.plating.kpi.value</field>
|
||||
<field name="view_mode">graph,pivot,list</field>
|
||||
<field name="search_view_id" ref="fp_kpi_value_view_search"/>
|
||||
<field name="domain">[('kpi_type', 'in', ['bath_uptime', 'cost_per_part', 'custom'])]</field>
|
||||
<field name="context">{
|
||||
'search_default_filter_quarter': 1,
|
||||
'search_default_group_kpi_type': 1,
|
||||
'search_default_group_date': 1,
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No sales or finance KPI data yet
|
||||
</p>
|
||||
<p>
|
||||
Sales & Finance KPIs include Bath Uptime, Cost Per Part,
|
||||
and any custom revenue or margin metrics.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
143
fusion_plating/fusion_plating_kpi/views/fp_kpi_value_views.xml
Normal file
143
fusion_plating/fusion_plating_kpi/views/fp_kpi_value_views.xml
Normal file
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== LIST VIEW ===== -->
|
||||
<record id="fp_kpi_value_view_list" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.kpi.value.list</field>
|
||||
<field name="model">fusion.plating.kpi.value</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="KPI History" editable="bottom">
|
||||
<field name="kpi_id"/>
|
||||
<field name="kpi_type" optional="show"/>
|
||||
<field name="date"/>
|
||||
<field name="value"/>
|
||||
<field name="facility_id" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== LINE GRAPH VIEW (default) ===== -->
|
||||
<record id="fp_kpi_value_view_graph" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.kpi.value.graph.line</field>
|
||||
<field name="model">fusion.plating.kpi.value</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="KPI Trend (Line)" type="line">
|
||||
<field name="date" type="row"/>
|
||||
<field name="value" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== STACKED BAR GRAPH VIEW ===== -->
|
||||
<record id="fp_kpi_value_view_graph_bar" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.kpi.value.graph.bar</field>
|
||||
<field name="model">fusion.plating.kpi.value</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="KPI Values (Bar)" type="bar" stacked="True">
|
||||
<field name="date" interval="month" type="row"/>
|
||||
<field name="kpi_type" type="col"/>
|
||||
<field name="value" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== PIVOT VIEW ===== -->
|
||||
<record id="fp_kpi_value_view_pivot" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.kpi.value.pivot</field>
|
||||
<field name="model">fusion.plating.kpi.value</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="KPI Values">
|
||||
<field name="kpi_id" type="row"/>
|
||||
<field name="date" type="col" interval="month"/>
|
||||
<field name="value" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== SEARCH VIEW ===== -->
|
||||
<record id="fp_kpi_value_view_search" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.kpi.value.search</field>
|
||||
<field name="model">fusion.plating.kpi.value</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search KPI Values">
|
||||
<field name="kpi_id"/>
|
||||
<field name="kpi_type"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="date"/>
|
||||
<separator/>
|
||||
<filter string="Today" name="filter_today"
|
||||
domain="[('date', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Last 7 Days" name="filter_week"
|
||||
domain="[('date', '>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Last 30 Days" name="filter_month"
|
||||
domain="[('date', '>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Last 90 Days" name="filter_quarter"
|
||||
domain="[('date', '>=', (context_today() - datetime.timedelta(days=90)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Last 365 Days" name="filter_year"
|
||||
domain="[('date', '>=', (context_today() - datetime.timedelta(days=365)).strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<!-- KPI Type Filters -->
|
||||
<filter string="On-Time Delivery" name="filter_otd"
|
||||
domain="[('kpi_type', '=', 'otd')]"/>
|
||||
<filter string="Dock-to-Dock" name="filter_d2d"
|
||||
domain="[('kpi_type', '=', 'dock_to_dock')]"/>
|
||||
<filter string="Throughput" name="filter_throughput"
|
||||
domain="[('kpi_type', '=', 'throughput')]"/>
|
||||
<filter string="Quality Yield" name="filter_quality"
|
||||
domain="[('kpi_type', '=', 'quality_yield')]"/>
|
||||
<filter string="Bath Uptime" name="filter_bath"
|
||||
domain="[('kpi_type', '=', 'bath_uptime')]"/>
|
||||
<filter string="Rework Events" name="filter_rework"
|
||||
domain="[('kpi_type', '=', 'rework_count')]"/>
|
||||
<filter string="Failed Specs" name="filter_failed_spec"
|
||||
domain="[('kpi_type', '=', 'failed_spec_count')]"/>
|
||||
<filter string="Processed Parts" name="filter_processed"
|
||||
domain="[('kpi_type', '=', 'processed_parts')]"/>
|
||||
<filter string="Shipped Parts" name="filter_shipped"
|
||||
domain="[('kpi_type', '=', 'shipped_parts')]"/>
|
||||
<filter string="Cost Per Part" name="filter_cost_per_part"
|
||||
domain="[('kpi_type', '=', 'cost_per_part')]"/>
|
||||
<separator/>
|
||||
<group>
|
||||
<filter string="KPI" name="group_kpi"
|
||||
context="{'group_by': 'kpi_id'}"/>
|
||||
<filter string="KPI Type" name="group_kpi_type"
|
||||
context="{'group_by': 'kpi_type'}"/>
|
||||
<filter string="Facility" name="group_facility"
|
||||
context="{'group_by': 'facility_id'}"/>
|
||||
<filter string="Date (Day)" name="group_date_day"
|
||||
context="{'group_by': 'date:day'}"/>
|
||||
<filter string="Date (Week)" name="group_date_week"
|
||||
context="{'group_by': 'date:week'}"/>
|
||||
<filter string="Date (Month)" name="group_date"
|
||||
context="{'group_by': 'date:month'}"/>
|
||||
<filter string="Date (Quarter)" name="group_date_quarter"
|
||||
context="{'group_by': 'date:quarter'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== ACTION (generic history) ===== -->
|
||||
<record id="action_fp_kpi_value" model="ir.actions.act_window">
|
||||
<field name="name">KPI History</field>
|
||||
<field name="res_model">fusion.plating.kpi.value</field>
|
||||
<field name="view_mode">list,graph,pivot</field>
|
||||
<field name="search_view_id" ref="fp_kpi_value_view_search"/>
|
||||
<field name="context">{'search_default_filter_month': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No KPI values recorded yet
|
||||
</p>
|
||||
<p>
|
||||
Values are computed daily by the scheduled action or entered manually.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
146
fusion_plating/fusion_plating_kpi/views/fp_kpi_views.xml
Normal file
146
fusion_plating/fusion_plating_kpi/views/fp_kpi_views.xml
Normal file
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== LIST VIEW ===== -->
|
||||
<record id="fp_kpi_view_list" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.kpi.list</field>
|
||||
<field name="model">fusion.plating.kpi</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="KPIs"
|
||||
decoration-success="trend == 'up'"
|
||||
decoration-danger="trend == 'down'"
|
||||
decoration-muted="trend == 'flat'">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="kpi_type"/>
|
||||
<field name="facility_id" optional="show"/>
|
||||
<field name="target_value"/>
|
||||
<field name="current_value"/>
|
||||
<field name="uom"/>
|
||||
<field name="trend"/>
|
||||
<field name="compute_method"/>
|
||||
<field name="active" column_invisible="True"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== FORM VIEW ===== -->
|
||||
<record id="fp_kpi_view_form" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.kpi.form</field>
|
||||
<field name="model">fusion.plating.kpi</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="KPI Definition">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="e.g. On-Time Delivery %"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
<field name="kpi_type"/>
|
||||
<field name="compute_method"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="target_value"/>
|
||||
<field name="uom"/>
|
||||
<field name="current_value"/>
|
||||
<field name="trend"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Value History" name="history">
|
||||
<field name="value_ids">
|
||||
<list editable="bottom">
|
||||
<field name="date"/>
|
||||
<field name="value"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Trend Chart" name="chart">
|
||||
<p class="text-muted">
|
||||
Use the <strong>KPI History</strong> menu to view trend charts, or click the history count button above.
|
||||
</p>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== SEARCH VIEW ===== -->
|
||||
<record id="fp_kpi_view_search" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.kpi.search</field>
|
||||
<field name="model">fusion.plating.kpi</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search KPIs">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="facility_id"/>
|
||||
<separator/>
|
||||
<filter string="On-Time Delivery" name="filter_otd"
|
||||
domain="[('kpi_type', '=', 'otd')]"/>
|
||||
<filter string="Dock-to-Dock" name="filter_d2d"
|
||||
domain="[('kpi_type', '=', 'dock_to_dock')]"/>
|
||||
<filter string="Throughput" name="filter_throughput"
|
||||
domain="[('kpi_type', '=', 'throughput')]"/>
|
||||
<filter string="Quality Yield" name="filter_quality"
|
||||
domain="[('kpi_type', '=', 'quality_yield')]"/>
|
||||
<filter string="Bath Uptime" name="filter_bath"
|
||||
domain="[('kpi_type', '=', 'bath_uptime')]"/>
|
||||
<filter string="Rework Events" name="filter_rework"
|
||||
domain="[('kpi_type', '=', 'rework_count')]"/>
|
||||
<filter string="Failed Specs" name="filter_failed_spec"
|
||||
domain="[('kpi_type', '=', 'failed_spec_count')]"/>
|
||||
<filter string="Processed Parts" name="filter_processed"
|
||||
domain="[('kpi_type', '=', 'processed_parts')]"/>
|
||||
<filter string="Shipped Parts" name="filter_shipped"
|
||||
domain="[('kpi_type', '=', 'shipped_parts')]"/>
|
||||
<filter string="Cost Per Part" name="filter_cost_per_part"
|
||||
domain="[('kpi_type', '=', 'cost_per_part')]"/>
|
||||
<filter string="Custom" name="filter_custom"
|
||||
domain="[('kpi_type', '=', 'custom')]"/>
|
||||
<separator/>
|
||||
<filter string="Automatic" name="filter_auto"
|
||||
domain="[('compute_method', '=', 'auto')]"/>
|
||||
<filter string="Manual" name="filter_manual"
|
||||
domain="[('compute_method', '=', 'manual')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="filter_archived"
|
||||
domain="[('active', '=', False)]"/>
|
||||
<group>
|
||||
<filter string="Type" name="group_type"
|
||||
context="{'group_by': 'kpi_type'}"/>
|
||||
<filter string="Facility" name="group_facility"
|
||||
context="{'group_by': 'facility_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== ACTION ===== -->
|
||||
<record id="action_fp_kpi" model="ir.actions.act_window">
|
||||
<field name="name">KPIs</field>
|
||||
<field name="res_model">fusion.plating.kpi</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="fp_kpi_view_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Define your first KPI
|
||||
</p>
|
||||
<p>
|
||||
Track on-time delivery, throughput, quality yield, and more.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
46
fusion_plating/fusion_plating_kpi/views/fp_menu.xml
Normal file
46
fusion_plating/fusion_plating_kpi/views/fp_menu.xml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== KPIs top-level menu ===== -->
|
||||
<menuitem id="menu_fp_dashboard"
|
||||
name="KPIs"
|
||||
parent="fusion_plating.menu_fp_root"
|
||||
sequence="85"/>
|
||||
|
||||
<menuitem id="menu_fp_kpis"
|
||||
name="KPIs"
|
||||
parent="menu_fp_dashboard"
|
||||
action="action_fp_kpi"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_kpi_history"
|
||||
name="KPI History"
|
||||
parent="menu_fp_dashboard"
|
||||
action="action_fp_kpi_value"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- ===== Dashboard Category Actions ===== -->
|
||||
<menuitem id="menu_fp_kpi_production"
|
||||
name="Production KPIs"
|
||||
parent="menu_fp_dashboard"
|
||||
action="action_fp_kpi_dashboard_production"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_fp_kpi_quality"
|
||||
name="Quality KPIs"
|
||||
parent="menu_fp_dashboard"
|
||||
action="action_fp_kpi_dashboard_quality"
|
||||
sequence="40"/>
|
||||
|
||||
<menuitem id="menu_fp_kpi_finance"
|
||||
name="Sales & Finance"
|
||||
parent="menu_fp_dashboard"
|
||||
action="action_fp_kpi_dashboard_finance"
|
||||
sequence="50"/>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user