# 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](../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`: ```python # -*- 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`: ```python # -*- 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: ```bash 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: ```python 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): ```python 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: ```bash 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** ```bash 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) 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`: ```python 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: ```bash 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: ```python @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: ```python @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: ```python order.x_fc_trail_has_signed_pages = bool(order.x_fc_signed_pages_11_12) ``` Replace with: ```python order.x_fc_trail_has_signed_pages = order.x_fc_has_signed_pages_11_12 ``` - [ ] **Step 4: Run tests to verify they pass** Run: ```bash 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** ```bash 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) 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`: ```python 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: ```bash 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: ```python wizard.has_documents = bool(order.x_fc_original_application and order.x_fc_signed_pages_11_12) ``` Replace with: ```python wizard.has_documents = bool(order.x_fc_original_application and order.x_fc_has_signed_pages_11_12) ``` Then find line 148: ```python if not order.x_fc_signed_pages_11_12: missing.append('Page 11 & 12 Signed (upload in Application Received step)') ``` Replace with: ```python 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: ```bash 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** ```bash 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) 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`: ```python 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: ```bash 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: ```python wizard.has_signed_pages = bool(order.x_fc_signed_pages_11_12) ``` Replace with: ```python wizard.has_signed_pages = order.x_fc_has_signed_pages_11_12 ``` - [ ] **Step 4: Run tests to verify they pass** Run: ```bash 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** ```bash 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) 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`: ```python # -*- 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: ```bash 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: ```python # -*- 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'

Notes: {self.notes}

' if self.notes else '' ) body = Markup( '
' '

' ' {headline}

' '

Date: {today}

' '

{detail}

' '

' 'Original: {orig_name}

' '{notes}' '
' ).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: ```bash 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: ```bash 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** ```bash 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) 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 fusion_claims.application.received.wizard.form fusion_claims.application.received.wizard
Don't have signed pages? Send a remote signing link to a family member or agent.
A remote signing request has been sent. You can confirm now - the signed PDF will be auto-attached when received.
Page 11 has been signed remotely.
Application Received fusion_claims.application.received.wizard form new
``` - [ ] **Step 2: Update the module so the new XML loads** Run: ```bash 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** ```bash 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) 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: ```python 'version': '19.0.8.0.6', ``` Replace with: ```python 'version': '19.0.8.0.7', ``` - [ ] **Step 2: Reload module to apply version bump and bust asset cache** Run: ```bash 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: ```bash 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** ```bash 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) 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.