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>
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 & 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 & 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 & 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.
- Open a Sales Order whose
ADP Application StatusisWaiting for Application(orAssessment Completed). - Click Mark Application Received.
- Confirm three radio options appear and
Bundledis preselected. - Pick Bundled, upload any small PDF — the
Detected pagesfield should show a number. - Click Confirm Application Received.
- On the order, verify:
- Status moved to
Application Received. - Chatter shows the bundled message.
x_fc_pages_11_12_in_originalis checked (set to True).
- Status moved to
- Re-open the wizard on another order, switch radio to Separate — the Signed Pages group should appear and require a file.
- 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.