Initial commit
This commit is contained in:
395
fusion_claims/wizard/odsp_discretionary_wizard.py
Normal file
395
fusion_claims/wizard/odsp_discretionary_wizard.py
Normal file
@@ -0,0 +1,395 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import pdfrw
|
||||
except ImportError:
|
||||
pdfrw = None
|
||||
_logger.warning("pdfrw not installed. Discretionary Benefits PDF filling will not work.")
|
||||
|
||||
|
||||
class DiscretionaryBenefitWizard(models.TransientModel):
|
||||
_name = 'fusion_claims.discretionary.benefit.wizard'
|
||||
_description = 'ODSP Discretionary Benefits Form Wizard'
|
||||
|
||||
sale_order_id = fields.Many2one('sale.order', required=True, readonly=True)
|
||||
|
||||
# --- Auto-populated from partner ---
|
||||
client_name = fields.Char(string='Client Name', readonly=True)
|
||||
address = fields.Char(string='Address', readonly=True)
|
||||
city = fields.Char(string='City', readonly=True)
|
||||
province = fields.Char(string='Province', default='ON')
|
||||
postal_code = fields.Char(string='Postal Code', readonly=True)
|
||||
phone_number = fields.Char(string='Phone', readonly=True)
|
||||
alt_phone = fields.Char(string='Alternate Phone', readonly=True)
|
||||
email = fields.Char(string='Email', readonly=True)
|
||||
member_id = fields.Char(string='ODSP Member ID', readonly=True)
|
||||
form_date = fields.Date(string='Date', default=fields.Date.today)
|
||||
|
||||
odsp_office_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='ODSP Office',
|
||||
domain="[('x_fc_contact_type', '=', 'odsp_office')]",
|
||||
help='Override the ODSP office for this submission',
|
||||
)
|
||||
|
||||
# --- User-editable ---
|
||||
item_type = fields.Selection([
|
||||
('medical_equipment', 'Medical Equipment'),
|
||||
('vision_care', 'Vision Care'),
|
||||
('dentures', 'Dentures'),
|
||||
('other', 'Other'),
|
||||
], string='Item Type', default='medical_equipment', required=True)
|
||||
|
||||
other_description = fields.Text(
|
||||
string='Description / Request Details',
|
||||
help='Details about the request (visible for all types, required for Other)',
|
||||
)
|
||||
|
||||
email_body_notes = fields.Text(
|
||||
string='Email Body Notes',
|
||||
help='Urgency or priority notes that appear at the top of the email body, '
|
||||
'right below the title. Use this for time-sensitive requests.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
"""Pre-populate from sale order and partner."""
|
||||
res = super().default_get(fields_list)
|
||||
order_id = self.env.context.get('active_id')
|
||||
if not order_id:
|
||||
return res
|
||||
|
||||
order = self.env['sale.order'].browse(order_id)
|
||||
partner = order.partner_id
|
||||
res['sale_order_id'] = order.id
|
||||
|
||||
if partner:
|
||||
res['client_name'] = partner.name or ''
|
||||
res['address'] = partner.street or ''
|
||||
res['city'] = partner.city or ''
|
||||
res['province'] = partner.state_id.code if partner.state_id else 'ON'
|
||||
res['postal_code'] = partner.zip or ''
|
||||
res['phone_number'] = partner.phone or ''
|
||||
res['alt_phone'] = ''
|
||||
res['email'] = partner.email or ''
|
||||
|
||||
res['member_id'] = order.x_fc_odsp_member_id or (partner and partner.x_fc_odsp_member_id) or ''
|
||||
|
||||
if order.x_fc_odsp_office_id:
|
||||
res['odsp_office_id'] = order.x_fc_odsp_office_id.id
|
||||
|
||||
return res
|
||||
|
||||
def _get_template_path(self):
|
||||
"""Get the path to the Discretionary Benefits form template PDF."""
|
||||
module_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
return os.path.join(module_path, 'static', 'src', 'pdf', 'discretionary_benefits_form_template.pdf')
|
||||
|
||||
def _build_field_mapping(self):
|
||||
"""Build a dictionary mapping PDF field names to values."""
|
||||
self.ensure_one()
|
||||
mapping = {}
|
||||
|
||||
mapping['txt_First[0]'] = self.client_name or ''
|
||||
# txt_CITY[1] is physically the Member ID field (despite the name)
|
||||
mapping['txt_CITY[1]'] = self.member_id or ''
|
||||
mapping['txt_add[0]'] = self.address or ''
|
||||
mapping['txt_CITY[0]'] = self.city or ''
|
||||
mapping['txt_prov[0]'] = self.province or 'ON'
|
||||
mapping['txt_postalcodes[0]'] = self.postal_code or ''
|
||||
# txt_email[0] is physically the Phone field (despite the name)
|
||||
mapping['txt_email[0]'] = self.phone_number or ''
|
||||
mapping['txt_bphone[0]'] = self.alt_phone or ''
|
||||
# txt_emp_phone[0] is physically the Email field (despite the name)
|
||||
mapping['txt_emp_phone[0]'] = self.email or ''
|
||||
# txt_clientnumber[0] is physically the Date field (despite the name)
|
||||
date_str = ''
|
||||
if self.form_date:
|
||||
date_str = self.form_date.strftime('%b %d, %Y')
|
||||
mapping['txt_clientnumber[0]'] = date_str
|
||||
|
||||
# Item type checkboxes (mapped by physical position on the form)
|
||||
mapping['CheckBox15[0]'] = self.item_type == 'medical_equipment'
|
||||
mapping['CheckBox11[0]'] = self.item_type == 'dentures'
|
||||
mapping['CheckBox11[1]'] = self.item_type == 'vision_care'
|
||||
mapping['CheckBox13[0]'] = self.item_type == 'other'
|
||||
|
||||
# Description / request details
|
||||
mapping['TextField1[0]'] = self.other_description or ''
|
||||
|
||||
return mapping
|
||||
|
||||
def _fill_pdf(self):
|
||||
"""Fill the Discretionary Benefits PDF using PyPDF2.
|
||||
This PDF is AES-encrypted, so we use PyPDF2 which handles
|
||||
decryption and form filling natively (pdfrw cannot).
|
||||
"""
|
||||
self.ensure_one()
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
from PyPDF2.generic import NameObject, BooleanObject
|
||||
|
||||
template_path = self._get_template_path()
|
||||
if not os.path.exists(template_path):
|
||||
raise UserError(_("Discretionary Benefits form template not found at %s") % template_path)
|
||||
|
||||
mapping = self._build_field_mapping()
|
||||
|
||||
reader = PdfReader(template_path)
|
||||
if reader.is_encrypted:
|
||||
reader.decrypt('')
|
||||
|
||||
writer = PdfWriter()
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
|
||||
# Preserve AcroForm from original
|
||||
if '/AcroForm' in reader.trailer['/Root']:
|
||||
writer._root_object[NameObject('/AcroForm')] = reader.trailer['/Root']['/AcroForm']
|
||||
writer._root_object['/AcroForm'][NameObject('/NeedAppearances')] = BooleanObject(True)
|
||||
|
||||
# Split text fields and checkbox fields
|
||||
text_fields = {}
|
||||
checkbox_fields = {}
|
||||
for key, value in mapping.items():
|
||||
if isinstance(value, bool):
|
||||
checkbox_fields[key] = value
|
||||
else:
|
||||
text_fields[key] = str(value)
|
||||
|
||||
# Fill text fields via PyPDF2 bulk method
|
||||
writer.update_page_form_field_values(writer.pages[0], text_fields)
|
||||
|
||||
# Fill checkboxes by directly updating each annotation
|
||||
page = writer.pages[0]
|
||||
for annot_ref in page['/Annots']:
|
||||
annot = annot_ref.get_object()
|
||||
if annot.get('/FT') != '/Btn':
|
||||
continue
|
||||
field_name = str(annot.get('/T', ''))
|
||||
if field_name not in checkbox_fields:
|
||||
continue
|
||||
if checkbox_fields[field_name]:
|
||||
annot[NameObject('/V')] = NameObject('/1')
|
||||
annot[NameObject('/AS')] = NameObject('/1')
|
||||
else:
|
||||
annot[NameObject('/V')] = NameObject('/Off')
|
||||
annot[NameObject('/AS')] = NameObject('/Off')
|
||||
|
||||
output = io.BytesIO()
|
||||
writer.write(output)
|
||||
return output.getvalue()
|
||||
|
||||
def _sync_odsp_office(self):
|
||||
"""Sync ODSP office back to sale order if changed in wizard."""
|
||||
if self.odsp_office_id and self.odsp_office_id != self.sale_order_id.x_fc_odsp_office_id:
|
||||
self.sale_order_id.x_fc_odsp_office_id = self.odsp_office_id
|
||||
|
||||
def _generate_and_attach(self):
|
||||
"""Generate filled PDF and quotation, attach both to sale order.
|
||||
Returns (disc_attachment, quote_attachment) ir.attachment records.
|
||||
"""
|
||||
order = self.sale_order_id
|
||||
|
||||
pdf_data = self._fill_pdf()
|
||||
disc_filename = f'Discretionary_Benefits_{order.name}.pdf'
|
||||
encoded_pdf = base64.b64encode(pdf_data)
|
||||
disc_attachment = self.env['ir.attachment'].create({
|
||||
'name': disc_filename,
|
||||
'type': 'binary',
|
||||
'datas': encoded_pdf,
|
||||
'res_model': 'sale.order',
|
||||
'res_id': order.id,
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
|
||||
if order.x_fc_odsp_division == 'ontario_works':
|
||||
order.write({
|
||||
'x_fc_ow_discretionary_form': encoded_pdf,
|
||||
'x_fc_ow_discretionary_form_filename': disc_filename,
|
||||
})
|
||||
|
||||
report = self.env.ref('sale.action_report_saleorder')
|
||||
quote_pdf, _ct = report._render_qweb_pdf(report.id, [order.id])
|
||||
quote_filename = f'{order.name}.pdf'
|
||||
quote_attachment = self.env['ir.attachment'].create({
|
||||
'name': quote_filename,
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(quote_pdf),
|
||||
'res_model': 'sale.order',
|
||||
'res_id': order.id,
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
|
||||
return disc_attachment, quote_attachment
|
||||
|
||||
def action_fill_and_attach(self):
|
||||
"""Fill the Discretionary Benefits PDF and attach to sale order via chatter."""
|
||||
self.ensure_one()
|
||||
self._sync_odsp_office()
|
||||
order = self.sale_order_id
|
||||
|
||||
disc_att, quote_att = self._generate_and_attach()
|
||||
|
||||
if order._get_odsp_status() == 'quotation':
|
||||
order._odsp_advance_status('documents_ready',
|
||||
"Documents ready after Discretionary Benefits form attached.")
|
||||
|
||||
order.message_post(
|
||||
body=_("Discretionary Benefits form filled and attached."),
|
||||
message_type='comment',
|
||||
attachment_ids=[disc_att.id, quote_att.id],
|
||||
)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def _advance_status_on_submit(self, order):
|
||||
"""Advance status when form is submitted (sent via email/fax)."""
|
||||
current = order._get_odsp_status()
|
||||
if order.x_fc_odsp_division == 'ontario_works' and current in ('quotation', 'documents_ready'):
|
||||
order._odsp_advance_status('submitted_to_ow',
|
||||
"Discretionary Benefits form submitted to Ontario Works.")
|
||||
elif current == 'quotation':
|
||||
order._odsp_advance_status('documents_ready',
|
||||
"Documents ready after Discretionary Benefits form attached.")
|
||||
|
||||
def action_send_fax(self):
|
||||
"""Fill form, generate quotation, and open the fax wizard with both attached.
|
||||
Chatter posting is handled by the fax module when the fax is actually sent.
|
||||
"""
|
||||
self.ensure_one()
|
||||
self._sync_odsp_office()
|
||||
order = self.sale_order_id
|
||||
office = self.odsp_office_id or order.x_fc_odsp_office_id
|
||||
|
||||
disc_att, quote_att = self._generate_and_attach()
|
||||
|
||||
self._advance_status_on_submit(order)
|
||||
|
||||
ctx = {
|
||||
'default_sale_order_id': order.id,
|
||||
'default_partner_id': office.id if office else False,
|
||||
'default_generate_pdf': False,
|
||||
'forward_attachment_ids': [disc_att.id, quote_att.id],
|
||||
}
|
||||
if office and hasattr(office, 'x_ff_fax_number') and office.x_ff_fax_number:
|
||||
ctx['default_fax_number'] = office.x_ff_fax_number
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Send Fax - Discretionary Benefits'),
|
||||
'res_model': 'fusion_faxes.send.fax.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def action_send_email(self):
|
||||
"""Fill form, generate quotation, and email both to ODSP office."""
|
||||
self.ensure_one()
|
||||
self._sync_odsp_office()
|
||||
order = self.sale_order_id
|
||||
office = self.odsp_office_id or order.x_fc_odsp_office_id
|
||||
|
||||
if not office or not office.email:
|
||||
raise UserError(_(
|
||||
"No ODSP Office with an email address is set. "
|
||||
"Please select an ODSP Office before sending."
|
||||
))
|
||||
|
||||
disc_att, quote_att = self._generate_and_attach()
|
||||
|
||||
order._send_odsp_submission_email(
|
||||
attachment_ids=[disc_att.id, quote_att.id],
|
||||
email_body_notes=self.email_body_notes,
|
||||
)
|
||||
|
||||
self._advance_status_on_submit(order)
|
||||
|
||||
order.message_post(
|
||||
body=_("Discretionary Benefits form and quotation emailed to %s.") % office.name,
|
||||
message_type='comment',
|
||||
attachment_ids=[disc_att.id, quote_att.id],
|
||||
)
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Email Sent'),
|
||||
'message': _('Discretionary Benefits form emailed to %s.') % office.email,
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
},
|
||||
}
|
||||
|
||||
def action_send_fax_and_email(self):
|
||||
"""Fill form, generate quotation, send email, then open fax wizard.
|
||||
Email is sent first with a notification, then fax wizard opens.
|
||||
"""
|
||||
self.ensure_one()
|
||||
self._sync_odsp_office()
|
||||
order = self.sale_order_id
|
||||
office = self.odsp_office_id or order.x_fc_odsp_office_id
|
||||
|
||||
if not office or not office.email:
|
||||
raise UserError(_(
|
||||
"No ODSP Office with an email address is set. "
|
||||
"Please select an ODSP Office before sending."
|
||||
))
|
||||
|
||||
disc_att, quote_att = self._generate_and_attach()
|
||||
|
||||
order._send_odsp_submission_email(
|
||||
attachment_ids=[disc_att.id, quote_att.id],
|
||||
email_body_notes=self.email_body_notes,
|
||||
)
|
||||
|
||||
self._advance_status_on_submit(order)
|
||||
|
||||
order.message_post(
|
||||
body=_("Discretionary Benefits form and quotation emailed to %s.") % office.name,
|
||||
message_type='comment',
|
||||
attachment_ids=[disc_att.id, quote_att.id],
|
||||
)
|
||||
|
||||
ctx = {
|
||||
'default_sale_order_id': order.id,
|
||||
'default_partner_id': office.id,
|
||||
'default_generate_pdf': False,
|
||||
'forward_attachment_ids': [disc_att.id, quote_att.id],
|
||||
}
|
||||
if hasattr(office, 'x_ff_fax_number') and office.x_ff_fax_number:
|
||||
ctx['default_fax_number'] = office.x_ff_fax_number
|
||||
|
||||
fax_action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Send Fax - Discretionary Benefits'),
|
||||
'res_model': 'fusion_faxes.send.fax.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Email Sent'),
|
||||
'message': _('Email sent to %s. Now proceeding to fax...') % office.email,
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
'next': fax_action,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user