Files
Odoo-Modules/docs/superpowers/plans/2026-05-19-adp-application-received-bundled-pages-plan.md
gsinghpal df53ab956f docs(plan): ADP application received — bundled pages 11 & 12
Seven-task TDD implementation plan for the design at
2026-05-19-adp-application-received-bundled-pages-design.md. Adds the
bundled-flag + computed gate to sale.order, updates downstream gates
(ready-for-submission, case-close, audit trail), rewrites the
Application Received wizard with a three-mode radio, and bumps the
module version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:26:10 -04:00

47 KiB

ADP Application Received — Bundled Pages 11 & 12 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Refine the ADP Application Received wizard so staff can mark applications received with a single PDF when pages 11 & 12 are inside it — while preserving the separate-file and remote-signing paths.

Architecture: Add a single boolean (x_fc_pages_11_12_in_original) and a computed helper (x_fc_has_signed_pages_11_12) on sale.order. Downstream gates (ready-for-submission, case-close, audit trail) read the computed field. The wizard gets a three-mode radio (bundled / separate / remote) that drives both view visibility and action_confirm validation. Fold in three small fixes: PDF magic-bytes check, status-gate message, and page-count indicator.

Tech Stack: Odoo 19 (Python), pdfrw for PDF page count, XML views.

Spec: docs/superpowers/specs/2026-05-19-adp-application-received-bundled-pages-design.md


File Structure

File Responsibility Action
fusion_claims/models/sale_order.py Order model — add new fields + computed gate; update trail compute Modify
fusion_claims/wizard/application_received_wizard.py Wizard logic — new mode + PDF check + page count + rewrite action_confirm Modify (large)
fusion_claims/wizard/application_received_wizard_views.xml Wizard view — three-mode radio + conditional visibility Modify
fusion_claims/wizard/ready_for_submission_wizard.py Downstream gate — switch to computed field Modify (2 lines)
fusion_claims/wizard/case_close_verification_wizard.py Downstream gate — switch to computed field Modify (1 line)
fusion_claims/__manifest__.py Bump version + add tests to data list if needed Modify
fusion_claims/tests/__init__.py Test package init Create
fusion_claims/tests/test_application_received_wizard.py Wizard unit tests Create
fusion_claims/tests/test_signed_pages_gate.py x_fc_has_signed_pages_11_12 + downstream gate tests Create

Task 1: Add x_fc_pages_11_12_in_original and x_fc_has_signed_pages_11_12 to sale.order

Files:

  • Modify: fusion_claims/models/sale_order.py:2895-2911 (insert after existing pages_11_12 fields)
  • Create: fusion_claims/tests/__init__.py
  • Create: fusion_claims/tests/test_signed_pages_gate.py

Steps

  • Step 1: Create tests package init

Create fusion_claims/tests/__init__.py:

# -*- coding: utf-8 -*-

from . import test_signed_pages_gate
from . import test_application_received_wizard

(test_application_received_wizard is created in Task 5 — the import will fail until then. That's fine; we run tests per-file with --test-tags until the package is complete.)

  • Step 2: Write the failing test for the new fields

Create fusion_claims/tests/test_signed_pages_gate.py:

# -*- coding: utf-8 -*-
import base64

from odoo.tests.common import TransactionCase, tagged


PDF_MAGIC = b'%PDF-1.4\n%fake pdf for tests'


def _b64_pdf():
    return base64.b64encode(PDF_MAGIC)


@tagged('-at_install', 'post_install', 'fusion_claims')
class TestSignedPagesGate(TransactionCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.partner = cls.env['res.partner'].create({'name': 'Test Client'})
        cls.order = cls.env['sale.order'].create({
            'partner_id': cls.partner.id,
            'x_fc_adp_application_status': 'waiting_for_application',
        })

    def test_pages_11_12_in_original_defaults_false(self):
        self.assertFalse(self.order.x_fc_pages_11_12_in_original)

    def test_has_signed_pages_false_when_nothing_set(self):
        self.assertFalse(self.order.x_fc_has_signed_pages_11_12)

    def test_has_signed_pages_true_when_bundled_flag_set(self):
        self.order.x_fc_pages_11_12_in_original = True
        self.order.flush_recordset()
        self.assertTrue(self.order.x_fc_has_signed_pages_11_12)

    def test_has_signed_pages_true_when_separate_file_uploaded(self):
        self.order.x_fc_signed_pages_11_12 = _b64_pdf()
        self.order.flush_recordset()
        self.assertTrue(self.order.x_fc_has_signed_pages_11_12)

    def test_has_signed_pages_true_when_remote_request_signed(self):
        self.env['fusion.page11.sign.request'].create({
            'sale_order_id': self.order.id,
            'signer_email': 'test@example.com',
            'signer_type': 'client',
            'state': 'signed',
        })
        self.order.invalidate_recordset()
        self.assertTrue(self.order.x_fc_has_signed_pages_11_12)

    def test_has_signed_pages_false_when_remote_request_only_sent(self):
        self.env['fusion.page11.sign.request'].create({
            'sale_order_id': self.order.id,
            'signer_email': 'test@example.com',
            'signer_type': 'client',
            'state': 'sent',
        })
        self.order.invalidate_recordset()
        self.assertFalse(self.order.x_fc_has_signed_pages_11_12)
  • Step 3: Run test to verify it fails

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims \
  --stop-after-init 2>&1 | tail -40

Expected: FAILures referencing x_fc_pages_11_12_in_original and/or x_fc_has_signed_pages_11_12 not existing.

  • Step 4: Add the new fields to sale.order

Open fusion_claims/models/sale_order.py, find the existing block at line 2904-2911:

    x_fc_signed_pages_11_12 = fields.Binary(
        string='Page 11 & 12 (Signed)',
        attachment=True,
        help='Signed pages 11 and 12 of the ADP application',
    )
    x_fc_signed_pages_filename = fields.Char(
        string='Signed Pages Filename',
    )

Insert the two new fields immediately after (before the # PAGE 11 SIGNATURE TRACKING comment block at line 2913):

    x_fc_pages_11_12_in_original = fields.Boolean(
        string='Pages 11 & 12 in Original Application',
        default=False,
        tracking=True,
        copy=False,
        help='True when the original application PDF already contains the signed pages 11 & 12.',
    )

    x_fc_has_signed_pages_11_12 = fields.Boolean(
        string='Has Signed Pages 11 & 12',
        compute='_compute_has_signed_pages_11_12',
        store=True,
        help=(
            'True if pages 11 & 12 are satisfied — either bundled in the original '
            'application, uploaded as a separate file, or signed via remote signing.'
        ),
    )

    @api.depends(
        'x_fc_signed_pages_11_12',
        'x_fc_pages_11_12_in_original',
        'page11_sign_request_ids.state',
    )
    def _compute_has_signed_pages_11_12(self):
        for order in self:
            order.x_fc_has_signed_pages_11_12 = bool(
                order.x_fc_pages_11_12_in_original
                or order.x_fc_signed_pages_11_12
                or order.page11_sign_request_ids.filtered(lambda r: r.state == 'signed')
            )
  • Step 5: Run tests to verify they pass

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims:TestSignedPagesGate \
  --stop-after-init 2>&1 | tail -20

Expected: 6 tests run, 0 failed.

  • Step 6: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_claims/models/sale_order.py fusion_claims/tests/__init__.py fusion_claims/tests/test_signed_pages_gate.py
git commit -m "$(cat <<'EOF'
feat(claims): add x_fc_pages_11_12_in_original + computed gate

New boolean on sale.order tracks whether pages 11 & 12 are bundled
inside the original application PDF. Computed helper
x_fc_has_signed_pages_11_12 ORs bundled flag with separate-file and
remote-signing presence so downstream gates can read one field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 2: Update audit trail x_fc_trail_has_signed_pages to use new gate

Files:

  • Modify: fusion_claims/models/sale_order.py:3236-3248
  • Modify: fusion_claims/tests/test_signed_pages_gate.py (append test)

Steps

  • Step 1: Add failing test

Append to fusion_claims/tests/test_signed_pages_gate.py:

    def test_trail_has_signed_pages_true_when_bundled(self):
        self.order.x_fc_pages_11_12_in_original = True
        self.order.flush_recordset()
        self.assertTrue(self.order.x_fc_trail_has_signed_pages)

    def test_trail_has_signed_pages_false_when_nothing(self):
        self.assertFalse(self.order.x_fc_trail_has_signed_pages)

    def test_trail_has_signed_pages_true_when_separate_file(self):
        self.order.x_fc_signed_pages_11_12 = _b64_pdf()
        self.order.flush_recordset()
        self.assertTrue(self.order.x_fc_trail_has_signed_pages)
  • Step 2: Run test to verify the bundled-flag case fails

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims:TestSignedPagesGate.test_trail_has_signed_pages_true_when_bundled \
  --stop-after-init 2>&1 | tail -20

Expected: FAIL — x_fc_trail_has_signed_pages is False because the existing compute reads x_fc_signed_pages_11_12 directly.

  • Step 3: Update the compute body and depends

In fusion_claims/models/sale_order.py, change the @api.depends block at line 3235-3240 and the assignment at line 3248.

Find:

    @api.depends(
        'x_fc_assessment_start_date', 'x_fc_assessment_end_date',
        'x_fc_claim_authorization_date', 'x_fc_original_application',
        'x_fc_signed_pages_11_12', 'x_fc_final_submitted_application',
        'x_fc_xml_file', 'x_fc_approval_letter', 'x_fc_proof_of_delivery',
        'x_fc_vendor_bill_ids', 'invoice_ids', 'invoice_ids.state'
    )
    def _compute_order_trail(self):

Replace 'x_fc_signed_pages_11_12' with 'x_fc_has_signed_pages_11_12' so the trail recomputes when the new gate flips:

    @api.depends(
        'x_fc_assessment_start_date', 'x_fc_assessment_end_date',
        'x_fc_claim_authorization_date', 'x_fc_original_application',
        'x_fc_has_signed_pages_11_12', 'x_fc_final_submitted_application',
        'x_fc_xml_file', 'x_fc_approval_letter', 'x_fc_proof_of_delivery',
        'x_fc_vendor_bill_ids', 'invoice_ids', 'invoice_ids.state'
    )
    def _compute_order_trail(self):

Then find line 3248:

            order.x_fc_trail_has_signed_pages = bool(order.x_fc_signed_pages_11_12)

Replace with:

            order.x_fc_trail_has_signed_pages = order.x_fc_has_signed_pages_11_12
  • Step 4: Run tests to verify they pass

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims:TestSignedPagesGate \
  --stop-after-init 2>&1 | tail -20

Expected: 9 tests run, 0 failed.

  • Step 5: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_claims/models/sale_order.py fusion_claims/tests/test_signed_pages_gate.py
git commit -m "$(cat <<'EOF'
feat(claims): audit trail honours bundled pages flag

x_fc_trail_has_signed_pages now reads x_fc_has_signed_pages_11_12, so
the trail correctly shows complete when pages 11 & 12 are bundled inside
the original application.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 3: Update ready_for_submission_wizard to use the computed gate

Files:

  • Modify: fusion_claims/wizard/ready_for_submission_wizard.py:95, 148
  • Modify: fusion_claims/tests/test_signed_pages_gate.py (append test)

Steps

  • Step 1: Add failing test

Append to fusion_claims/tests/test_signed_pages_gate.py:

    def test_ready_for_submission_passes_with_bundled_flag_only(self):
        """Ready-for-submission gate passes when bundled flag is True even
        without a separate signed-pages file."""
        # Move order into Application Received and fill required fields
        self.order.write({
            'x_fc_adp_application_status': 'application_received',
            'x_fc_original_application': _b64_pdf(),
            'x_fc_original_application_filename': 'app.pdf',
            'x_fc_pages_11_12_in_original': True,
            'x_fc_client_ref_1': 'JODO',
            'x_fc_client_ref_2': '1234',
            'x_fc_reason_for_application': 'first_access',
        })
        self.order.flush_recordset()

        wizard = self.env['fusion_claims.ready.for.submission.wizard'].with_context(
            active_id=self.order.id, active_model='sale.order',
        ).create({
            'sale_order_id': self.order.id,
            'claim_authorization_date': '2026-05-01',
        })
        # Should not raise
        wizard.action_confirm()
        self.assertEqual(self.order.x_fc_adp_application_status, 'ready_submission')
  • Step 2: Run test to verify it fails

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims:TestSignedPagesGate.test_ready_for_submission_passes_with_bundled_flag_only \
  --stop-after-init 2>&1 | tail -30

Expected: FAIL — UserError raised by the gate at line 148, because x_fc_signed_pages_11_12 is empty.

  • Step 3: Update the wizard's two read sites

In fusion_claims/wizard/ready_for_submission_wizard.py, find line 95:

            wizard.has_documents = bool(order.x_fc_original_application and order.x_fc_signed_pages_11_12)

Replace with:

            wizard.has_documents = bool(order.x_fc_original_application and order.x_fc_has_signed_pages_11_12)

Then find line 148:

        if not order.x_fc_signed_pages_11_12:
            missing.append('Page 11 & 12 Signed (upload in Application Received step)')

Replace with:

        if not order.x_fc_has_signed_pages_11_12:
            missing.append('Page 11 & 12 Signed (upload in Application Received step)')
  • Step 4: Run tests to verify they pass

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims:TestSignedPagesGate \
  --stop-after-init 2>&1 | tail -20

Expected: 10 tests run, 0 failed.

  • Step 5: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_claims/wizard/ready_for_submission_wizard.py fusion_claims/tests/test_signed_pages_gate.py
git commit -m "$(cat <<'EOF'
feat(claims): ready-for-submission gate accepts bundled pages flag

Both the has_documents indicator and the action_confirm missing-items
gate now read x_fc_has_signed_pages_11_12, so orders with pages 11 & 12
bundled inside the original PDF can move to Ready for Submission without
a separate signed-pages file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 4: Update case_close_verification_wizard to use the computed gate

Files:

  • Modify: fusion_claims/wizard/case_close_verification_wizard.py:81
  • Modify: fusion_claims/tests/test_signed_pages_gate.py (append test)

Steps

  • Step 1: Add failing test

Append to fusion_claims/tests/test_signed_pages_gate.py:

    def test_case_close_audit_accepts_bundled_flag(self):
        """Case-close audit treats bundled flag as 'signed pages present'."""
        self.order.x_fc_pages_11_12_in_original = True
        self.order.flush_recordset()

        wizard = self.env['fusion_claims.case.close.verification.wizard'].with_context(
            active_id=self.order.id, active_model='sale.order',
        ).create({
            'sale_order_id': self.order.id,
        })
        # Force recompute by reading the field
        self.assertTrue(wizard.has_signed_pages)
  • Step 2: Run test to verify it fails

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims:TestSignedPagesGate.test_case_close_audit_accepts_bundled_flag \
  --stop-after-init 2>&1 | tail -20

Expected: FAIL — wizard.has_signed_pages is False because the compute reads x_fc_signed_pages_11_12 directly.

  • Step 3: Update the wizard's read site

In fusion_claims/wizard/case_close_verification_wizard.py, find line 81:

            wizard.has_signed_pages = bool(order.x_fc_signed_pages_11_12)

Replace with:

            wizard.has_signed_pages = order.x_fc_has_signed_pages_11_12
  • Step 4: Run tests to verify they pass

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims:TestSignedPagesGate \
  --stop-after-init 2>&1 | tail -20

Expected: 11 tests run, 0 failed.

  • Step 5: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_claims/wizard/case_close_verification_wizard.py fusion_claims/tests/test_signed_pages_gate.py
git commit -m "$(cat <<'EOF'
feat(claims): case-close audit accepts bundled pages flag

The signed-pages verification step on case close now treats the bundled
flag as 'pages present', matching the ready-for-submission gate and the
audit trail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 5: Rewrite application_received_wizard.py with three-mode intake_mode

Files:

  • Modify: fusion_claims/wizard/application_received_wizard.py (whole file rewrite)
  • Create: fusion_claims/tests/test_application_received_wizard.py

Steps

  • Step 1: Create failing tests for the new wizard behaviour

Create fusion_claims/tests/test_application_received_wizard.py:

# -*- coding: utf-8 -*-
import base64

from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase, tagged


PDF_BYTES = b'%PDF-1.4\n%fake pdf for tests'
NOT_PDF_BYTES = b'this is not a pdf'


def _b64(data):
    return base64.b64encode(data)


@tagged('-at_install', 'post_install', 'fusion_claims')
class TestApplicationReceivedWizard(TransactionCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.partner = cls.env['res.partner'].create({'name': 'ARW Test Client'})

    def _make_order(self):
        return self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'x_fc_adp_application_status': 'waiting_for_application',
        })

    def _open_wizard(self, order, vals=None):
        wizard = self.env['fusion_claims.application.received.wizard'].with_context(
            active_id=order.id, active_model='sale.order',
        ).create({
            'sale_order_id': order.id,
            **(vals or {}),
        })
        return wizard

    # ---- bundled mode ----
    def test_bundled_mode_marks_received_with_only_original(self):
        order = self._make_order()
        wizard = self._open_wizard(order, {
            'intake_mode': 'bundled',
            'original_application': _b64(PDF_BYTES),
            'original_application_filename': 'app.pdf',
        })
        wizard.action_confirm()

        self.assertEqual(order.x_fc_adp_application_status, 'application_received')
        self.assertTrue(order.x_fc_pages_11_12_in_original)
        self.assertFalse(order.x_fc_signed_pages_11_12)
        self.assertTrue(order.x_fc_has_signed_pages_11_12)

    # ---- separate mode ----
    def test_separate_mode_requires_signed_pages(self):
        order = self._make_order()
        wizard = self._open_wizard(order, {
            'intake_mode': 'separate',
            'original_application': _b64(PDF_BYTES),
            'original_application_filename': 'app.pdf',
        })
        with self.assertRaises(UserError):
            wizard.action_confirm()

    def test_separate_mode_writes_both_files(self):
        order = self._make_order()
        wizard = self._open_wizard(order, {
            'intake_mode': 'separate',
            'original_application': _b64(PDF_BYTES),
            'original_application_filename': 'app.pdf',
            'signed_pages_11_12': _b64(PDF_BYTES),
            'signed_pages_filename': 'p11_12.pdf',
        })
        wizard.action_confirm()
        self.assertEqual(order.x_fc_adp_application_status, 'application_received')
        self.assertFalse(order.x_fc_pages_11_12_in_original)
        self.assertTrue(order.x_fc_signed_pages_11_12)

    # ---- remote mode ----
    def test_remote_mode_requires_sent_or_signed_request(self):
        order = self._make_order()
        wizard = self._open_wizard(order, {
            'intake_mode': 'remote',
            'original_application': _b64(PDF_BYTES),
            'original_application_filename': 'app.pdf',
        })
        with self.assertRaises(UserError):
            wizard.action_confirm()

    def test_remote_mode_passes_when_request_sent(self):
        order = self._make_order()
        self.env['fusion.page11.sign.request'].create({
            'sale_order_id': order.id,
            'signer_email': 'sign@example.com',
            'signer_type': 'client',
            'state': 'sent',
        })
        wizard = self._open_wizard(order, {
            'intake_mode': 'remote',
            'original_application': _b64(PDF_BYTES),
            'original_application_filename': 'app.pdf',
        })
        wizard.action_confirm()
        self.assertEqual(order.x_fc_adp_application_status, 'application_received')
        self.assertFalse(order.x_fc_pages_11_12_in_original)

    # ---- PDF magic-byte check ----
    def test_non_pdf_original_is_rejected(self):
        order = self._make_order()
        wizard = self._open_wizard(order, {
            'intake_mode': 'bundled',
            'original_application': _b64(NOT_PDF_BYTES),
            'original_application_filename': 'fake.pdf',
        })
        with self.assertRaises(UserError):
            wizard.action_confirm()

    def test_non_pdf_signed_pages_is_rejected(self):
        order = self._make_order()
        wizard = self._open_wizard(order, {
            'intake_mode': 'separate',
            'original_application': _b64(PDF_BYTES),
            'original_application_filename': 'app.pdf',
            'signed_pages_11_12': _b64(NOT_PDF_BYTES),
            'signed_pages_filename': 'p11_12.pdf',
        })
        with self.assertRaises(UserError):
            wizard.action_confirm()

    # ---- status gate ----
    def test_blocks_from_wrong_status(self):
        order = self._make_order()
        order.x_fc_adp_application_status = 'submitted'
        wizard = self._open_wizard(order, {
            'intake_mode': 'bundled',
            'original_application': _b64(PDF_BYTES),
            'original_application_filename': 'app.pdf',
        })
        with self.assertRaises(UserError):
            wizard.action_confirm()

    # ---- default_get picks initial mode ----
    def _get_defaults(self, order, fields_list=('intake_mode',)):
        return self.env['fusion_claims.application.received.wizard'].with_context(
            active_id=order.id, active_model='sale.order',
        ).default_get(list(fields_list))

    def test_default_intake_mode_bundled_on_fresh_order(self):
        order = self._make_order()
        defaults = self._get_defaults(order)
        self.assertEqual(defaults.get('intake_mode'), 'bundled')

    def test_default_intake_mode_bundled_when_flag_set(self):
        order = self._make_order()
        order.x_fc_pages_11_12_in_original = True
        defaults = self._get_defaults(order)
        self.assertEqual(defaults.get('intake_mode'), 'bundled')

    def test_default_intake_mode_separate_when_file_present(self):
        order = self._make_order()
        order.x_fc_signed_pages_11_12 = _b64(PDF_BYTES)
        order.x_fc_signed_pages_filename = 'p.pdf'
        defaults = self._get_defaults(order)
        self.assertEqual(defaults.get('intake_mode'), 'separate')

    def test_default_intake_mode_remote_when_request_pending(self):
        order = self._make_order()
        self.env['fusion.page11.sign.request'].create({
            'sale_order_id': order.id,
            'signer_email': 'a@b.com',
            'signer_type': 'client',
            'state': 'sent',
        })
        defaults = self._get_defaults(order)
        self.assertEqual(defaults.get('intake_mode'), 'remote')

    # ---- chatter ----
    def test_chatter_message_mentions_bundled(self):
        order = self._make_order()
        wizard = self._open_wizard(order, {
            'intake_mode': 'bundled',
            'original_application': _b64(PDF_BYTES),
            'original_application_filename': 'app.pdf',
        })
        wizard.action_confirm()
        messages = order.message_ids.mapped('body')
        self.assertTrue(
            any('bundled' in (m or '').lower() or 'included in original' in (m or '').lower()
                for m in messages),
            f"Expected bundled-mode chatter; got: {messages}",
        )
  • Step 2: Run tests to verify they fail

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims:TestApplicationReceivedWizard \
  --stop-after-init 2>&1 | tail -30

Expected: multiple FAILures, especially intake_mode field not found.

  • Step 3: Rewrite application_received_wizard.py

Replace the entire contents of fusion_claims/wizard/application_received_wizard.py with:

# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)

import base64
import logging
from datetime import date

from markupsafe import Markup

from odoo import models, fields, api, _
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)

try:
    import pdfrw
except ImportError:  # pragma: no cover
    pdfrw = None


class ApplicationReceivedWizard(models.TransientModel):
    """Wizard to upload ADP application documents when application is received."""
    _name = 'fusion_claims.application.received.wizard'
    _description = 'Application Received Wizard'

    sale_order_id = fields.Many2one(
        'sale.order',
        string='Sale Order',
        required=True,
        readonly=True,
    )

    intake_mode = fields.Selection(
        selection=[
            ('bundled',  'Pages 11 & 12 are INCLUDED in the original application'),
            ('separate', 'Pages 11 & 12 are a SEPARATE file'),
            ('remote',   'Pages 11 & 12 will be SIGNED REMOTELY'),
        ],
        string='Intake Mode',
        required=True,
        default='bundled',
        help=(
            'Bundled: a single PDF that already contains the signed pages 11 & 12.\n'
            'Separate: original application + a separate PDF with the signed pages 11 & 12.\n'
            'Remote: send Page 11 to a family member / agent for digital signing.'
        ),
    )

    # Document uploads
    original_application = fields.Binary(
        string='Original ADP Application',
        required=True,
        help='Upload the original ADP application PDF received from the client',
    )
    original_application_filename = fields.Char(string='Application Filename')

    original_page_count = fields.Integer(
        string='Original PDF Page Count',
        compute='_compute_original_page_count',
        help='Number of pages detected in the uploaded original PDF.',
    )

    signed_pages_11_12 = fields.Binary(
        string='Signed Pages 11 & 12',
        help='Upload the signed pages 11 and 12 from the application '
             '(only used in Separate-file mode).',
    )
    signed_pages_filename = fields.Char(string='Pages Filename')

    has_pending_page11_request = fields.Boolean(
        compute='_compute_has_pending_page11_request',
    )
    has_signed_page11 = fields.Boolean(
        compute='_compute_has_pending_page11_request',
    )

    notes = fields.Text(
        string='Notes',
        help='Any notes about the received application',
    )

    # ------------------------------------------------------------------
    # COMPUTED
    # ------------------------------------------------------------------
    @api.depends('sale_order_id')
    def _compute_has_pending_page11_request(self):
        for wiz in self:
            order = wiz.sale_order_id
            if order:
                requests = order.page11_sign_request_ids
                wiz.has_pending_page11_request = bool(
                    requests.filtered(lambda r: r.state in ('draft', 'sent'))
                )
                wiz.has_signed_page11 = bool(
                    order.x_fc_signed_pages_11_12
                    or requests.filtered(lambda r: r.state == 'signed')
                )
            else:
                wiz.has_pending_page11_request = False
                wiz.has_signed_page11 = False

    @api.depends('original_application')
    def _compute_original_page_count(self):
        for wiz in self:
            wiz.original_page_count = wiz._count_pdf_pages(wiz.original_application)

    @staticmethod
    def _count_pdf_pages(b64_data):
        """Return PDF page count, or 0 if unknown/unparseable."""
        if not b64_data or pdfrw is None:
            return 0
        try:
            import io
            raw = base64.b64decode(b64_data)
            reader = pdfrw.PdfReader(fdata=raw)
            return len(reader.pages) if reader and reader.pages else 0
        except Exception:  # pragma: no cover  (corrupted PDFs)
            return 0

    # ------------------------------------------------------------------
    # DEFAULTS
    # ------------------------------------------------------------------
    @api.model
    def default_get(self, fields_list):
        res = super().default_get(fields_list)
        active_id = self._context.get('active_id')
        if not active_id:
            return res

        order = self.env['sale.order'].browse(active_id)
        res['sale_order_id'] = order.id

        if order.x_fc_original_application:
            res['original_application'] = order.x_fc_original_application
            res['original_application_filename'] = order.x_fc_original_application_filename
        if order.x_fc_signed_pages_11_12:
            res['signed_pages_11_12'] = order.x_fc_signed_pages_11_12
            res['signed_pages_filename'] = order.x_fc_signed_pages_filename

        # Choose initial intake mode based on order state.
        if order.x_fc_pages_11_12_in_original:
            res['intake_mode'] = 'bundled'
        elif order.x_fc_signed_pages_11_12:
            res['intake_mode'] = 'separate'
        elif order.page11_sign_request_ids.filtered(
            lambda r: r.state in ('sent', 'signed')
        ):
            res['intake_mode'] = 'remote'
        else:
            res['intake_mode'] = 'bundled'
        return res

    # ------------------------------------------------------------------
    # CONSTRAINTS (filename defence-in-depth)
    # ------------------------------------------------------------------
    @api.constrains('original_application_filename')
    def _check_application_file_type(self):
        for wizard in self:
            name = wizard.original_application_filename
            if name and not name.lower().endswith('.pdf'):
                raise UserError(
                    f"Original Application must be a PDF file.\n"
                    f"Uploaded file: '{name}'"
                )

    @api.constrains('signed_pages_filename')
    def _check_pages_file_type(self):
        for wizard in self:
            name = wizard.signed_pages_filename
            if name and not name.lower().endswith('.pdf'):
                raise UserError(
                    f"Signed Pages 11 & 12 must be a PDF file.\n"
                    f"Uploaded file: '{name}'"
                )

    # ------------------------------------------------------------------
    # ACTIONS
    # ------------------------------------------------------------------
    def action_confirm(self):
        """Save documents and mark application as received."""
        self.ensure_one()
        order = self.sale_order_id

        if order.x_fc_adp_application_status not in (
            'assessment_completed', 'waiting_for_application',
        ):
            raise UserError(
                "Can only mark application received from 'Assessment Completed' "
                "or 'Waiting for Application' status."
            )

        if not self.original_application:
            raise UserError("Please upload the Original ADP Application.")

        self._validate_pdf_bytes(self.original_application, 'Original ADP Application')

        vals = {
            'x_fc_adp_application_status': 'application_received',
            'x_fc_original_application': self.original_application,
            'x_fc_original_application_filename': self.original_application_filename,
            'x_fc_pages_11_12_in_original': (self.intake_mode == 'bundled'),
        }

        if self.intake_mode == 'separate':
            if not (self.signed_pages_11_12 or order.x_fc_signed_pages_11_12):
                raise UserError(
                    "Signed Pages 11 & 12 file is required when "
                    "'Separate file' mode is selected."
                )
            if self.signed_pages_11_12:
                self._validate_pdf_bytes(
                    self.signed_pages_11_12, 'Signed Pages 11 & 12',
                )
                vals['x_fc_signed_pages_11_12'] = self.signed_pages_11_12
                vals['x_fc_signed_pages_filename'] = self.signed_pages_filename

        elif self.intake_mode == 'remote':
            has_request = order.page11_sign_request_ids.filtered(
                lambda r: r.state in ('sent', 'signed')
            )
            if not has_request:
                raise UserError(
                    "No remote-signing request found. Click "
                    "'Request Remote Signature' first, or pick a different mode."
                )

        order.with_context(skip_status_validation=True).write(vals)
        self._post_chatter(order)
        return {'type': 'ir.actions.act_window_close'}

    def action_request_page11_signature(self):
        """Open the Page 11 remote signing wizard from within this wizard."""
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Request Page 11 Signature',
            'res_model': 'fusion_claims.send.page11.wizard',
            'view_mode': 'form',
            'target': 'new',
            'context': {'default_sale_order_id': self.sale_order_id.id},
        }

    # ------------------------------------------------------------------
    # HELPERS
    # ------------------------------------------------------------------
    @staticmethod
    def _validate_pdf_bytes(b64_data, label):
        """Raise UserError if the uploaded binary is not a real PDF."""
        if not b64_data:
            return
        try:
            head = base64.b64decode(b64_data)[:5]
        except Exception:
            raise UserError(f"{label}: could not decode uploaded file.")
        if head != b'%PDF-':
            raise UserError(
                f"{label} must be a PDF file "
                f"(content check failed — the file does not start with %PDF-)."
            )

    def _post_chatter(self, order):
        """Post a mode-aware Application Received message to the chatter."""
        self.ensure_one()
        mode = self.intake_mode
        if mode == 'bundled':
            headline = 'Application Received — bundled'
            detail = 'Pages 11 & 12 included in original PDF'
        elif mode == 'separate':
            headline = 'Application Received — separate files'
            detail = 'Original + separate signed pages uploaded'
        else:  # remote
            n = len(order.page11_sign_request_ids.filtered(
                lambda r: r.state in ('sent', 'signed')
            ))
            headline = 'Application Received — remote signature pending'
            detail = f'Page 11 sent for remote signature ({n} request(s) outstanding)'

        notes_html = (
            f'<p style="margin: 4px 0 0 0;"><strong>Notes:</strong> {self.notes}</p>'
            if self.notes else ''
        )
        body = Markup(
            '<div style="background:#e8f4fd;border-left:4px solid #17a2b8;'
            'padding:12px;margin:8px 0;border-radius:4px;">'
            '<h4 style="color:#17a2b8;margin:0 0 8px 0;">'
            '<i class="fa fa-file-text-o"/> {headline}</h4>'
            '<p style="margin:0;"><strong>Date:</strong> {today}</p>'
            '<p style="margin:8px 0 4px 0;">{detail}</p>'
            '<p style="margin:0;color:#666;">'
            'Original: {orig_name}</p>'
            '{notes}'
            '</div>'
        ).format(
            headline=headline,
            today=date.today().strftime('%B %d, %Y'),
            detail=detail,
            orig_name=self.original_application_filename or '(no filename)',
            notes=notes_html,
        )
        order.message_post(
            body=body,
            message_type='notification',
            subtype_xmlid='mail.mt_note',
        )
  • Step 4: Run tests to verify they pass

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims:TestApplicationReceivedWizard \
  --stop-after-init 2>&1 | tail -30

Expected: 13 tests run, 0 failed.

  • Step 5: Run all module tests to confirm nothing regressed

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims \
  --stop-after-init 2>&1 | tail -20

Expected: 24 tests run (11 from TestSignedPagesGate + 13 from TestApplicationReceivedWizard), 0 failed.

  • Step 6: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_claims/wizard/application_received_wizard.py fusion_claims/tests/test_application_received_wizard.py
git commit -m "$(cat <<'EOF'
feat(claims): three-mode Application Received wizard

Adds intake_mode (bundled / separate / remote) so staff can mark
applications received with a single bundled PDF, the existing
separate-pages-file flow, or a pending remote signature. Folds in
content-based PDF validation, a friendlier status-gate message,
and a page-count helper for the original application.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 6: Update wizard view — radio + conditional visibility + page count

Files:

  • Modify: fusion_claims/wizard/application_received_wizard_views.xml

Steps

  • Step 1: Replace the wizard form view

Replace the contents of fusion_claims/wizard/application_received_wizard_views.xml with:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="view_application_received_wizard_form" model="ir.ui.view">
        <field name="name">fusion_claims.application.received.wizard.form</field>
        <field name="model">fusion_claims.application.received.wizard</field>
        <field name="arch" type="xml">
            <form string="Application Received">
                <div class="alert alert-info mb-3" role="alert">
                    <strong><i class="fa fa-info-circle"/> Upload Required Documents</strong>
                    <p class="mb-0">
                        Please upload the ADP application documents received from the client,
                        then tell the system how pages 11 &amp; 12 were provided.
                    </p>
                </div>

                <field name="sale_order_id" invisible="1"/>
                <field name="has_pending_page11_request" invisible="1"/>
                <field name="has_signed_page11" invisible="1"/>

                <group string="How were pages 11 &amp; 12 provided?">
                    <field name="intake_mode" widget="radio" nolabel="1"/>
                </group>

                <group>
                    <group string="Original ADP Application">
                        <field name="original_application"
                               filename="original_application_filename"
                               widget="binary" class="oe_inline"/>
                        <field name="original_application_filename" invisible="1"/>
                        <field name="original_page_count" readonly="1"
                               string="Detected pages"
                               invisible="not original_application"/>
                    </group>

                    <group string="Signed Pages 11 &amp; 12"
                           invisible="intake_mode != 'separate'">
                        <field name="signed_pages_11_12"
                               filename="signed_pages_filename"
                               widget="binary" class="oe_inline"
                               required="intake_mode == 'separate'"/>
                        <field name="signed_pages_filename" invisible="1"/>
                    </group>

                    <group string="Remote Signature"
                           invisible="intake_mode != 'remote'">
                        <div invisible="has_pending_page11_request or has_signed_page11"
                             class="mt-2">
                            <span class="text-muted small">
                                Don't have signed pages? Send a remote signing link to a family
                                member or agent.
                            </span>
                            <button name="action_request_page11_signature" type="object"
                                    string="Request Remote Signature"
                                    class="btn btn-sm btn-outline-warning"
                                    icon="fa-pencil-square-o"/>
                        </div>

                        <div invisible="not has_pending_page11_request" class="mt-2">
                            <div class="alert alert-warning mb-0 py-2 px-3">
                                <i class="fa fa-clock-o"/>
                                A remote signing request has been sent. You can confirm now -
                                the signed PDF will be auto-attached when received.
                            </div>
                        </div>

                        <div invisible="not has_signed_page11" class="mt-2">
                            <div class="alert alert-success mb-0 py-2 px-3">
                                <i class="fa fa-check-circle"/>
                                Page 11 has been signed remotely.
                            </div>
                        </div>
                    </group>
                </group>

                <group>
                    <field name="notes" placeholder="Any notes about the received application..."/>
                </group>

                <footer>
                    <button name="action_confirm" type="object"
                            string="Confirm Application Received"
                            class="btn-primary" icon="fa-check"/>
                    <button string="Cancel" class="btn-secondary" special="cancel"/>
                </footer>
            </form>
        </field>
    </record>

    <record id="action_application_received_wizard" model="ir.actions.act_window">
        <field name="name">Application Received</field>
        <field name="res_model">fusion_claims.application.received.wizard</field>
        <field name="view_mode">form</field>
        <field name="target">new</field>
    </record>
</odoo>
  • Step 2: Update the module so the new XML loads

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --stop-after-init 2>&1 | tail -15

Expected: no errors. The module reload should complete cleanly.

  • Step 3: Manual smoke test (UI)

Open http://localhost:8069 in a browser. Hard-refresh to drop cached assets.

  1. Open a Sales Order whose ADP Application Status is Waiting for Application (or Assessment Completed).
  2. Click Mark Application Received.
  3. Confirm three radio options appear and Bundled is preselected.
  4. Pick Bundled, upload any small PDF — the Detected pages field should show a number.
  5. Click Confirm Application Received.
  6. On the order, verify:
    • Status moved to Application Received.
    • Chatter shows the bundled message.
    • x_fc_pages_11_12_in_original is checked (set to True).
  7. Re-open the wizard on another order, switch radio to Separate — the Signed Pages group should appear and require a file.
  8. Re-open the wizard on a third order, switch radio to Remote — the Signed Pages group should disappear and the Request Remote Signature button should appear (when no request exists yet).

If any step misbehaves visually, inspect the browser console + the docker logs.

  • Step 4: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_claims/wizard/application_received_wizard_views.xml
git commit -m "$(cat <<'EOF'
feat(claims): wizard view — intake-mode radio + conditional groups

Three-mode radio at the top of the Application Received wizard. The
Signed Pages 11 & 12 group is only shown in Separate mode; the remote
sign banner/button is only shown in Remote mode. Adds a read-only
'Detected pages' indicator next to the uploaded original PDF.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 7: Bump module version

Files:

  • Modify: fusion_claims/__manifest__.py:8

Steps

  • Step 1: Bump version

In fusion_claims/__manifest__.py, find line 8:

    'version': '19.0.8.0.6',

Replace with:

    'version': '19.0.8.0.7',
  • Step 2: Reload module to apply version bump and bust asset cache

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --stop-after-init 2>&1 | tail -5

Expected: no errors.

  • Step 3: Final test suite run

Run:

docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims \
  --test-enable --test-tags=fusion_claims \
  --stop-after-init 2>&1 | tail -15

Expected: 24 tests, 0 failed.

  • Step 4: Commit
cd /Users/gurpreet/Github/Odoo-Modules
git add fusion_claims/__manifest__.py
git commit -m "$(cat <<'EOF'
chore(claims): bump version to 19.0.8.0.7

Bumps fusion_claims version to bust the asset bundle cache after the
Application Received wizard refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Self-Review Notes

Spec coverage check:

Spec section Covered by
New fields on sale.order Task 1
_compute_has_signed_pages_11_12 with three-way OR Task 1
Audit trail field update + dep refresh Task 2
ready_for_submission_wizard.py:95 change Task 3
ready_for_submission_wizard.py:148 change Task 3
case_close_verification_wizard.py:81 change Task 4
intake_mode Selection + radio UX Task 5 + Task 6
default_get initial-mode selection Task 5
PDF magic-bytes check Task 5
Status-gate message fix Task 5
Page-count indicator Task 5 + Task 6
Mode-aware chatter Task 5
action_confirm three-mode logic Task 5
View — conditional visibility Task 6
Manual smoke test Task 6
Module version bump Task 7

No gaps.

Placeholder scan: no TBD/TODO; every step has either complete code or an exact command.

Type consistency: field names match between model (Task 1), wizard logic (Task 5), and view (Task 6). _validate_pdf_bytes defined in Task 5 is used inside the same module. _post_chatter helper defined in Task 5 is called inside the same wizard.