Files
Odoo-Modules/docs/superpowers/specs/2026-05-19-adp-application-received-bundled-pages-design.md
gsinghpal 5ff271a7b1 docs(spec): ADP application received — bundled pages 11 & 12 design
Design for refining the Application Received wizard so staff can mark
applications received with a single PDF when pages 11 & 12 are inside
the original application — without losing the existing separate-file
and remote-signing paths.

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

14 KiB

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

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. 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

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) 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

# 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)

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

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 _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 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 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 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

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.
  • 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.