Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View 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,
},
}