396 lines
15 KiB
Python
396 lines
15 KiB
Python
# -*- 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,
|
|
},
|
|
}
|