fusion_claims: MOD workflow rework — two-assessment split, 3 submission paths, recovery actions (v19.0.8.0.3)
Reworks the March of Dimes workflow to match reality: the OT does their own disability assessment and provides the VOD letter; our accessibility specialist then visits to produce the proposal/drawings/quote; and the application can be submitted by us (internal), the client, or the authorizer themselves. The old workflow flattened all this into one assessment state with a dead-end funding_denied and no document tracking. Data model (13 new sale.order fields): - 5 new document binaries + filenames: VOD letter, Application Form (filled), Notice of Assessment, Property Tax, Proposal Document - x_fc_mod_submitted_by Selection (internal/client/authorizer) - x_fc_mod_handoff_date, x_fc_mod_vod_requested_date - x_fc_mod_accessibility_specialist_id (m2o res.partner — internal or external) - x_fc_mod_previous_status_before_hold (for proper resume) - x_fc_mod_funding_denial_reason (captured via wizard) Settings (4 res.company fields + res_config_settings mirrors): - x_fc_mod_application_form (blank) + filename - x_fc_mod_vod_form (blank) + filename - x_fc_mod_followup_assignee_mode (office_contact / sales_rep) - x_fc_mod_followup_office_contact_id res.partner: added 'accessibility_specialist' to x_fc_contact_type. State machine: - New state handoff_to_client between quote_submitted and awaiting_funding, used for paths B/C (client or authorizer submits themselves) - Fixed action_mod_on_hold to save x_fc_mod_previous_status_before_hold - Fixed action_mod_resume to restore previous status (was hardcoded to in_production, losing context for cases held earlier) 4 new wizards: - mod_submission_path_wizard — chooses submitted_by, auto-fires VOD request email on first switch to 'internal' - mod_funding_denied_wizard — captures denial category + reason - mod_resubmit_wizard — revises + resubmits denied cases (with optional doc clearing) - mod_submission_confirmed_wizard — records client/authorizer confirmed submission, advances to awaiting_funding 8 new action methods: - action_mod_set_submission_path, action_mod_request_vod, action_mod_handoff_to_client (validates docs, fires handoff email), action_mod_confirmed_submission, action_mod_resubmit_from_denied, action_mod_cancel_from_denied, action_mod_reopen_cancelled - action_mod_funding_denied now opens the denial wizard 3 new email methods + 2 existing fixes: - _send_mod_vod_request_email — auto-attaches blank VOD form from company settings, sent to authorizer when we are handling submission - _send_mod_handoff_email — two templates (client vs authorizer), attaches proposal + drawing + blank MOD Application Form - _mod_company_attachment helper for building attachments from company Binary - Fixed _send_mod_assessment_completed_email to include authorizer - Fixed _send_mod_pod_submitted_email to include client New cron: - _cron_mod_handoff_followup (daily 09:00) — creates mail.activity for office to confirm MOD submission. Assignee via company setting (office contact or sales rep). Uses existing rolling-window cap (2/month per order). Views: - sale_order form: new status-bar buttons (set path, request VOD, handoff, confirm, resubmit, cancel, reopen), new document section in MOD Documents tab with submission-path tracking, denial details, hold history - res_config_settings: new MOD blank forms upload + assignee config Deployed to odoo-westin (westin-v19) and odoo-mobility (mobility). Pre-deploy FK cleanup from earlier session means mobility updated cleanly without workaround. HTTP 200 on both, cron verified active, all new fields present.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Claims',
|
||||
'version': '19.0.8.0.2',
|
||||
'version': '19.0.8.0.3',
|
||||
'category': 'Sales',
|
||||
'summary': 'Complete ADP Claims Management with Dashboard, Sales Integration, Billing Automation, and Two-Stage Verification.',
|
||||
'description': """
|
||||
@@ -122,6 +122,10 @@
|
||||
'wizard/mod_awaiting_funding_wizard_views.xml',
|
||||
'wizard/mod_funding_approved_wizard_views.xml',
|
||||
'wizard/mod_pca_received_wizard_views.xml',
|
||||
'wizard/mod_submission_path_wizard_views.xml',
|
||||
'wizard/mod_funding_denied_wizard_views.xml',
|
||||
'wizard/mod_resubmit_wizard_views.xml',
|
||||
'wizard/mod_submission_confirmed_wizard_views.xml',
|
||||
'wizard/odsp_sa_mobility_wizard_views.xml',
|
||||
'wizard/odsp_discretionary_wizard_views.xml',
|
||||
'wizard/odsp_submit_to_odsp_wizard_views.xml',
|
||||
|
||||
@@ -136,6 +136,22 @@
|
||||
<field name="nextcall" eval="DateTime.now().replace(hour=2, minute=0, second=0)"/>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: MOD Handoff Follow-up (2026-04 update)
|
||||
Creates a mail.activity for the office to call the client or
|
||||
authorizer to confirm they have submitted the MOD application.
|
||||
Uses the rolling-window cap (shared with existing MOD follow-up)
|
||||
so activities are spaced out, max 2 per 30-day window per order. -->
|
||||
<record id="ir_cron_mod_handoff_followup" model="ir.cron">
|
||||
<field name="name">Fusion Claims: MOD Handoff Follow-up</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_mod_handoff_followup()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
<field name="nextcall" eval="DateTime.now().replace(hour=9, minute=0, second=0)"/>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: ADP 12-month auto-expire for approved applications.
|
||||
Approved / approved_deduction orders past the configured funding
|
||||
window (fusion_claims.adp_approval_expiry_months, default 12 months)
|
||||
|
||||
@@ -62,8 +62,63 @@ class ResCompany(models.Model):
|
||||
help='Contacts who will receive a copy (CC) of all automated ADP notifications',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# MARCH OF DIMES SETTINGS (shared blank forms + follow-up assignment)
|
||||
# Added 2026-04 MOD update. Latest revision of the MOD-issued blank forms
|
||||
# lives here so every case can auto-attach them to outgoing emails.
|
||||
# ==========================================================================
|
||||
x_fc_mod_application_form = fields.Binary(
|
||||
string='MOD Application Form (blank)',
|
||||
attachment=True,
|
||||
help='Blank March of Dimes Application Form (latest MOD revision). '
|
||||
'Auto-attached to the handoff email sent to the client or authorizer '
|
||||
'when they will be submitting the application themselves.',
|
||||
)
|
||||
x_fc_mod_application_form_filename = fields.Char(
|
||||
string='MOD Application Form Filename',
|
||||
)
|
||||
x_fc_mod_vod_form = fields.Binary(
|
||||
string='Verification of Disability Form (blank)',
|
||||
attachment=True,
|
||||
help='Blank March of Dimes Verification of Disability form. '
|
||||
'Auto-attached to the VOD request email sent to the authorizer when '
|
||||
'our office is handling the MOD submission internally.',
|
||||
)
|
||||
x_fc_mod_vod_form_filename = fields.Char(
|
||||
string='VOD Form Filename',
|
||||
)
|
||||
x_fc_mod_followup_assignee_mode = fields.Selection(
|
||||
selection=[
|
||||
('office_contact', 'Designated Office Contact'),
|
||||
('sales_rep', 'Order Sales Rep'),
|
||||
],
|
||||
string='MOD Follow-up Assignee',
|
||||
default='office_contact',
|
||||
help='Who gets the follow-up activity when a client or authorizer is '
|
||||
'handling the MOD submission themselves. Office contact = one '
|
||||
'designated person handles all follow-ups. Sales rep = the rep '
|
||||
'on each order is responsible for their own follow-ups.',
|
||||
)
|
||||
x_fc_mod_followup_office_contact_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='MOD Follow-up Office Contact',
|
||||
help='The office user who receives MOD handoff follow-up activities '
|
||||
'when assignee mode is set to Office Contact. Only used when '
|
||||
'x_fc_mod_followup_assignee_mode == office_contact.',
|
||||
)
|
||||
|
||||
def _get_cheque_payable_name(self):
|
||||
"""Get the name for cheque payments, defaulting to company name."""
|
||||
self.ensure_one()
|
||||
return self.x_fc_cheque_payable_to or self.name
|
||||
|
||||
def _get_mod_followup_assignee(self, sale_order):
|
||||
"""Return the res.users who should receive the MOD handoff follow-up
|
||||
activity for the given order, based on this company's configuration.
|
||||
Falls back to the order's sales rep when the office contact is missing.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.x_fc_mod_followup_assignee_mode == 'office_contact' and self.x_fc_mod_followup_office_contact_id:
|
||||
return self.x_fc_mod_followup_office_contact_id
|
||||
return sale_order.user_id or self.env.user
|
||||
|
||||
|
||||
@@ -59,6 +59,38 @@ class ResConfigSettings(models.TransientModel):
|
||||
string='Refund Policy',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# MARCH OF DIMES SETTINGS (added 2026-04 MOD workflow update)
|
||||
# =========================================================================
|
||||
fc_mod_application_form = fields.Binary(
|
||||
related='company_id.x_fc_mod_application_form',
|
||||
readonly=False,
|
||||
string='MOD Application Form (blank)',
|
||||
)
|
||||
fc_mod_application_form_filename = fields.Char(
|
||||
related='company_id.x_fc_mod_application_form_filename',
|
||||
readonly=False,
|
||||
)
|
||||
fc_mod_vod_form = fields.Binary(
|
||||
related='company_id.x_fc_mod_vod_form',
|
||||
readonly=False,
|
||||
string='Verification of Disability Form (blank)',
|
||||
)
|
||||
fc_mod_vod_form_filename = fields.Char(
|
||||
related='company_id.x_fc_mod_vod_form_filename',
|
||||
readonly=False,
|
||||
)
|
||||
fc_mod_followup_assignee_mode = fields.Selection(
|
||||
related='company_id.x_fc_mod_followup_assignee_mode',
|
||||
readonly=False,
|
||||
string='MOD Follow-up Assignee',
|
||||
)
|
||||
fc_mod_followup_office_contact_id = fields.Many2one(
|
||||
related='company_id.x_fc_mod_followup_office_contact_id',
|
||||
readonly=False,
|
||||
string='Office Follow-up Contact',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ADP BILLING SETTINGS
|
||||
# =========================================================================
|
||||
|
||||
@@ -26,6 +26,7 @@ class ResPartner(models.Model):
|
||||
('muscular_dystrophy', 'Muscular Dystrophy'),
|
||||
('occupational_therapist', 'Occupational Therapist'),
|
||||
('physiotherapist', 'Physiotherapist'),
|
||||
('accessibility_specialist', 'Accessibility Specialist'),
|
||||
('vendor', 'Vendor'),
|
||||
('funding_agency', 'Funding Agency'),
|
||||
('government_agency', 'Government Agency'),
|
||||
|
||||
@@ -432,6 +432,7 @@ class SaleOrder(models.Model):
|
||||
('contract_received', 'PCA Received'),
|
||||
('in_production', 'In Production'),
|
||||
('project_complete', 'Complete'),
|
||||
('handoff_to_client', 'Handed Off (Client/Authorizer Submitting)'),
|
||||
('pod_submitted', 'POD Sent'),
|
||||
('case_closed', 'Closed'),
|
||||
('on_hold', 'On Hold'),
|
||||
@@ -441,7 +442,9 @@ class SaleOrder(models.Model):
|
||||
default='need_to_schedule',
|
||||
tracking=True,
|
||||
group_expand='_expand_mod_statuses',
|
||||
help='March of Dimes case workflow status',
|
||||
help='March of Dimes case workflow status. handoff_to_client means we '
|
||||
'have given the proposal+quote+drawing to the client or authorizer '
|
||||
'and are waiting for them to submit the application to MOD.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
@@ -598,6 +601,108 @@ class SaleOrder(models.Model):
|
||||
)
|
||||
x_fc_mod_completion_photos_filename = fields.Char(string='Completion Photos Filename')
|
||||
|
||||
# ==========================================================================
|
||||
# MOD DOCUMENTS — added 2026-04 workflow update
|
||||
# Captures the full application package when WE submit to MOD on behalf
|
||||
# of the client. The first 4 are required for internal submission; the
|
||||
# proposal is the narrative document separate from the generated quotation PDF.
|
||||
# ==========================================================================
|
||||
x_fc_mod_vod_letter = fields.Binary(
|
||||
string='Verification of Disability Letter',
|
||||
attachment=True,
|
||||
copy=False,
|
||||
help='OT-signed Verification of Disability letter. Either on the OT\'s '
|
||||
'letterhead or on the MOD-issued VOD form. Required before internal '
|
||||
'submission to MOD.',
|
||||
)
|
||||
x_fc_mod_vod_letter_filename = fields.Char(string='VOD Letter Filename', copy=False)
|
||||
|
||||
x_fc_mod_application_form_doc = fields.Binary(
|
||||
string='MOD Application Form (filled)',
|
||||
attachment=True,
|
||||
copy=False,
|
||||
help='The filled-out and signed March of Dimes application form.',
|
||||
)
|
||||
x_fc_mod_application_form_doc_filename = fields.Char(string='Application Form Filename', copy=False)
|
||||
|
||||
x_fc_mod_notice_of_assessment = fields.Binary(
|
||||
string='Notice of Assessment',
|
||||
attachment=True,
|
||||
copy=False,
|
||||
help='CRA Notice of Assessment — required by MOD as proof of income.',
|
||||
)
|
||||
x_fc_mod_notice_of_assessment_filename = fields.Char(string='Notice of Assessment Filename', copy=False)
|
||||
|
||||
x_fc_mod_property_tax = fields.Binary(
|
||||
string='Property Tax Bill',
|
||||
attachment=True,
|
||||
copy=False,
|
||||
help='Property tax bill — required by MOD as proof of residency.',
|
||||
)
|
||||
x_fc_mod_property_tax_filename = fields.Char(string='Property Tax Filename', copy=False)
|
||||
|
||||
x_fc_mod_proposal_doc = fields.Binary(
|
||||
string='Proposal Document',
|
||||
attachment=True,
|
||||
copy=False,
|
||||
help='Our narrative proposal document describing the accessibility project. '
|
||||
'Separate from the generated quotation PDF.',
|
||||
)
|
||||
x_fc_mod_proposal_doc_filename = fields.Char(string='Proposal Document Filename', copy=False)
|
||||
|
||||
# ==========================================================================
|
||||
# MOD SUBMISSION PATH — who is submitting the application to March of Dimes?
|
||||
# ==========================================================================
|
||||
x_fc_mod_submitted_by = fields.Selection(
|
||||
selection=[
|
||||
('internal', 'We submit on client\'s behalf'),
|
||||
('client', 'Client submits themselves'),
|
||||
('authorizer', 'Authorizer (OT) submits'),
|
||||
],
|
||||
string='Application Submitted By',
|
||||
copy=False,
|
||||
tracking=True,
|
||||
help='Who is submitting the MOD application. Determines which document '
|
||||
'gates apply and whether we hand off materials to the client/OT.',
|
||||
)
|
||||
x_fc_mod_handoff_date = fields.Date(
|
||||
string='Handoff Date',
|
||||
copy=False,
|
||||
tracking=True,
|
||||
help='Date we handed the application package to the client or authorizer '
|
||||
'for them to submit to MOD themselves.',
|
||||
)
|
||||
# NOTE: x_fc_mod_application_submitted_date already exists at line ~865 —
|
||||
# we reuse it for the "MOD actually received the application" date across
|
||||
# all 3 submission paths (internal/client/authorizer).
|
||||
x_fc_mod_accessibility_specialist_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Accessibility Specialist',
|
||||
tracking=True,
|
||||
help='Internal employee or external contractor who performed the '
|
||||
'accessibility assessment visit. Can be a contact tagged as '
|
||||
'"Accessibility Specialist" in the contact type.',
|
||||
)
|
||||
x_fc_mod_vod_requested_date = fields.Date(
|
||||
string='VOD Request Sent',
|
||||
copy=False,
|
||||
help='Date we auto-emailed the authorizer the blank VOD form to fill out. '
|
||||
'Set by action_mod_request_vod.',
|
||||
)
|
||||
x_fc_mod_previous_status_before_hold = fields.Char(
|
||||
string='MOD Status Before Hold',
|
||||
copy=False,
|
||||
help='Previous MOD status before the case was put on hold. Used by '
|
||||
'action_mod_resume to restore the correct state.',
|
||||
)
|
||||
x_fc_mod_funding_denial_reason = fields.Text(
|
||||
string='Funding Denial Reason',
|
||||
copy=False,
|
||||
tracking=True,
|
||||
help='Reason provided when MOD denied funding for this case. Captured '
|
||||
'by mod_funding_denied_wizard.',
|
||||
)
|
||||
|
||||
# Trail computed fields for MOD documents
|
||||
x_fc_mod_trail_has_drawing = fields.Boolean(compute='_compute_mod_trail', string='Has Drawing')
|
||||
x_fc_mod_trail_has_initial_photos = fields.Boolean(compute='_compute_mod_trail', string='Has Initial Photos')
|
||||
@@ -7540,8 +7645,21 @@ class SaleOrder(models.Model):
|
||||
}
|
||||
|
||||
def action_mod_funding_denied(self):
|
||||
"""Open the funding denied wizard to capture denial reason + category.
|
||||
|
||||
2026-04 MOD update — was previously a bare write with no reason
|
||||
capture. The wizard records the category (income/residency/scope/etc),
|
||||
free-text reason, and fires the existing funding_denied email.
|
||||
"""
|
||||
self.ensure_one()
|
||||
self.write({'x_fc_mod_status': 'funding_denied'})
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'MOD Funding Denied',
|
||||
'res_model': 'fusion_claims.mod.funding.denied.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'active_id': self.id},
|
||||
}
|
||||
|
||||
def action_mod_contract_received(self):
|
||||
"""Open wizard to upload PCA document and record receipt."""
|
||||
@@ -7593,13 +7711,178 @@ class SaleOrder(models.Model):
|
||||
})
|
||||
|
||||
def action_mod_on_hold(self):
|
||||
"""Put the MOD case on hold, remembering the previous state for resume.
|
||||
|
||||
2026-04 fix — previously lost the previous state so action_mod_resume
|
||||
was hardcoded to in_production. Now saves the current state and
|
||||
action_mod_resume restores it properly.
|
||||
"""
|
||||
self.ensure_one()
|
||||
self.write({'x_fc_mod_status': 'on_hold'})
|
||||
if self.x_fc_mod_status in ('on_hold', 'case_closed', 'cancelled'):
|
||||
raise UserError(_(
|
||||
"This case cannot be put on hold from its current status (%s)."
|
||||
) % self.x_fc_mod_status)
|
||||
self.write({
|
||||
'x_fc_mod_status': 'on_hold',
|
||||
'x_fc_mod_previous_status_before_hold': self.x_fc_mod_status,
|
||||
})
|
||||
|
||||
def action_mod_resume(self):
|
||||
"""Resume from on_hold - go back to in_production."""
|
||||
"""Resume from on_hold — restore the previous status.
|
||||
|
||||
2026-04 fix — previously hardcoded to 'in_production' which was
|
||||
destructive for cases that were held earlier in the workflow. Now
|
||||
uses x_fc_mod_previous_status_before_hold to restore the exact
|
||||
state the case was in before the hold.
|
||||
"""
|
||||
self.ensure_one()
|
||||
self.write({'x_fc_mod_status': 'in_production'})
|
||||
if self.x_fc_mod_status != 'on_hold':
|
||||
raise UserError(_("Only held cases can be resumed."))
|
||||
previous = self.x_fc_mod_previous_status_before_hold or 'in_production'
|
||||
self.write({
|
||||
'x_fc_mod_status': previous,
|
||||
'x_fc_mod_previous_status_before_hold': False,
|
||||
})
|
||||
labels = dict(self._fields['x_fc_mod_status'].selection)
|
||||
self.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-success" role="alert">'
|
||||
'<strong><i class="fa fa-play-circle"></i> Case resumed</strong> — '
|
||||
f'restored to <strong>{labels.get(previous, previous)}</strong>'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# MOD 2026-04 UPDATE — new submission-path actions + recovery actions
|
||||
# ==========================================================================
|
||||
|
||||
def action_mod_set_submission_path(self):
|
||||
"""Open the wizard asking who will submit the MOD application."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Set MOD Submission Path',
|
||||
'res_model': 'fusion_claims.mod.submission.path.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'active_id': self.id},
|
||||
}
|
||||
|
||||
def action_mod_request_vod(self):
|
||||
"""Email the authorizer the blank VOD form to complete + return.
|
||||
|
||||
Fired automatically the first time submitted_by is set to 'internal',
|
||||
and also available as a manual button for follow-up requests.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.x_fc_authorizer_id or not self.x_fc_authorizer_id.email:
|
||||
raise UserError(_(
|
||||
"No authorizer with an email address is set on this order. "
|
||||
"Please assign an authorizer before requesting the VOD form."
|
||||
))
|
||||
self._send_mod_vod_request_email()
|
||||
return True
|
||||
|
||||
def action_mod_handoff_to_client(self):
|
||||
"""Move a MOD order to 'handoff_to_client' and email the package
|
||||
to whoever is going to submit (client or authorizer).
|
||||
|
||||
Requires x_fc_mod_submitted_by in ('client', 'authorizer') and the
|
||||
proposal + drawing + quotation to be present. The blank MOD
|
||||
application form from company settings is attached to the email
|
||||
so the submitter has everything in one place.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.x_fc_mod_submitted_by not in ('client', 'authorizer'):
|
||||
raise UserError(_(
|
||||
"Handoff is only valid when submitted_by is 'client' or 'authorizer'. "
|
||||
"Use 'Set MOD Submission Path' to choose first."
|
||||
))
|
||||
missing = []
|
||||
if not self.x_fc_mod_proposal_doc:
|
||||
missing.append(_("Proposal Document"))
|
||||
if not self.x_fc_mod_drawing:
|
||||
missing.append(_("Drawing"))
|
||||
if missing:
|
||||
raise UserError(_(
|
||||
"Cannot hand off without: %s. Upload the missing documents "
|
||||
"on the MOD Documents section of the order."
|
||||
) % ", ".join(missing))
|
||||
|
||||
self.with_context(skip_status_emails=True).write({
|
||||
'x_fc_mod_status': 'handoff_to_client',
|
||||
'x_fc_mod_handoff_date': fields.Date.today(),
|
||||
})
|
||||
try:
|
||||
self._send_mod_handoff_email()
|
||||
except Exception as e:
|
||||
_logger.error(f"action_mod_handoff_to_client: email failed for {self.name}: {e}")
|
||||
return True
|
||||
|
||||
def action_mod_confirmed_submission(self):
|
||||
"""Open wizard to record that client/authorizer submitted the app."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Confirm MOD Submission',
|
||||
'res_model': 'fusion_claims.mod.submission.confirmed.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'active_id': self.id},
|
||||
}
|
||||
|
||||
def action_mod_resubmit_from_denied(self):
|
||||
"""Open the resubmit wizard to revise + resubmit a denied case."""
|
||||
self.ensure_one()
|
||||
if self.x_fc_mod_status != 'funding_denied':
|
||||
raise UserError(_("This action is only available for denied cases."))
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Resubmit to MOD',
|
||||
'res_model': 'fusion_claims.mod.resubmit.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'active_id': self.id},
|
||||
}
|
||||
|
||||
def action_mod_cancel_from_denied(self):
|
||||
"""Cancel a denied MOD case."""
|
||||
self.ensure_one()
|
||||
if self.x_fc_mod_status != 'funding_denied':
|
||||
raise UserError(_("This action is only available for denied cases."))
|
||||
self.write({'x_fc_mod_status': 'cancelled'})
|
||||
self.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-secondary" role="alert">'
|
||||
'<strong><i class="fa fa-times"></i> MOD case cancelled after funding denial.</strong>'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def action_mod_reopen_cancelled(self):
|
||||
"""Reopen a cancelled MOD case back to the entry state."""
|
||||
self.ensure_one()
|
||||
if self.x_fc_mod_status != 'cancelled':
|
||||
raise UserError(_("This action is only available for cancelled cases."))
|
||||
self.write({
|
||||
'x_fc_mod_status': 'need_to_schedule',
|
||||
'x_fc_mod_funding_denial_reason': False,
|
||||
})
|
||||
self.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-info" role="alert">'
|
||||
'<strong><i class="fa fa-refresh"></i> MOD case reopened.</strong> '
|
||||
'Status reset to "Need to Schedule Assessment".'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def action_cancel(self):
|
||||
"""Override: also set MOD status to cancelled when order is cancelled."""
|
||||
@@ -7870,7 +8153,12 @@ class SaleOrder(models.Model):
|
||||
)
|
||||
|
||||
def _send_mod_assessment_completed_email(self):
|
||||
"""Email: Assessment completed. To: Client."""
|
||||
"""Email: Accessibility assessment completed. Client + Authorizer.
|
||||
|
||||
2026-04 email audit fix — authorizer was previously excluded. They
|
||||
need to know our accessibility specialist has finished the visit so
|
||||
they can expect the proposal/drawings for their VOD letter context.
|
||||
"""
|
||||
self.ensure_one()
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
return self._send_mod_email(
|
||||
@@ -7878,7 +8166,7 @@ class SaleOrder(models.Model):
|
||||
title='Assessment Completed',
|
||||
summary=f'The accessibility assessment for <strong>{client_name}</strong> has been completed.',
|
||||
email_type='success',
|
||||
include_client=True, include_authorizer=False,
|
||||
include_client=True, include_authorizer=True,
|
||||
note='<strong>Next steps:</strong> Our team is now preparing the drawings and quotation '
|
||||
'based on the assessment. We will send you the proposal once it is ready for review.',
|
||||
note_color='#38a169',
|
||||
@@ -8003,7 +8291,12 @@ class SaleOrder(models.Model):
|
||||
)
|
||||
|
||||
def _send_mod_pod_submitted_email(self):
|
||||
"""Email: Photos/POD submitted to MOD. To: MOD contact, CC: Authorizer."""
|
||||
"""Email: Photos/POD submitted to MOD. Client + Authorizer + MOD contact.
|
||||
|
||||
2026-04 email audit fix — client was previously excluded. POD
|
||||
submission is a near-delivery milestone and the client should know
|
||||
their case is wrapping up.
|
||||
"""
|
||||
self.ensure_one()
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
return self._send_mod_email(
|
||||
@@ -8011,7 +8304,7 @@ class SaleOrder(models.Model):
|
||||
title='Proof of Delivery Submitted',
|
||||
summary=f'Photos and proof of delivery for <strong>{client_name}</strong> have been submitted.',
|
||||
email_type='info',
|
||||
include_client=False, include_authorizer=True, include_mod_contact=True,
|
||||
include_client=True, include_authorizer=True, include_mod_contact=True,
|
||||
note='Please process the final payment (10%) as per the Payment Commitment Agreement terms.',
|
||||
)
|
||||
|
||||
@@ -8046,6 +8339,313 @@ class SaleOrder(models.Model):
|
||||
'If you experience any issues, please contact us immediately.',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# MOD 2026-04 UPDATE — new email methods for submission-path workflow
|
||||
# ==========================================================================
|
||||
|
||||
def _mod_company_attachment(self, field_name, filename_field, default_name):
|
||||
"""Build an ir.attachment record from a Binary on res.company so it
|
||||
can be attached to an outgoing mail.mail. Returns the attachment ID
|
||||
list (empty if the company field is not set).
|
||||
"""
|
||||
self.ensure_one()
|
||||
company = self.company_id or self.env.company
|
||||
data = getattr(company, field_name, False)
|
||||
if not data:
|
||||
return []
|
||||
filename = getattr(company, filename_field, False) or default_name
|
||||
attachment = self.env['ir.attachment'].sudo().create({
|
||||
'name': filename,
|
||||
'datas': data,
|
||||
'res_model': 'sale.order',
|
||||
'res_id': self.id,
|
||||
'type': 'binary',
|
||||
})
|
||||
return [attachment.id]
|
||||
|
||||
def _send_mod_vod_request_email(self):
|
||||
"""Email the authorizer the blank VOD form for them to complete.
|
||||
|
||||
Fires when submitted_by is first set to 'internal' (via the submission
|
||||
path wizard) and also available as a manual button (action_mod_request_vod).
|
||||
Attaches the blank VOD form from company settings.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self._is_email_notifications_enabled():
|
||||
return False
|
||||
authorizer = self.x_fc_authorizer_id
|
||||
if not authorizer or not authorizer.email:
|
||||
_logger.warning(f"_send_mod_vod_request_email: no authorizer email for {self.name}")
|
||||
return False
|
||||
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
sales_rep = self.user_id
|
||||
sender_name = sales_rep.name if sales_rep else self.env.user.name
|
||||
|
||||
body_html = self._mod_email_build(
|
||||
title='Verification of Disability Needed',
|
||||
summary=(
|
||||
f'Our office is handling the March of Dimes application for '
|
||||
f'<strong>{client_name}</strong> on their behalf. To complete '
|
||||
f'the submission we need the <strong>Verification of Disability</strong> '
|
||||
f'signed by you.'
|
||||
),
|
||||
email_type='attention',
|
||||
sections=[('Case Details', self._build_mod_case_detail_rows())],
|
||||
note=(
|
||||
'<strong>What we need:</strong> Please complete the attached '
|
||||
'blank Verification of Disability form (or provide the same '
|
||||
'information on your own letterhead) and send it back to us so '
|
||||
'we can finalise the application submission to March of Dimes.'
|
||||
),
|
||||
note_color='#d69e2e',
|
||||
sender_name=sender_name,
|
||||
)
|
||||
attachment_ids = self._mod_company_attachment(
|
||||
'x_fc_mod_vod_form',
|
||||
'x_fc_mod_vod_form_filename',
|
||||
'Verification_of_Disability_Form.pdf',
|
||||
)
|
||||
cc = []
|
||||
if sales_rep and sales_rep.email:
|
||||
cc.append(sales_rep.email)
|
||||
try:
|
||||
mail_vals = {
|
||||
'subject': f'Verification of Disability needed - {client_name} - {self.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': authorizer.email,
|
||||
'email_cc': ', '.join(cc),
|
||||
'model': 'sale.order',
|
||||
'res_id': self.id,
|
||||
}
|
||||
if attachment_ids:
|
||||
mail_vals['attachment_ids'] = [(6, 0, attachment_ids)]
|
||||
self.env['mail.mail'].sudo().create(mail_vals).send()
|
||||
self.with_context(skip_all_validations=True).write({
|
||||
'x_fc_mod_vod_requested_date': fields.Date.today(),
|
||||
})
|
||||
self._email_chatter_log(
|
||||
'VOD request email sent to authorizer',
|
||||
authorizer.email,
|
||||
', '.join(cc) if cc else None,
|
||||
['VOD form attached' if attachment_ids else 'No VOD form configured on company'],
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send MOD VOD request for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
def _send_mod_handoff_email(self):
|
||||
"""Email the MOD application package to whoever is submitting it
|
||||
(client OR authorizer). Different subject/body depending on which
|
||||
party. Attaches proposal + drawing + blank MOD Application Form.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self._is_email_notifications_enabled():
|
||||
return False
|
||||
if self.x_fc_mod_submitted_by not in ('client', 'authorizer'):
|
||||
return False
|
||||
|
||||
client = self.partner_id
|
||||
authorizer = self.x_fc_authorizer_id
|
||||
sales_rep = self.user_id
|
||||
client_name = client.name or 'Client'
|
||||
sender_name = sales_rep.name if sales_rep else self.env.user.name
|
||||
|
||||
if self.x_fc_mod_submitted_by == 'client':
|
||||
primary = client
|
||||
subject_prefix = 'Your March of Dimes Application Package'
|
||||
title = 'Your MOD Application Package'
|
||||
summary = (
|
||||
f'Hi {client_name}, please find attached everything you need to '
|
||||
f'submit your application to March of Dimes: the proposal, drawings, '
|
||||
f'quotation, and the blank MOD application form.'
|
||||
)
|
||||
note = (
|
||||
'<strong>Next steps:</strong><ol>'
|
||||
'<li>Review the proposal and quotation.</li>'
|
||||
'<li>Complete the attached MOD Application Form.</li>'
|
||||
'<li>Submit the completed package to March of Dimes.</li>'
|
||||
'<li><strong>Call us</strong> once you have submitted so we can track the progress.</li>'
|
||||
'</ol>'
|
||||
)
|
||||
cc_partners = [authorizer] if authorizer and authorizer.email else []
|
||||
else:
|
||||
primary = authorizer
|
||||
subject_prefix = f'MOD Application Package for {client_name}'
|
||||
title = f'MOD Application Package — {client_name}'
|
||||
summary = (
|
||||
f'Please find attached the full application package for '
|
||||
f'<strong>{client_name}</strong>: proposal, drawings, quotation, '
|
||||
f'and the blank MOD application form. You are handling the '
|
||||
f'submission to March of Dimes on behalf of your client.'
|
||||
)
|
||||
note = (
|
||||
'<strong>Please confirm with us</strong> once you have submitted '
|
||||
'the application so we can advance the case in our system.'
|
||||
)
|
||||
cc_partners = [client] if client and client.email else []
|
||||
|
||||
if not primary or not primary.email:
|
||||
_logger.warning(
|
||||
f"_send_mod_handoff_email: no email for {self.x_fc_mod_submitted_by} on {self.name}"
|
||||
)
|
||||
return False
|
||||
|
||||
body_html = self._mod_email_build(
|
||||
title=title,
|
||||
summary=summary,
|
||||
email_type='info',
|
||||
sections=[('Case Details', self._build_mod_case_detail_rows())],
|
||||
note=note,
|
||||
sender_name=sender_name,
|
||||
)
|
||||
|
||||
# Build attachments: our proposal + drawing + blank MOD Application Form.
|
||||
attachment_ids = []
|
||||
if self.x_fc_mod_proposal_doc:
|
||||
att = self.env['ir.attachment'].sudo().create({
|
||||
'name': self.x_fc_mod_proposal_doc_filename or 'Proposal.pdf',
|
||||
'datas': self.x_fc_mod_proposal_doc,
|
||||
'res_model': 'sale.order',
|
||||
'res_id': self.id,
|
||||
'type': 'binary',
|
||||
})
|
||||
attachment_ids.append(att.id)
|
||||
if self.x_fc_mod_drawing:
|
||||
att = self.env['ir.attachment'].sudo().create({
|
||||
'name': self.x_fc_mod_drawing_filename or 'Drawing.pdf',
|
||||
'datas': self.x_fc_mod_drawing,
|
||||
'res_model': 'sale.order',
|
||||
'res_id': self.id,
|
||||
'type': 'binary',
|
||||
})
|
||||
attachment_ids.append(att.id)
|
||||
attachment_ids.extend(self._mod_company_attachment(
|
||||
'x_fc_mod_application_form',
|
||||
'x_fc_mod_application_form_filename',
|
||||
'MOD_Application_Form.pdf',
|
||||
))
|
||||
|
||||
cc_emails = [p.email for p in cc_partners if p and p.email]
|
||||
if sales_rep and sales_rep.email and sales_rep.email not in cc_emails:
|
||||
cc_emails.append(sales_rep.email)
|
||||
try:
|
||||
mail_vals = {
|
||||
'subject': f'{subject_prefix} - {self.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': primary.email,
|
||||
'email_cc': ', '.join(cc_emails),
|
||||
'model': 'sale.order',
|
||||
'res_id': self.id,
|
||||
}
|
||||
if attachment_ids:
|
||||
mail_vals['attachment_ids'] = [(6, 0, attachment_ids)]
|
||||
self.env['mail.mail'].sudo().create(mail_vals).send()
|
||||
self._email_chatter_log(
|
||||
f'MOD handoff email sent ({self.x_fc_mod_submitted_by})',
|
||||
primary.email,
|
||||
', '.join(cc_emails) if cc_emails else None,
|
||||
[f'{len(attachment_ids)} attachment(s)'],
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send MOD handoff email for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
def _cron_mod_handoff_followup(self):
|
||||
"""Daily cron: create mail.activity follow-ups for MOD orders in
|
||||
handoff_to_client state so the office remembers to call the client
|
||||
and confirm whether they have submitted the application to MOD yet.
|
||||
|
||||
Assignee comes from company settings:
|
||||
- x_fc_mod_followup_assignee_mode == 'office_contact' → the
|
||||
designated office user
|
||||
- x_fc_mod_followup_assignee_mode == 'sales_rep' → the order's
|
||||
sales rep
|
||||
|
||||
Reuses the existing rolling-window cap logic via
|
||||
x_fc_mod_followup_month_count so the office doesn't get spammed
|
||||
with daily activities.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
try:
|
||||
max_per_month = int(ICP.get_param('fusion_claims.mod_followup_max_per_month', '2'))
|
||||
except (TypeError, ValueError):
|
||||
max_per_month = 2
|
||||
try:
|
||||
window_days = int(ICP.get_param('fusion_claims.mod_followup_window_days', '30'))
|
||||
except (TypeError, ValueError):
|
||||
window_days = 30
|
||||
|
||||
activity_type = self.env.ref(
|
||||
'fusion_claims.mail_activity_type_mod_followup',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if not activity_type:
|
||||
return
|
||||
|
||||
today = fields.Date.today()
|
||||
orders = self.search([
|
||||
('x_fc_sale_type', '=', 'march_of_dimes'),
|
||||
('x_fc_mod_status', '=', 'handoff_to_client'),
|
||||
])
|
||||
for order in orders:
|
||||
try:
|
||||
# Respect the existing rolling-window cap (shared with the
|
||||
# existing MOD follow-up cron).
|
||||
start = order.x_fc_mod_followup_month_start
|
||||
count = order.x_fc_mod_followup_month_count or 0
|
||||
if start and (today - start).days < window_days and count >= max_per_month:
|
||||
continue
|
||||
if not start or (today - start).days >= window_days:
|
||||
order.with_context(skip_all_validations=True).write({
|
||||
'x_fc_mod_followup_month_start': today,
|
||||
'x_fc_mod_followup_month_count': 0,
|
||||
})
|
||||
count = 0
|
||||
|
||||
# Don't double-schedule — skip if an open activity of our type
|
||||
# already exists.
|
||||
existing = self.env['mail.activity'].search([
|
||||
('res_model', '=', 'sale.order'),
|
||||
('res_id', '=', order.id),
|
||||
('activity_type_id', '=', activity_type.id),
|
||||
], limit=1)
|
||||
if existing:
|
||||
continue
|
||||
|
||||
company = order.company_id or self.env.company
|
||||
assignee = company._get_mod_followup_assignee(order)
|
||||
if not assignee:
|
||||
continue
|
||||
|
||||
client_name = order.partner_id.name or 'client'
|
||||
handoff_date = order.x_fc_mod_handoff_date
|
||||
days_since = (today - handoff_date).days if handoff_date else None
|
||||
summary = f'MOD Follow-up: call {client_name} — confirm application submission'
|
||||
if days_since is not None:
|
||||
summary += f' ({days_since} days since handoff)'
|
||||
|
||||
order.activity_schedule(
|
||||
'fusion_claims.mail_activity_type_mod_followup',
|
||||
date_deadline=today + timedelta(days=3),
|
||||
user_id=assignee.id,
|
||||
summary=summary,
|
||||
)
|
||||
order.with_context(skip_all_validations=True).write({
|
||||
'x_fc_mod_followup_month_count': count + 1,
|
||||
})
|
||||
_logger.info(
|
||||
f"MOD handoff follow-up scheduled for {order.name} "
|
||||
f"→ {assignee.name}"
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(
|
||||
f"_cron_mod_handoff_followup failed for {order.name}: {e}"
|
||||
)
|
||||
|
||||
def _mod_followup_cap_state(self):
|
||||
"""Return (within_cap, reset_needed, new_start, max_per_month) tuple.
|
||||
|
||||
|
||||
@@ -330,6 +330,77 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- MOD BLANK FORMS + HANDOFF FOLLOW-UP (2026-04 update) -->
|
||||
<!-- ============================================================= -->
|
||||
<h2>March of Dimes — Blank Forms & Handoff Follow-Up</h2>
|
||||
<p class="text-muted">
|
||||
Upload the latest revision of the MOD-issued blank forms here.
|
||||
They are auto-attached to outgoing emails so every case uses the
|
||||
current version. Re-upload whenever March of Dimes publishes a
|
||||
new revision.
|
||||
</p>
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">MOD Application Form (blank)</span>
|
||||
<div class="text-muted">
|
||||
Attached to the handoff email when client or authorizer
|
||||
is submitting the application themselves.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_mod_application_form"
|
||||
filename="fc_mod_application_form_filename"
|
||||
widget="binary"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Verification of Disability Form (blank)</span>
|
||||
<div class="text-muted">
|
||||
Attached to the VOD request email sent to the authorizer
|
||||
when our office is handling the MOD submission internally.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_mod_vod_form"
|
||||
filename="fc_mod_vod_form_filename"
|
||||
widget="binary"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Handoff Follow-up Assignee</span>
|
||||
<div class="text-muted">
|
||||
Who gets the follow-up activity when a client or authorizer
|
||||
is handling the MOD submission themselves. "Office contact"
|
||||
routes all follow-ups to a single designated person. "Sales
|
||||
rep" assigns to the rep on each order.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_mod_followup_assignee_mode" widget="radio"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Office Follow-up Contact</span>
|
||||
<div class="text-muted">
|
||||
The office user who receives MOD handoff follow-up activities
|
||||
when assignee mode is set to Office Contact.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_mod_followup_office_contact_id"
|
||||
invisible="fc_mod_followup_assignee_mode != 'office_contact'"
|
||||
options="{'no_create': True}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- DATA MIGRATION -->
|
||||
<!-- ============================================================= -->
|
||||
|
||||
@@ -193,12 +193,57 @@
|
||||
<!-- Exception buttons -->
|
||||
<button name="action_mod_on_hold" type="object"
|
||||
string="Hold" class="btn-warning" icon="fa-pause"
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_status not in ('contract_received', 'in_production', 'project_complete')"/>
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_status in ('on_hold', 'case_closed', 'cancelled', False)"/>
|
||||
|
||||
<button name="action_mod_resume" type="object"
|
||||
string="Resume" class="btn-success" icon="fa-play"
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_status != 'on_hold'"/>
|
||||
|
||||
<!-- ======================================================= -->
|
||||
<!-- MOD 2026-04 UPDATE — new submission-path + recovery btns -->
|
||||
<!-- ======================================================= -->
|
||||
|
||||
<!-- Set MOD Submission Path (internal / client / authorizer) -->
|
||||
<button name="action_mod_set_submission_path" type="object"
|
||||
string="Set Submission Path" class="btn-info" icon="fa-sitemap"
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_status not in ('assessment_completed', 'processing_drawings', 'quote_submitted')"
|
||||
help="Choose who is submitting the MOD application (our office, the client, or the authorizer)"/>
|
||||
|
||||
<!-- Request VOD from Authorizer (manual re-send) -->
|
||||
<button name="action_mod_request_vod" type="object"
|
||||
string="Request VOD from Authorizer" class="btn-warning" icon="fa-envelope"
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_submitted_by != 'internal' or x_fc_mod_vod_letter"
|
||||
help="Email the authorizer the blank Verification of Disability form to complete. Fires automatically once on first setting submitted_by=internal, this button is for manual re-sends."/>
|
||||
|
||||
<!-- Handoff to Client/Authorizer (paths B/C) -->
|
||||
<button name="action_mod_handoff_to_client" type="object"
|
||||
string="Handoff Package" class="btn-primary" icon="fa-share"
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_submitted_by not in ('client', 'authorizer') or x_fc_mod_status not in ('processing_drawings', 'quote_submitted')"
|
||||
confirm="This will email the full MOD application package to the client or authorizer and move the case to Handoff state. Continue?"
|
||||
help="Email proposal + drawing + blank MOD application form to whoever is submitting, and move the case to handoff_to_client"/>
|
||||
|
||||
<!-- Confirm Submission (for handoff_to_client state) -->
|
||||
<button name="action_mod_confirmed_submission" type="object"
|
||||
string="Confirm Submission" class="btn-success" icon="fa-check-circle"
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_status != 'handoff_to_client'"
|
||||
help="Client or authorizer confirmed they submitted the application to MOD. Record the date and advance to Awaiting Funding."/>
|
||||
|
||||
<!-- Recovery buttons for funding_denied -->
|
||||
<button name="action_mod_resubmit_from_denied" type="object"
|
||||
string="Resubmit" class="btn-primary" icon="fa-undo"
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_status != 'funding_denied'"
|
||||
help="Revise and resubmit the case to March of Dimes after a denial"/>
|
||||
|
||||
<button name="action_mod_cancel_from_denied" type="object"
|
||||
string="Cancel Case" class="btn-danger" icon="fa-times"
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_status != 'funding_denied'"
|
||||
confirm="Cancel this denied MOD case? This action can be reversed with Reopen."/>
|
||||
|
||||
<button name="action_mod_reopen_cancelled" type="object"
|
||||
string="Reopen" class="btn-info" icon="fa-refresh"
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_status != 'cancelled'"
|
||||
confirm="Reopen this cancelled MOD case? Status will reset to Need to Schedule."/>
|
||||
|
||||
</xpath>
|
||||
|
||||
<!-- ============================================= -->
|
||||
@@ -234,6 +279,59 @@
|
||||
Upload Completion Photos and Proof of Delivery to submit to the case worker.
|
||||
</div>
|
||||
|
||||
<!-- ===== 2026-04 MOD Update: Submission Path + Application Package ===== -->
|
||||
<separator string="MOD Submission Path"/>
|
||||
<div class="alert alert-secondary" role="alert"
|
||||
invisible="x_fc_mod_submitted_by">
|
||||
<i class="fa fa-question-circle"/> <strong>Who is submitting this application?</strong>
|
||||
Click <strong>Set Submission Path</strong> in the status bar above to choose:
|
||||
internal (we submit), client, or authorizer.
|
||||
</div>
|
||||
<group col="4">
|
||||
<field name="x_fc_mod_submitted_by" readonly="1"/>
|
||||
<field name="x_fc_mod_handoff_date" readonly="1"
|
||||
invisible="x_fc_mod_submitted_by == 'internal'"/>
|
||||
<field name="x_fc_mod_application_submitted_date" readonly="1"/>
|
||||
<field name="x_fc_mod_vod_requested_date" readonly="1"
|
||||
invisible="x_fc_mod_submitted_by != 'internal'"/>
|
||||
<field name="x_fc_mod_accessibility_specialist_id"
|
||||
options="{'no_create': True}"/>
|
||||
</group>
|
||||
|
||||
<separator string="MOD Application Package (when WE submit on client's behalf)"
|
||||
invisible="x_fc_mod_submitted_by != 'internal'"/>
|
||||
<group col="2" invisible="x_fc_mod_submitted_by != 'internal'">
|
||||
<field name="x_fc_mod_vod_letter"
|
||||
filename="x_fc_mod_vod_letter_filename"
|
||||
widget="binary"/>
|
||||
<field name="x_fc_mod_application_form_doc"
|
||||
filename="x_fc_mod_application_form_doc_filename"
|
||||
widget="binary"/>
|
||||
<field name="x_fc_mod_notice_of_assessment"
|
||||
filename="x_fc_mod_notice_of_assessment_filename"
|
||||
widget="binary"/>
|
||||
<field name="x_fc_mod_property_tax"
|
||||
filename="x_fc_mod_property_tax_filename"
|
||||
widget="binary"/>
|
||||
</group>
|
||||
|
||||
<separator string="Proposal Document"/>
|
||||
<group col="2">
|
||||
<field name="x_fc_mod_proposal_doc"
|
||||
filename="x_fc_mod_proposal_doc_filename"
|
||||
widget="binary"/>
|
||||
</group>
|
||||
|
||||
<separator string="Funding Denial Details" invisible="x_fc_mod_status != 'funding_denied'"/>
|
||||
<group invisible="x_fc_mod_status != 'funding_denied'">
|
||||
<field name="x_fc_mod_funding_denial_reason" readonly="1" nolabel="1"/>
|
||||
</group>
|
||||
|
||||
<separator string="Hold History" invisible="not x_fc_mod_previous_status_before_hold"/>
|
||||
<group invisible="not x_fc_mod_previous_status_before_hold">
|
||||
<field name="x_fc_mod_previous_status_before_hold" readonly="1"/>
|
||||
</group>
|
||||
|
||||
<!-- ===== Row 1: Assessment Documents ===== -->
|
||||
<div class="row">
|
||||
<!-- Column 1: Assessment and Design -->
|
||||
|
||||
@@ -23,6 +23,10 @@ from . import send_to_mod_wizard
|
||||
from . import mod_awaiting_funding_wizard
|
||||
from . import mod_funding_approved_wizard
|
||||
from . import mod_pca_received_wizard
|
||||
from . import mod_submission_path_wizard
|
||||
from . import mod_funding_denied_wizard
|
||||
from . import mod_resubmit_wizard
|
||||
from . import mod_submission_confirmed_wizard
|
||||
from . import odsp_sa_mobility_wizard
|
||||
from . import odsp_discretionary_wizard
|
||||
from . import odsp_pre_approved_wizard
|
||||
|
||||
84
fusion_claims/wizard/mod_funding_denied_wizard.py
Normal file
84
fusion_claims/wizard/mod_funding_denied_wizard.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""
|
||||
MOD Funding Denied Wizard — captures the denial reason and sets the order
|
||||
status. Previously action_mod_funding_denied was a bare write with no reason
|
||||
capture; this wizard replaces that flow.
|
||||
"""
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from markupsafe import Markup
|
||||
|
||||
|
||||
class ModFundingDeniedWizard(models.TransientModel):
|
||||
_name = 'fusion_claims.mod.funding.denied.wizard'
|
||||
_description = 'MOD - Funding Denied Reason Capture'
|
||||
|
||||
sale_order_id = fields.Many2one('sale.order', required=True, readonly=True)
|
||||
denial_date = fields.Date(
|
||||
string='Denial Date',
|
||||
required=True,
|
||||
default=fields.Date.context_today,
|
||||
)
|
||||
denial_reason_category = fields.Selection(
|
||||
selection=[
|
||||
('income_too_high', 'Client income exceeds MOD threshold'),
|
||||
('residency', 'Residency requirement not met'),
|
||||
('project_scope', 'Project scope not eligible'),
|
||||
('missing_docs', 'Missing documentation'),
|
||||
('funding_depleted', 'MOD funding depleted for this period'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
string='Denial Category',
|
||||
required=True,
|
||||
)
|
||||
denial_reason = fields.Text(
|
||||
string='Denial Details',
|
||||
required=True,
|
||||
help='MOD\'s stated reason for denying funding. Captured for the '
|
||||
'audit trail and used when generating the client denial email.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
if self.env.context.get('active_id'):
|
||||
order = self.env['sale.order'].browse(self.env.context['active_id'])
|
||||
res['sale_order_id'] = order.id
|
||||
return res
|
||||
|
||||
def action_confirm(self):
|
||||
self.ensure_one()
|
||||
order = self.sale_order_id
|
||||
if order.x_fc_mod_status not in ('awaiting_funding', 'quote_submitted', 'handoff_to_client'):
|
||||
raise UserError(
|
||||
_("Funding can only be denied from awaiting_funding, "
|
||||
"quote_submitted or handoff_to_client. Current: %s") % order.x_fc_mod_status
|
||||
)
|
||||
|
||||
labels = dict(self._fields['denial_reason_category'].selection)
|
||||
category_label = labels.get(self.denial_reason_category, self.denial_reason_category)
|
||||
full_reason = f'[{category_label}] {self.denial_reason}'
|
||||
|
||||
order.write({
|
||||
'x_fc_mod_status': 'funding_denied',
|
||||
'x_fc_mod_funding_denial_reason': full_reason,
|
||||
})
|
||||
|
||||
body = (
|
||||
f'<div class="alert alert-danger" role="alert">'
|
||||
f'<strong><i class="fa fa-ban"></i> Funding Denied by March of Dimes</strong>'
|
||||
f'<ul>'
|
||||
f'<li><strong>Date:</strong> {self.denial_date.strftime("%B %d, %Y")}</li>'
|
||||
f'<li><strong>Category:</strong> {category_label}</li>'
|
||||
f'<li><strong>Details:</strong> {self.denial_reason}</li>'
|
||||
f'</ul>'
|
||||
f'</div>'
|
||||
)
|
||||
order.message_post(
|
||||
body=Markup(body),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
40
fusion_claims/wizard/mod_funding_denied_wizard_views.xml
Normal file
40
fusion_claims/wizard/mod_funding_denied_wizard_views.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_mod_funding_denied_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion_claims.mod.funding.denied.wizard.form</field>
|
||||
<field name="model">fusion_claims.mod.funding.denied.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="MOD Funding Denied">
|
||||
<sheet>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<strong><i class="fa fa-ban"/> Record MOD Funding Denial</strong>
|
||||
<p class="mb-0 mt-2">
|
||||
Capture the denial reason from March of Dimes. This will be logged
|
||||
to the case history and included in the notification sent to the
|
||||
client and authorizer.
|
||||
</p>
|
||||
</div>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
<group>
|
||||
<field name="denial_date"/>
|
||||
<field name="denial_reason_category" required="1"/>
|
||||
<field name="denial_reason" required="1"
|
||||
placeholder="Paste or summarise MOD's stated reason..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_confirm" type="object"
|
||||
string="Record Denial" class="btn-danger"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mod_funding_denied_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Funding Denied by MOD</field>
|
||||
<field name="res_model">fusion_claims.mod.funding.denied.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
87
fusion_claims/wizard/mod_resubmit_wizard.py
Normal file
87
fusion_claims/wizard/mod_resubmit_wizard.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""
|
||||
MOD Resubmit Wizard — lets the office revise an order that MOD has denied,
|
||||
and kick it back into the workflow at `processing_drawings` so the specialist
|
||||
can update drawings/proposal/quotation before it is re-submitted to MOD.
|
||||
"""
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from markupsafe import Markup
|
||||
|
||||
|
||||
class ModResubmitWizard(models.TransientModel):
|
||||
_name = 'fusion_claims.mod.resubmit.wizard'
|
||||
_description = 'MOD - Resubmit After Denial'
|
||||
|
||||
sale_order_id = fields.Many2one('sale.order', required=True, readonly=True)
|
||||
revision_notes = fields.Text(
|
||||
string='Revision Notes',
|
||||
required=True,
|
||||
help='Describe what is being revised for the resubmission '
|
||||
'(scope changes, updated pricing, additional documentation, etc).',
|
||||
)
|
||||
clear_old_documents = fields.Boolean(
|
||||
string='Clear old drawings / proposal / quotation',
|
||||
default=False,
|
||||
help='Tick this if the new submission needs entirely new drawings and '
|
||||
'proposal. The previous documents are preserved in chatter before '
|
||||
'being cleared.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
if self.env.context.get('active_id'):
|
||||
order = self.env['sale.order'].browse(self.env.context['active_id'])
|
||||
res['sale_order_id'] = order.id
|
||||
return res
|
||||
|
||||
def action_confirm(self):
|
||||
self.ensure_one()
|
||||
order = self.sale_order_id
|
||||
if order.x_fc_mod_status != 'funding_denied':
|
||||
raise UserError(_("Only denied orders can be resubmitted via this wizard."))
|
||||
|
||||
if self.clear_old_documents:
|
||||
# Preserve the old docs in chatter before clearing them.
|
||||
preserved = []
|
||||
if order.x_fc_mod_drawing:
|
||||
preserved.append('Drawing')
|
||||
if order.x_fc_mod_proposal_doc:
|
||||
preserved.append('Proposal')
|
||||
if preserved:
|
||||
order.message_post(
|
||||
body=Markup(
|
||||
f'<div class="alert alert-warning" role="alert">'
|
||||
f'<strong>Previous documents cleared for resubmission:</strong> '
|
||||
f'{", ".join(preserved)}<br/>'
|
||||
f'See prior chatter for the originals.'
|
||||
f'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
order.with_context(skip_status_emails=True).write({
|
||||
'x_fc_mod_drawing': False,
|
||||
'x_fc_mod_drawing_filename': False,
|
||||
'x_fc_mod_proposal_doc': False,
|
||||
'x_fc_mod_proposal_doc_filename': False,
|
||||
})
|
||||
|
||||
order.with_context(skip_status_emails=True).write({
|
||||
'x_fc_mod_status': 'processing_drawings',
|
||||
})
|
||||
body = (
|
||||
f'<div class="alert alert-info" role="alert">'
|
||||
f'<strong><i class="fa fa-undo"></i> Resubmitting to March of Dimes after denial</strong>'
|
||||
f'<p class="mb-0 mt-2"><strong>Revision notes:</strong> {self.revision_notes}</p>'
|
||||
f'</div>'
|
||||
)
|
||||
order.message_post(
|
||||
body=Markup(body),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
39
fusion_claims/wizard/mod_resubmit_wizard_views.xml
Normal file
39
fusion_claims/wizard/mod_resubmit_wizard_views.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_mod_resubmit_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion_claims.mod.resubmit.wizard.form</field>
|
||||
<field name="model">fusion_claims.mod.resubmit.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Resubmit to MOD">
|
||||
<sheet>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<strong><i class="fa fa-undo"/> Resubmit Denied Application</strong>
|
||||
<p class="mb-0 mt-2">
|
||||
Return this denied case to <strong>Processing Drawings</strong> so the
|
||||
accessibility specialist can update the proposal, drawings, or quote
|
||||
before resubmitting to March of Dimes.
|
||||
</p>
|
||||
</div>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
<group>
|
||||
<field name="revision_notes" required="1"
|
||||
placeholder="What is being revised? Scope change, updated pricing, new docs..."/>
|
||||
<field name="clear_old_documents"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_confirm" type="object"
|
||||
string="Resubmit" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mod_resubmit_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Resubmit to MOD</field>
|
||||
<field name="res_model">fusion_claims.mod.resubmit.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
92
fusion_claims/wizard/mod_submission_confirmed_wizard.py
Normal file
92
fusion_claims/wizard/mod_submission_confirmed_wizard.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""
|
||||
MOD Submission Confirmed Wizard — office confirms that a client or authorizer
|
||||
has submitted the application to MOD, captures the actual submission date,
|
||||
and advances the order from handoff_to_client to awaiting_funding.
|
||||
"""
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from markupsafe import Markup
|
||||
|
||||
|
||||
class ModSubmissionConfirmedWizard(models.TransientModel):
|
||||
_name = 'fusion_claims.mod.submission.confirmed.wizard'
|
||||
_description = 'MOD - Confirm Application Submitted'
|
||||
|
||||
sale_order_id = fields.Many2one('sale.order', required=True, readonly=True)
|
||||
submitted_by_label = fields.Char(string='Submitted By', readonly=True)
|
||||
application_submitted_date = fields.Date(
|
||||
string='Actual Submission Date',
|
||||
required=True,
|
||||
default=fields.Date.context_today,
|
||||
help='Date the client or authorizer told us they actually submitted '
|
||||
'the application to March of Dimes.',
|
||||
)
|
||||
confirmation_source = fields.Selection(
|
||||
selection=[
|
||||
('phone_call', 'Phone call with client'),
|
||||
('email', 'Email from client'),
|
||||
('client_portal', 'Client used our portal'),
|
||||
('authorizer', 'Confirmed by authorizer (OT)'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
string='How was this confirmed?',
|
||||
required=True,
|
||||
default='phone_call',
|
||||
)
|
||||
confirmation_notes = fields.Text(
|
||||
string='Notes from confirmation',
|
||||
help='What did the client say? Confirmation number from MOD if given?',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
if self.env.context.get('active_id'):
|
||||
order = self.env['sale.order'].browse(self.env.context['active_id'])
|
||||
res['sale_order_id'] = order.id
|
||||
path_labels = {
|
||||
'internal': 'We (internal)',
|
||||
'client': 'Client',
|
||||
'authorizer': 'Authorizer (OT)',
|
||||
}
|
||||
res['submitted_by_label'] = path_labels.get(
|
||||
order.x_fc_mod_submitted_by, 'Unknown')
|
||||
return res
|
||||
|
||||
def action_confirm(self):
|
||||
self.ensure_one()
|
||||
order = self.sale_order_id
|
||||
if order.x_fc_mod_status != 'handoff_to_client':
|
||||
raise UserError(
|
||||
_("This wizard is only for orders that have been handed off "
|
||||
"to the client or authorizer for submission. Current status: %s")
|
||||
% order.x_fc_mod_status
|
||||
)
|
||||
|
||||
order.write({
|
||||
'x_fc_mod_status': 'awaiting_funding',
|
||||
'x_fc_mod_application_submitted_date': self.application_submitted_date,
|
||||
})
|
||||
|
||||
source_labels = dict(self._fields['confirmation_source'].selection)
|
||||
body = (
|
||||
f'<div class="alert alert-success" role="alert">'
|
||||
f'<strong><i class="fa fa-check-circle"></i> Application Submission Confirmed</strong>'
|
||||
f'<ul>'
|
||||
f'<li><strong>Submitted By:</strong> {self.submitted_by_label}</li>'
|
||||
f'<li><strong>Submission Date:</strong> {self.application_submitted_date.strftime("%B %d, %Y")}</li>'
|
||||
f'<li><strong>Confirmed Via:</strong> {source_labels[self.confirmation_source]}</li>'
|
||||
f'</ul>'
|
||||
)
|
||||
if self.confirmation_notes:
|
||||
body += f'<p class="mb-0"><strong>Notes:</strong> {self.confirmation_notes}</p>'
|
||||
body += '</div>'
|
||||
order.message_post(
|
||||
body=Markup(body),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_mod_submission_confirmed_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion_claims.mod.submission.confirmed.wizard.form</field>
|
||||
<field name="model">fusion_claims.mod.submission.confirmed.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Confirm MOD Submission">
|
||||
<sheet>
|
||||
<div class="alert alert-success" role="alert">
|
||||
<strong><i class="fa fa-check-circle"/> Confirm Application Submission</strong>
|
||||
<p class="mb-0 mt-2">
|
||||
Record that the <strong>client</strong> or <strong>authorizer</strong>
|
||||
has actually submitted the application to March of Dimes. The order
|
||||
will advance to <strong>Awaiting Funding</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
<group>
|
||||
<field name="submitted_by_label" readonly="1"/>
|
||||
<field name="application_submitted_date" required="1"/>
|
||||
<field name="confirmation_source" required="1"/>
|
||||
<field name="confirmation_notes"
|
||||
placeholder="What did the client say? Any reference number from MOD?"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_confirm" type="object"
|
||||
string="Confirm Submission" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mod_submission_confirmed_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Confirm MOD Submission</field>
|
||||
<field name="res_model">fusion_claims.mod.submission.confirmed.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
76
fusion_claims/wizard/mod_submission_path_wizard.py
Normal file
76
fusion_claims/wizard/mod_submission_path_wizard.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""
|
||||
MOD Submission Path Wizard — asks the office which party is submitting the
|
||||
March of Dimes application. Sets x_fc_mod_submitted_by on the order and,
|
||||
when the path is 'internal', auto-triggers the VOD request email to the
|
||||
authorizer the first time it is selected.
|
||||
"""
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from markupsafe import Markup
|
||||
|
||||
|
||||
class ModSubmissionPathWizard(models.TransientModel):
|
||||
_name = 'fusion_claims.mod.submission.path.wizard'
|
||||
_description = 'MOD - Set Submission Path'
|
||||
|
||||
sale_order_id = fields.Many2one('sale.order', required=True, readonly=True)
|
||||
submitted_by = fields.Selection(
|
||||
selection=[
|
||||
('internal', 'We submit on client\'s behalf'),
|
||||
('client', 'Client submits themselves'),
|
||||
('authorizer', 'Authorizer (OT) submits'),
|
||||
],
|
||||
string='Who is submitting to March of Dimes?',
|
||||
required=True,
|
||||
)
|
||||
notes = fields.Text(string='Notes')
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
if self.env.context.get('active_id'):
|
||||
order = self.env['sale.order'].browse(self.env.context['active_id'])
|
||||
res['sale_order_id'] = order.id
|
||||
if order.x_fc_mod_submitted_by:
|
||||
res['submitted_by'] = order.x_fc_mod_submitted_by
|
||||
return res
|
||||
|
||||
def action_confirm(self):
|
||||
self.ensure_one()
|
||||
order = self.sale_order_id
|
||||
previous = order.x_fc_mod_submitted_by
|
||||
vals = {'x_fc_mod_submitted_by': self.submitted_by}
|
||||
order.with_context(skip_status_emails=True).write(vals)
|
||||
|
||||
labels = dict(self._fields['submitted_by'].selection)
|
||||
body = (
|
||||
f'<div class="alert alert-info" role="alert">'
|
||||
f'<strong>MOD submission path set:</strong> {labels[self.submitted_by]}'
|
||||
)
|
||||
if self.notes:
|
||||
body += f'<br/>{self.notes}'
|
||||
body += '</div>'
|
||||
order.message_post(
|
||||
body=Markup(body),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
# First-time internal path → auto-trigger the VOD request email so
|
||||
# the authorizer knows to fill and send the VOD form back.
|
||||
if (
|
||||
self.submitted_by == 'internal'
|
||||
and previous != 'internal'
|
||||
and not order.x_fc_mod_vod_requested_date
|
||||
):
|
||||
try:
|
||||
order._send_mod_vod_request_email()
|
||||
except Exception:
|
||||
# Don't block the wizard if the email fails — office can
|
||||
# retry via the manual "Request VOD" button.
|
||||
pass
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
39
fusion_claims/wizard/mod_submission_path_wizard_views.xml
Normal file
39
fusion_claims/wizard/mod_submission_path_wizard_views.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_mod_submission_path_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion_claims.mod.submission.path.wizard.form</field>
|
||||
<field name="model">fusion_claims.mod.submission.path.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Set MOD Submission Path">
|
||||
<sheet>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<strong><i class="fa fa-info-circle"/> Who is submitting this application to March of Dimes?</strong>
|
||||
<p class="mb-0 mt-2">
|
||||
This determines which document gates apply and whether we hand off materials
|
||||
to the client or authorizer for them to submit. Selecting
|
||||
<strong>"We submit on client's behalf"</strong> will automatically email the
|
||||
authorizer the blank Verification of Disability form to complete.
|
||||
</p>
|
||||
</div>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
<group>
|
||||
<field name="submitted_by" widget="radio" required="1"/>
|
||||
<field name="notes" placeholder="Optional notes..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_confirm" type="object"
|
||||
string="Confirm" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mod_submission_path_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Set MOD Submission Path</field>
|
||||
<field name="res_model">fusion_claims.mod.submission.path.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user