Files
Odoo-Modules/fusion_authorizer_portal/models/sale_order.py
Nexa Admin 431052920e feat: separate fusion field service and LTC into standalone modules, update core modules
- fusion_claims: separated field service logic, updated controllers/views
- fusion_tasks: updated task views and map integration
- fusion_authorizer_portal: added page 11 signing, schedule booking, migrations
- fusion_shipping: new standalone shipping module (Canada Post, FedEx, DHL, Purolator)
- fusion_ltc_management: new standalone LTC management module
2026-03-11 16:19:52 +00:00

254 lines
9.4 KiB
Python

# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
import logging
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
# Comments from portal users
portal_comment_ids = fields.One2many(
'fusion.authorizer.comment',
'sale_order_id',
string='Portal Comments',
)
portal_comment_count = fields.Integer(
string='Comment Count',
compute='_compute_portal_comment_count',
)
# Documents uploaded via portal
portal_document_ids = fields.One2many(
'fusion.adp.document',
'sale_order_id',
string='Portal Documents',
)
portal_document_count = fields.Integer(
string='Document Count',
compute='_compute_portal_document_count',
)
# Link to assessment
assessment_id = fields.Many2one(
'fusion.assessment',
string='Source Assessment',
readonly=True,
help='The assessment that created this sale order',
)
# Authorizer helper field (consolidates multiple possible fields)
portal_authorizer_id = fields.Many2one(
'res.partner',
string='Authorizer (Portal)',
compute='_compute_portal_authorizer_id',
store=True,
help='Consolidated authorizer field for portal access',
)
@api.depends('portal_comment_ids')
def _compute_portal_comment_count(self):
for order in self:
order.portal_comment_count = len(order.portal_comment_ids)
@api.depends('portal_document_ids')
def _compute_portal_document_count(self):
for order in self:
order.portal_document_count = len(order.portal_document_ids)
@api.depends('x_fc_authorizer_id')
def _compute_portal_authorizer_id(self):
"""Get authorizer from x_fc_authorizer_id field"""
for order in self:
order.portal_authorizer_id = order.x_fc_authorizer_id
def write(self, vals):
"""Override write to send notification when authorizer is assigned."""
old_authorizers = {
order.id: order.x_fc_authorizer_id.id if order.x_fc_authorizer_id else False
for order in self
}
result = super().write(vals)
# Check for authorizer changes
if 'x_fc_authorizer_id' in vals:
for order in self:
old_auth = old_authorizers.get(order.id)
new_auth = vals.get('x_fc_authorizer_id')
if new_auth and new_auth != old_auth:
order._send_authorizer_assignment_notification()
# NOTE: Generic status change notifications removed.
# Each status transition already sends its own detailed email
# from fusion_claims (approval, denial, submission, billed, etc.)
# A generic "status changed" email on top was redundant and lacked detail.
return result
def action_message_authorizer(self):
"""Open composer to send message to authorizer only"""
self.ensure_one()
if not self.x_fc_authorizer_id:
return {'type': 'ir.actions.act_window_close'}
return {
'type': 'ir.actions.act_window',
'name': 'Message Authorizer',
'res_model': 'mail.compose.message',
'view_mode': 'form',
'views': [(False, 'form')],
'target': 'new',
'context': {
'default_model': 'sale.order',
'default_res_ids': [self.id],
'default_partner_ids': [self.x_fc_authorizer_id.id],
'default_composition_mode': 'comment',
'default_subtype_xmlid': 'mail.mt_note',
},
}
def _send_authorizer_assignment_notification(self):
"""Send email when an authorizer is assigned to the order"""
self.ensure_one()
if not self.x_fc_authorizer_id or not self.x_fc_authorizer_id.email:
return
try:
template = self.env.ref('fusion_authorizer_portal.mail_template_case_assigned', raise_if_not_found=False)
if template:
template.send_mail(self.id, force_send=False)
_logger.info(f"Sent case assignment notification to {self.x_fc_authorizer_id.email} for {self.name}")
except Exception as e:
_logger.error(f"Failed to send authorizer assignment notification: {e}")
# _send_status_change_notification removed -- redundant.
# Each workflow transition in fusion_claims sends its own detailed email.
def action_view_portal_comments(self):
"""View portal comments"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Portal Comments'),
'res_model': 'fusion.authorizer.comment',
'view_mode': 'list,form',
'views': [(False, 'list'), (False, 'form')],
'domain': [('sale_order_id', '=', self.id)],
'context': {'default_sale_order_id': self.id},
}
def action_view_portal_documents(self):
"""View portal documents"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Portal Documents'),
'res_model': 'fusion.adp.document',
'view_mode': 'list,form',
'views': [(False, 'list'), (False, 'form')],
'domain': [('sale_order_id', '=', self.id)],
'context': {'default_sale_order_id': self.id},
}
def get_portal_display_data(self):
"""Get data for portal display, excluding sensitive information"""
self.ensure_one()
return {
'id': self.id,
'name': self.name,
'date_order': self.date_order,
'state': self.state,
'state_display': dict(self._fields['state'].selection).get(self.state, self.state),
'partner_name': self.partner_id.name if self.partner_id else '',
'partner_address': self._get_partner_address_display(),
'client_reference_1': self.x_fc_client_ref_1 or '',
'client_reference_2': self.x_fc_client_ref_2 or '',
'claim_number': self.x_fc_claim_number or '',
'authorizer_name': self.x_fc_authorizer_id.name if self.x_fc_authorizer_id else '',
'sales_rep_name': self.user_id.name if self.user_id else '',
'product_lines': self._get_product_lines_for_portal(),
'comment_count': self.portal_comment_count,
'document_count': self.portal_document_count,
}
def _get_partner_address_display(self):
"""Get formatted partner address for display"""
if not self.partner_id:
return ''
parts = []
if self.partner_id.street:
parts.append(self.partner_id.street)
if self.partner_id.city:
city_part = self.partner_id.city
if self.partner_id.state_id:
city_part += f", {self.partner_id.state_id.name}"
if self.partner_id.zip:
city_part += f" {self.partner_id.zip}"
parts.append(city_part)
return ', '.join(parts)
def _get_product_lines_for_portal(self):
"""Get product lines for portal display (excluding costs)"""
lines = []
for line in self.order_line:
lines.append({
'id': line.id,
'product_name': line.product_id.name if line.product_id else line.name,
'quantity': line.product_uom_qty,
'uom': line.product_uom_id.name if line.product_uom_id else '',
'adp_code': line.x_fc_adp_device_code or '' if hasattr(line, 'x_fc_adp_device_code') else '',
'device_type': '',
'serial_number': line.x_fc_serial_number or '' if hasattr(line, 'x_fc_serial_number') else '',
})
return lines
@api.model
def get_authorizer_portal_cases(self, partner_id, search_query=None, limit=100, offset=0):
"""Get cases for authorizer portal with optional search"""
domain = [('x_fc_authorizer_id', '=', partner_id)]
# Add search if provided
if search_query:
search_domain = self._build_search_domain(search_query)
domain = ['&'] + domain + search_domain
orders = self.sudo().search(domain, limit=limit, offset=offset, order='date_order desc, id desc')
return orders
@api.model
def get_sales_rep_portal_cases(self, user_id, search_query=None, limit=100, offset=0):
"""Get cases for sales rep portal with optional search"""
domain = [('user_id', '=', user_id)]
# Add search if provided
if search_query:
search_domain = self._build_search_domain(search_query)
domain = domain + search_domain
orders = self.sudo().search(domain, limit=limit, offset=offset, order='date_order desc, id desc')
return orders
def _build_search_domain(self, query):
"""Build search domain for portal search"""
if not query or len(query) < 2:
return []
search_domain = [
'|', '|', '|', '|',
('partner_id.name', 'ilike', query),
('x_fc_claim_number', 'ilike', query),
('x_fc_client_ref_1', 'ilike', query),
('x_fc_client_ref_2', 'ilike', query),
]
return search_domain