Initial commit
This commit is contained in:
247
fusion_claims/models/account_move_line.py
Normal file
247
fusion_claims/models/account_move_line.py
Normal file
@@ -0,0 +1,247 @@
|
||||
# -*- 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'
|
||||
Reference in New Issue
Block a user