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

248 lines
9.8 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 AccountMoveLine(models.Model):
_inherit = 'account.move.line'
# ==========================================================================
# 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',
help='Device type from ADP mobility manual (for approval matching)',
)
# ==========================================================================
# ADP PORTIONS - Stored fields set during invoice creation
# ==========================================================================
x_fc_adp_portion = fields.Monetary(
string='ADP Portion',
currency_field='currency_id',
help='ADP portion for this line (calculated during invoice creation from device codes database)',
)
x_fc_client_portion = fields.Monetary(
string='Client Portion',
currency_field='currency_id',
help='Client portion for this line (calculated during invoice creation from device codes database)',
)
def _compute_adp_portions(self):
"""Compute ADP and client portions based on device codes database.
This is called during invoice type/client type changes to recalculate portions.
"""
self.action_recalculate_portions()
def action_recalculate_portions(self):
"""Manually recalculate ADP and client portions based on device codes database.
This can be called to recalculate portions if values are incorrect.
Uses the same logic as invoice creation.
"""
ADPDevice = self.env['fusion.adp.device.code'].sudo()
for line in self:
move = line.move_id
if not move or move.move_type not in ['out_invoice', 'out_refund']:
continue
if not line.product_id or line.quantity <= 0:
continue
# Get client type
client_type = move._get_client_type()
if client_type == 'REG':
base_adp_pct = 0.75
base_client_pct = 0.25
else:
base_adp_pct = 1.0
base_client_pct = 0.0
# Get ADP price from device codes database (priority)
device_code = line._get_adp_device_code()
adp_price = 0
if device_code:
adp_device = ADPDevice.search([
('device_code', '=', device_code),
('active', '=', True)
], limit=1)
if adp_device:
adp_price = adp_device.adp_price or 0
# Fallback to product fields
if not adp_price and line.product_id:
product_tmpl = line.product_id.product_tmpl_id
if hasattr(product_tmpl, 'x_fc_adp_price'):
adp_price = getattr(product_tmpl, 'x_fc_adp_price', 0) or 0
# (Studio field fallback removed)
# Fallback to line max price or unit price
if not adp_price:
adp_price = line.x_fc_adp_max_price or line.price_unit
qty = line.quantity
adp_base_total = adp_price * qty
# Apply deductions
if line.x_fc_deduction_type == 'pct' and line.x_fc_deduction_value:
effective_adp_pct = base_adp_pct * (line.x_fc_deduction_value / 100)
adp_portion = adp_base_total * effective_adp_pct
client_portion = adp_base_total - adp_portion
elif line.x_fc_deduction_type == 'amt' and line.x_fc_deduction_value:
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:
adp_portion = adp_base_total * base_adp_pct
client_portion = adp_base_total * base_client_pct
line.write({
'x_fc_adp_portion': adp_portion,
'x_fc_client_portion': client_portion,
'x_fc_adp_max_price': adp_price,
})
# ==========================================================================
# GETTER METHODS
# ==========================================================================
def _get_adp_device_code(self):
"""Get ADP device code from product.
Checks multiple sources in order and validates against ADP device database:
1. x_fc_adp_device_code (module field) - verified in ADP database
2. x_adp_code (Studio/custom field) - verified in ADP database
3. default_code - verified in ADP database
4. Code in parentheses in product name (e.g., "Product Name (SE0001109)")
Returns empty string if no valid ADP code found.
"""
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 - ONLY if it's a valid ADP 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 ''
match = re.search(r'\(([A-Z]{2}\d{7})\)', product_name)
if match:
code = match.group(1)
if ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0:
return code
# 5. Last resort: check if there's a linked sale order line with ADP code
if self.sale_line_ids:
for sale_line in self.sale_line_ids:
if hasattr(sale_line, '_get_adp_device_code'):
sale_code = sale_line._get_adp_device_code()
if sale_code:
return sale_code
# No valid ADP code found - return empty to skip this line in export
return ''
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_aml_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'