This commit is contained in:
gsinghpal
2026-03-09 15:21:22 -04:00
parent a3e85a23ef
commit acd3fc455e
243 changed files with 20459 additions and 4197 deletions

View File

@@ -0,0 +1,849 @@
# -*- 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 = '&#x26A1;' if line.is_upcharge else '&#x2705;'
lines_html += (
f'<li>{icon} {line.product_name or line.adp_device_code} '
f'x{line.quantity} — ${line.unit_price:.2f}</li>'
)
measurements_html = ''
if self.seat_width:
measurements_html += (
f'<li>Seat Width: {self.seat_width} {self.seat_width_unit}</li>')
if self.seat_depth:
measurements_html += (
f'<li>Seat Depth: {self.seat_depth} {self.seat_depth_unit}</li>')
if self.finished_seat_to_floor_height:
measurements_html += (
f'<li>Seat to Floor: {self.finished_seat_to_floor_height} '
f'{self.seat_to_floor_unit}</li>')
if self.back_cane_height:
measurements_html += (
f'<li>Back Cane Height: {self.back_cane_height} '
f'{self.cane_height_unit}</li>')
if self.finished_back_height:
measurements_html += (
f'<li>Finished Back Height: {self.finished_back_height} '
f'{self.back_height_unit}</li>')
if self.finished_leg_rest_length:
measurements_html += (
f'<li>Leg Rest Length: {self.finished_leg_rest_length} '
f'{self.leg_rest_unit}</li>')
if self.client_weight:
measurements_html += (
f'<li>Client Weight: {self.client_weight} {self.client_weight_unit}</li>')
upcharges_html = ''
if triggered_rules:
upcharges_html = '<h4>Auto-Applied Upcharges</h4><ul>'
for rule in triggered_rules:
upcharges_html += (
f'<li>&#x26A1; {rule.name} ({rule.adp_device_code})</li>')
upcharges_html += '</ul>'
body = Markup(
f'<h3>Wheelchair Assessment Completed</h3>'
f'<p>Assessment: {self.reference}<br/>'
f'Equipment: {dict(self._fields["equipment_type"].selection).get(self.equipment_type, "")}<br/>'
f'Build Type: {dict(self._fields["build_type"].selection).get(self.build_type, "")}</p>'
f'<h4>Measurements</h4><ul>{measurements_html}</ul>'
f'<h4>Selected Items</h4><ul>{lines_html}</ul>'
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',
}