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>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import test_signed_pages_gate
|
from . import test_signed_pages_gate
|
||||||
|
from . import test_application_received_wizard
|
||||||
|
|||||||
191
fusion_claims/tests/test_application_received_wizard.py
Normal file
191
fusion_claims/tests/test_application_received_wizard.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# -*- 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}",
|
||||||
|
)
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2024-2025 Nexa Systems Inc.
|
# Copyright 2024-2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# 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 import models, fields, api, _
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
from markupsafe import Markup
|
|
||||||
import logging
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pdfrw
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
pdfrw = None
|
||||||
|
|
||||||
|
|
||||||
class ApplicationReceivedWizard(models.TransientModel):
|
class ApplicationReceivedWizard(models.TransientModel):
|
||||||
"""Wizard to upload ADP application documents when application is received."""
|
"""Wizard to upload ADP application documents when application is received."""
|
||||||
@@ -21,25 +30,43 @@ class ApplicationReceivedWizard(models.TransientModel):
|
|||||||
required=True,
|
required=True,
|
||||||
readonly=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
|
# Document uploads
|
||||||
original_application = fields.Binary(
|
original_application = fields.Binary(
|
||||||
string='Original ADP Application',
|
string='Original ADP Application',
|
||||||
required=True,
|
required=True,
|
||||||
help='Upload the original ADP application PDF received from the client',
|
help='Upload the original ADP application PDF received from the client',
|
||||||
)
|
)
|
||||||
original_application_filename = fields.Char(
|
original_application_filename = fields.Char(string='Application Filename')
|
||||||
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(
|
signed_pages_11_12 = fields.Binary(
|
||||||
string='Signed Pages 11 & 12',
|
string='Signed Pages 11 & 12',
|
||||||
help='Upload the signed pages 11 and 12 from the application. '
|
help='Upload the signed pages 11 and 12 from the application '
|
||||||
'Not required if a remote signing request has been sent.',
|
'(only used in Separate-file mode).',
|
||||||
)
|
|
||||||
signed_pages_filename = fields.Char(
|
|
||||||
string='Pages Filename',
|
|
||||||
)
|
)
|
||||||
|
signed_pages_filename = fields.Char(string='Pages Filename')
|
||||||
|
|
||||||
has_pending_page11_request = fields.Boolean(
|
has_pending_page11_request = fields.Boolean(
|
||||||
compute='_compute_has_pending_page11_request',
|
compute='_compute_has_pending_page11_request',
|
||||||
@@ -47,12 +74,15 @@ class ApplicationReceivedWizard(models.TransientModel):
|
|||||||
has_signed_page11 = fields.Boolean(
|
has_signed_page11 = fields.Boolean(
|
||||||
compute='_compute_has_pending_page11_request',
|
compute='_compute_has_pending_page11_request',
|
||||||
)
|
)
|
||||||
|
|
||||||
notes = fields.Text(
|
notes = fields.Text(
|
||||||
string='Notes',
|
string='Notes',
|
||||||
help='Any notes about the received application',
|
help='Any notes about the received application',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# COMPUTED
|
||||||
|
# ------------------------------------------------------------------
|
||||||
@api.depends('sale_order_id')
|
@api.depends('sale_order_id')
|
||||||
def _compute_has_pending_page11_request(self):
|
def _compute_has_pending_page11_request(self):
|
||||||
for wiz in self:
|
for wiz in self:
|
||||||
@@ -70,103 +100,136 @@ class ApplicationReceivedWizard(models.TransientModel):
|
|||||||
wiz.has_pending_page11_request = False
|
wiz.has_pending_page11_request = False
|
||||||
wiz.has_signed_page11 = 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:
|
||||||
|
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
|
@api.model
|
||||||
def default_get(self, fields_list):
|
def default_get(self, fields_list):
|
||||||
res = super().default_get(fields_list)
|
res = super().default_get(fields_list)
|
||||||
active_id = self._context.get('active_id')
|
active_id = self._context.get('active_id')
|
||||||
if active_id:
|
if not active_id:
|
||||||
order = self.env['sale.order'].browse(active_id)
|
return res
|
||||||
res['sale_order_id'] = order.id
|
|
||||||
if order.x_fc_original_application:
|
order = self.env['sale.order'].browse(active_id)
|
||||||
res['original_application'] = order.x_fc_original_application
|
res['sale_order_id'] = order.id
|
||||||
res['original_application_filename'] = order.x_fc_original_application_filename
|
|
||||||
if order.x_fc_signed_pages_11_12:
|
if order.x_fc_original_application:
|
||||||
res['signed_pages_11_12'] = order.x_fc_signed_pages_11_12
|
res['original_application'] = order.x_fc_original_application
|
||||||
res['signed_pages_filename'] = order.x_fc_signed_pages_filename
|
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
|
return res
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# CONSTRAINTS (filename defence-in-depth)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
@api.constrains('original_application_filename')
|
@api.constrains('original_application_filename')
|
||||||
def _check_application_file_type(self):
|
def _check_application_file_type(self):
|
||||||
for wizard in self:
|
for wizard in self:
|
||||||
if wizard.original_application_filename:
|
name = wizard.original_application_filename
|
||||||
if not wizard.original_application_filename.lower().endswith('.pdf'):
|
if name and not name.lower().endswith('.pdf'):
|
||||||
raise UserError(
|
raise UserError(
|
||||||
"Original Application must be a PDF file.\n"
|
f"Original Application must be a PDF file.\n"
|
||||||
f"Uploaded file: '{wizard.original_application_filename}'"
|
f"Uploaded file: '{name}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.constrains('signed_pages_filename')
|
@api.constrains('signed_pages_filename')
|
||||||
def _check_pages_file_type(self):
|
def _check_pages_file_type(self):
|
||||||
for wizard in self:
|
for wizard in self:
|
||||||
if wizard.signed_pages_filename:
|
name = wizard.signed_pages_filename
|
||||||
if not wizard.signed_pages_filename.lower().endswith('.pdf'):
|
if name and not name.lower().endswith('.pdf'):
|
||||||
raise UserError(
|
raise UserError(
|
||||||
"Signed Pages 11 & 12 must be a PDF file.\n"
|
f"Signed Pages 11 & 12 must be a PDF file.\n"
|
||||||
f"Uploaded file: '{wizard.signed_pages_filename}'"
|
f"Uploaded file: '{name}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# ACTIONS
|
||||||
|
# ------------------------------------------------------------------
|
||||||
def action_confirm(self):
|
def action_confirm(self):
|
||||||
"""Save documents and mark application as received."""
|
"""Save documents and mark application as received."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
order = self.sale_order_id
|
order = self.sale_order_id
|
||||||
|
|
||||||
if order.x_fc_adp_application_status not in ('assessment_completed', 'waiting_for_application'):
|
if order.x_fc_adp_application_status not in (
|
||||||
raise UserError("Can only receive application from 'Waiting for Application' status.")
|
'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:
|
if not self.original_application:
|
||||||
raise UserError("Please upload the Original ADP Application.")
|
raise UserError("Please upload the Original ADP Application.")
|
||||||
|
|
||||||
page11_covered = bool(
|
self._validate_pdf_bytes(self.original_application, 'Original ADP Application')
|
||||||
self.signed_pages_11_12
|
|
||||||
or order.x_fc_signed_pages_11_12
|
|
||||||
or order.page11_sign_request_ids.filtered(
|
|
||||||
lambda r: r.state in ('sent', 'signed')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not page11_covered:
|
|
||||||
raise UserError(
|
|
||||||
"Signed Pages 11 & 12 are required.\n\n"
|
|
||||||
"You can either upload the file here, or use the "
|
|
||||||
"'Request Page 11 Signature' button on the sale order "
|
|
||||||
"to send it for remote signing before confirming."
|
|
||||||
)
|
|
||||||
|
|
||||||
vals = {
|
vals = {
|
||||||
'x_fc_adp_application_status': 'application_received',
|
'x_fc_adp_application_status': 'application_received',
|
||||||
'x_fc_original_application': self.original_application,
|
'x_fc_original_application': self.original_application,
|
||||||
'x_fc_original_application_filename': self.original_application_filename,
|
'x_fc_original_application_filename': self.original_application_filename,
|
||||||
|
'x_fc_pages_11_12_in_original': (self.intake_mode == 'bundled'),
|
||||||
}
|
}
|
||||||
if self.signed_pages_11_12:
|
|
||||||
vals['x_fc_signed_pages_11_12'] = self.signed_pages_11_12
|
if self.intake_mode == 'separate':
|
||||||
vals['x_fc_signed_pages_filename'] = self.signed_pages_filename
|
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)
|
order.with_context(skip_status_validation=True).write(vals)
|
||||||
|
self._post_chatter(order)
|
||||||
# Post to chatter
|
|
||||||
from datetime import date
|
|
||||||
notes_html = f'<p style="margin: 4px 0 0 0;"><strong>Notes:</strong> {self.notes}</p>' if self.notes else ''
|
|
||||||
|
|
||||||
order.message_post(
|
|
||||||
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"/> Application Received</h4>'
|
|
||||||
f'<p style="margin: 0;"><strong>Date:</strong> {date.today().strftime("%B %d, %Y")}</p>'
|
|
||||||
'<p style="margin: 8px 0 4px 0;"><strong>Documents Uploaded:</strong></p>'
|
|
||||||
'<ul style="margin: 0; padding-left: 20px;">'
|
|
||||||
f'<li><i class="fa fa-check text-success"/> Original ADP Application: {self.original_application_filename}</li>'
|
|
||||||
f'<li><i class="fa fa-check text-success"/> Signed Pages 11 & 12: {self.signed_pages_filename}</li>'
|
|
||||||
'</ul>'
|
|
||||||
f'{notes_html}'
|
|
||||||
'</div>'
|
|
||||||
),
|
|
||||||
message_type='notification',
|
|
||||||
subtype_xmlid='mail.mt_note',
|
|
||||||
)
|
|
||||||
|
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
||||||
def action_request_page11_signature(self):
|
def action_request_page11_signature(self):
|
||||||
"""Open the Page 11 remote signing wizard from within the Application Received wizard."""
|
"""Open the Page 11 remote signing wizard from within this wizard."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
@@ -176,3 +239,66 @@ class ApplicationReceivedWizard(models.TransientModel):
|
|||||||
'target': 'new',
|
'target': 'new',
|
||||||
'context': {'default_sale_order_id': self.sale_order_id.id},
|
'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',
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user