# -*- 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. from odoo import models, fields, api class SaleOrderLine(models.Model): _inherit = 'sale.order.line' # ========================================================================== # PARENT FIELD FOR VISIBILITY (used by Studio conditions) # ========================================================================== x_fc_is_adp_sale = fields.Boolean( compute='_compute_is_adp_sale_line', string='Is ADP Sale', store=False, help='True if parent order is an ADP sale - used for column visibility', ) def _compute_is_adp_sale_line(self): """Check if parent order is an ADP sale.""" for line in self: is_adp = False if line.order_id and line.order_id.x_fc_sale_type: is_adp = 'adp' in str(line.order_id.x_fc_sale_type).lower() line.x_fc_is_adp_sale = is_adp @api.depends('product_id', 'product_id.default_code') def _compute_adp_device_type(self): """Compute ADP device type from the product's device code.""" ADPDevice = self.env['fusion.adp.device.code'].sudo() for line in self: device_type = '' if line.product_id: # Get the device code from product (default_code or custom field) device_code = line._get_adp_device_code() if device_code: # Look up device type in ADP database adp_device = ADPDevice.search([ ('device_code', '=', device_code), ('active', '=', True) ], limit=1) if adp_device: device_type = adp_device.device_type or '' line.x_fc_adp_device_type = device_type # ========================================================================== # SERIAL NUMBER AND DEVICE PLACEMENT # ========================================================================== x_fc_serial_number = fields.Char( string='Serial Number', help='Serial number for this product', ) x_fc_device_placement = fields.Selection( selection=[ ('L', 'Left'), ('R', 'Right'), ('NA', 'N/A'), ], string='Device Placement', default='NA', help='Device placement position (Left/Right/N/A)', ) # ========================================================================== # DEDUCTION FIELDS # ========================================================================== x_fc_deduction_type = fields.Selection( selection=[ ('none', 'No Deduction'), ('pct', 'Percentage'), ('amt', 'Amount'), ], string='Deduction Type', default='none', help='Type of ADP deduction applied to this line', ) x_fc_deduction_value = fields.Float( string='Deduction Value', digits='Product Price', help='Deduction value (percentage if PCT, dollar amount if AMT)', ) # ========================================================================== # ADP REFERENCE FIELDS # ========================================================================== x_fc_adp_max_price = fields.Float( string='ADP Max Price', digits='Product Price', help='Maximum price ADP will cover for this device (from mobility manual)', ) x_fc_sn_required = fields.Boolean( string='S/N Required', help='Is serial number required for this device?', ) # ========================================================================== # ADP DEVICE APPROVAL TRACKING # ========================================================================== x_fc_adp_approved = fields.Boolean( string='ADP Approved', default=False, help='Was this device approved by ADP in the application approval?', ) x_fc_adp_device_type = fields.Char( string='ADP Device Type', compute='_compute_adp_device_type', store=True, help='Device type from ADP mobility manual (for approval matching)', ) # ========================================================================== # COMPUTED ADP PORTIONS # ========================================================================== x_fc_adp_portion = fields.Monetary( string='ADP Portion', compute='_compute_adp_portions', store=True, currency_field='currency_id', help='ADP portion for this line', ) x_fc_client_portion = fields.Monetary( string='Client Portion', compute='_compute_adp_portions', store=True, currency_field='currency_id', help='Client portion for this line', ) # ========================================================================== # COMPUTE ADP PORTIONS WITH DEDUCTIONS AND APPROVAL STATUS # ========================================================================== @api.depends('price_subtotal', 'product_uom_qty', 'price_unit', 'product_id', 'order_id.x_fc_sale_type', 'order_id.x_fc_client_type', 'order_id.x_fc_device_verification_complete', 'x_fc_deduction_type', 'x_fc_deduction_value', 'x_fc_adp_max_price', 'x_fc_adp_approved') def _compute_adp_portions(self): """Compute ADP and client portions based on product's ADP price, client type, and approval status. IMPORTANT: 1. If a product has NON-ADP code (NON-ADP, NON-FUNDED, etc.): Client pays 100% 2. If a product is NOT in the ADP device database: Client pays 100% 3. If a device is NOT approved by ADP: Client pays 100% 4. Only products with valid ADP codes that are approved get the 75%/25% (or 100%/0%) split """ ADPDevice = self.env['fusion.adp.device.code'].sudo() for line in self: # Get sale type and client type from parent order order = line.order_id if not order: line.x_fc_adp_portion = 0 line.x_fc_client_portion = 0 continue # Check if this is an ADP sale if not order._is_adp_sale(): line.x_fc_adp_portion = 0 line.x_fc_client_portion = 0 continue # Skip non-product lines if not line.product_id or line.product_uom_qty <= 0: line.x_fc_adp_portion = 0 line.x_fc_client_portion = 0 continue # ================================================================= # CHECK 1: Is this a NON-ADP funded product? # Products with NON-ADP, NON-FUNDED, UNFUNDED codes = 100% client # ================================================================= if line.product_id.is_non_adp_funded(): line.x_fc_adp_portion = 0 line.x_fc_client_portion = line.price_subtotal continue # ================================================================= # CHECK 2: Does this product have a valid ADP device code? # Products without valid ADP codes in the database = 100% client # ================================================================= device_code = line._get_adp_device_code() is_adp_device = False if device_code: # Check if this code exists in the ADP mobility manual database is_adp_device = ADPDevice.search_count([ ('device_code', '=', device_code), ('active', '=', True) ]) > 0 # If product has NO valid ADP code in database: client pays 100% if not is_adp_device: line.x_fc_adp_portion = 0 line.x_fc_client_portion = line.price_subtotal continue # ================================================================= # CHECK 3: If this is an ADP device but NOT approved: 100% client # ================================================================= if order.x_fc_device_verification_complete and not line.x_fc_adp_approved: line.x_fc_adp_portion = 0 line.x_fc_client_portion = line.price_subtotal continue # ================================================================= # STANDARD CALCULATION: Product is a valid, approved ADP device # ================================================================= # Get client type and determine base percentages client_type = order._get_client_type() if client_type == 'REG': # REG: 75% ADP, 25% Client base_adp_pct = 0.75 base_client_pct = 0.25 else: # ODS, OWP, ACS, LTC, SEN, CCA: 100% ADP, 0% Client base_adp_pct = 1.0 base_client_pct = 0.0 # Get the ADP price from the product adp_price = 0 if line.product_id and line.product_id.product_tmpl_id: product_tmpl = line.product_id.product_tmpl_id if hasattr(product_tmpl, 'x_fc_adp_price'): adp_price = product_tmpl.x_fc_adp_price or 0 if not adp_price and line.x_fc_adp_max_price: adp_price = line.x_fc_adp_max_price if not adp_price: adp_price = line.price_unit qty = line.product_uom_qty adp_base_total = adp_price * qty # Apply deductions if line.x_fc_deduction_type == 'pct' and line.x_fc_deduction_value: # PCT: ADP only covers deduction_value% of their portion effective_adp_pct = base_adp_pct * (line.x_fc_deduction_value / 100) effective_client_pct = 1 - effective_adp_pct adp_portion = adp_base_total * effective_adp_pct client_portion = adp_base_total * effective_client_pct elif line.x_fc_deduction_type == 'amt' and line.x_fc_deduction_value: # AMT: Subtract fixed amount from ADP portion base_adp_amount = adp_base_total * base_adp_pct adp_portion = max(0, base_adp_amount - line.x_fc_deduction_value) client_portion = adp_base_total - adp_portion else: # No deduction - standard calculation based on ADP price adp_portion = adp_base_total * base_adp_pct client_portion = adp_base_total * base_client_pct line.x_fc_adp_portion = adp_portion line.x_fc_client_portion = client_portion # ========================================================================== # GETTER METHODS # ========================================================================== def _get_adp_device_code(self): """Get ADP device code from product. Checks multiple sources in order: 1. x_fc_adp_device_code (module field) 2. x_adp_code (Studio/custom field) 3. default_code 4. Code in parentheses in product name (e.g., "Product Name (SE0001109)") """ import re self.ensure_one() if not self.product_id: return '' product_tmpl = self.product_id.product_tmpl_id ADPDevice = self.env['fusion.adp.device.code'].sudo() # 1. Check x_fc_adp_device_code (module field) code = '' if hasattr(product_tmpl, 'x_fc_adp_device_code'): code = getattr(product_tmpl, 'x_fc_adp_device_code', '') or '' # Verify code exists in ADP database if code and ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0: return code # 2. Check x_adp_code (Studio/custom field) if hasattr(product_tmpl, 'x_adp_code'): code = getattr(product_tmpl, 'x_adp_code', '') or '' if code and ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0: return code # 3. Check default_code code = self.product_id.default_code or '' if code and ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0: return code # 4. Try to extract code from product name in parentheses # E.g., "[MXA-1618] GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE (SE0001109)" product_name = self.product_id.name or '' # Find all codes in parentheses matches = re.findall(r'\(([A-Z0-9]+)\)', product_name) for potential_code in matches: if ADPDevice.search_count([('device_code', '=', potential_code), ('active', '=', True)]) > 0: return potential_code # 5. Final fallback - return default_code even if not in ADP database return self.product_id.default_code or '' def _get_serial_number(self): """Get serial number from mapped field or native field.""" self.ensure_one() ICP = self.env['ir.config_parameter'].sudo() field_name = ICP.get_param('fusion_claims.field_sol_serial', 'x_fc_serial_number') # Try mapped field first if hasattr(self, field_name): value = getattr(self, field_name, None) if value: return value # Fallback to native field return self.x_fc_serial_number or '' def _get_device_placement(self): """Get device placement.""" self.ensure_one() return self.x_fc_device_placement or 'NA' # ========================================================================== # INVOICE LINE PREPARATION # ========================================================================== def _prepare_invoice_line(self, **optional_values): """Override to copy ADP line fields to the invoice line.""" vals = super()._prepare_invoice_line(**optional_values) vals.update({ 'x_fc_serial_number': self.x_fc_serial_number, 'x_fc_device_placement': self.x_fc_device_placement, 'x_fc_deduction_type': self.x_fc_deduction_type, 'x_fc_deduction_value': self.x_fc_deduction_value, 'x_fc_adp_max_price': self.x_fc_adp_max_price, 'x_fc_sn_required': self.x_fc_sn_required, 'x_fc_adp_approved': self.x_fc_adp_approved, 'x_fc_adp_device_type': self.x_fc_adp_device_type, }) return vals # ========================================================================== # ONCHANGE FOR ADP MAX PRICE LOOKUP # ========================================================================== @api.onchange('product_id') def _onchange_product_adp_info(self): """Lookup ADP info from device codes when product changes.""" if self.product_id: # Try to find device code in the reference table device_code = self._get_adp_device_code() if device_code: adp_device = self.env['fusion.adp.device.code'].sudo().search([ ('device_code', '=', device_code) ], limit=1) if adp_device: self.x_fc_adp_max_price = adp_device.adp_price self.x_fc_sn_required = adp_device.sn_required