# -*- coding: utf-8 -*- # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) import base64 import json import logging import xml.etree.ElementTree as ET from collections import OrderedDict from datetime import datetime from odoo import api, models _logger = logging.getLogger(__name__) class FusionXmlParser(models.AbstractModel): """Utility to parse ADP application XML files and create/update client profiles and application data records. Captures ALL ~300 XML fields for round-trip export fidelity. """ _name = 'fusion.xml.parser' _description = 'ADP XML Parser' # ------------------------------------------------------------------ # PUBLIC API # ------------------------------------------------------------------ @api.model def parse_from_binary(self, binary_data, sale_order=None): """Parse from binary field (base64 encoded). Returns tuple (profile, application_data) or (False, False). """ if not binary_data: return False, False try: xml_content = base64.b64decode(binary_data).decode('utf-8') except Exception as e: _logger.warning('Failed to decode XML binary: %s', e) return False, False return self.parse_and_create(xml_content, sale_order) @api.model def parse_and_create(self, xml_content, sale_order=None): """Parse raw XML string, create/update profile and application data. Returns tuple (profile, application_data) or (False, False). """ try: root = ET.fromstring(xml_content) except ET.ParseError as e: _logger.warning('Failed to parse ADP XML: %s', e) return False, False form = root.find('Form') if form is None: form = root # Step 1: Build complete JSON dict (every field, dot-notation keys) json_dict = self._xml_to_json(form) # Step 2: Extract individual model fields from JSON model_vals = self._json_to_model_vals(json_dict) model_vals['raw_xml'] = xml_content model_vals['xml_data_json'] = json.dumps(json_dict, ensure_ascii=False) # Step 3: Create/update profile profile = self._find_or_create_profile(model_vals, sale_order) # Step 4: Create application data record model_vals['profile_id'] = profile.id model_vals['sale_order_id'] = sale_order.id if sale_order else False app_data = self.env['fusion.adp.application.data'].create(model_vals) return profile, app_data @api.model def reparse_existing(self, app_data_record): """Re-parse an existing application data record from its raw_xml. Updates all fields in place without creating a new record. """ if not app_data_record.raw_xml: return False try: root = ET.fromstring(app_data_record.raw_xml) except ET.ParseError as e: _logger.warning('Failed to re-parse XML: %s', e) return False form = root.find('Form') if form is None: form = root json_dict = self._xml_to_json(form) model_vals = self._json_to_model_vals(json_dict) model_vals['xml_data_json'] = json.dumps(json_dict, ensure_ascii=False) # Remove fields that shouldn't be overwritten model_vals.pop('raw_xml', None) model_vals.pop('profile_id', None) model_vals.pop('sale_order_id', None) app_data_record.write(model_vals) # Also update the linked profile if app_data_record.profile_id: profile_vals = {} if model_vals.get('medical_condition'): profile_vals['medical_condition'] = model_vals['medical_condition'] if model_vals.get('mobility_status'): profile_vals['mobility_status'] = model_vals['mobility_status'] if model_vals.get('applicant_first_name'): profile_vals['first_name'] = model_vals['applicant_first_name'] if model_vals.get('applicant_last_name'): profile_vals['last_name'] = model_vals['applicant_last_name'] assessment = model_vals.get('assessment_date') if assessment: profile_vals['last_assessment_date'] = assessment if profile_vals: app_data_record.profile_id.write(profile_vals) return True # ------------------------------------------------------------------ # STEP 1: XML -> FLAT JSON DICT (every field preserved) # ------------------------------------------------------------------ def _xml_to_json(self, form): """Convert the entire Form element to a flat JSON dict with dot-notation keys.""" d = OrderedDict() d['deviceCategory'] = self._t(form, 'deviceCategory') d['VersionNumber'] = self._t(form, 'VersionNumber') # Section 1 s1 = form.find('section1') if s1 is not None: for tag in ['applicantLastname', 'applicantFirstname', 'applicantMiddleinitial', 'healthNo', 'versionNo', 'DateOfBirth', 'nameLTCH', 'unitNo', 'streetNo', 'streetName', 'rrRoute', 'city', 'province', 'postalCode', 'homePhone', 'busPhone', 'phoneExtension']: d[f'section1.{tag}'] = self._t(s1, tag) cob = s1.find('confirmationOfBenefit') if cob is not None: for tag in ['q1Yn', 'q1Ifyes', 'q2Yn', 'q3Yn']: d[f'section1.confirmationOfBenefit.{tag}'] = self._t(cob, tag) # Section 2 s2 = form.find('section2') if s2 is not None: de = s2.find('devicesandEligibility') if de is not None: for tag in ['condition', 'status', 'none', 'forearm', 'wheeled', 'manual', 'power', 'addOn', 'scooter', 'seating', 'tiltSystem', 'reclineSystem', 'legRests', 'frame', 'stroller', 'deviceForearm', 'deviceWheeled', 'deviceManual', 'deviceAmbulation', 'deviceDependent', 'deviceDynamic', 'manualDyanmic', 'manualWheelchair', 'powerBase', 'powerScooter', 'ambulation', 'positioning', 'highTech', 'standingFrame', 'adpFunded', 'nonADPFunded']: d[f'section2.devicesandEligibility.{tag}'] = self._t(de, tag) # Section 2a s2a = s2.find('section2a') if s2a is not None: for tag in ['walker', 'paediatricFrame', 'forearmCrutches', 'none', 'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial', 'confirmation1', 'confirmation2', 'confirmation3', 'confirmation4', 'confirmation5', 'confirmation6', 'seatHeight', 'seatHeightmeasurement', 'handleHeight', 'handleHeightmeasurement', 'handGrips', 'forearm', 'widthHandles', 'widthHandlesmeasurement', 'clientWeight', 'clientWeightmeasurement', 'brakes', 'brakeType', 'noWheels', 'wheelSize', 'backSupport', 'adpWalker', 'adpFrame', 'adpStanding', 'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9', 'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9', 'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18', 'custom', 'costLabour']: d[f'section2.section2a.{tag}'] = self._t(s2a, tag) # Section 2b s2b = s2.find('section2b') if s2b is not None: for tag in ['baseDevice', 'powerAddOndevice', 'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial', 'confirmation1', 'confirmation2', 'confirmation3', 'confirmation4', 'confirmation5', 'confirmation6', 'confirmation7', 'confirmation8', 'confirmation9', 'confirmation10', 'confirmation11', 'confirmation12', 'confirmation13', 'seatWidth', 'seatWidthmeasurement', 'seatDepth', 'seatDepthmeasurement', 'floorHeight', 'floorHeightmeasurement', 'caneHeight', 'caneHeightmeasurement', 'backHeight', 'backHeightmeasurement', 'restLength', 'restLengthmeasurement', 'clientWeight', 'clientWeightmeasurement', 'adjustableTension', 'heavyDuty', 'recliner', 'footplates', 'legrests', 'spoke', 'projected', 'standardManual', 'gradeAids', 'casterPin', 'amputeeAxle', 'quickRelease', 'stroller', 'oxygen', 'ventilator', 'titanium', 'clothingGuards', 'oneArm', 'uniLateral', 'plastic', 'rationale', 'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9', 'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9', 'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18', 'custom', 'costLabour']: d[f'section2.section2b.{tag}'] = self._t(s2b, tag) # Section 2c s2c = s2.find('section2c') if s2c is not None: for tag in ['baseDevice', 'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial', 'confirmation1', 'confirmation2', 'confirmation3', 'confirmation4', 'confirmation5', 'seatWidth', 'seatWidthmeasurement', 'backHeight', 'backHeightmeasurement', 'floorHeight', 'floorHeightmeasurement', 'restLength', 'restLengthmeasurement', 'seatDepth', 'seatDepthmeasurement', 'clientWeight', 'clientWeightmeasurement', 'adjustableTension', 'midline', 'manualRecline', 'footplates', 'legrests', 'swingaway', 'onePiece', 'seatPackage1', 'seatPackage2', 'oxygen', 'ventilator', 'spControls1', 'spControls2', 'spControls3', 'spControls4', 'spControls5', 'spControls6', 'autoCorrection', 'rationale', 'powerTilt', 'powerRecline', 'tiltAndRecline', 'powerElevating', 'ControlBox', 'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9', 'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9', 'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18', 'custom', 'costLabour']: d[f'section2.section2c.{tag}'] = self._t(s2c, tag) # Section 2d s2d = s2.find('section2d') if s2d is not None: for tag in ['seatM', 'seatCF', 'coverM', 'coverCF', 'optionM', 'optionCF', 'hardwareM', 'hardwareCF', 'adductorM', 'adductorCF', 'pommelCF', 'backM', 'backCF', 'supportoptionM', 'supportoptionCF', 'backcoverCF', 'backHardwareM', 'backHardwareCF', 'completeM', 'completeCF', 'headrestM', 'headrestCF', 'headoptionCF', 'headhardwareM', 'headhardwareCF', 'beltM', 'beltCF', 'beltoptionCF', 'armsupportM', 'armsupportCF', 'armoptionM', 'armoptionCF', 'armhardwareM', 'armhardwareCF', 'trayM', 'trayCF', 'trayoptionM', 'trayoptionCF', 'lateralsupportM', 'lateralsupportCF', 'lateraloptionCF', 'lateralhardwareCF', 'footsupportM', 'footsupportCF', 'footoptionM', 'footoptionCF', 'foothardwareM', 'foothardwareCF', 'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial', 'confirmation1', 'confirmation2', 'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9', 'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9', 'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18', 'custom', 'costLabour']: d[f'section2.section2d.{tag}'] = self._t(s2d, tag) # Section 3 s3 = form.find('section3') if s3 is not None: sig = s3.find('sig') if sig is not None: for tag in ['signature', 'person', 'Date']: d[f'section3.sig.{tag}'] = self._t(sig, tag) contact = s3.find('contact') if contact is not None: for tag in ['relationship', 'applicantLastname', 'applicantFirstname', 'applicantMiddleinitial', 'unitNo', 'streetNo', 'streetName', 'rrRoute', 'city', 'province', 'postalCode', 'homePhone', 'busPhone', 'phoneExtension']: d[f'section3.contact.{tag}'] = self._t(contact, tag) # Section 4 s4 = form.find('section4') if s4 is not None: auth = s4.find('authorizer') if auth is not None: for tag in ['authorizerLastname', 'authorizerFirstname', 'busPhone', 'phoneExtension', 'adpNo', 'signature', 'Date']: d[f'section4.authorizer.{tag}'] = self._t(auth, tag) vendor = s4.find('vendor') if vendor is not None: for tag in ['vendorBusName', 'adpVendorRegNo', 'vendorLastfirstname', 'positionTitle', 'vendorLocation', 'busPhone', 'phoneExtension', 'signature', 'Date']: d[f'section4.vendor.{tag}'] = self._t(vendor, tag) v2 = s4.find('vendor2') if v2 is not None: for tag in ['vendorBusName', 'adpVendorRegNo', 'vendorLastfirstname', 'positionTitle', 'vendorLocation', 'busPhone', 'phoneExtension', 'signature', 'Date']: d[f'section4.vendor2.{tag}'] = self._t(v2, tag) eq = s4.find('equipmentSpec') if eq is not None: d['section4.equipmentSpec.vendorInvoiceNo'] = self._t(eq, 'vendorInvoiceNo') d['section4.equipmentSpec.vendorADPRegNo'] = self._t(eq, 'vendorADPRegNo') t2 = eq.find('Table2') if t2 is not None: r1 = t2.find('Row1') if r1 is not None: for tag in ['Cell1', 'Cell2', 'Cell3', 'Cell4', 'Cell5']: d[f'section4.equipmentSpec.Table2.Row1.{tag}'] = self._t(r1, tag) pod = s4.find('proofOfDelivery') if pod is not None: for tag in ['signature', 'receivedBy', 'Date']: d[f'section4.proofOfDelivery.{tag}'] = self._t(pod, tag) note = s4.find('noteToADP') if note is not None: for tag in ['section1', 'section2a', 'section2b', 'section2c', 'section2d', 'section3and4', 'vendorReplacement', 'vendorCustom', 'fundingChart', 'letter']: d[f'section4.noteToADP.{tag}'] = self._t(note, tag) return d # ------------------------------------------------------------------ # STEP 2: JSON DICT -> MODEL FIELD VALUES # ------------------------------------------------------------------ def _json_to_model_vals(self, d): """Map flat JSON dict to fusion.adp.application.data field values.""" g = d.get # shorthand vals = {} # Metadata vals['device_category'] = g('deviceCategory', '') or 'MD' vals['version_number'] = g('VersionNumber', '') # Section 1 - Applicant vals['applicant_last_name'] = g('section1.applicantLastname', '') vals['applicant_first_name'] = g('section1.applicantFirstname', '') vals['applicant_middle_initial'] = g('section1.applicantMiddleinitial', '') vals['health_card_number'] = g('section1.healthNo', '') vals['health_card_version'] = g('section1.versionNo', '') vals['date_of_birth'] = self._pd(g('section1.DateOfBirth', '')) vals['ltch_name'] = g('section1.nameLTCH', '') vals['unit_number'] = g('section1.unitNo', '') vals['street_number'] = g('section1.streetNo', '') vals['street_name'] = g('section1.streetName', '') vals['rural_route'] = g('section1.rrRoute', '') vals['city'] = g('section1.city', '') vals['province'] = g('section1.province', '') vals['postal_code'] = g('section1.postalCode', '') vals['home_phone'] = g('section1.homePhone', '') vals['business_phone'] = g('section1.busPhone', '') vals['phone_extension'] = g('section1.phoneExtension', '') # Benefits q1 = g('section1.confirmationOfBenefit.q1Yn', '').lower() vals['receives_social_assistance'] = q1 == 'yes' q1type = g('section1.confirmationOfBenefit.q1Ifyes', '').lower() vals['benefit_owp'] = 'owp' in q1type if q1type else False vals['benefit_odsp'] = 'odsp' in q1type if q1type else False vals['benefit_acsd'] = 'acsd' in q1type if q1type else False if vals['benefit_owp']: vals['benefit_type'] = 'owp' elif vals['benefit_odsp']: vals['benefit_type'] = 'odsp' elif vals['benefit_acsd']: vals['benefit_type'] = 'acsd' vals['wsib_eligible'] = g('section1.confirmationOfBenefit.q2Yn', '').lower() == 'yes' vals['vac_eligible'] = g('section1.confirmationOfBenefit.q3Yn', '').lower() == 'yes' # Section 2 - Devices & Eligibility vals['medical_condition'] = g('section2.devicesandEligibility.condition', '') vals['mobility_status'] = g('section2.devicesandEligibility.status', '') # Previously funded vals['prev_funded_none'] = bool(g('section2.devicesandEligibility.none', '')) vals['prev_funded_forearm'] = bool(g('section2.devicesandEligibility.forearm', '')) vals['prev_funded_wheeled'] = bool(g('section2.devicesandEligibility.wheeled', '')) vals['prev_funded_manual'] = bool(g('section2.devicesandEligibility.manual', '')) vals['prev_funded_power'] = bool(g('section2.devicesandEligibility.power', '')) vals['prev_funded_addon'] = bool(g('section2.devicesandEligibility.addOn', '')) vals['prev_funded_scooter'] = bool(g('section2.devicesandEligibility.scooter', '')) vals['prev_funded_seating'] = bool(g('section2.devicesandEligibility.seating', '')) vals['prev_funded_tilt'] = bool(g('section2.devicesandEligibility.tiltSystem', '')) vals['prev_funded_recline'] = bool(g('section2.devicesandEligibility.reclineSystem', '')) vals['prev_funded_legrests'] = bool(g('section2.devicesandEligibility.legRests', '')) vals['prev_funded_frame'] = bool(g('section2.devicesandEligibility.frame', '')) vals['prev_funded_stroller'] = bool(g('section2.devicesandEligibility.stroller', '')) # Devices currently required vals['device_forearm_crutches'] = bool(g('section2.devicesandEligibility.deviceForearm', '')) vals['device_wheeled_walker'] = bool(g('section2.devicesandEligibility.deviceWheeled', '')) vals['device_manual_wheelchair'] = bool(g('section2.devicesandEligibility.deviceManual', '')) vals['device_ambulation_manual'] = bool(g('section2.devicesandEligibility.deviceAmbulation', '')) vals['device_dependent_wheelchair'] = bool(g('section2.devicesandEligibility.deviceDependent', '')) vals['device_dynamic_tilt'] = bool(g('section2.devicesandEligibility.deviceDynamic', '')) vals['device_manual_dynamic'] = bool(g('section2.devicesandEligibility.manualDyanmic', '')) vals['device_manual_power_addon'] = bool(g('section2.devicesandEligibility.manualWheelchair', '')) vals['device_power_base'] = bool(g('section2.devicesandEligibility.powerBase', '')) vals['device_power_scooter'] = bool(g('section2.devicesandEligibility.powerScooter', '')) vals['device_ambulation_power'] = bool(g('section2.devicesandEligibility.ambulation', '')) vals['device_positioning'] = bool(g('section2.devicesandEligibility.positioning', '')) vals['device_high_tech'] = bool(g('section2.devicesandEligibility.highTech', '')) vals['device_standing_frame'] = bool(g('section2.devicesandEligibility.standingFrame', '')) vals['device_adp_funded_mods'] = bool(g('section2.devicesandEligibility.adpFunded', '')) vals['device_non_adp_funded_mods'] = bool(g('section2.devicesandEligibility.nonADPFunded', '')) # Section 2a - Walkers vals['s2a_base_device'] = g('section2.section2a.walker', '') vals['s2a_paediatric_frame'] = g('section2.section2a.paediatricFrame', '') vals['s2a_forearm_crutches'] = g('section2.section2a.forearmCrutches', '') vals['s2a_none'] = g('section2.section2a.none', '') vals['s2a_reason'] = g('section2.section2a.reason', '') vals['s2a_replacement_status'] = g('section2.section2a.replacementStatus', '') vals['s2a_replacement_size'] = g('section2.section2a.replacementSize', '') vals['s2a_replacement_adp'] = g('section2.section2a.replacementADP', '') vals['s2a_replacement_special'] = g('section2.section2a.replacementSpecial', '') for i in range(1, 7): vals[f's2a_confirm{i}'] = g(f'section2.section2a.confirmation{i}', '') vals['s2a_seat_height'] = g('section2.section2a.seatHeight', '') vals['s2a_seat_height_unit'] = g('section2.section2a.seatHeightmeasurement', '') vals['s2a_handle_height'] = g('section2.section2a.handleHeight', '') vals['s2a_handle_height_unit'] = g('section2.section2a.handleHeightmeasurement', '') vals['s2a_hand_grips'] = g('section2.section2a.handGrips', '') vals['s2a_forearm_attachments'] = g('section2.section2a.forearm', '') vals['s2a_width_handles'] = g('section2.section2a.widthHandles', '') vals['s2a_width_handles_unit'] = g('section2.section2a.widthHandlesmeasurement', '') vals['s2a_client_weight'] = g('section2.section2a.clientWeight', '') vals['s2a_client_weight_unit'] = g('section2.section2a.clientWeightmeasurement', '') vals['s2a_brakes'] = g('section2.section2a.brakes', '') vals['s2a_brake_type'] = g('section2.section2a.brakeType', '') vals['s2a_num_wheels'] = g('section2.section2a.noWheels', '') vals['s2a_wheel_size'] = g('section2.section2a.wheelSize', '') vals['s2a_back_support'] = g('section2.section2a.backSupport', '') vals['s2a_adp_walker'] = g('section2.section2a.adpWalker', '') vals['s2a_adp_frame'] = g('section2.section2a.adpFrame', '') vals['s2a_adp_standing'] = g('section2.section2a.adpStanding', '') vals['s2a_custom'] = g('section2.section2a.custom', '') vals['s2a_cost_labour'] = g('section2.section2a.costLabour', '') # Section 2b - Manual Wheelchairs vals['s2b_base_device'] = g('section2.section2b.baseDevice', '') vals['s2b_power_addon'] = g('section2.section2b.powerAddOndevice', '') vals['s2b_reason'] = g('section2.section2b.reason', '') vals['s2b_replacement_status'] = g('section2.section2b.replacementStatus', '') vals['s2b_replacement_size'] = g('section2.section2b.replacementSize', '') vals['s2b_replacement_adp'] = g('section2.section2b.replacementADP', '') vals['s2b_replacement_special'] = g('section2.section2b.replacementSpecial', '') for i in range(1, 14): vals[f's2b_confirm{i}'] = g(f'section2.section2b.confirmation{i}', '') vals['s2b_seat_width'] = g('section2.section2b.seatWidth', '') vals['s2b_seat_width_unit'] = g('section2.section2b.seatWidthmeasurement', '') vals['s2b_seat_depth'] = g('section2.section2b.seatDepth', '') vals['s2b_seat_depth_unit'] = g('section2.section2b.seatDepthmeasurement', '') vals['s2b_floor_height'] = g('section2.section2b.floorHeight', '') vals['s2b_floor_height_unit'] = g('section2.section2b.floorHeightmeasurement', '') vals['s2b_cane_height'] = g('section2.section2b.caneHeight', '') vals['s2b_cane_height_unit'] = g('section2.section2b.caneHeightmeasurement', '') vals['s2b_back_height'] = g('section2.section2b.backHeight', '') vals['s2b_back_height_unit'] = g('section2.section2b.backHeightmeasurement', '') vals['s2b_rest_length'] = g('section2.section2b.restLength', '') vals['s2b_rest_length_unit'] = g('section2.section2b.restLengthmeasurement', '') vals['s2b_client_weight'] = g('section2.section2b.clientWeight', '') vals['s2b_client_weight_unit'] = g('section2.section2b.clientWeightmeasurement', '') vals['s2b_adjustable_tension'] = bool(g('section2.section2b.adjustableTension', '')) vals['s2b_heavy_duty'] = bool(g('section2.section2b.heavyDuty', '')) vals['s2b_recliner'] = bool(g('section2.section2b.recliner', '')) vals['s2b_footplates'] = bool(g('section2.section2b.footplates', '')) vals['s2b_legrests'] = bool(g('section2.section2b.legrests', '')) vals['s2b_spoke'] = bool(g('section2.section2b.spoke', '')) vals['s2b_projected'] = bool(g('section2.section2b.projected', '')) vals['s2b_standard_manual'] = bool(g('section2.section2b.standardManual', '')) vals['s2b_grade_aids'] = bool(g('section2.section2b.gradeAids', '')) vals['s2b_caster_pin'] = bool(g('section2.section2b.casterPin', '')) vals['s2b_amputee_axle'] = bool(g('section2.section2b.amputeeAxle', '')) vals['s2b_quick_release'] = bool(g('section2.section2b.quickRelease', '')) vals['s2b_stroller'] = bool(g('section2.section2b.stroller', '')) vals['s2b_oxygen'] = bool(g('section2.section2b.oxygen', '')) vals['s2b_ventilator'] = bool(g('section2.section2b.ventilator', '')) vals['s2b_titanium'] = bool(g('section2.section2b.titanium', '')) vals['s2b_clothing_guards'] = bool(g('section2.section2b.clothingGuards', '')) vals['s2b_one_arm'] = bool(g('section2.section2b.oneArm', '')) vals['s2b_uni_lateral'] = bool(g('section2.section2b.uniLateral', '')) vals['s2b_plastic'] = bool(g('section2.section2b.plastic', '')) vals['s2b_rationale'] = g('section2.section2b.rationale', '') vals['s2b_custom'] = g('section2.section2b.custom', '') vals['s2b_cost_labour'] = g('section2.section2b.costLabour', '') # Section 2c - Power Bases / Scooters vals['s2c_base_device'] = g('section2.section2c.baseDevice', '') vals['s2c_reason'] = g('section2.section2c.reason', '') vals['s2c_replacement_status'] = g('section2.section2c.replacementStatus', '') vals['s2c_replacement_size'] = g('section2.section2c.replacementSize', '') vals['s2c_replacement_adp'] = g('section2.section2c.replacementADP', '') vals['s2c_replacement_special'] = g('section2.section2c.replacementSpecial', '') for i in range(1, 6): vals[f's2c_confirm{i}'] = g(f'section2.section2c.confirmation{i}', '') vals['s2c_seat_width'] = g('section2.section2c.seatWidth', '') vals['s2c_seat_width_unit'] = g('section2.section2c.seatWidthmeasurement', '') vals['s2c_back_height'] = g('section2.section2c.backHeight', '') vals['s2c_back_height_unit'] = g('section2.section2c.backHeightmeasurement', '') vals['s2c_floor_height'] = g('section2.section2c.floorHeight', '') vals['s2c_floor_height_unit'] = g('section2.section2c.floorHeightmeasurement', '') vals['s2c_rest_length'] = g('section2.section2c.restLength', '') vals['s2c_rest_length_unit'] = g('section2.section2c.restLengthmeasurement', '') vals['s2c_seat_depth'] = g('section2.section2c.seatDepth', '') vals['s2c_seat_depth_unit'] = g('section2.section2c.seatDepthmeasurement', '') vals['s2c_client_weight'] = g('section2.section2c.clientWeight', '') vals['s2c_client_weight_unit'] = g('section2.section2c.clientWeightmeasurement', '') vals['s2c_adjustable_tension'] = bool(g('section2.section2c.adjustableTension', '')) vals['s2c_midline'] = bool(g('section2.section2c.midline', '')) vals['s2c_manual_recline'] = bool(g('section2.section2c.manualRecline', '')) vals['s2c_footplates'] = bool(g('section2.section2c.footplates', '')) vals['s2c_legrests'] = bool(g('section2.section2c.legrests', '')) vals['s2c_swingaway'] = bool(g('section2.section2c.swingaway', '')) vals['s2c_one_piece'] = bool(g('section2.section2c.onePiece', '')) vals['s2c_seat_package_1'] = bool(g('section2.section2c.seatPackage1', '')) vals['s2c_seat_package_2'] = bool(g('section2.section2c.seatPackage2', '')) vals['s2c_oxygen'] = bool(g('section2.section2c.oxygen', '')) vals['s2c_ventilator'] = bool(g('section2.section2c.ventilator', '')) vals['s2c_sp_controls_1'] = bool(g('section2.section2c.spControls1', '')) vals['s2c_sp_controls_2'] = bool(g('section2.section2c.spControls2', '')) vals['s2c_sp_controls_3'] = bool(g('section2.section2c.spControls3', '')) vals['s2c_sp_controls_4'] = bool(g('section2.section2c.spControls4', '')) vals['s2c_sp_controls_5'] = bool(g('section2.section2c.spControls5', '')) vals['s2c_sp_controls_6'] = bool(g('section2.section2c.spControls6', '')) vals['s2c_auto_correction'] = bool(g('section2.section2c.autoCorrection', '')) vals['s2c_rationale'] = g('section2.section2c.rationale', '') vals['s2c_power_tilt'] = bool(g('section2.section2c.powerTilt', '')) vals['s2c_power_recline'] = bool(g('section2.section2c.powerRecline', '')) vals['s2c_tilt_and_recline'] = bool(g('section2.section2c.tiltAndRecline', '')) vals['s2c_power_elevating'] = bool(g('section2.section2c.powerElevating', '')) vals['s2c_control_box'] = bool(g('section2.section2c.ControlBox', '')) vals['s2c_custom'] = g('section2.section2c.custom', '') vals['s2c_cost_labour'] = g('section2.section2c.costLabour', '') # Section 2d - Positioning/Seating vals['s2d_seat_modular'] = bool(g('section2.section2d.seatM', '')) vals['s2d_seat_custom'] = bool(g('section2.section2d.seatCF', '')) vals['s2d_seat_cover_modular'] = bool(g('section2.section2d.coverM', '')) vals['s2d_seat_cover_custom'] = bool(g('section2.section2d.coverCF', '')) vals['s2d_seat_option_modular'] = bool(g('section2.section2d.optionM', '')) vals['s2d_seat_option_custom'] = bool(g('section2.section2d.optionCF', '')) vals['s2d_seat_hardware_modular'] = bool(g('section2.section2d.hardwareM', '')) vals['s2d_seat_hardware_custom'] = bool(g('section2.section2d.hardwareCF', '')) vals['s2d_adductor_modular'] = bool(g('section2.section2d.adductorM', '')) vals['s2d_adductor_custom'] = bool(g('section2.section2d.adductorCF', '')) vals['s2d_pommel_custom'] = bool(g('section2.section2d.pommelCF', '')) vals['s2d_back_modular'] = bool(g('section2.section2d.backM', '')) vals['s2d_back_custom'] = bool(g('section2.section2d.backCF', '')) vals['s2d_back_option_modular'] = bool(g('section2.section2d.supportoptionM', '')) vals['s2d_back_option_custom'] = bool(g('section2.section2d.supportoptionCF', '')) vals['s2d_back_cover_custom'] = bool(g('section2.section2d.backcoverCF', '')) vals['s2d_back_hardware_modular'] = bool(g('section2.section2d.backHardwareM', '')) vals['s2d_back_hardware_custom'] = bool(g('section2.section2d.backHardwareCF', '')) vals['s2d_complete_modular'] = bool(g('section2.section2d.completeM', '')) vals['s2d_complete_custom'] = bool(g('section2.section2d.completeCF', '')) vals['s2d_headrest_modular'] = bool(g('section2.section2d.headrestM', '')) vals['s2d_headrest_custom'] = bool(g('section2.section2d.headrestCF', '')) vals['s2d_head_option_custom'] = bool(g('section2.section2d.headoptionCF', '')) vals['s2d_head_hardware_modular'] = bool(g('section2.section2d.headhardwareM', '')) vals['s2d_head_hardware_custom'] = bool(g('section2.section2d.headhardwareCF', '')) vals['s2d_belt_modular'] = bool(g('section2.section2d.beltM', '')) vals['s2d_belt_custom'] = bool(g('section2.section2d.beltCF', '')) vals['s2d_belt_option_custom'] = bool(g('section2.section2d.beltoptionCF', '')) vals['s2d_arm_modular'] = bool(g('section2.section2d.armsupportM', '')) vals['s2d_arm_custom'] = bool(g('section2.section2d.armsupportCF', '')) vals['s2d_arm_option_modular'] = bool(g('section2.section2d.armoptionM', '')) vals['s2d_arm_option_custom'] = bool(g('section2.section2d.armoptionCF', '')) vals['s2d_arm_hardware_modular'] = bool(g('section2.section2d.armhardwareM', '')) vals['s2d_arm_hardware_custom'] = bool(g('section2.section2d.armhardwareCF', '')) vals['s2d_tray_modular'] = bool(g('section2.section2d.trayM', '')) vals['s2d_tray_custom'] = bool(g('section2.section2d.trayCF', '')) vals['s2d_tray_option_modular'] = bool(g('section2.section2d.trayoptionM', '')) vals['s2d_tray_option_custom'] = bool(g('section2.section2d.trayoptionCF', '')) vals['s2d_lateral_modular'] = bool(g('section2.section2d.lateralsupportM', '')) vals['s2d_lateral_custom'] = bool(g('section2.section2d.lateralsupportCF', '')) vals['s2d_lateral_option_custom'] = bool(g('section2.section2d.lateraloptionCF', '')) vals['s2d_lateral_hardware_custom'] = bool(g('section2.section2d.lateralhardwareCF', '')) vals['s2d_foot_modular'] = bool(g('section2.section2d.footsupportM', '')) vals['s2d_foot_custom'] = bool(g('section2.section2d.footsupportCF', '')) vals['s2d_foot_option_modular'] = bool(g('section2.section2d.footoptionM', '')) vals['s2d_foot_option_custom'] = bool(g('section2.section2d.footoptionCF', '')) vals['s2d_foot_hardware_modular'] = bool(g('section2.section2d.foothardwareM', '')) vals['s2d_foot_hardware_custom'] = bool(g('section2.section2d.foothardwareCF', '')) vals['s2d_reason'] = g('section2.section2d.reason', '') vals['s2d_replacement_status'] = g('section2.section2d.replacementStatus', '') vals['s2d_replacement_size'] = g('section2.section2d.replacementSize', '') vals['s2d_replacement_adp'] = g('section2.section2d.replacementADP', '') vals['s2d_replacement_special'] = g('section2.section2d.replacementSpecial', '') vals['s2d_confirm1'] = g('section2.section2d.confirmation1', '') vals['s2d_confirm2'] = g('section2.section2d.confirmation2', '') vals['s2d_custom'] = g('section2.section2d.custom', '') vals['s2d_cost_labour'] = g('section2.section2d.costLabour', '') # Section 3 - Consent vals['consent_date'] = self._pd(g('section3.sig.Date', '')) person = g('section3.sig.person', '').lower() vals['consent_signed_by'] = 'applicant' if 'applicant' in person else ('agent' if 'agent' in person else False) vals['agent_relationship'] = g('section3.contact.relationship', '') vals['agent_last_name'] = g('section3.contact.applicantLastname', '') vals['agent_first_name'] = g('section3.contact.applicantFirstname', '') vals['agent_middle_initial'] = g('section3.contact.applicantMiddleinitial', '') vals['agent_unit'] = g('section3.contact.unitNo', '') vals['agent_street_no'] = g('section3.contact.streetNo', '') vals['agent_street_name'] = g('section3.contact.streetName', '') vals['agent_rural_route'] = g('section3.contact.rrRoute', '') vals['agent_city'] = g('section3.contact.city', '') vals['agent_province'] = g('section3.contact.province', '') vals['agent_postal_code'] = g('section3.contact.postalCode', '') vals['agent_home_phone'] = g('section3.contact.homePhone', '') vals['agent_bus_phone'] = g('section3.contact.busPhone', '') vals['agent_phone_ext'] = g('section3.contact.phoneExtension', '') # Section 4 - Authorizer vals['authorizer_last_name'] = g('section4.authorizer.authorizerLastname', '') vals['authorizer_first_name'] = g('section4.authorizer.authorizerFirstname', '') vals['authorizer_phone'] = g('section4.authorizer.busPhone', '') vals['authorizer_phone_ext'] = g('section4.authorizer.phoneExtension', '') vals['authorizer_adp_number'] = g('section4.authorizer.adpNo', '') vals['assessment_date'] = self._pd(g('section4.authorizer.Date', '')) vals['application_date'] = vals['consent_date'] or vals['assessment_date'] # Section 4 - Vendor 1 vals['vendor_business_name'] = g('section4.vendor.vendorBusName', '') vals['vendor_adp_number'] = g('section4.vendor.adpVendorRegNo', '') vals['vendor_representative'] = g('section4.vendor.vendorLastfirstname', '') vals['vendor_position'] = g('section4.vendor.positionTitle', '') vals['vendor_location'] = g('section4.vendor.vendorLocation', '') vals['vendor_phone'] = g('section4.vendor.busPhone', '') vals['vendor_phone_ext'] = g('section4.vendor.phoneExtension', '') vals['vendor_sign_date'] = self._pd(g('section4.vendor.Date', '')) # Section 4 - Vendor 2 vals['vendor2_business_name'] = g('section4.vendor2.vendorBusName', '') vals['vendor2_adp_number'] = g('section4.vendor2.adpVendorRegNo', '') vals['vendor2_representative'] = g('section4.vendor2.vendorLastfirstname', '') vals['vendor2_position'] = g('section4.vendor2.positionTitle', '') vals['vendor2_location'] = g('section4.vendor2.vendorLocation', '') vals['vendor2_phone'] = g('section4.vendor2.busPhone', '') vals['vendor2_phone_ext'] = g('section4.vendor2.phoneExtension', '') vals['vendor2_sign_date'] = self._pd(g('section4.vendor2.Date', '')) # Equipment Spec vals['equip_vendor_invoice_no'] = g('section4.equipmentSpec.vendorInvoiceNo', '') vals['equip_vendor_adp_reg'] = g('section4.equipmentSpec.vendorADPRegNo', '') vals['equip_cell1'] = g('section4.equipmentSpec.Table2.Row1.Cell1', '') vals['equip_cell2'] = g('section4.equipmentSpec.Table2.Row1.Cell2', '') vals['equip_cell3'] = g('section4.equipmentSpec.Table2.Row1.Cell3', '') vals['equip_cell4'] = g('section4.equipmentSpec.Table2.Row1.Cell4', '') vals['equip_cell5'] = g('section4.equipmentSpec.Table2.Row1.Cell5', '') vals['pod_received_by'] = g('section4.proofOfDelivery.receivedBy', '') vals['pod_date'] = self._pd(g('section4.proofOfDelivery.Date', '')) # Note to ADP vals['note_section1'] = bool(g('section4.noteToADP.section1', '')) vals['note_section2a'] = bool(g('section4.noteToADP.section2a', '')) vals['note_section2b'] = bool(g('section4.noteToADP.section2b', '')) vals['note_section2c'] = bool(g('section4.noteToADP.section2c', '')) vals['note_section2d'] = bool(g('section4.noteToADP.section2d', '')) vals['note_section3and4'] = bool(g('section4.noteToADP.section3and4', '')) vals['note_vendor_replacement'] = g('section4.noteToADP.vendorReplacement', '') vals['note_vendor_custom'] = g('section4.noteToADP.vendorCustom', '') vals['note_funding_chart'] = g('section4.noteToADP.fundingChart', '') vals['note_letter'] = g('section4.noteToADP.letter', '') return vals # ------------------------------------------------------------------ # PROFILE MANAGEMENT # ------------------------------------------------------------------ def _find_or_create_profile(self, vals, sale_order=None): """Find or create a client profile from parsed application data.""" Profile = self.env['fusion.client.profile'] hc = (vals.get('health_card_number') or '').strip() first = (vals.get('applicant_first_name') or '').strip() last = (vals.get('applicant_last_name') or '').strip() dob = vals.get('date_of_birth') profile = False if hc: profile = Profile.search([('health_card_number', '=', hc)], limit=1) if not profile and first and last and dob: profile = Profile.search([ ('first_name', '=ilike', first), ('last_name', '=ilike', last), ('date_of_birth', '=', dob), ], limit=1) profile_vals = { 'first_name': first, 'last_name': last, 'middle_initial': vals.get('applicant_middle_initial', ''), 'health_card_number': hc, 'health_card_version': vals.get('health_card_version', ''), 'date_of_birth': dob, 'ltch_name': vals.get('ltch_name', ''), 'unit_number': vals.get('unit_number', ''), 'street_number': vals.get('street_number', ''), 'street_name': vals.get('street_name', ''), 'rural_route': vals.get('rural_route', ''), 'city': vals.get('city', ''), 'province': vals.get('province', '') or 'ON', 'postal_code': vals.get('postal_code', ''), 'home_phone': vals.get('home_phone', ''), 'business_phone': vals.get('business_phone', ''), 'phone_extension': vals.get('phone_extension', ''), 'medical_condition': vals.get('medical_condition', ''), 'mobility_status': vals.get('mobility_status', ''), } if vals.get('receives_social_assistance'): profile_vals['receives_social_assistance'] = True profile_vals['benefit_type'] = vals.get('benefit_type') if vals.get('wsib_eligible'): profile_vals['wsib_eligible'] = True if vals.get('vac_eligible'): profile_vals['vac_eligible'] = True if vals.get('assessment_date'): profile_vals['last_assessment_date'] = vals['assessment_date'] # Link to partner if sale_order and sale_order.partner_id: profile_vals['partner_id'] = sale_order.partner_id.id elif not profile or not profile.partner_id: partner = self._find_partner(first, last) if partner: profile_vals['partner_id'] = partner.id if profile: profile.write(profile_vals) else: profile = Profile.create(profile_vals) return profile def _find_partner(self, first_name, last_name): """Try to find a matching res.partner.""" if not first_name or not last_name: return False Partner = self.env['res.partner'] partner = Partner.search([('name', 'ilike', f'{first_name} {last_name}')], limit=1) if not partner: partner = Partner.search([('name', 'ilike', f'{last_name}, {first_name}')], limit=1) return partner or False # ------------------------------------------------------------------ # HELPERS # ------------------------------------------------------------------ @staticmethod def _t(element, tag): """Get text of child element, empty string if missing.""" child = element.find(tag) if child is not None and child.text: return child.text.strip() return '' @staticmethod def _pd(date_str): """Parse date string, return date or False.""" if not date_str: return False for fmt in ('%Y/%m/%d', '%Y-%m-%d', '%Y%m%d'): try: return datetime.strptime(date_str.strip(), fmt).date() except ValueError: continue return False