- 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
254 lines
9.4 KiB
Python
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
|