diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py index af07c27e..b5c25e37 100644 --- a/fusion_plating/fusion_plating/__manifest__.py +++ b/fusion_plating/fusion_plating/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating', - 'version': '19.0.8.4.1', + 'version': '19.0.8.5.0', 'category': 'Manufacturing/Plating', 'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.', 'description': """ diff --git a/fusion_plating/fusion_plating/models/fp_job_step.py b/fusion_plating/fusion_plating/models/fp_job_step.py index 6cca637f..01f09211 100644 --- a/fusion_plating/fusion_plating/models/fp_job_step.py +++ b/fusion_plating/fusion_plating/models/fp_job_step.py @@ -76,6 +76,83 @@ class FpJobStep(models.Model): duration_actual = fields.Float(string='Actual Minutes', readonly=True) 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 # ------------------------------------------------------------------ diff --git a/fusion_plating/fusion_plating/tests/test_fp_job_step_state_machine.py b/fusion_plating/fusion_plating/tests/test_fp_job_step_state_machine.py index 60ec2ac6..3169641c 100644 --- a/fusion_plating/fusion_plating/tests/test_fp_job_step_state_machine.py +++ b/fusion_plating/fusion_plating/tests/test_fp_job_step_state_machine.py @@ -71,3 +71,35 @@ class TestFpJobStepStateMachine(TransactionCase): self.job.invalidate_recordset(['step_done_count', 'step_progress_pct']) self.assertEqual(self.job.step_done_count, 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)