# -*- coding: utf-8 -*- from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError from markupsafe import Markup import json import uuid import logging _logger = logging.getLogger(__name__) class WheelchairAssessment(models.Model): _name = 'fusion.wc.assessment' _description = 'Wheelchair Assessment & Quotation Builder' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'create_date desc, id desc' _rec_name = 'display_name' # ========================================================================= # STATE & IDENTITY # ========================================================================= state = fields.Selection([ ('draft', 'In Progress'), ('review', 'Ready for Review'), ('quoted', 'Quotation Generated'), ('cancelled', 'Cancelled'), ], string='Status', default='draft', tracking=True, index=True) display_name = fields.Char( string='Display Name', compute='_compute_display_name', store=True, ) reference = fields.Char( string='Reference', readonly=True, copy=False, default=lambda self: _('New'), ) # ========================================================================= # STEP 1: CLIENT & EQUIPMENT # ========================================================================= partner_id = fields.Many2one('res.partner', string='Client', tracking=True, index=True) create_new_partner = fields.Boolean(string='New Client', default=False) # Client info (for new clients or override) client_first_name = fields.Char(string='First Name') client_last_name = fields.Char(string='Last Name') client_name = fields.Char(string='Client Name', compute='_compute_client_name', store=True) client_phone = fields.Char(string='Phone') client_email = fields.Char(string='Email') client_street = fields.Char(string='Street') client_street2 = fields.Char(string='Unit/Apt') client_city = fields.Char(string='City') client_state_id = fields.Many2one('res.country.state', string='Province') client_zip = fields.Char(string='Postal Code') client_country_id = fields.Many2one('res.country', string='Country') client_dob = fields.Date(string='Date of Birth') client_health_card = fields.Char(string='Health Card Number') equipment_type = fields.Selection( selection='_get_equipment_type_selection', string='Equipment Type', required=True, tracking=True, index=True) @api.model def _get_equipment_type_selection(self): types = self.env['fusion.equipment.type'].sudo().search([], order='sequence') if types: return [(t.code, t.name) for t in types] return [ ('manual_wheelchair', 'Manual Wheelchair'), ('power_wheelchair', 'Power Wheelchair / Scooter'), ('walker', 'Walker / Rollator / Ambulation Aid'), ] wheelchair_type = fields.Selection([ ('type_1', 'Type 1 - Standard'), ('type_2', 'Type 2 - Lightweight'), ('type_3', 'Type 3 - Ultra Lightweight'), ('type_4', 'Type 4 - Rigid Frame'), ('type_5', 'Type 5 - Dynamic Tilt'), ], string='Wheelchair Category') powerchair_type = fields.Selection([ ('type_1', 'Power Base Type 1'), ('type_2', 'Power Base Type 2'), ('type_3', 'Power Base Type 3'), ('scooter', 'Power Scooter'), ], string='Power Device Category') walker_type = fields.Selection([ ('adult_walker_1', 'Adult Wheeled Walker Type 1'), ('adult_walker_2', 'Adult Wheeled Walker Type 2'), ('adult_walker_3', 'Adult Wheeled Walker Type 3'), ('paed_walker_1', 'Paediatric Specific Wheeled Walker Type 1'), ('paed_walker_2', 'Paediatric Specific Wheeled Walker Type 2'), ('paed_walking_frame', 'Paediatric Specific Walking Frame'), ('paed_standing_1', 'Paediatric Standing Frame Type 1'), ('paed_standing_2', 'Paediatric Standing Frame Type 2'), ('forearm_crutches', 'Forearm Crutches'), ('stroller', 'Paediatric Specific Specialty Stroller'), ], string='Walker / Aid Category') client_type = fields.Selection([ ('reg', 'REG - Regular ADP (75/25)'), ('ods', 'ODS - ODSP (100% ADP)'), ('acs', 'ACS - ACSD (100% ADP)'), ('owp', 'OWP - Ontario Works (100% ADP)'), ('ltc', 'LTC - Long Term Care'), ('sen', 'SEN - Senior'), ('cca', 'CCA - Community Care'), ], string='Client Type', default='reg') build_type = fields.Selection([ ('modular', 'Modular'), ('custom_fabricated', 'Custom Fabricated'), ], string='Build Type', default='modular') reason_for_application = fields.Selection([ ('first_access', 'First Access'), ('additions', 'Additions'), ('modifications', 'Modifications'), ('replacements', 'Replacements'), ], string='Reason for Application') # Assessment context sales_rep_id = fields.Many2one('res.users', string='Sales Rep', default=lambda self: self.env.user, tracking=True, index=True) authorizer_id = fields.Many2one('res.partner', string='Authorizer/OT', tracking=True) assessment_date = fields.Datetime(string='Assessment Date', default=fields.Datetime.now) # ========================================================================= # STEP 2: ADP PRESCRIPTION MEASUREMENTS # ========================================================================= seat_width = fields.Float(string='Seat Width', digits=(10, 2)) seat_width_unit = fields.Selection([ ('inches', 'Inches'), ('cm', 'cm') ], string='Seat Width Unit', default='inches') seat_depth = fields.Float(string='Seat Depth', digits=(10, 2)) seat_depth_unit = fields.Selection([ ('inches', 'Inches'), ('cm', 'cm') ], string='Seat Depth Unit', default='inches') finished_seat_to_floor_height = fields.Float( string='Finished Seat to Floor Height', digits=(10, 2)) seat_to_floor_unit = fields.Selection([ ('inches', 'Inches'), ('cm', 'cm') ], string='Seat to Floor Unit', default='inches') back_cane_height = fields.Float(string='Back Cane Height', digits=(10, 2)) cane_height_unit = fields.Selection([ ('inches', 'Inches'), ('cm', 'cm') ], string='Cane Height Unit', default='inches') finished_back_height = fields.Float( string='Finished Back Height', digits=(10, 2)) back_height_unit = fields.Selection([ ('inches', 'Inches'), ('cm', 'cm') ], string='Back Height Unit', default='inches') finished_leg_rest_length = fields.Float( string='Finished Leg Rest Length', digits=(10, 2)) leg_rest_unit = fields.Selection([ ('inches', 'Inches'), ('cm', 'cm') ], string='Leg Rest Unit', default='inches') client_weight = fields.Float(string='Client Weight', digits=(10, 1)) client_weight_unit = fields.Selection([ ('lbs', 'lbs'), ('kg', 'kg') ], string='Weight Unit', default='lbs') # ========================================================================= # WALKER / AMBULATION AID PRESCRIPTION (Section 2a) # ========================================================================= walker_seat_height = fields.Float(string='Seat Height', digits=(10, 2)) walker_seat_height_unit = fields.Selection([ ('inches', 'Inches'), ('cm', 'cm'), ('na', 'N/A') ], string='Seat Height Unit', default='inches') push_handle_height = fields.Float(string='Push Handle Height', digits=(10, 2)) push_handle_height_unit = fields.Selection([ ('inches', 'Inches'), ('cm', 'cm') ], string='Push Handle Height Unit', default='inches') hand_grips = fields.Selection([ ('none', 'None'), ('standard', 'Standard'), ('anatomical', 'Anatomical'), ], string='Hand Grips') forearm_attachments = fields.Selection([ ('one', 'One'), ('two', 'Two'), ], string='Forearm Attachments') width_between_push_handles = fields.Float( string='Width Between Push Handles', digits=(10, 2)) push_handle_width_unit = fields.Selection([ ('inches', 'Inches'), ('cm', 'cm') ], string='Push Handle Width Unit', default='inches') walker_brakes = fields.Selection([ ('none', 'None'), ('push_to_lock', 'Push-To-Lock'), ('auto_stop', 'Auto Stop'), ], string='Brakes') walker_brake_type = fields.Selection([ ('none', 'None'), ('bilateral', 'Bilateral'), ('one_hand', 'One Hand'), ], string='Brake Type') walker_num_wheels = fields.Selection([ ('two', 'Two'), ('three', 'Three'), ('four', 'Four'), ], string='Number of Wheels') walker_wheel_size = fields.Selection([ ('4_6', '4-6 inches'), ('6_8', '6-8 inches'), ('8_10', '8-10 inches'), ], string='Wheel Size') walker_back_support = fields.Selection([ ('yes', 'Yes'), ('no', 'No'), ], string='Back Support') # Walker ADP options (checkboxes) walker_adolescent_wheeled_walker = fields.Boolean( string='Adolescent Size Paediatric Specific Wheeled Walker') walker_adolescent_walking_frame = fields.Boolean( string='Adolescent Size Paediatric Wheeled Walker – Walking Frame') walker_adolescent_standing_frame = fields.Boolean( string='Adolescent Size Paediatric Standing Frame') # ========================================================================= # POWER BASE / SCOOTER PRESCRIPTION (Section 2c) # ========================================================================= # Note: power bases share seat_width, finished_back_height, # finished_seat_to_floor_height, finished_leg_rest_length, seat_depth, # and client_weight from the main measurement fields above. # Scooters only require client_weight (field 6). # Power ADP options (checkboxes from screenshot) pw_adjustable_tension_back = fields.Boolean( string='Adjustable Tension Back Upholstery') pw_midline_control = fields.Boolean(string='Midline Control') pw_manual_recline = fields.Boolean(string='Manual Recline Option') pw_angle_adjustable_footplates = fields.Boolean( string='Angle Adjustable Footplates (pair)') pw_manual_elevating_legrests = fields.Boolean( string='Manual Elevating Legrests (pair)') pw_swingaway_bracket = fields.Boolean(string='Swingaway Mounting Bracket') pw_front_riggings = fields.Boolean(string='One Piece 90/90 Front Riggings') pw_seat_package_1 = fields.Boolean( string='Seat Package 1 for Power Bases', help='Includes frame, sling upholstery, armrests, footrests') pw_seat_package_2 = fields.Boolean( string='Seat Package 2 for Power Bases', help='Includes deluxe seat and back, armrests, footrests') pw_oxygen_tank = fields.Boolean(string='Oxygen Tank Holder') pw_ventilator_tray = fields.Boolean(string='Ventilator Tray') # Specialty Components (* require clinical rationale) pw_specialty_1_joystick = fields.Boolean( string='Specialty Controls 1 Non Standard Joystick*') pw_specialty_2_chin = fields.Boolean( string='Specialty Controls 2 Chin/Rim Control*') pw_specialty_3_touch = fields.Boolean( string='Specialty Controls 3 Simple Touch*') pw_specialty_4_proximity = fields.Boolean( string='Specialty Controls 4 Proximity Control*') pw_specialty_5_breath = fields.Boolean( string='Specialty Controls 5 Breath Control*') pw_specialty_6_scanners = fields.Boolean( string='Specialty Controls 6 Scanners*') pw_auto_correction = fields.Boolean(string='Auto Correction System*') pw_specialty_rationale = fields.Text( string='Clinical Rationale for Specialty Components') # Power Positioning Devices (require Justification for Funding Chart) pw_power_tilt_only = fields.Boolean(string='Power Tilt Only') pw_power_recline_only = fields.Boolean(string='Power Recline Only') pw_power_tilt_recline = fields.Boolean(string='Power Tilt and Recline') pw_power_elevating_footrests = fields.Boolean( string='Power Elevating Footrests') pw_multi_function_control = fields.Boolean( string='Multi-Function Control Box') pw_power_add_on = fields.Boolean(string='Power Add-On Device') # Computed: convert to inches/lbs for rule evaluation seat_width_inches = fields.Float( compute='_compute_normalized_measurements', store=True) seat_depth_inches = fields.Float( compute='_compute_normalized_measurements', store=True) back_height_inches = fields.Float( compute='_compute_normalized_measurements', store=True) client_weight_lbs = fields.Float( compute='_compute_normalized_measurements', store=True) # ========================================================================= # STEP 3: FRAME # ========================================================================= frame_product_tmpl_id = fields.Many2one('product.template', string='Frame Template') frame_product_id = fields.Many2one('product.product', string='Wheelchair Frame') frame_notes = fields.Text(string='Frame Notes') # ========================================================================= # STEP 4 & 5: SEATING, OPTIONS, ACCESSORIES (via lines) # ========================================================================= line_ids = fields.One2many('fusion.wc.assessment.line', 'assessment_id', string='Selected Items') upcharge_line_ids = fields.One2many('fusion.wc.assessment.line', 'assessment_id', domain=[('is_upcharge', '=', True)], string='Auto-Applied Upcharges') # ========================================================================= # STEP 6: REVIEW & RESULT # ========================================================================= sale_order_id = fields.Many2one('sale.order', string='Generated Quotation', readonly=True, copy=False) total_estimate = fields.Float(string='Total Estimate', compute='_compute_totals', store=True) total_adp_estimate = fields.Float(string='Estimated ADP Portion', compute='_compute_totals', store=True) total_client_estimate = fields.Float(string='Estimated Client Portion', compute='_compute_totals', store=True) notes = fields.Text(string='Additional Notes') # Form state for save/resume current_step = fields.Integer(string='Current Step', default=1) form_data_json = fields.Text(string='Form State (JSON)', help='Stores intermediate form state for save/resume') # Generic equipment data (for non-wheelchair equipment types) equipment_data_json = fields.Text(string='Equipment Data (JSON)', help='JSON storage for equipment-specific field values. ' 'Used by dynamic measurement/custom steps for non-wheelchair types.') # ========================================================================= # SHARING & PUBLIC ACCESS # ========================================================================= access_token = fields.Char( string='Access Token', copy=False, index=True, help='Token for public access without login') portal_url = fields.Char( string='Portal URL', compute='_compute_portal_url') public_url = fields.Char( string='Public URL', compute='_compute_public_url') # ========================================================================= # COMPUTED FIELDS # ========================================================================= @api.depends('client_first_name', 'client_last_name', 'partner_id') def _compute_client_name(self): for rec in self: if rec.partner_id: rec.client_name = rec.partner_id.name elif rec.client_first_name or rec.client_last_name: parts = [rec.client_first_name or '', rec.client_last_name or ''] rec.client_name = ' '.join(p for p in parts if p) else: rec.client_name = '' @api.depends('reference', 'client_name') def _compute_display_name(self): for rec in self: parts = [rec.reference or _('New')] if rec.client_name: parts.append(rec.client_name) rec.display_name = ' - '.join(parts) @api.depends( 'seat_width', 'seat_width_unit', 'seat_depth', 'seat_depth_unit', 'finished_back_height', 'back_height_unit', 'client_weight', 'client_weight_unit', ) def _compute_normalized_measurements(self): """Convert all measurements to inches/lbs for consistent rule evaluation.""" for rec in self: rec.seat_width_inches = ( rec.seat_width if rec.seat_width_unit == 'inches' else rec.seat_width / 2.54 ) rec.seat_depth_inches = ( rec.seat_depth if rec.seat_depth_unit == 'inches' else rec.seat_depth / 2.54 ) rec.back_height_inches = ( rec.finished_back_height if rec.back_height_unit == 'inches' else rec.finished_back_height / 2.54 ) rec.client_weight_lbs = ( rec.client_weight if rec.client_weight_unit == 'lbs' else rec.client_weight * 2.20462 ) @api.depends('line_ids.subtotal', 'line_ids.adp_portion', 'line_ids.client_portion') def _compute_totals(self): for rec in self: rec.total_estimate = sum(rec.line_ids.mapped('subtotal')) rec.total_adp_estimate = sum(rec.line_ids.mapped('adp_portion')) rec.total_client_estimate = sum(rec.line_ids.mapped('client_portion')) def _compute_portal_url(self): base = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for rec in self: rec.portal_url = f'{base}/my/quotation/builder/{rec.id}/edit' if rec.id else False def _compute_public_url(self): base = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for rec in self: if rec.access_token: rec.public_url = f'{base}/quotation/form/{rec.access_token}' else: rec.public_url = False # ========================================================================= # SHARING ACTIONS # ========================================================================= def _generate_access_token(self): """Generate a unique access token for public sharing.""" for rec in self: if not rec.access_token: rec.access_token = uuid.uuid4().hex def action_open_portal_form(self): """Open the portal assessment form in a new browser tab.""" self.ensure_one() return { 'type': 'ir.actions.act_url', 'url': self.portal_url, 'target': 'new', } def action_generate_share_link(self): """Generate a public access token and show the shareable URL.""" self.ensure_one() self._generate_access_token() return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Share Link Generated'), 'message': _('Public URL copied to clipboard — ' 'also visible in the Sharing section of this form.'), 'type': 'info', 'sticky': False, }, } # ========================================================================= # CRUD OVERRIDES # ========================================================================= @api.model_create_multi def create(self, vals_list): for vals in vals_list: if vals.get('reference', _('New')) == _('New'): vals['reference'] = self.env['ir.sequence'].next_by_code( 'fusion.wc.assessment') or _('New') return super().create(vals_list) # ========================================================================= # UPCHARGE RULE ENGINE # ========================================================================= def _evaluate_upcharge_rules(self): """Evaluate all active upcharge rules against current assessment data. Creates assessment lines for triggered rules. Returns recordset of triggered rules. """ self.ensure_one() # Remove previously auto-applied upcharges (allows re-evaluation) self.line_ids.filtered(lambda l: l.is_upcharge).unlink() rules = self.env['fusion.wc.upcharge.rule'].search([ ('active', '=', True), ('equipment_type', 'in', [self.equipment_type, 'both']), ]) triggered = self.env['fusion.wc.upcharge.rule'] # Track exclusive groups — only the first matching rule per group applies exclusive_triggered = {} for rule in rules.sorted('sequence'): # Check mutual exclusion if rule.mutually_exclusive_group: if rule.mutually_exclusive_group in exclusive_triggered: continue matched = False if rule.trigger_type == 'measurement': value = self._get_measurement_value(rule.measurement_field) if value and self._compare_values(value, rule.comparison, rule.threshold_value): matched = True elif rule.trigger_type == 'weight': weight = self.client_weight_lbs if weight > rule.weight_min: if not rule.weight_max or weight <= rule.weight_max: matched = True elif rule.trigger_type == 'dimension_mismatch': val1 = self._get_measurement_value(rule.compare_field_1) val2 = self._get_measurement_value(rule.compare_field_2) if val1 and val2 and abs(val1 - val2) > 0.01: matched = True if matched: self._create_upcharge_line(rule) triggered |= rule if rule.mutually_exclusive_group: exclusive_triggered[rule.mutually_exclusive_group] = rule return triggered def _get_measurement_value(self, field_name): """Map measurement field selection to actual normalized (inches) value.""" field_map = { 'seat_width': self.seat_width_inches, 'seat_depth': self.seat_depth_inches, 'back_width': self.seat_width_inches, # backrest width defaults to seat width 'back_height': self.back_height_inches, 'seat_to_floor': ( self.finished_seat_to_floor_height if self.seat_to_floor_unit == 'inches' else self.finished_seat_to_floor_height / 2.54 ), 'leg_rest_length': ( self.finished_leg_rest_length if self.leg_rest_unit == 'inches' else self.finished_leg_rest_length / 2.54 ), } return field_map.get(field_name, 0.0) @staticmethod def _compare_values(value, comparison, threshold): """Evaluate a comparison between value and threshold.""" if comparison == 'gt': return value > threshold elif comparison == 'gte': return value >= threshold elif comparison == 'lt': return value < threshold elif comparison == 'eq': return abs(value - threshold) < 0.01 elif comparison == 'neq': return abs(value - threshold) >= 0.01 return False def _create_upcharge_line(self, rule): """Create an assessment line from an upcharge rule.""" ADPDevice = self.env['fusion.adp.device.code'].sudo() adp_device = ADPDevice.search( [('device_code', '=', rule.adp_device_code), ('active', '=', True)], limit=1 ) vals = { 'assessment_id': self.id, 'product_id': rule.product_id.id if rule.product_id else False, 'product_name': ( adp_device.device_description if adp_device else rule.name ), 'quantity': 1, 'adp_device_code': rule.adp_device_code, 'adp_price': adp_device.adp_price if adp_device else 0, 'unit_price': adp_device.adp_price if adp_device else 0, 'is_upcharge': True, 'upcharge_rule_id': rule.id, 'upcharge_reason': rule.description or rule.name, } return self.env['fusion.wc.assessment.line'].create(vals) # ========================================================================= # QUOTATION GENERATION # ========================================================================= def action_generate_quotation(self): """Generate a draft sale.order from this assessment.""" self.ensure_one() if self.sale_order_id: raise UserError(_('A quotation has already been generated for this assessment.')) # Run upcharge engine first triggered_rules = self._evaluate_upcharge_rules() # Resolve or create partner partner = self._resolve_partner() # Add frame as an assessment line if not already present self._ensure_frame_line() # Create sale order so_vals = self._prepare_sale_order_vals(partner) sale_order = self.env['sale.order'].sudo().create(so_vals) # Create sale order lines from assessment lines for line in self.line_ids: sol_vals = self._prepare_sale_order_line_vals(sale_order, line) self.env['sale.order.line'].sudo().create(sol_vals) # Update assessment self.write({ 'sale_order_id': sale_order.id, 'state': 'quoted', }) # Post chatter message self._post_quotation_message(sale_order, triggered_rules) return { 'type': 'ir.actions.act_window', 'res_model': 'sale.order', 'res_id': sale_order.id, 'view_mode': 'form', 'target': 'current', } def _resolve_partner(self): """Get existing partner or create a new one.""" self.ensure_one() if self.partner_id: return self.partner_id if not self.client_first_name and not self.client_last_name: raise UserError(_('Please select an existing client or provide client name.')) partner_vals = { 'name': self.client_name, 'phone': self.client_phone, 'email': self.client_email, 'street': self.client_street, 'street2': self.client_street2, 'city': self.client_city, 'state_id': self.client_state_id.id if self.client_state_id else False, 'zip': self.client_zip, 'country_id': self.client_country_id.id if self.client_country_id else False, 'customer_rank': 1, } partner = self.env['res.partner'].sudo().create(partner_vals) self.partner_id = partner return partner def _ensure_frame_line(self): """Add the frame product as an assessment line if not already present.""" self.ensure_one() # If we have a template but no resolved variant, resolve to first variant if not self.frame_product_id and self.frame_product_tmpl_id: variant = self.frame_product_tmpl_id.product_variant_ids[:1] if variant: self.frame_product_id = variant.id if not self.frame_product_id: return # Pick the right frame section based on equipment type frame_code_map = { 'manual_wheelchair': 'mw_frame', 'power_wheelchair': 'pw_frame', 'walker': 'walker_frame', } code = frame_code_map.get(self.equipment_type, 'mw_frame') frame_section = self.env['fusion.wc.section'].search( [('code', '=', code)], limit=1) if not frame_section: # Fall back to legacy code frame_section = self.env['fusion.wc.section'].search( [('code', '=', 'frame')], limit=1) existing = self.line_ids.filtered( lambda l: l.product_id == self.frame_product_id and not l.is_upcharge) if existing: return # Build description from measurements desc_parts = [self.frame_product_id.display_name] if self.seat_width: desc_parts.append(f"Seat Width: {self.seat_width}{self.seat_width_unit}") if self.seat_depth: desc_parts.append(f"Seat Depth: {self.seat_depth}{self.seat_depth_unit}") if self.finished_seat_to_floor_height: desc_parts.append( f"Seat to Floor: {self.finished_seat_to_floor_height}" f"{self.seat_to_floor_unit}") if self.finished_leg_rest_length: desc_parts.append( f"Leg Rest: {self.finished_leg_rest_length}{self.leg_rest_unit}") if self.back_cane_height: desc_parts.append( f"Cane Height: {self.back_cane_height}{self.cane_height_unit}") product = self.frame_product_id adp_code = product.product_tmpl_id.x_fc_adp_device_code or '' adp_price = product.product_tmpl_id.x_fc_adp_price or product.lst_price self.env['fusion.wc.assessment.line'].create({ 'assessment_id': self.id, 'section_id': frame_section.id if frame_section else False, 'product_id': product.id, 'product_name': '\n'.join(desc_parts), 'quantity': 1, 'adp_device_code': adp_code, 'adp_price': adp_price, 'unit_price': product.lst_price, }) def _prepare_sale_order_vals(self, partner): """Prepare values for sale.order creation.""" self.ensure_one() # Map client_type to fusion_claims x_fc_client_type client_type_map = { 'reg': 'REG', 'ods': 'ODS', 'acs': 'ACS', 'owp': 'OWP', 'ltc': 'LTC', 'sen': 'SEN', 'cca': 'CCA', } vals = { 'partner_id': partner.id, 'user_id': self.sales_rep_id.id, 'state': 'draft', 'origin': f'WC Assessment: {self.reference}', 'wc_assessment_id': self.id, } # Set fusion_claims fields if available if hasattr(self.env['sale.order'], 'x_fc_sale_type'): vals['x_fc_sale_type'] = 'adp' if hasattr(self.env['sale.order'], 'x_fc_client_type'): vals['x_fc_client_type'] = client_type_map.get(self.client_type, 'REG') if hasattr(self.env['sale.order'], 'x_fc_adp_application_status'): vals['x_fc_adp_application_status'] = 'quotation' if self.authorizer_id and hasattr(self.env['sale.order'], 'x_fc_authorizer_id'): vals['x_fc_authorizer_id'] = self.authorizer_id.id return vals def _prepare_sale_order_line_vals(self, sale_order, line): """Prepare values for sale.order.line creation from assessment line.""" vals = { 'order_id': sale_order.id, 'product_id': line.product_id.id if line.product_id else False, 'name': line.product_name or ( line.product_id.display_name if line.product_id else line.adp_device_code ), 'product_uom_qty': line.quantity, 'price_unit': line.unit_price or ( line.product_id.lst_price if line.product_id else 0 ), } return vals def _post_quotation_message(self, sale_order, triggered_rules): """Post a chatter message summarizing the assessment.""" self.ensure_one() lines_html = '' for line in self.line_ids: icon = '⚡' if line.is_upcharge else '✅' lines_html += ( f'
  • {icon} {line.product_name or line.adp_device_code} ' f'x{line.quantity} — ${line.unit_price:.2f}
  • ' ) measurements_html = '' if self.seat_width: measurements_html += ( f'
  • Seat Width: {self.seat_width} {self.seat_width_unit}
  • ') if self.seat_depth: measurements_html += ( f'
  • Seat Depth: {self.seat_depth} {self.seat_depth_unit}
  • ') if self.finished_seat_to_floor_height: measurements_html += ( f'
  • Seat to Floor: {self.finished_seat_to_floor_height} ' f'{self.seat_to_floor_unit}
  • ') if self.back_cane_height: measurements_html += ( f'
  • Back Cane Height: {self.back_cane_height} ' f'{self.cane_height_unit}
  • ') if self.finished_back_height: measurements_html += ( f'
  • Finished Back Height: {self.finished_back_height} ' f'{self.back_height_unit}
  • ') if self.finished_leg_rest_length: measurements_html += ( f'
  • Leg Rest Length: {self.finished_leg_rest_length} ' f'{self.leg_rest_unit}
  • ') if self.client_weight: measurements_html += ( f'
  • Client Weight: {self.client_weight} {self.client_weight_unit}
  • ') upcharges_html = '' if triggered_rules: upcharges_html = '

    Auto-Applied Upcharges

    ' body = Markup( f'

    Wheelchair Assessment Completed

    ' f'

    Assessment: {self.reference}
    ' f'Equipment: {dict(self._fields["equipment_type"].selection).get(self.equipment_type, "")}
    ' f'Build Type: {dict(self._fields["build_type"].selection).get(self.build_type, "")}

    ' f'

    Measurements

    ' f'

    Selected Items

    ' f'{upcharges_html}' ) sale_order.message_post(body=body, message_type='comment', subtype_xmlid='mail.mt_note') # ========================================================================= # ACTIONS # ========================================================================= def action_mark_review(self): """Mark assessment as ready for review.""" for rec in self: rec.state = 'review' def action_cancel(self): """Cancel the assessment.""" for rec in self: rec.state = 'cancelled' def action_reset_draft(self): """Reset to draft.""" for rec in self: if rec.state == 'cancelled': rec.state = 'draft' def action_view_quotation(self): """Open the generated quotation.""" self.ensure_one() if not self.sale_order_id: raise UserError(_('No quotation has been generated yet.')) return { 'type': 'ir.actions.act_window', 'res_model': 'sale.order', 'res_id': self.sale_order_id.id, 'view_mode': 'form', 'target': 'current', }