Initial commit
This commit is contained in:
716
fusion_claims/wizard/device_approval_wizard.py
Normal file
716
fusion_claims/wizard/device_approval_wizard.py
Normal file
@@ -0,0 +1,716 @@
|
||||
# -*- 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, _
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeviceApprovalWizard(models.TransientModel):
|
||||
"""Wizard to confirm which device types were approved by ADP and set deductions.
|
||||
|
||||
This is Stage 2 of the two-stage verification system:
|
||||
- Stage 1 (Submission): What device types were submitted (stored in x_fc_submitted_device_types)
|
||||
- Stage 2 (Approval): What device types were approved by ADP (this wizard)
|
||||
"""
|
||||
_name = 'fusion_claims.device.approval.wizard'
|
||||
_description = 'ADP Device Approval Wizard'
|
||||
|
||||
# ==========================================================================
|
||||
# MAIN FIELDS
|
||||
# ==========================================================================
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Sale Order',
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
line_ids = fields.One2many(
|
||||
'fusion_claims.device.approval.wizard.line',
|
||||
'wizard_id',
|
||||
string='Device Lines',
|
||||
)
|
||||
all_approved = fields.Boolean(
|
||||
string='All Approved',
|
||||
default=True,
|
||||
help='Check this to approve all devices at once',
|
||||
)
|
||||
has_unapproved = fields.Boolean(
|
||||
string='Has Unapproved Devices',
|
||||
compute='_compute_has_unapproved',
|
||||
)
|
||||
has_deductions = fields.Boolean(
|
||||
string='Has Deductions',
|
||||
compute='_compute_has_deductions',
|
||||
help='True if any device has a deduction applied',
|
||||
)
|
||||
has_invoices = fields.Boolean(
|
||||
string='Has Invoices',
|
||||
compute='_compute_has_invoices',
|
||||
help='True if invoices already exist for this order',
|
||||
)
|
||||
|
||||
# Stage 1 comparison fields
|
||||
submitted_device_types = fields.Text(
|
||||
string='Submitted Device Types',
|
||||
compute='_compute_submitted_device_types',
|
||||
help='Device types that were verified during submission (Stage 1)',
|
||||
)
|
||||
has_submission_data = fields.Boolean(
|
||||
string='Has Submission Data',
|
||||
compute='_compute_submitted_device_types',
|
||||
)
|
||||
|
||||
# Claim Number - required for Mark as Approved
|
||||
claim_number = fields.Char(
|
||||
string='Claim Number',
|
||||
help='ADP Claim Number from the approval letter',
|
||||
)
|
||||
|
||||
# Approval Documents - for Mark as Approved mode
|
||||
is_mark_approved_mode = fields.Boolean(
|
||||
string='Mark Approved Mode',
|
||||
compute='_compute_is_mark_approved_mode',
|
||||
)
|
||||
approval_letter = fields.Binary(
|
||||
string='ADP Approval Letter',
|
||||
help='Upload the ADP approval letter PDF',
|
||||
)
|
||||
approval_letter_filename = fields.Char(
|
||||
string='Approval Letter Filename',
|
||||
)
|
||||
# For multiple approval photos, we'll use a Many2many to ir.attachment
|
||||
approval_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'device_approval_wizard_attachment_rel',
|
||||
'wizard_id',
|
||||
'attachment_id',
|
||||
string='Approval Screenshots',
|
||||
help='Upload screenshots from the ADP approval document',
|
||||
)
|
||||
|
||||
@api.depends_context('mark_as_approved')
|
||||
def _compute_is_mark_approved_mode(self):
|
||||
for wizard in self:
|
||||
wizard.is_mark_approved_mode = self.env.context.get('mark_as_approved', False)
|
||||
|
||||
# ==========================================================================
|
||||
# COMPUTED FIELDS
|
||||
# ==========================================================================
|
||||
@api.depends('line_ids.approved')
|
||||
def _compute_has_unapproved(self):
|
||||
for wizard in self:
|
||||
wizard.has_unapproved = any(not line.approved for line in wizard.line_ids)
|
||||
|
||||
@api.depends('line_ids.deduction_type')
|
||||
def _compute_has_deductions(self):
|
||||
for wizard in self:
|
||||
wizard.has_deductions = any(
|
||||
line.deduction_type and line.deduction_type != 'none'
|
||||
for line in wizard.line_ids
|
||||
)
|
||||
|
||||
@api.depends('sale_order_id', 'sale_order_id.invoice_ids')
|
||||
def _compute_has_invoices(self):
|
||||
for wizard in self:
|
||||
if wizard.sale_order_id:
|
||||
wizard.has_invoices = bool(wizard.sale_order_id.invoice_ids.filtered(
|
||||
lambda inv: inv.state != 'cancel'
|
||||
))
|
||||
else:
|
||||
wizard.has_invoices = False
|
||||
|
||||
@api.depends('sale_order_id', 'sale_order_id.x_fc_submitted_device_types')
|
||||
def _compute_submitted_device_types(self):
|
||||
"""Compute the submitted device types from Stage 1 for comparison display."""
|
||||
import json
|
||||
for wizard in self:
|
||||
if wizard.sale_order_id and wizard.sale_order_id.x_fc_submitted_device_types:
|
||||
try:
|
||||
data = json.loads(wizard.sale_order_id.x_fc_submitted_device_types)
|
||||
# Format as readable list
|
||||
submitted = [dt for dt, selected in data.items() if selected]
|
||||
wizard.submitted_device_types = '\n'.join([f'• {dt}' for dt in sorted(submitted)])
|
||||
wizard.has_submission_data = bool(submitted)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
wizard.submitted_device_types = ''
|
||||
wizard.has_submission_data = False
|
||||
else:
|
||||
wizard.submitted_device_types = ''
|
||||
wizard.has_submission_data = False
|
||||
|
||||
@api.onchange('all_approved')
|
||||
def _onchange_all_approved(self):
|
||||
"""Toggle all lines when 'All Approved' is changed.
|
||||
|
||||
Only triggers when toggling ON - sets all devices to approved.
|
||||
When toggling OFF, users manually uncheck individual items.
|
||||
"""
|
||||
if self.all_approved:
|
||||
# Iterate and set approved flag - avoid replacing the entire line
|
||||
for line in self.line_ids:
|
||||
if not line.approved:
|
||||
line.approved = True
|
||||
|
||||
# ==========================================================================
|
||||
# DEFAULT GET - Populate with order lines
|
||||
# ==========================================================================
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
|
||||
active_id = self._context.get('active_id')
|
||||
if not active_id:
|
||||
return res
|
||||
|
||||
order = self.env['sale.order'].browse(active_id)
|
||||
res['sale_order_id'] = order.id
|
||||
|
||||
# Build line data from order lines that have ADP device codes ONLY
|
||||
# Non-ADP items are excluded from the verification list
|
||||
ADPDevice = self.env['fusion.adp.device.code'].sudo()
|
||||
lines_data = []
|
||||
|
||||
for so_line in order.order_line:
|
||||
# Skip non-product lines
|
||||
if so_line.display_type in ('line_section', 'line_note'):
|
||||
continue
|
||||
if not so_line.product_id or so_line.product_uom_qty <= 0:
|
||||
continue
|
||||
|
||||
# Get device code and look up device type
|
||||
device_code = so_line._get_adp_device_code()
|
||||
|
||||
# SKIP items without a valid ADP device code in the database
|
||||
# These are non-ADP items and don't need verification
|
||||
if not device_code:
|
||||
continue
|
||||
|
||||
adp_device = ADPDevice.search([('device_code', '=', device_code), ('active', '=', True)], limit=1)
|
||||
|
||||
# Skip if device code not found in ADP database (non-ADP item)
|
||||
if not adp_device:
|
||||
continue
|
||||
|
||||
device_type = adp_device.device_type or ''
|
||||
device_description = adp_device.device_description or ''
|
||||
|
||||
# Default to NOT approved - user must actively check each item
|
||||
# Unless it was already approved in a previous verification
|
||||
is_approved = so_line.x_fc_adp_approved if so_line.x_fc_adp_approved else False
|
||||
|
||||
lines_data.append((0, 0, {
|
||||
'sale_line_id': so_line.id,
|
||||
'product_name': so_line.product_id.display_name,
|
||||
'device_code': device_code,
|
||||
'device_type': device_type or so_line.x_fc_adp_device_type or '',
|
||||
'device_description': device_description,
|
||||
'serial_number': so_line.x_fc_serial_number or '',
|
||||
'quantity': so_line.product_uom_qty,
|
||||
'unit_price': so_line.price_unit,
|
||||
'adp_portion': so_line.x_fc_adp_portion,
|
||||
'client_portion': so_line.x_fc_client_portion,
|
||||
'approved': is_approved,
|
||||
'deduction_type': so_line.x_fc_deduction_type or 'none',
|
||||
'deduction_value': so_line.x_fc_deduction_value or 0,
|
||||
}))
|
||||
|
||||
res['line_ids'] = lines_data
|
||||
|
||||
# All Approved checkbox - only true if ALL lines are already approved
|
||||
if lines_data:
|
||||
all_approved = all(line[2].get('approved', False) for line in lines_data)
|
||||
res['all_approved'] = all_approved
|
||||
else:
|
||||
res['all_approved'] = True # No ADP items = nothing to verify
|
||||
|
||||
return res
|
||||
|
||||
# ==========================================================================
|
||||
# ACTION METHODS
|
||||
# ==========================================================================
|
||||
def action_confirm_approval(self):
|
||||
"""Confirm the approval status and deductions, update order lines and invoices."""
|
||||
self.ensure_one()
|
||||
|
||||
approved_count = 0
|
||||
unapproved_count = 0
|
||||
deduction_count = 0
|
||||
updated_lines = self.env['sale.order.line']
|
||||
|
||||
for wiz_line in self.line_ids:
|
||||
if wiz_line.sale_line_id:
|
||||
# NOTE: Do NOT write x_fc_adp_device_type here - it's a computed field
|
||||
# that should be computed from the product's device code
|
||||
vals = {
|
||||
'x_fc_adp_approved': wiz_line.approved,
|
||||
'x_fc_deduction_type': wiz_line.deduction_type or 'none',
|
||||
'x_fc_deduction_value': wiz_line.deduction_value or 0,
|
||||
}
|
||||
wiz_line.sale_line_id.write(vals)
|
||||
updated_lines |= wiz_line.sale_line_id
|
||||
|
||||
if wiz_line.approved:
|
||||
approved_count += 1
|
||||
else:
|
||||
unapproved_count += 1
|
||||
|
||||
if wiz_line.deduction_type and wiz_line.deduction_type != 'none':
|
||||
deduction_count += 1
|
||||
|
||||
# MARK VERIFICATION AS COMPLETE on the sale order
|
||||
# This allows invoice creation even if some items are unapproved
|
||||
if self.sale_order_id:
|
||||
self.sale_order_id.write({'x_fc_device_verification_complete': True})
|
||||
|
||||
# FORCE RECALCULATION of ADP/Client portions on all order lines
|
||||
# This ensures unapproved items get 100% assigned to client portion
|
||||
for line in self.sale_order_id.order_line:
|
||||
line._compute_adp_portions()
|
||||
# Recalculate order totals
|
||||
self.sale_order_id._compute_adp_totals()
|
||||
|
||||
# If we're in "mark_as_approved" mode, also update the status and save approval documents
|
||||
if self.env.context.get('mark_as_approved') and self.sale_order_id:
|
||||
# Determine status based on whether there are deductions
|
||||
new_status = 'approved_deduction' if deduction_count > 0 else 'approved'
|
||||
|
||||
update_vals = {
|
||||
'x_fc_adp_application_status': new_status,
|
||||
}
|
||||
|
||||
# Save claim number if provided
|
||||
if self.claim_number:
|
||||
update_vals['x_fc_claim_number'] = self.claim_number
|
||||
|
||||
# Save approval letter if uploaded
|
||||
if self.approval_letter:
|
||||
update_vals['x_fc_approval_letter'] = self.approval_letter
|
||||
update_vals['x_fc_approval_letter_filename'] = self.approval_letter_filename
|
||||
|
||||
self.sale_order_id.with_context(skip_status_validation=True).write(update_vals)
|
||||
|
||||
# Collect attachment IDs for chatter post
|
||||
chatter_attachment_ids = []
|
||||
|
||||
# IMPORTANT: When files are uploaded via many2many_binary to a transient model,
|
||||
# they are linked to the wizard and may be garbage collected when the wizard closes.
|
||||
# We need to COPY the attachment data to create persistent attachments linked to the sale order.
|
||||
|
||||
photos_attached = 0
|
||||
if self.approval_photo_ids:
|
||||
photo_ids_to_link = []
|
||||
IrAttachment = self.env['ir.attachment'].sudo()
|
||||
|
||||
for attachment in self.approval_photo_ids:
|
||||
# Create a NEW attachment linked to the sale order (copy the data)
|
||||
# This ensures the attachment persists after the wizard is deleted
|
||||
new_attachment = IrAttachment.create({
|
||||
'name': attachment.name or f'approval_screenshot_{photos_attached + 1}',
|
||||
'datas': attachment.datas,
|
||||
'mimetype': attachment.mimetype,
|
||||
'res_model': 'sale.order',
|
||||
'res_id': self.sale_order_id.id,
|
||||
'type': 'binary',
|
||||
})
|
||||
chatter_attachment_ids.append(new_attachment.id)
|
||||
photo_ids_to_link.append(new_attachment.id)
|
||||
photos_attached += 1
|
||||
|
||||
# Link photos to the Many2many field for easy access in ADP Documents tab
|
||||
if photo_ids_to_link:
|
||||
existing_photo_ids = self.sale_order_id.x_fc_approval_photo_ids.ids
|
||||
self.sale_order_id.write({
|
||||
'x_fc_approval_photo_ids': [(6, 0, existing_photo_ids + photo_ids_to_link)]
|
||||
})
|
||||
|
||||
# Create attachment for approval letter if uploaded
|
||||
if self.approval_letter:
|
||||
letter_attachment = self.env['ir.attachment'].create({
|
||||
'name': self.approval_letter_filename or 'ADP_Approval_Letter.pdf',
|
||||
'datas': self.approval_letter,
|
||||
'res_model': 'sale.order',
|
||||
'res_id': self.sale_order_id.id,
|
||||
})
|
||||
chatter_attachment_ids.append(letter_attachment.id)
|
||||
|
||||
# Post approval to chatter with all documents in ONE message
|
||||
from markupsafe import Markup
|
||||
from datetime import date
|
||||
|
||||
device_details = f'{approved_count} approved'
|
||||
if unapproved_count > 0:
|
||||
device_details += f', {unapproved_count} not approved'
|
||||
if deduction_count > 0:
|
||||
device_details += f', {deduction_count} with deductions'
|
||||
|
||||
# Build documents list - show individual file names for screenshots
|
||||
docs_items = ''
|
||||
if self.approval_letter:
|
||||
docs_items += f'<li>Approval Letter: {self.approval_letter_filename}</li>'
|
||||
if photos_attached > 0:
|
||||
docs_items += f'<li>{photos_attached} approval screenshot(s) attached below</li>'
|
||||
docs_html = ''
|
||||
if docs_items:
|
||||
docs_html = f'<p class="mb-1"><strong>Documents:</strong></p><ul class="mb-0">{docs_items}</ul>'
|
||||
|
||||
# Post to chatter with all attachments in one message
|
||||
if chatter_attachment_ids:
|
||||
self.sale_order_id.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-success" role="alert">'
|
||||
'<h5 class="alert-heading"><i class="fa fa-check-circle"/> Application Approved</h5>'
|
||||
f'<p class="mb-1"><strong>Date:</strong> {date.today().strftime("%B %d, %Y")}</p>'
|
||||
f'<p class="mb-1"><strong>Devices:</strong> {device_details}</p>'
|
||||
f'{docs_html}'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
attachment_ids=chatter_attachment_ids,
|
||||
)
|
||||
else:
|
||||
# No attachments, just post the status update
|
||||
self.sale_order_id.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-success" role="alert">'
|
||||
'<h5 class="alert-heading"><i class="fa fa-check-circle"/> Application Approved</h5>'
|
||||
f'<p class="mb-1"><strong>Date:</strong> {date.today().strftime("%B %d, %Y")}</p>'
|
||||
f'<p class="mb-1"><strong>Devices:</strong> {device_details}</p>'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
# Sync deductions and approval status to existing invoices
|
||||
invoices_updated = self._sync_approval_to_invoices(updated_lines)
|
||||
|
||||
# Build notification message
|
||||
parts = []
|
||||
if unapproved_count > 0:
|
||||
parts.append(_("%d approved, %d NOT approved (will bill to client)") % (approved_count, unapproved_count))
|
||||
msg_type = 'warning'
|
||||
else:
|
||||
parts.append(_("All %d devices approved") % approved_count)
|
||||
msg_type = 'success'
|
||||
|
||||
if deduction_count > 0:
|
||||
parts.append(_("%d deduction(s) applied") % deduction_count)
|
||||
|
||||
if invoices_updated > 0:
|
||||
parts.append(_("%d invoice(s) updated") % invoices_updated)
|
||||
|
||||
# Add status update note if applicable
|
||||
if self.env.context.get('mark_as_approved'):
|
||||
parts.append(_("Status updated to Approved"))
|
||||
|
||||
message = ". ".join(parts) + "."
|
||||
|
||||
# Close the wizard and show notification
|
||||
return {
|
||||
'type': 'ir.actions.act_window_close',
|
||||
'infos': {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Application Approved') if self.env.context.get('mark_as_approved') else _('Device Verification Complete'),
|
||||
'message': message,
|
||||
'type': msg_type,
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def _sync_approval_to_invoices(self, sale_lines):
|
||||
"""Sync approval status and deductions to existing invoices.
|
||||
|
||||
When approval status or deductions change on SO lines:
|
||||
- Client Invoice: unapproved items get 100% price, approved items get normal client portion
|
||||
- ADP Invoice: unapproved items get removed (price = 0), approved items get normal ADP portion
|
||||
|
||||
Returns number of invoices updated.
|
||||
"""
|
||||
if not sale_lines:
|
||||
return 0
|
||||
|
||||
invoices_updated = set()
|
||||
order = self.sale_order_id
|
||||
ADPDevice = self.env['fusion.adp.device.code'].sudo()
|
||||
|
||||
# Get all non-cancelled invoices for this order
|
||||
invoices = order.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
|
||||
if not invoices:
|
||||
return 0
|
||||
|
||||
for so_line in sale_lines:
|
||||
# =================================================================
|
||||
# CHECK 1: Is this a NON-ADP funded product?
|
||||
# =================================================================
|
||||
is_non_adp_funded = so_line.product_id.is_non_adp_funded() if so_line.product_id else False
|
||||
|
||||
# =================================================================
|
||||
# CHECK 2: Does this product have a valid ADP device code?
|
||||
# =================================================================
|
||||
device_code = so_line._get_adp_device_code()
|
||||
is_adp_device = False
|
||||
if device_code and not is_non_adp_funded:
|
||||
is_adp_device = ADPDevice.search_count([
|
||||
('device_code', '=', device_code),
|
||||
('active', '=', True)
|
||||
]) > 0
|
||||
|
||||
is_approved = so_line.x_fc_adp_approved
|
||||
|
||||
# Find linked invoice lines
|
||||
invoice_lines = self.env['account.move.line'].sudo().search([
|
||||
('sale_line_ids', 'in', so_line.id),
|
||||
('move_id', 'in', invoices.ids),
|
||||
])
|
||||
|
||||
for inv_line in invoice_lines:
|
||||
invoice = inv_line.move_id
|
||||
|
||||
# Update approval and deduction fields on invoice line
|
||||
inv_line_vals = {
|
||||
'x_fc_deduction_type': so_line.x_fc_deduction_type,
|
||||
'x_fc_deduction_value': so_line.x_fc_deduction_value,
|
||||
'x_fc_adp_approved': is_approved,
|
||||
}
|
||||
|
||||
# Check invoice portion type
|
||||
portion_type = getattr(invoice, 'x_fc_adp_invoice_portion', '') or ''
|
||||
|
||||
# =================================================================
|
||||
# INVOICE LINE LOGIC:
|
||||
# - Non-ADP items (NON-ADP code OR not in ADP database):
|
||||
# -> Client Invoice: 100% price
|
||||
# -> ADP Invoice: $0 (should not be there)
|
||||
# - Unapproved ADP items:
|
||||
# -> Client Invoice: 100% price
|
||||
# -> ADP Invoice: $0 (excluded)
|
||||
# - Approved ADP items:
|
||||
# -> Client Invoice: client portion %
|
||||
# -> ADP Invoice: ADP portion %
|
||||
# =================================================================
|
||||
|
||||
if portion_type == 'client':
|
||||
if is_non_adp_funded or not is_adp_device:
|
||||
# NON-ADP item: Client pays 100%
|
||||
new_portion = so_line.price_subtotal
|
||||
inv_line_vals['name'] = so_line.name
|
||||
elif is_adp_device and not is_approved:
|
||||
# UNAPPROVED ADP device: Client pays 100%
|
||||
new_portion = so_line.price_subtotal
|
||||
inv_line_vals['name'] = f"{so_line.name} [NOT APPROVED - 100% Client]"
|
||||
else:
|
||||
# Normal client portion (approved ADP item)
|
||||
new_portion = so_line.x_fc_client_portion
|
||||
inv_line_vals['name'] = so_line.name
|
||||
elif portion_type == 'adp':
|
||||
if is_non_adp_funded or not is_adp_device:
|
||||
# NON-ADP item: Remove from ADP invoice (set to 0)
|
||||
new_portion = 0
|
||||
inv_line_vals['name'] = f"{so_line.name} [NON-ADP - Excluded]"
|
||||
elif is_adp_device and not is_approved:
|
||||
# UNAPPROVED ADP device: Remove from ADP invoice (set to 0)
|
||||
new_portion = 0
|
||||
inv_line_vals['name'] = f"{so_line.name} [NOT APPROVED - Excluded]"
|
||||
else:
|
||||
# Normal ADP portion
|
||||
new_portion = so_line.x_fc_adp_portion
|
||||
inv_line_vals['name'] = so_line.name
|
||||
else:
|
||||
# Unknown type - just update fields, skip price recalc
|
||||
inv_line.write(inv_line_vals)
|
||||
invoices_updated.add(invoice.id)
|
||||
continue
|
||||
|
||||
# Calculate new unit price
|
||||
if so_line.product_uom_qty > 0:
|
||||
new_unit_price = new_portion / so_line.product_uom_qty
|
||||
else:
|
||||
new_unit_price = 0
|
||||
|
||||
inv_line_vals['price_unit'] = new_unit_price
|
||||
|
||||
# Need to handle draft vs posted invoices differently
|
||||
if invoice.state == 'draft':
|
||||
inv_line.write(inv_line_vals)
|
||||
else:
|
||||
# For posted invoices, reset to draft first
|
||||
try:
|
||||
invoice.button_draft()
|
||||
inv_line.write(inv_line_vals)
|
||||
invoice.action_post()
|
||||
_logger.info(f"Reset and updated invoice {invoice.name} for approval/deduction change")
|
||||
except Exception as e:
|
||||
_logger.warning(f"Could not update posted invoice {invoice.name}: {e}")
|
||||
|
||||
invoices_updated.add(invoice.id)
|
||||
|
||||
return len(invoices_updated)
|
||||
|
||||
def action_approve_all(self):
|
||||
"""Approve all devices."""
|
||||
self.ensure_one()
|
||||
|
||||
# Write to lines to ensure proper update
|
||||
self.line_ids.write({'approved': True})
|
||||
self.write({'all_approved': True})
|
||||
|
||||
# Return action to refresh the wizard view
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': self._name,
|
||||
'res_id': self.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': self.env.context,
|
||||
}
|
||||
|
||||
|
||||
class DeviceApprovalWizardLine(models.TransientModel):
|
||||
"""Lines for the device approval wizard."""
|
||||
_name = 'fusion_claims.device.approval.wizard.line'
|
||||
_description = 'Device Approval Wizard Line'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'fusion_claims.device.approval.wizard',
|
||||
string='Wizard',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
sale_line_id = fields.Many2one(
|
||||
'sale.order.line',
|
||||
string='Sale Line',
|
||||
required=True,
|
||||
)
|
||||
product_name = fields.Char(
|
||||
string='Product',
|
||||
readonly=True,
|
||||
)
|
||||
device_code = fields.Char(
|
||||
string='Device Code',
|
||||
readonly=True,
|
||||
)
|
||||
device_type = fields.Char(
|
||||
string='Device Type',
|
||||
readonly=True,
|
||||
)
|
||||
device_description = fields.Char(
|
||||
string='Description',
|
||||
readonly=True,
|
||||
)
|
||||
serial_number = fields.Char(
|
||||
string='Serial Number',
|
||||
help='Serial number from the sale order line',
|
||||
)
|
||||
quantity = fields.Float(
|
||||
string='Qty',
|
||||
readonly=True,
|
||||
)
|
||||
unit_price = fields.Float(
|
||||
string='Unit Price',
|
||||
readonly=True,
|
||||
digits='Product Price',
|
||||
)
|
||||
adp_portion = fields.Float(
|
||||
string='ADP Portion',
|
||||
readonly=True,
|
||||
digits='Product Price',
|
||||
)
|
||||
client_portion = fields.Float(
|
||||
string='Client Portion',
|
||||
readonly=True,
|
||||
digits='Product Price',
|
||||
)
|
||||
approved = fields.Boolean(
|
||||
string='Approved',
|
||||
default=True,
|
||||
help='Check if this device type was approved by ADP',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# DEDUCTION FIELDS
|
||||
# ==========================================================================
|
||||
deduction_type = fields.Selection(
|
||||
selection=[
|
||||
('none', 'No Deduction'),
|
||||
('pct', 'Percentage (%)'),
|
||||
('amt', 'Amount ($)'),
|
||||
],
|
||||
string='Deduction Type',
|
||||
default='none',
|
||||
help='Type of ADP deduction. PCT = ADP covers X% of normal. AMT = Fixed $ deducted.',
|
||||
)
|
||||
deduction_value = fields.Float(
|
||||
string='Deduction',
|
||||
digits='Product Price',
|
||||
help='For PCT: enter percentage (e.g., 75 means ADP covers 75%). For AMT: enter dollar amount.',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# COMPUTED FIELDS FOR PREVIEW
|
||||
# ==========================================================================
|
||||
estimated_adp = fields.Float(
|
||||
string='Est. ADP',
|
||||
compute='_compute_estimated_portions',
|
||||
digits='Product Price',
|
||||
help='Estimated ADP portion after deduction',
|
||||
)
|
||||
estimated_client = fields.Float(
|
||||
string='Est. Client',
|
||||
compute='_compute_estimated_portions',
|
||||
digits='Product Price',
|
||||
help='Estimated client portion after deduction',
|
||||
)
|
||||
|
||||
@api.depends('deduction_type', 'deduction_value', 'adp_portion', 'client_portion',
|
||||
'unit_price', 'quantity', 'approved')
|
||||
def _compute_estimated_portions(self):
|
||||
"""Compute estimated portions based on current deduction settings."""
|
||||
for line in self:
|
||||
if not line.approved:
|
||||
# If not approved, show 0 for ADP portion
|
||||
line.estimated_adp = 0
|
||||
line.estimated_client = line.unit_price * line.quantity
|
||||
continue
|
||||
|
||||
# Get base values from the sale line
|
||||
so_line = line.sale_line_id
|
||||
if not so_line or not so_line.order_id:
|
||||
line.estimated_adp = line.adp_portion
|
||||
line.estimated_client = line.client_portion
|
||||
continue
|
||||
|
||||
# Get client type for base percentages
|
||||
client_type = so_line.order_id._get_client_type()
|
||||
if client_type == 'REG':
|
||||
base_adp_pct = 0.75
|
||||
else:
|
||||
base_adp_pct = 1.0
|
||||
|
||||
# Get ADP price
|
||||
adp_price = so_line.x_fc_adp_max_price or line.unit_price
|
||||
total = adp_price * line.quantity
|
||||
|
||||
# Apply deduction
|
||||
if line.deduction_type == 'pct' and line.deduction_value:
|
||||
# PCT: ADP only covers deduction_value% of their portion
|
||||
effective_pct = base_adp_pct * (line.deduction_value / 100)
|
||||
line.estimated_adp = total * effective_pct
|
||||
line.estimated_client = total - line.estimated_adp
|
||||
elif line.deduction_type == 'amt' and line.deduction_value:
|
||||
# AMT: Subtract fixed amount from ADP portion
|
||||
base_adp = total * base_adp_pct
|
||||
line.estimated_adp = max(0, base_adp - line.deduction_value)
|
||||
line.estimated_client = total - line.estimated_adp
|
||||
else:
|
||||
# No deduction
|
||||
line.estimated_adp = total * base_adp_pct
|
||||
line.estimated_client = total * (1 - base_adp_pct)
|
||||
Reference in New Issue
Block a user