Files
Odoo-Modules/Work in Progress/fusion_quotations/models/wc_assessment.py
gsinghpal fc3c966484 changes
2026-03-13 12:38:28 -04:00

850 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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',
}