feat(jobs): add equipment, audit, plating-spec fields to fp.job.step
Equipment: bath_id, tank_id, rack_id (all in core). oven_id deferred to a bridge module — fusion.plating.bake.oven lives in shopfloor and core can't depend on it. masking_material_id deferred too — model fusion.plating.masking.material does not yet exist anywhere; will be added when the masking model lands. Audit: signoff_user_id (readonly), facility_id (related from work_centre_id, stored). Plating spec: thickness_target, thickness_uom (um/mil/in), dwell_time_minutes, bake_setpoint_temp, bake_actual_duration, bake_chart_recorder_ref (Nadcap audit trail). Recipe-related: requires_signoff, auto_complete, is_manual, customer_visible (all related from recipe_node_id, stored, so operator sees current values without re-querying process.node). Cost rollup: cost_per_hour related from work_centre_id, cost_total computed (duration_actual / 60 x rate), currency_id related too. Full rollup-from-timelogs lands in Task 1.7. Tests cover: facility_id related-field, thickness_uom default, cost_total zero/non-zero paths. Manifest 19.0.8.4.1 -> 19.0.8.5.0. Part of: native job model migration (spec 2026-04-25) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating',
|
'name': 'Fusion Plating',
|
||||||
'version': '19.0.8.4.1',
|
'version': '19.0.8.5.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -76,6 +76,83 @@ class FpJobStep(models.Model):
|
|||||||
duration_actual = fields.Float(string='Actual Minutes', readonly=True)
|
duration_actual = fields.Float(string='Actual Minutes', readonly=True)
|
||||||
instructions = fields.Html(string='Step Instructions')
|
instructions = fields.Html(string='Step Instructions')
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Equipment + audit (Task 1.6)
|
||||||
|
# oven_id is deferred to a bridge module — fusion.plating.bake.oven
|
||||||
|
# lives in fusion_plating_shopfloor and core can't depend on it.
|
||||||
|
# masking_material_id is deferred — fusion.plating.masking.material
|
||||||
|
# does not yet exist in any installed module; will be added when
|
||||||
|
# the masking model lands (likely in fusion_plating_process_en
|
||||||
|
# or a future fusion_plating_masking module).
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
bath_id = fields.Many2one('fusion.plating.bath')
|
||||||
|
tank_id = fields.Many2one('fusion.plating.tank')
|
||||||
|
rack_id = fields.Many2one('fusion.plating.rack')
|
||||||
|
signoff_user_id = fields.Many2one('res.users', readonly=True)
|
||||||
|
facility_id = fields.Many2one(
|
||||||
|
'fusion.plating.facility',
|
||||||
|
related='work_centre_id.facility_id',
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Plating spec (Task 1.6)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
thickness_target = fields.Float(string='Target Thickness')
|
||||||
|
thickness_uom = fields.Selection(
|
||||||
|
[('um', 'µm'), ('mil', 'mil'), ('inch', 'in')],
|
||||||
|
default='um',
|
||||||
|
)
|
||||||
|
dwell_time_minutes = fields.Float()
|
||||||
|
bake_setpoint_temp = fields.Float(string='Bake Setpoint °C')
|
||||||
|
bake_actual_duration = fields.Float(string='Bake Actual Minutes')
|
||||||
|
bake_chart_recorder_ref = fields.Char(string='Bake Chart Recorder Ref')
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Recipe-related (Task 1.6)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
requires_signoff = fields.Boolean(
|
||||||
|
related='recipe_node_id.requires_signoff',
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
auto_complete = fields.Boolean(
|
||||||
|
related='recipe_node_id.auto_complete',
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
is_manual = fields.Boolean(
|
||||||
|
related='recipe_node_id.is_manual',
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
customer_visible = fields.Boolean(
|
||||||
|
related='recipe_node_id.customer_visible',
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Cost rollup (Task 1.6)
|
||||||
|
# cost_per_hour comes from fp.work.centre (Task 1.2 added it there).
|
||||||
|
# cost_total recomputes when duration_actual or rate changes —
|
||||||
|
# duration_actual will be sum of timelog rows once Task 1.7 lands.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
cost_per_hour = fields.Monetary(
|
||||||
|
related='work_centre_id.cost_per_hour',
|
||||||
|
currency_field='currency_id',
|
||||||
|
)
|
||||||
|
cost_total = fields.Monetary(
|
||||||
|
compute='_compute_cost_total',
|
||||||
|
store=True,
|
||||||
|
currency_field='currency_id',
|
||||||
|
)
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
'res.currency',
|
||||||
|
related='work_centre_id.currency_id',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('duration_actual', 'cost_per_hour')
|
||||||
|
def _compute_cost_total(self):
|
||||||
|
for step in self:
|
||||||
|
step.cost_total = (step.duration_actual / 60.0) * step.cost_per_hour
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# State machine — actions
|
# State machine — actions
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -71,3 +71,35 @@ class TestFpJobStepStateMachine(TransactionCase):
|
|||||||
self.job.invalidate_recordset(['step_done_count', 'step_progress_pct'])
|
self.job.invalidate_recordset(['step_done_count', 'step_progress_pct'])
|
||||||
self.assertEqual(self.job.step_done_count, 1)
|
self.assertEqual(self.job.step_done_count, 1)
|
||||||
self.assertAlmostEqual(self.job.step_progress_pct, 33.33, places=1)
|
self.assertAlmostEqual(self.job.step_progress_pct, 33.33, places=1)
|
||||||
|
|
||||||
|
def test_facility_id_related_from_work_centre(self):
|
||||||
|
# Work centre with a facility -> step inherits via related field.
|
||||||
|
facility = self.env['fusion.plating.facility'].create({
|
||||||
|
'name': 'Test Facility',
|
||||||
|
'code': 'TFAC',
|
||||||
|
})
|
||||||
|
wc = self.env['fp.work.centre'].create({
|
||||||
|
'name': 'WC2', 'code': 'WC2', 'kind': 'wet_line',
|
||||||
|
'facility_id': facility.id,
|
||||||
|
})
|
||||||
|
step = self._make_step(work_centre_id=wc.id)
|
||||||
|
self.assertEqual(step.facility_id, facility)
|
||||||
|
|
||||||
|
def test_thickness_uom_default(self):
|
||||||
|
step = self._make_step()
|
||||||
|
self.assertEqual(step.thickness_uom, 'um')
|
||||||
|
|
||||||
|
def test_cost_total_zero_when_no_duration(self):
|
||||||
|
step = self._make_step()
|
||||||
|
self.assertEqual(step.cost_total, 0.0)
|
||||||
|
|
||||||
|
def test_cost_total_with_duration_and_rate(self):
|
||||||
|
wc = self.env['fp.work.centre'].create({
|
||||||
|
'name': 'WC3', 'code': 'WC3', 'kind': 'wet_line',
|
||||||
|
'cost_per_hour': 60.0, # $1/min
|
||||||
|
})
|
||||||
|
step = self._make_step(work_centre_id=wc.id)
|
||||||
|
# Force duration_actual since we don't have timelogs in 1.6
|
||||||
|
step.duration_actual = 30.0
|
||||||
|
# Recompute happens on read after a write to a depends field
|
||||||
|
self.assertEqual(step.cost_total, 30.0)
|
||||||
|
|||||||
Reference in New Issue
Block a user