# -*- 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'}