Files
Odoo-Modules/fusion_claims/wizard/field_mapping_config_wizard.py
2026-02-22 01:22:18 -05:00

461 lines
18 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2024-2025 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Claim Assistant product family.
import logging
from odoo import models, fields, api, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
# =============================================================================
# DEFAULT FIELD MAPPINGS
# =============================================================================
# These define the default FC field mappings and their config parameter keys
DEFAULT_FIELD_MAPPINGS = [
# Sale Order Header Fields
{
'model_name': 'sale.order',
'label': 'Sale Type',
'default_fc_field': 'x_fc_sale_type',
'config_param_key': 'fusion_claims.field_sale_type',
},
{
'model_name': 'sale.order',
'label': 'Client Type',
'default_fc_field': 'x_fc_client_type',
'config_param_key': 'fusion_claims.field_so_client_type',
},
{
'model_name': 'sale.order',
'label': 'Authorizer',
'default_fc_field': 'x_fc_authorizer_id',
'config_param_key': 'fusion_claims.field_so_authorizer',
},
{
'model_name': 'sale.order',
'label': 'Claim Number',
'default_fc_field': 'x_fc_claim_number',
'config_param_key': 'fusion_claims.field_so_claim_number',
},
{
'model_name': 'sale.order',
'label': 'Client Reference 1',
'default_fc_field': 'x_fc_client_ref_1',
'config_param_key': 'fusion_claims.field_so_client_ref_1',
},
{
'model_name': 'sale.order',
'label': 'Client Reference 2',
'default_fc_field': 'x_fc_client_ref_2',
'config_param_key': 'fusion_claims.field_so_client_ref_2',
},
{
'model_name': 'sale.order',
'label': 'Delivery Date',
'default_fc_field': 'x_fc_adp_delivery_date',
'config_param_key': 'fusion_claims.field_so_delivery_date',
},
{
'model_name': 'sale.order',
'label': 'Service Start Date',
'default_fc_field': 'x_fc_service_start_date',
'config_param_key': 'fusion_claims.field_so_service_start',
},
{
'model_name': 'sale.order',
'label': 'Service End Date',
'default_fc_field': 'x_fc_service_end_date',
'config_param_key': 'fusion_claims.field_so_service_end',
},
{
'model_name': 'sale.order',
'label': 'ADP Status',
'default_fc_field': 'x_fc_adp_status',
'config_param_key': 'fusion_claims.field_so_adp_status',
},
{
'model_name': 'sale.order',
'label': 'Primary Serial Number',
'default_fc_field': 'x_fc_primary_serial',
'config_param_key': 'fusion_claims.field_so_primary_serial',
},
# Sale Order Line Fields
{
'model_name': 'sale.order.line',
'label': 'Serial Number',
'default_fc_field': 'x_fc_serial_number',
'config_param_key': 'fusion_claims.field_sol_serial',
},
{
'model_name': 'sale.order.line',
'label': 'Device Placement',
'default_fc_field': 'x_fc_device_placement',
'config_param_key': 'fusion_claims.field_sol_placement',
},
# Invoice Header Fields
{
'model_name': 'account.move',
'label': 'Invoice Type',
'default_fc_field': 'x_fc_invoice_type',
'config_param_key': 'fusion_claims.field_invoice_type',
},
{
'model_name': 'account.move',
'label': 'Client Type',
'default_fc_field': 'x_fc_client_type',
'config_param_key': 'fusion_claims.field_inv_client_type',
},
{
'model_name': 'account.move',
'label': 'Authorizer',
'default_fc_field': 'x_fc_authorizer_id',
'config_param_key': 'fusion_claims.field_inv_authorizer',
},
{
'model_name': 'account.move',
'label': 'Claim Number',
'default_fc_field': 'x_fc_claim_number',
'config_param_key': 'fusion_claims.field_inv_claim_number',
},
{
'model_name': 'account.move',
'label': 'Client Reference 1',
'default_fc_field': 'x_fc_client_ref_1',
'config_param_key': 'fusion_claims.field_inv_client_ref_1',
},
{
'model_name': 'account.move',
'label': 'Client Reference 2',
'default_fc_field': 'x_fc_client_ref_2',
'config_param_key': 'fusion_claims.field_inv_client_ref_2',
},
{
'model_name': 'account.move',
'label': 'Delivery Date',
'default_fc_field': 'x_fc_adp_delivery_date',
'config_param_key': 'fusion_claims.field_inv_delivery_date',
},
{
'model_name': 'account.move',
'label': 'Service Start Date',
'default_fc_field': 'x_fc_service_start_date',
'config_param_key': 'fusion_claims.field_inv_service_start',
},
{
'model_name': 'account.move',
'label': 'Service End Date',
'default_fc_field': 'x_fc_service_end_date',
'config_param_key': 'fusion_claims.field_inv_service_end',
},
{
'model_name': 'account.move',
'label': 'Primary Serial Number',
'default_fc_field': 'x_fc_primary_serial',
'config_param_key': 'fusion_claims.field_inv_primary_serial',
},
# Invoice Line Fields
{
'model_name': 'account.move.line',
'label': 'Serial Number',
'default_fc_field': 'x_fc_serial_number',
'config_param_key': 'fusion_claims.field_aml_serial',
},
{
'model_name': 'account.move.line',
'label': 'Device Placement',
'default_fc_field': 'x_fc_device_placement',
'config_param_key': 'fusion_claims.field_aml_placement',
},
# Product Fields
{
'model_name': 'product.template',
'label': 'ADP Device Code',
'default_fc_field': 'x_fc_adp_device_code',
'config_param_key': 'fusion_claims.field_product_code',
},
{
'model_name': 'product.template',
'label': 'ADP Price',
'default_fc_field': 'x_fc_adp_price',
'config_param_key': 'fusion_claims.field_product_adp_price',
},
]
# =============================================================================
# AUTO-DETECT PATTERNS
# =============================================================================
# Keywords to look for when auto-detecting existing custom fields
AUTO_DETECT_PATTERNS = {
# Sale Order Header
'fusion_claims.field_sale_type': ['sale_type', 'saletype', 'type_of_sale', 'order_type'],
'fusion_claims.field_so_client_type': ['client_type', 'clienttype', 'customer_type', 'cust_type'],
'fusion_claims.field_so_authorizer': ['authorizer', 'authorized', 'approver', 'authorizer_name'],
'fusion_claims.field_so_claim_number': ['claim_number', 'claimnumber', 'claim_no', 'adp_claim', 'claim_num'],
'fusion_claims.field_so_client_ref_1': ['client_ref_1', 'clientref1', 'reference_1', 'client_reference_1', 'ref1', 'ref_1'],
'fusion_claims.field_so_client_ref_2': ['client_ref_2', 'clientref2', 'reference_2', 'client_reference_2', 'ref2', 'ref_2'],
'fusion_claims.field_so_delivery_date': ['delivery_date', 'deliverydate', 'adp_delivery', 'deliver_date', 'date_delivery'],
'fusion_claims.field_so_service_start': ['service_start', 'servicestart', 'start_date', 'service_start_date', 'svc_start'],
'fusion_claims.field_so_service_end': ['service_end', 'serviceend', 'end_date', 'service_end_date', 'svc_end'],
'fusion_claims.field_so_adp_status': ['adp_status', 'adpstatus', 'claim_status', 'status'],
'fusion_claims.field_so_primary_serial': ['primary_serial', 'primaryserial', 'main_serial', 'serial_primary'],
# Sale Order Line
'fusion_claims.field_sol_serial': ['serial_number', 'serial', 'sn', 'serialno'],
'fusion_claims.field_sol_placement': ['placement', 'device_placement', 'place'],
# Invoice Header
'fusion_claims.field_invoice_type': ['invoice_type', 'invoicetype', 'inv_type', 'type_of_invoice', 'bill_type'],
'fusion_claims.field_inv_client_type': ['client_type', 'clienttype', 'customer_type', 'cust_type'],
'fusion_claims.field_inv_authorizer': ['authorizer', 'authorized', 'approver', 'authorizer_name'],
'fusion_claims.field_inv_claim_number': ['claim_number', 'claimnumber', 'claim_no', 'claim_num'],
'fusion_claims.field_inv_client_ref_1': ['client_ref_1', 'clientref1', 'reference_1', 'client_reference_1', 'ref1', 'ref_1'],
'fusion_claims.field_inv_client_ref_2': ['client_ref_2', 'clientref2', 'reference_2', 'client_reference_2', 'ref2', 'ref_2'],
'fusion_claims.field_inv_delivery_date': ['delivery_date', 'deliverydate', 'adp_delivery', 'deliver_date', 'date_delivery'],
'fusion_claims.field_inv_service_start': ['service_start', 'servicestart', 'start_date', 'service_start_date', 'svc_start'],
'fusion_claims.field_inv_service_end': ['service_end', 'serviceend', 'end_date', 'service_end_date', 'svc_end'],
'fusion_claims.field_inv_primary_serial': ['primary_serial', 'primaryserial', 'main_serial', 'serial_primary'],
# Invoice Line
'fusion_claims.field_aml_serial': ['serial_number', 'serial', 'sn', 'serialno'],
'fusion_claims.field_aml_placement': ['placement', 'device_placement', 'place'],
# Product
'fusion_claims.field_product_code': ['adp_code', 'adp_device', 'device_code', 'adp_sku', 'product_code'],
'fusion_claims.field_product_adp_price': ['adp_price', 'adp_retail_price', 'retail_price'],
}
class FieldMappingLine(models.TransientModel):
"""Individual field mapping configuration line."""
_name = 'fusion_claims.field_mapping_line'
_description = 'Field Mapping Line'
_order = 'sequence, id'
wizard_id = fields.Many2one(
'fusion_claims.field_mapping_config',
string='Wizard',
required=True,
ondelete='cascade',
)
sequence = fields.Integer(default=10)
model_name = fields.Selection(
selection=[
('sale.order', 'Sale Order'),
('sale.order.line', 'Sale Order Line'),
('account.move', 'Invoice'),
('account.move.line', 'Invoice Line'),
('product.template', 'Product'),
],
string='Model',
)
label = fields.Char(string='Field Label')
field_name = fields.Char(string='Field Name', help='The field name to use on the model')
default_fc_field = fields.Char(string='Default FC Field')
config_param_key = fields.Char(string='Config Parameter')
field_exists = fields.Boolean(
string='Valid',
compute='_compute_field_exists',
help='Does this field exist on the model?',
)
@api.depends('field_name', 'model_name')
def _compute_field_exists(self):
"""Check if the configured field exists on the model."""
IrModelFields = self.env['ir.model.fields'].sudo()
for line in self:
line.field_exists = False
if not line.field_name or not line.model_name:
continue
# Check database for field existence (more reliable for custom fields)
field_count = IrModelFields.search_count([
('model', '=', line.model_name),
('name', '=', line.field_name),
])
line.field_exists = field_count > 0
@api.onchange('field_name')
def _onchange_field_name(self):
"""Force recomputation of field_exists when field_name changes."""
# This triggers the compute to run when user edits the field
pass
class FieldMappingConfigWizard(models.TransientModel):
"""Wizard for configuring field mappings."""
_name = 'fusion_claims.field_mapping_config'
_description = 'Field Mapping Configuration Wizard'
# =========================================================================
# FIELD MAPPINGS
# =========================================================================
mapping_ids = fields.One2many(
'fusion_claims.field_mapping_line',
'wizard_id',
string='Field Mappings',
)
# =========================================================================
# SUMMARY FIELDS
# =========================================================================
total_mappings = fields.Integer(
string='Total Mappings',
compute='_compute_summary',
)
valid_mappings = fields.Integer(
string='Valid Mappings',
compute='_compute_summary',
)
invalid_mappings = fields.Integer(
string='Invalid Mappings',
compute='_compute_summary',
)
@api.depends('mapping_ids.field_exists')
def _compute_summary(self):
"""Compute summary statistics."""
for wizard in self:
wizard.total_mappings = len(wizard.mapping_ids)
wizard.valid_mappings = len(wizard.mapping_ids.filtered('field_exists'))
wizard.invalid_mappings = wizard.total_mappings - wizard.valid_mappings
# =========================================================================
# DEFAULT VALUES
# =========================================================================
@api.model
def default_get(self, fields_list):
"""Load field mappings from ir.config_parameter when wizard opens."""
res = super().default_get(fields_list)
if 'mapping_ids' in fields_list:
ICP = self.env['ir.config_parameter'].sudo()
mappings = []
seq = 10
for mapping_def in DEFAULT_FIELD_MAPPINGS:
# Get current value from config, fall back to default
current_value = ICP.get_param(
mapping_def['config_param_key'],
mapping_def['default_fc_field']
)
mappings.append((0, 0, {
'sequence': seq,
'model_name': mapping_def['model_name'],
'label': mapping_def['label'],
'field_name': current_value,
'default_fc_field': mapping_def['default_fc_field'],
'config_param_key': mapping_def['config_param_key'],
}))
seq += 10
res['mapping_ids'] = mappings
return res
# =========================================================================
# ACTION METHODS
# =========================================================================
def action_save(self):
"""Save all field mappings to ir.config_parameter."""
ICP = self.env['ir.config_parameter'].sudo()
saved_count = 0
for line in self.mapping_ids:
if line.config_param_key and line.field_name:
ICP.set_param(line.config_param_key, line.field_name)
saved_count += 1
_logger.info("Saved %d field mapping configurations", saved_count)
# Return False to indicate no follow-up action needed
return False
def action_save_and_close(self):
"""Save mappings and close the wizard."""
self.action_save()
return {'type': 'ir.actions.act_window_close'}
def action_reset_defaults(self):
"""Reset all mappings to their default FC field values."""
ICP = self.env['ir.config_parameter'].sudo()
reset_count = 0
for line in self.mapping_ids:
if line.config_param_key and line.default_fc_field:
# Update the transient record so the form shows the new value
line.field_name = line.default_fc_field
# Also save to ir.config_parameter so it persists
ICP.set_param(line.config_param_key, line.default_fc_field)
reset_count += 1
_logger.info("Reset %d field mappings to defaults", reset_count)
# Return False to indicate no follow-up action needed
return False
def action_auto_detect(self):
"""Auto-detect existing custom fields and update mappings."""
IrModelFields = self.env['ir.model.fields'].sudo()
ICP = self.env['ir.config_parameter'].sudo()
detected_count = 0
detected_fields = []
# Get all custom fields
models_to_search = ['sale.order', 'sale.order.line', 'account.move', 'account.move.line', 'product.template']
all_custom_fields = IrModelFields.search([
('model', 'in', models_to_search),
('name', '=like', 'x_%'),
('state', '=', 'manual'),
])
for line in self.mapping_ids:
if not line.config_param_key:
continue
patterns = AUTO_DETECT_PATTERNS.get(line.config_param_key, [])
if not patterns:
continue
# Get fields for this model
model_fields = all_custom_fields.filtered(lambda f: f.model == line.model_name)
model_fields_sorted = sorted(model_fields, key=lambda f: f.name)
# Find matching field
matched_field = None
for field in model_fields_sorted:
# Skip our own x_fc_* fields
if field.name.startswith('x_fc_'):
continue
field_name_lower = field.name.lower()
for pattern in patterns:
if pattern in field_name_lower:
matched_field = field
break
if matched_field:
break
if matched_field and matched_field.name != line.field_name:
# Update the transient record so the form shows the new value
line.field_name = matched_field.name
# Also save to ir.config_parameter so it persists across sessions
ICP.set_param(line.config_param_key, matched_field.name)
detected_fields.append(f"{line.label}: {matched_field.name}")
detected_count += 1
# Log results
_logger.info("Auto-detected %d Studio field mappings: %s", detected_count, detected_fields)
# Return False to indicate no follow-up action needed
return False
def action_close(self):
"""Close the wizard without saving."""
return {'type': 'ir.actions.act_window_close'}