461 lines
18 KiB
Python
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'}
|