feat(jobs): add lifecycle hooks — portal/QC/delivery/invoice (Tasks 2.6-2.9)

- Task 2.6: fp.job.action_confirm auto-creates fusion.plating.portal.job
  with x_fc_job_id back-reference. Idempotent (skip if already linked).
- Task 2.7: fp.job.action_confirm checks customer.x_fc_requires_qc
  and best-effort creates a fusion.plating.quality.check (the model
  lives in bridge_mrp; runtime-detected to avoid dep cycle).
- Task 2.8: fp.job.button_mark_done sets state='done', date_finished,
  auto-creates draft fusion.plating.delivery and best-effort triggers
  fp.certificate generation.
- Task 2.9: account.move.action_post links invoice -> fp.job via SO
  origin lookup, updates portal_job state to complete and stamps
  invoice_ref.

5 new tests cover: portal job creation + idempotency, mark_done state
+ delivery, cancel-then-mark-done blocked.

Best-effort patterns (try/except + runtime model detection) used for
QC + cert because their target models are in dependent modules
that this module doesn't depend on by design.

qc_check_id field on fp.job still deferred — adding it here would
require depending on bridge_mrp.

Manifest 19.0.1.4.0 -> 19.0.1.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:
gsinghpal
2026-04-24 23:27:38 -04:00
parent 294cea0e50
commit dd88afdf53
6 changed files with 289 additions and 1 deletions

View File

@@ -332,3 +332,53 @@ class TestSoConfirmHook(TransactionCase):
self.assertEqual(count_after_first, count_after_second)
else:
self.skipTest('x_fc_part_catalog_id field not present')
class TestJobLifecycleHooks(TransactionCase):
def setUp(self):
super().setUp()
self.partner = self.env['res.partner'].create({'name': 'C'})
self.product = self.env['product.product'].create({'name': 'P'})
def _make_job(self, **kw):
vals = {
'partner_id': self.partner.id,
'product_id': self.product.id,
'qty': 1.0,
}
vals.update(kw)
return self.env['fp.job'].create(vals)
def test_confirm_creates_portal_job(self):
job = self._make_job()
job.action_confirm()
self.assertTrue(job.portal_job_id)
self.assertEqual(job.portal_job_id.partner_id, self.partner)
def test_confirm_idempotent_portal_job(self):
job = self._make_job()
job.action_confirm()
portal_id = job.portal_job_id.id
# Second call (e.g. via a re-trigger) shouldn't create a duplicate
job._fp_create_portal_job()
self.assertEqual(job.portal_job_id.id, portal_id)
def test_button_mark_done_sets_state(self):
job = self._make_job()
job.action_confirm()
job.button_mark_done()
self.assertEqual(job.state, 'done')
self.assertTrue(job.date_finished)
def test_button_mark_done_creates_delivery(self):
job = self._make_job()
job.action_confirm()
job.button_mark_done()
self.assertTrue(job.delivery_id)
def test_button_mark_done_blocks_when_cancelled(self):
from odoo.exceptions import UserError
job = self._make_job()
job.action_cancel()
with self.assertRaises(UserError):
job.button_mark_done()