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

455 lines
20 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
import logging
_logger = logging.getLogger(__name__)
# =========================================================================
# ADP device_type -> section code mapping
# Maps each device_type string from fusion.adp.device.code to the
# fusion.wc.section code where matched products should be placed.
# =========================================================================
DEVICE_TYPE_SECTION_MAP = {
# =================================================================
# MANUAL WHEELCHAIR FRAMES (Section 2b base devices)
# =================================================================
'Adult Standard Manual Type 1 Wheelchair': 'mw_frame',
'Adult Lightweight Standard Type 1 Wheelchair': 'mw_frame',
'Adult Lightweight Performance Type 3 Wheelchair': 'mw_frame',
'Adult Lightweight Performance Manual Wheelchair': 'mw_frame',
'Adult High Performance Rigid Type 4 Wheelchair': 'mw_frame',
'Adult High Performance Rigid Manual Wheelchair': 'mw_frame',
'Adult Manual Dynamic Tilt Type 5 Wheelchair': 'mw_frame',
'Adult Manual Dynamic Tilt Wheelchair': 'mw_frame',
'Standard Manual Wheelchair Frame with Manual Dynamic Tilt': 'mw_frame',
'Paediatric Lightweight Standard Type 1 Wheelchair': 'mw_frame',
'Paediatric Lightweight Performance Type 4 Wheelchair': 'mw_frame',
'Paediatric High Performance Rigid Type 4 Wheelchair': 'mw_frame',
'Paediatric High Performance Rigid Manual Wheelchair': 'mw_frame',
'Paediatric Manual Dynamic Tilt Type 5 Wheelchair': 'mw_frame',
'Paediatric Specific Specialty Stroller': 'mw_frame',
# =================================================================
# WALKER / ROLLATOR / AMBULATION AIDS FRAMES (Section 2a)
# =================================================================
'Adult Wheeled Walker Type 1': 'walker_frame',
'Adult Wheeled Walker Type 2': 'walker_frame',
'Adult Wheeled Walker Type 3': 'walker_frame',
'Paediatric Wheeled Walker Type 1': 'walker_frame',
'Paediatric Wheeled Walker Type 2': 'walker_frame',
'Paediatric Walking Frame': 'walker_frame',
'Paediatric Specific Walking Frame': 'walker_frame',
'Paediatric Standing Frame Type 1': 'walker_frame',
'Paediatric Standing Frame Type 2': 'walker_frame',
'Forearm Crutches': 'walker_frame',
'Walker Addons': 'walker_accessories',
# Walker ADP options (adolescent size upgrades)
'AA - Custom Modifications': 'walker_adp_options',
# =================================================================
# POWER BASE / SCOOTER FRAMES (Section 2c)
# =================================================================
'Adult Power Base Type 1': 'pw_frame',
'Adult Power Base Type 2': 'pw_frame',
'Adult Power Base Type 3': 'pw_frame',
'Paediatric Power Base Type 2': 'pw_frame',
'Paediatric Power Base Type 3': 'pw_frame',
'Power Scooter': 'pw_frame',
# Power base ADP funded options
'MW - Adjustable Tension Back Upholstery up to 18" Frame Width': 'pw_adp_options',
'MW - Adjustable Tension Back Upholstery over 18" Frame Width': 'pw_adp_options',
'PW - Adjustable Tension Back Upholstery up to 18" Frame Width': 'pw_adp_options',
'PW - Adjustable Tension Back Upholstery over 18" Frame Width': 'pw_adp_options',
'Midline Control': 'pw_adp_options',
'Manual Recline Option': 'pw_adp_options',
'Recliner Option': 'pw_adp_options',
'MW - Angle Adjustable Footplates (pair)': 'pw_adp_options',
'PW - Angle Adjustable Footplates (pair)': 'pw_adp_options',
'Manual Elevating Legrests (pair)': 'pw_adp_options',
'Elevating Legrests (pair)': 'pw_adp_options',
'Swingaway Mounting Bracket': 'pw_adp_options',
'One Piece 90/90 Front Riggings': 'pw_adp_options',
'Seat Package 1 for Power Bases': 'pw_adp_options',
'Seat Package 2 for Power Bases': 'pw_adp_options',
'PW - Oxygen Tank': 'pw_adp_options',
'MW - Oxygen Tank Holder': 'pw_adp_options',
'PW - Ventilator Tray': 'pw_adp_options',
'MW - Ventilator Tray': 'pw_adp_options',
# Power specialty controls (* require clinical rationale)
'Specialty Controls 1 Non Standard Joystick*': 'pw_specialty_controls',
'Specialty Controls 2 Chin/Rim Control*': 'pw_specialty_controls',
'Specialty Controls 3 Simple Touch*': 'pw_specialty_controls',
'Specialty Controls 4 Proximity Control*': 'pw_specialty_controls',
'Specialty Controls 5 Breath Control*': 'pw_specialty_controls',
'Specialty Controls 6 Scanners*': 'pw_specialty_controls',
'Auto Correction System*': 'pw_specialty_controls',
# Power positioning devices (require Justification for Funding Chart)
'Power Tilt Only': 'pw_positioning',
'Power Recline Only': 'pw_positioning',
'Power Tilt and Recline': 'pw_positioning',
'Power Elevating Footrests': 'pw_positioning',
'Multi-function Control Box': 'pw_positioning',
'Power Add-On Device': 'pw_positioning',
# =================================================================
# SEATING SEAT CUSHION (shared across wheelchair types)
# =================================================================
'Seat Cushion': 'seat_cushion',
'Seat Cushion Cover(s)': 'seat_cushion_cover',
'Seat Options': 'seat_options',
'Seat Hardware': 'seat_hardware',
'Pommel/Adductors': 'pommel',
'Pommel Hardware': 'pommel_hardware',
# =================================================================
# SEATING BACK SUPPORT
# =================================================================
'Back Support': 'back_support',
'Back Support Options': 'back_support_options',
'Back Cover': 'back_cover',
'Back Hardware': 'back_hardware',
# =================================================================
# SEATING COMPLETE ASSEMBLY
# =================================================================
'Complete Assembly': 'complete_assembly',
# =================================================================
# SEATING HEADREST / NECKREST
# =================================================================
'Headrest/Neckrest': 'headrest',
'Headrest/Neckrest Options': 'headrest_options',
'Headrest/Neckrest Hardware': 'headrest_hardware',
# =================================================================
# SEATING POSITIONING BELTS
# =================================================================
'Positioning Belts': 'positioning_belts',
'Positioning Belts Options': 'positioning_belt_options',
# =================================================================
# SEATING ARM SUPPORT
# =================================================================
'Arm Support(s)': 'arm_support',
'Arm Support Options': 'arm_support_options',
'Arm Support Hardware': 'arm_support_hardware',
# =================================================================
# SEATING TRAY
# =================================================================
'Tray': 'tray',
'Tray Options': 'tray_options',
# =================================================================
# SEATING LATERAL SUPPORT
# =================================================================
'Lateral Support(s)': 'lateral_support',
'Lateral Support Options': 'lateral_support_options',
'Lateral Support Hardware': 'lateral_support_hardware',
# =================================================================
# SEATING FOOT / LEG SUPPORT
# =================================================================
'Foot/Leg Support(s)': 'foot_leg_support',
'Foot/Leg Support Options': 'foot_leg_support_options',
'Foot/Leg Support Hardware': 'foot_leg_support_hardware',
# =================================================================
# MANUAL WHEELCHAIR ACCESSORIES (Section 2b options/extras)
# =================================================================
'Amputee Axle Plates (pair)': 'mw_accessories',
'Caster Pin Locks (pair)': 'mw_accessories',
'Clothing Guards': 'mw_accessories',
'Grade Aids (pair)': 'mw_accessories',
'Heavy Duty Cross Braces & Upholstery': 'mw_accessories',
'One Arm/Lever Drive': 'mw_accessories',
'Plastic Coated Handrims': 'mw_accessories',
'Projected Handrims (pair)': 'mw_accessories',
'Quick Release Axles (pair)': 'mw_accessories',
'Spoke Protectors (pair)': 'mw_accessories',
'Stroller Handles/Paediatric': 'mw_accessories',
'Titanium Frame *': 'mw_accessories',
'Uni-Lateral Wheel Lock': 'mw_accessories',
'Unilateral Hand Brake': 'mw_accessories',
# =================================================================
# MW ADP UPCHARGE / MODIFICATION CODES
# =================================================================
'MW - Seat Width Required is Greater Than 18"': 'mw_adp_options',
'MW - Seat Depth Required is Greater Than 18"': 'mw_adp_options',
'MW - Heavy Duty Model, Client Weight Exceeds 250 Lbs': 'mw_adp_options',
'MW - Heavy Duty Model, Client Weight Exceeds 350 Lbs': 'mw_adp_options',
'MW - Heavy Duty Model, Client Weight Exceeds 400 Lbs': 'mw_adp_options',
'MW - Custom Modifications': 'mw_adp_options',
# =================================================================
# PW ADP UPCHARGE / MODIFICATION CODES
# =================================================================
'PW - Seat Width Required is Greater Than 18"': 'pw_adp_options',
'PW - Seat Depth Required is Greater Than 18"': 'pw_adp_options',
'PW - Heavy Duty Model, Client Weight Exceeds 250 Lbs': 'pw_adp_options',
'PW - Heavy Duty Model, Client Weight Exceeds 350 Lbs': 'pw_adp_options',
'PW - Heavy Duty Model, Client Weight Exceeds 400 Lbs': 'pw_adp_options',
'PW - Custom Modifications': 'pw_adp_options',
'SE - Custom Modifications': 'pw_adp_options',
}
class WheelchairSection(models.Model):
_name = 'fusion.wc.section'
_description = 'Wheelchair Configuration Section'
_order = 'sequence, id'
name = fields.Char(string='Name', required=True)
code = fields.Char(string='Code', required=True, index=True,
help='Unique identifier e.g. frame, cushion, backrest')
sequence = fields.Integer(string='Sequence', default=10)
active = fields.Boolean(string='Active', default=True)
equipment_type = fields.Selection(
selection='_get_equipment_type_selection',
string='Equipment Type', default='both', required=True)
@api.model
def _get_equipment_type_selection(self):
types = self.env['fusion.equipment.type'].sudo().search([], order='sequence')
if types:
result = [(t.code, t.name) for t in types]
# Group options for shared sections
result.append(('wheelchair', 'All Wheelchairs (Manual + Power)'))
result.append(('both', 'All Equipment Types'))
return result
return [
('manual_wheelchair', 'Manual Wheelchair'),
('power_wheelchair', 'Power Wheelchair'),
('walker', 'Walker / Ambulation Aid'),
('wheelchair', 'All Wheelchairs (Manual + Power)'),
('both', 'All Equipment Types'),
]
icon = fields.Char(string='Icon', help='FontAwesome icon class e.g. fa-wheelchair')
description = fields.Text(string='Description',
help='Help text shown to sales reps during assessment')
# Section behavior flags
is_adp_options_section = fields.Boolean(string='ADP Options Section',
help='If true, this section shows as a checkbox grid of ADP funded options')
has_build_type = fields.Boolean(string='Has Build Type',
help='If true, shows Modular / Custom Fabricated toggle for items in this section')
allow_multiple = fields.Boolean(string='Allow Multiple', default=True,
help='Can select multiple products in this section')
required = fields.Boolean(string='Required', default=False,
help='Must have at least one selection')
# Measurement fields configuration
has_width = fields.Boolean(string='Collect Width')
has_depth = fields.Boolean(string='Collect Depth')
has_height = fields.Boolean(string='Collect Height')
has_length = fields.Boolean(string='Collect Length')
width_label = fields.Char(string='Width Label', default='Width (inches)')
depth_label = fields.Char(string='Depth Label', default='Depth (inches)')
height_label = fields.Char(string='Height Label', default='Height (inches)')
length_label = fields.Char(string='Length Label', default='Length (inches)')
# Product filter for custom search
product_category_id = fields.Many2one('product.category',
string='Product Category Filter',
help='Default category to filter products when searching in this section')
# Hierarchy
parent_id = fields.Many2one('fusion.wc.section', string='Parent Section',
ondelete='cascade', index=True)
child_ids = fields.One2many('fusion.wc.section', 'parent_id',
string='Sub-Sections')
# Options
option_ids = fields.One2many('fusion.wc.section.option', 'section_id',
string='Product Options')
option_count = fields.Integer(string='Options', compute='_compute_option_count')
@api.depends('option_ids')
def _compute_option_count(self):
for record in self:
record.option_count = len(record.option_ids)
@api.depends('name', 'code')
def _compute_display_name(self):
for record in self:
if record.parent_id:
record.display_name = f"{record.parent_id.name} / {record.name}"
else:
record.display_name = record.name or ''
# =====================================================================
# AUTO-POPULATE OPTIONS FROM INVENTORY
# =====================================================================
def action_auto_populate_options(self):
"""Auto-populate product options for THIS section by matching
products in inventory whose ADP device code maps to this section
via the ADP device type lookup.
Called from a button on the section form view.
"""
self.ensure_one()
stats = self._populate_section_from_products()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Auto-Populate Complete'),
'message': _(
'Section "%(section)s": %(added)d products added, '
'%(skipped)d already existed, %(unmapped)d unmapped.',
section=self.name,
added=stats['added'],
skipped=stats['skipped'],
unmapped=stats['unmapped'],
),
'type': 'success',
'sticky': False,
},
}
@api.model
def action_auto_populate_all_sections(self):
"""Auto-populate ALL sections at once. Called from a menu action."""
sections = self.search([])
totals = {'added': 0, 'skipped': 0, 'unmapped': 0}
section_results = []
for section in sections:
stats = section._populate_section_from_products()
totals['added'] += stats['added']
totals['skipped'] += stats['skipped']
totals['unmapped'] += stats['unmapped']
if stats['added'] > 0:
section_results.append(f"{section.name}: +{stats['added']}")
details = ', '.join(section_results) if section_results else 'No new products found'
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Auto-Populate All Sections Complete'),
'message': _(
'Total: %(added)d products added across all sections. '
'%(skipped)d already existed. '
'%(unmapped)d products had unmapped device types.\n'
'%(details)s',
added=totals['added'],
skipped=totals['skipped'],
unmapped=totals['unmapped'],
details=details,
),
'type': 'success',
'sticky': True,
},
}
def _populate_section_from_products(self):
"""Find products whose ADP device type maps to this section,
and create one fusion.wc.section.option per product *template*.
Variants (size, colour, etc.) live under the template and are
selected when the sales rep adds the item to an assessment.
This keeps the option list compact and manageable.
Returns dict with counts: {added, skipped, unmapped}
"""
self.ensure_one()
SectionOption = self.env['fusion.wc.section.option'].sudo()
ADPDevice = self.env['fusion.adp.device.code'].sudo()
ProductTemplate = self.env['product.template'].sudo()
# Build reverse map: which device_types map to THIS section's code
my_device_types = [
dtype for dtype, section_code in DEVICE_TYPE_SECTION_MAP.items()
if section_code == self.code
]
if not my_device_types:
return {'added': 0, 'skipped': 0, 'unmapped': 0}
# Find ADP device codes with matching device_type
matching_adp_codes = ADPDevice.search([
('device_type', 'in', my_device_types),
('active', '=', True),
])
adp_code_strings = matching_adp_codes.mapped('device_code')
if not adp_code_strings:
return {'added': 0, 'skipped': 0, 'unmapped': 0}
# Find product templates with those ADP codes
products = ProductTemplate.search([
('x_fc_adp_device_code', 'in', adp_code_strings),
])
if not products:
return {'added': 0, 'skipped': 0, 'unmapped': 0}
# Get existing option template IDs for this section (avoid duplicates)
existing_tmpl_ids = set(
SectionOption.search([
('section_id', '=', self.id),
]).mapped('product_tmpl_id.id')
)
# Pre-fetch build types: ADP code -> build_type
build_type_cache = {}
for adp_code_str in set(products.mapped('x_fc_adp_device_code')):
adp_device = ADPDevice.search([
('device_code', '=', adp_code_str),
('active', '=', True),
], limit=1)
build_type = 'both'
if adp_device and adp_device.build_type:
if adp_device.build_type == 'modular':
build_type = 'modular'
elif adp_device.build_type == 'custom_fabricated':
build_type = 'custom_fabricated'
build_type_cache[adp_code_str] = build_type
added = 0
skipped = 0
unmapped = 0
# Batch-create one option record per product template
vals_list = []
for tmpl in products:
if tmpl.id in existing_tmpl_ids:
skipped += 1
continue
adp_code = tmpl.x_fc_adp_device_code
build_type = build_type_cache.get(adp_code, 'both')
vals_list.append({
'section_id': self.id,
'product_tmpl_id': tmpl.id,
'is_standard': True,
'available_build_types': build_type,
'sequence': 10 + added,
})
added += 1
existing_tmpl_ids.add(tmpl.id)
# Bulk create for performance
if vals_list:
SectionOption.create(vals_list)
_logger.info(
'Section "%s" auto-populate: %d product templates added, '
'%d already existed, %d unmapped',
self.name, added, skipped, unmapped,
)
return {'added': added, 'skipped': skipped, 'unmapped': unmapped}