This commit is contained in:
gsinghpal
2026-04-03 15:45:18 -04:00
parent 4cd7357aa0
commit c66bdf5089
71 changed files with 6721 additions and 118 deletions

View File

@@ -9,6 +9,14 @@ _logger = logging.getLogger(__name__)
class FusionAccountingChatController(http.Controller):
def _check_session_ownership(self, session):
"""S1-S3: Verify the current user owns the session."""
if session.user_id.id != request.env.user.id:
# Allow managers to access any session
if not request.env.user.has_group('fusion_accounting.group_fusion_accounting_manager'):
return {'error': 'Access denied: you do not own this session'}
return None
@http.route('/fusion_accounting/session/create', type='jsonrpc', auth='user')
def create_session(self, context_domain=None, **kwargs):
session = request.env['fusion.accounting.session'].create({
@@ -21,7 +29,13 @@ class FusionAccountingChatController(http.Controller):
@http.route('/fusion_accounting/session/close', type='jsonrpc', auth='user')
def close_session(self, session_id, **kwargs):
session = request.env['fusion.accounting.session'].browse(int(session_id))
if session.exists() and session.state == 'active':
if not session.exists():
return {'status': 'closed'}
# S2: Ownership check
error = self._check_session_ownership(session)
if error:
return error
if session.state == 'active':
session.action_close_session()
return {'status': 'closed'}
@@ -29,6 +43,12 @@ class FusionAccountingChatController(http.Controller):
def chat(self, session_id, message, context=None, **kwargs):
if not message:
return {'error': 'Message is required'}
# S3: Ownership check
session = request.env['fusion.accounting.session'].browse(int(session_id))
if session.exists():
error = self._check_session_ownership(session)
if error:
return error
agent = request.env['fusion.accounting.agent']
result = agent.chat(int(session_id), message, context=context)
return result
@@ -51,17 +71,35 @@ class FusionAccountingChatController(http.Controller):
@http.route('/fusion_accounting/dashboard/data', type='jsonrpc', auth='user')
def dashboard_data(self, **kwargs):
dashboard = request.env['fusion.accounting.dashboard'].new({
'company_id': request.env.company.id,
})
return {
'bank_recon': {'count': dashboard.bank_recon_count, 'amount': dashboard.bank_recon_amount},
'ar': {'total': dashboard.ar_total, 'overdue_count': dashboard.ar_overdue_count},
'ap': {'total': dashboard.ap_total, 'due_this_week': dashboard.ap_due_this_week},
'hst': {'balance': dashboard.hst_balance},
'audit': {'score': dashboard.audit_score, 'flags': dashboard.audit_flag_count},
'month_end': {'status': dashboard.month_end_status, 'open_items': dashboard.month_end_open_items},
}
# E2: Wrap in try/except so dashboard doesn't return 500
try:
dashboard = request.env['fusion.accounting.dashboard'].new({
'company_id': request.env.company.id,
})
return {
'bank_recon': {'count': dashboard.bank_recon_count, 'amount': dashboard.bank_recon_amount},
'ar': {'total': dashboard.ar_total, 'overdue_count': dashboard.ar_overdue_count},
'ap': {'total': dashboard.ap_total, 'due_this_week': dashboard.ap_due_this_week},
'hst': {'balance': dashboard.hst_balance},
'audit': {'score': dashboard.audit_score, 'flags': dashboard.audit_flag_count},
'month_end': {'status': dashboard.month_end_status, 'open_items': dashboard.month_end_open_items},
# E1: Include needs_attention and recent_activity
'needs_attention': json.loads(dashboard.needs_attention_json or '[]'),
'recent_activity': json.loads(dashboard.recent_activity_json or '[]'),
}
except Exception as e:
_logger.exception("Dashboard data computation failed")
return {
'error': 'Dashboard data could not be computed',
'bank_recon': {'count': 0, 'amount': 0},
'ar': {'total': 0, 'overdue_count': 0},
'ap': {'total': 0, 'due_this_week': 0},
'hst': {'balance': 0},
'audit': {'score': 0, 'flags': 0},
'month_end': {'status': 'Unknown', 'open_items': 0},
'needs_attention': [],
'recent_activity': [],
}
@http.route('/fusion_accounting/approve_all', type='jsonrpc', auth='user')
def approve_all(self, match_history_ids, **kwargs):
@@ -74,7 +112,9 @@ class FusionAccountingChatController(http.Controller):
result = agent.approve_action(int(mid))
results.append({'id': mid, 'status': 'approved', 'result': result})
except Exception as e:
results.append({'id': mid, 'status': 'error', 'error': str(e)})
# S4: Sanitize exception — log full error, return generic message
_logger.exception("Error approving match history %s", mid)
results.append({'id': mid, 'status': 'error', 'error': 'Action could not be approved. Check server logs for details.'})
return {'results': results}
@http.route('/fusion_accounting/reject_all', type='jsonrpc', auth='user')
@@ -86,19 +126,58 @@ class FusionAccountingChatController(http.Controller):
for mid in match_history_ids:
try:
result = agent.reject_action(int(mid), reason)
results.append({'id': mid, 'status': 'rejected'})
# E3: Consistent return shape with approve_all
results.append({'id': mid, 'status': 'rejected', 'result': result})
except Exception as e:
results.append({'id': mid, 'status': 'error', 'error': str(e)})
# S4: Sanitize exception
_logger.exception("Error rejecting match history %s", mid)
results.append({'id': mid, 'status': 'error', 'error': 'Action could not be rejected. Check server logs for details.'})
return {'results': results}
@http.route('/fusion_accounting/session/list', type='jsonrpc', auth='user')
def session_list(self, limit=20, **kwargs):
"""List recent sessions for the session picker dropdown."""
sessions = request.env['fusion.accounting.session'].search([
('user_id', '=', request.env.user.id),
], order='write_date desc', limit=int(limit))
return {
'sessions': [{
'id': s.id,
'name': s.name,
'state': s.state,
'date': s.write_date.isoformat() if s.write_date else '',
'message_count': len(json.loads(s.message_ids_json or '[]')),
'ai_model': s.ai_model or '',
} for s in sessions],
}
@http.route('/fusion_accounting/session/latest', type='jsonrpc', auth='user')
def session_latest(self, **kwargs):
session = request.env['fusion.accounting.session'].search([
# Find the most recent active session that has messages first,
# fall back to any active session (including empty ones)
sessions = request.env['fusion.accounting.session'].search([
('user_id', '=', request.env.user.id),
('state', '=', 'active'),
], limit=1, order='create_date desc')
if not session:
], order='write_date desc', limit=10)
if not sessions:
return {'session_id': None, 'messages': [], 'name': None}
# Prefer a session with actual messages
session = None
for s in sessions:
msg_json = s.message_ids_json or '[]'
if msg_json != '[]' and len(msg_json) > 5:
session = s
break
# If no session has messages, use the newest one
if not session:
session = sessions[0]
# Clean up empty stale sessions (created but never used)
for s in sessions:
if s.id != session.id and (s.message_ids_json or '[]') == '[]':
s.write({'state': 'closed'})
messages = json.loads(session.message_ids_json or '[]')
display_messages = []
for msg in messages:
@@ -119,6 +198,10 @@ class FusionAccountingChatController(http.Controller):
session = request.env['fusion.accounting.session'].browse(int(session_id))
if not session.exists():
return {'error': 'Session not found'}
# S1: Ownership check
error = self._check_session_ownership(session)
if error:
return error
return {
'messages': json.loads(session.message_ids_json or '[]'),
'session_id': session.id,