# ADP Application Received — Bundled Pages 11 & 12 (Design) **Date:** 2026-05-19 **Module:** `fusion_claims` **Owner:** Gurpreet **Status:** Approved (ready for implementation plan) ## Problem When marking an ADP application as Received, the `Application Received` wizard requires two separate PDF uploads: 1. **Original ADP Application** (`x_fc_original_application`) 2. **Signed Pages 11 & 12** (`x_fc_signed_pages_11_12`) In day-to-day operations the office or the client often scans (or emails) the **entire** ADP application as a single PDF — already including signed pages 11 & 12. Today, staff have to manually split pages 11 & 12 out of the bundled PDF and upload them again as a separate file, even though the same signatures are already present in the original PDF. The wizard must continue to support the existing flows (separate signed-pages file, remote signing via Page 11 signing request), but it should also accept the bundled case without manual splitting. ## Goals - Allow staff to mark Application Received with **one** PDF when pages 11 & 12 are inside it. - Preserve the two existing modes (separate file, remote signing). - Keep downstream audit/case-close checks correct without rewriting every consumer. - Make the wizard easier to use and slightly safer (real PDF detection, friendlier messages). ## Non-Goals - PDF page extraction or splitting (explicitly rejected by user — "no split"). - Capturing Page 11 signer identity in the bundled / separate-file modes (existing gap; out of scope). - Re-architecting the document-attachment model to de-duplicate identical binaries (out of scope). - Changes to the remote signing wizard or `fusion.page11.sign.request` model. ## High-Level Approach Add a **single boolean flag** on `sale.order` that records whether pages 11 & 12 are inside the original application PDF. Introduce a **computed helper field** that downstream consumers read instead of `x_fc_signed_pages_11_12` directly. Add a **three-mode radio** at the top of the Application Received wizard. Minimal blast radius: - One new boolean, one new computed field on `sale.order`. - Wizard view + Python rewritten to drive logic off the radio mode. - Four downstream call sites change which field they read (no logic change). - Three small complementary fixes folded in (status-gate text, PDF magic-bytes check, page-count indicator). ## Data Model ### `sale.order` — new fields ```python x_fc_pages_11_12_in_original = fields.Boolean( string='Pages 11 & 12 in Original Application', default=False, tracking=True, 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, uploaded separately, ' 'or signed via remote signing request.', ) @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') ) ``` ### Existing fields — unchanged meaning - `x_fc_original_application` — original (or bundled) PDF. - `x_fc_signed_pages_11_12` — separate signed-pages file when one exists. Stays optional. - `page11_sign_request_ids` — remote signing requests. Unchanged. ### Audit trail field `x_fc_trail_has_signed_pages` already exists at [models/sale_order.py:3248](../../fusion_claims/models/sale_order.py:3248). Its compute body changes from `bool(order.x_fc_signed_pages_11_12)` to `order.x_fc_has_signed_pages_11_12`. ### Migration None. Existing records get `x_fc_pages_11_12_in_original = False` by default; their existing `x_fc_signed_pages_11_12` binary continues to satisfy the new computed gate. Stored compute will populate `x_fc_has_signed_pages_11_12` for legacy rows on first read or recompute. ## Wizard Changes — `fusion_claims.application.received.wizard` ### New fields ```python intake_mode = fields.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', ) original_page_count = fields.Integer( string='Original PDF Page Count', compute='_compute_original_page_count', ) ``` `signed_pages_11_12` and `signed_pages_filename` keep their current definitions — they're only required in `separate` mode now. The existing computed fields `has_pending_page11_request` and `has_signed_page11` ([wizard/application_received_wizard.py:44-49](../../fusion_claims/wizard/application_received_wizard.py:44)) **stay** — they drive the "request pending" / "remote signature complete" banners now only shown when `intake_mode == 'remote'`. ### `default_get` — pick an initial mode from existing state ```python # When re-opening the wizard on an order that already has some data: 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' # new default for fresh records ``` ### View behaviour (declarative `invisible` on group containers) | Mode | Original upload | Signed Pages 11 & 12 upload | Remote-sign banner / button | |---|---|---|---| | `bundled` | shown, required | hidden | hidden | | `separate` | shown, required | shown, required | hidden | | `remote` | shown, required | hidden | shown (existing `action_request_page11_signature` button) | Page count is displayed read-only next to the original-application filename once a PDF is loaded. If `pdfrw` fails to parse, show *"(could not read PDF)"* — does not block confirmation. ### `action_confirm` (new shape) ```python def action_confirm(self): 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("Pages 11 & 12 file is required for Separate-file mode.") 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( "Remote-signing request not found. Click 'Request Remote Signature' " "first, or pick a different mode." ) # bundled flag stays False — signature lives in the request's signed_pdf order.with_context(skip_status_validation=True).write(vals) self._post_chatter(order) return {'type': 'ir.actions.act_window_close'} ``` When `intake_mode == 'bundled'`, any pre-existing `x_fc_signed_pages_11_12` from a prior wizard run is left alone (we don't clear it). The bundled flag plus the existing separate file together are harmless — the computed gate is `OR`. ### PDF magic-bytes check ```python def _validate_pdf_bytes(self, b64_data, label): import base64 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 (content check failed).") ``` The existing filename `.pdf` check stays in place as a defence-in-depth `@api.constrains`. ### Chatter message — mode-aware | Mode | Headline | Detail line | |---|---|---| | `bundled` | *Application Received — bundled* | "Pages 11 & 12 included in original PDF" | | `separate` | *Application Received — separate files* | "Original + separate signed pages uploaded" | | `remote` | *Application Received — remote signature pending* | "Page 11 sent for remote signature (`N` request(s) outstanding)" where `N` is the count of `page11_sign_request_ids` in state `sent` or `signed`. | Notes from the wizard, if any, are appended below as today. ## Downstream Consumer Changes These are mechanical: change which field they read. **No logic changes.** | File | Line | Old | New | |---|---|---|---| | [wizard/ready_for_submission_wizard.py:95](../../fusion_claims/wizard/ready_for_submission_wizard.py:95) | `_compute_field_status` | `bool(order.x_fc_original_application and order.x_fc_signed_pages_11_12)` | `bool(order.x_fc_original_application and order.x_fc_has_signed_pages_11_12)` | | [wizard/ready_for_submission_wizard.py:148](../../fusion_claims/wizard/ready_for_submission_wizard.py:148) | gate check | `if not order.x_fc_signed_pages_11_12` | `if not order.x_fc_has_signed_pages_11_12` | | [wizard/case_close_verification_wizard.py](../../fusion_claims/wizard/case_close_verification_wizard.py) | wherever pages-11-12 gate is checked | `x_fc_signed_pages_11_12` | `x_fc_has_signed_pages_11_12` | | [models/sale_order.py:3248](../../fusion_claims/models/sale_order.py:3248) | `x_fc_trail_has_signed_pages` compute | `bool(order.x_fc_signed_pages_11_12)` | `order.x_fc_has_signed_pages_11_12` | The `x_fc_signed_pages_11_12` field stays in the data model. Any download / preview / "open document" button that points at the literal binary stays as-is — bundled-mode orders simply won't have this field populated, and the UI should hide the "Open signed pages" button when the field is empty (it already does — Odoo hides empty binary widgets by default). ## Error / Edge Cases | Scenario | Behaviour | |---|---| | User toggles from `separate` to `bundled` after uploading a separate file | Wizard does not clear the upload field. On confirm, only the original application is written; bundled flag goes to True. The separate-file binary in the wizard is discarded (it was never written). | | User picks `remote` but has no sent/signed request | Block with the message above; user must click *Request Remote Signature* first. | | User picks `bundled` but the PDF is short (e.g. 4 pages) | Page-count indicator shows *"(4 pages)"* as a visual hint, but **does not block**. The 14-page ADP form is the norm but the system can't reliably enforce it across form versions. | | Legacy record without `x_fc_pages_11_12_in_original` set | Defaults to False. As long as `x_fc_signed_pages_11_12` is present, `x_fc_has_signed_pages_11_12` is True — gate still passes. | | Stored compute not populated for legacy rows | Triggered on first read or via a one-line `_recompute` on module load is **not** required — Odoo computes on first access. If users hit issues, a one-off psql `UPDATE` can be run manually. | | Remote signing completes after `bundled` mode was used | `_compute_has_signed_pages_11_12` already ORs in `page11_sign_request_ids.state == 'signed'` — harmless overlap; trail stays correct. | | Uploaded file is not really a PDF (wrong content) | Magic-byte check raises a UserError; record is not changed. | ## Testing ### Unit tests — wizard (`tests/test_application_received_wizard.py`, new) - `test_bundled_mode_marks_received_with_only_original` - `test_separate_mode_requires_signed_pages` - `test_remote_mode_requires_sent_or_signed_request` - `test_invalid_pdf_bytes_rejected` - `test_chatter_message_mentions_intake_mode` ### Unit tests — downstream gates - `test_ready_for_submission_passes_with_bundled_flag` (no `x_fc_signed_pages_11_12` set) - `test_case_close_audit_accepts_bundled_flag` - `test_trail_has_signed_pages_true_when_bundled` ### Manual smoke test on local dev DB ```bash docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims --stop-after-init ``` Then in the UI: 1. Take an order in *Waiting for Application*. 2. Click *Mark Application Received* → pick **Bundled** → upload a single PDF → confirm. 3. Confirm chatter shows the bundled message and `x_fc_pages_11_12_in_original = True`. 4. Click *Mark Ready for Submission* — the document gate should pass. 5. Repeat on another order with **Separate** mode to confirm the old flow still works. 6. Repeat on a third order with **Remote** mode after triggering a signing request. ## Rollout - Bump `version` in [fusion_claims/__manifest__.py](../../fusion_claims/__manifest__.py). - `docker exec odoo-dev-app odoo -d fusion-dev -u fusion_claims --stop-after-init`. - Reload browser with cache clear (per CLAUDE.md asset-bundle-cache rule). - No production deploy steps unique to this change. ## Open Questions (none blocking implementation) - Should bundled-mode capture Page 11 signer identity (signer name, relationship) the way the remote flow does? Currently neither bundled nor separate-file modes do — existing gap, deferred. - Should the bundled-mode chatter automatically attach a one-line note like *"Operator confirms pages 11 & 12 are within the original application"* with the user's name? The default chatter post already records the user. Leaving as-is.