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

363 lines
16 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.
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