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>
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:
- Original ADP Application (
x_fc_original_application) - 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.requestmodel.
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_originaltest_separate_mode_requires_signed_pagestest_remote_mode_requires_sent_or_signed_requesttest_invalid_pdf_bytes_rejectedtest_chatter_message_mentions_intake_mode
Unit tests — downstream gates
test_ready_for_submission_passes_with_bundled_flag(nox_fc_signed_pages_11_12set)test_case_close_audit_accepts_bundled_flagtest_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:
- Take an order in Waiting for Application.
- Click Mark Application Received → pick Bundled → upload a single PDF → confirm.
- Confirm chatter shows the bundled message and
x_fc_pages_11_12_in_original = True. - Click Mark Ready for Submission — the document gate should pass.
- Repeat on another order with Separate mode to confirm the old flow still works.
- Repeat on a third order with Remote mode after triggering a signing request.
Rollout
- Bump
versionin 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.