feat(jobs): add fp.job.step.timelog for granular timer tracking

Each button_start opens a fresh timelog row; button_finish closes
the open row and recomputes step.duration_actual as the sum of all
interval durations. Replicates Odoo MRP's mrp.workorder.time_ids
granularity natively (no mrp dep).

Schema: step_id (M2O cascade), user_id, date_started,
date_finished, duration_minutes (computed, stored).

ACLs: operator get create permission on timelogs because
button_start creates them.

Tests: test_start_creates_timelog (asserts the log row exists,
date_finished is False, user_id is the current user) and
test_finish_closes_timelog (asserts log gets date_finished, has a
non-negative duration, and step.duration_actual matches).

Manifest 19.0.8.5.1 -> 19.0.8.6.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:
gsinghpal
2026-04-24 22:27:26 -04:00
parent 57a3aea16f
commit 28892f56b5
6 changed files with 103 additions and 6 deletions

View File

@@ -120,3 +120,40 @@ class TestFpJobStepStateMachine(TransactionCase):
# Force recompute via invalidate (Odoo recomputes on next read).
step.invalidate_recordset(['cost_total'])
self.assertEqual(step.cost_total, 60.0)
class TestFpJobStepTimeLog(TransactionCase):
def setUp(self):
super().setUp()
self.partner = self.env['res.partner'].create({'name': 'Cust'})
self.product = self.env['product.product'].create({'name': 'Widget'})
self.wc = self.env['fp.work.centre'].create({
'name': 'WC', 'code': 'WC', 'kind': 'wet_line',
})
self.job = self.env['fp.job'].create({
'partner_id': self.partner.id,
'product_id': self.product.id,
'qty': 1.0,
})
self.step = self.env['fp.job.step'].create({
'job_id': self.job.id,
'name': 'S',
'sequence': 10,
'work_centre_id': self.wc.id,
'state': 'ready',
})
def test_start_creates_timelog(self):
self.step.button_start()
self.assertEqual(len(self.step.time_log_ids), 1)
self.assertFalse(self.step.time_log_ids[0].date_finished)
self.assertEqual(self.step.time_log_ids[0].user_id, self.env.user)
def test_finish_closes_timelog(self):
self.step.button_start()
self.step.button_finish()
log = self.step.time_log_ids[0]
self.assertTrue(log.date_finished)
self.assertGreaterEqual(log.duration_minutes, 0.0)
# duration_actual on the step should match the sum of timelog durations
self.assertEqual(self.step.duration_actual, log.duration_minutes)