Adds Task 15 controller layer: - /fusion/followup/list_overdue - /fusion/followup/get_partner_detail - /fusion/followup/generate_text - /fusion/followup/send - /fusion/followup/pause - /fusion/followup/reset All endpoints use V19 type='jsonrpc' and route through fusion.followup.engine. 6 HttpCase tests added (69 total). Made-with: Cursor
174 lines
7.6 KiB
Python
174 lines
7.6 KiB
Python
"""HTTP controller: 6 JSON-RPC endpoints for the OWL follow-up dashboard.
|
|
|
|
All endpoints route through fusion.followup.engine. V19 type='jsonrpc'.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import date, datetime
|
|
|
|
from odoo import _, http
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.http import request
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _parse_date(value):
|
|
if isinstance(value, date):
|
|
return value
|
|
if not value:
|
|
return None
|
|
return datetime.strptime(value, '%Y-%m-%d').date()
|
|
|
|
|
|
class FusionFollowupController(http.Controller):
|
|
|
|
@http.route('/fusion/followup/list_overdue', type='jsonrpc', auth='user')
|
|
def list_overdue(self, limit=50, offset=0, status=None, company_id=None):
|
|
company_id = int(company_id) if company_id else request.env.company.id
|
|
Partner = request.env['res.partner'].sudo()
|
|
Line = request.env['account.move.line'].sudo()
|
|
overdue_partner_ids = Line.search([
|
|
('parent_state', '=', 'posted'),
|
|
('account_id.account_type', '=', 'asset_receivable'),
|
|
('reconciled', '=', False),
|
|
('amount_residual', '>', 0),
|
|
('date_maturity', '<', date.today()),
|
|
('company_id', '=', company_id),
|
|
]).mapped('partner_id').ids
|
|
|
|
domain = [('id', 'in', overdue_partner_ids)]
|
|
if status:
|
|
domain.append(('fusion_followup_status', '=', status))
|
|
total = Partner.search_count(domain)
|
|
partners = Partner.search(domain, limit=int(limit), offset=int(offset))
|
|
|
|
engine = request.env['fusion.followup.engine']
|
|
rows = []
|
|
for p in partners:
|
|
try:
|
|
overdue = engine.get_overdue_for_partner(p)
|
|
rows.append({
|
|
'partner_id': p.id,
|
|
'partner_name': p.name,
|
|
'email': p.email or '',
|
|
'status': p.fusion_followup_status,
|
|
'paused_until': str(p.fusion_followup_paused_until)
|
|
if p.fusion_followup_paused_until else None,
|
|
'last_level_id': p.fusion_followup_last_level_id.id
|
|
if p.fusion_followup_last_level_id else None,
|
|
'last_level_name': p.fusion_followup_last_level_id.name
|
|
if p.fusion_followup_last_level_id else None,
|
|
'last_run_date': str(p.fusion_followup_last_run_date)
|
|
if p.fusion_followup_last_run_date else None,
|
|
'overdue_amount': overdue['aging']['total_overdue_amount'],
|
|
'overdue_line_count': overdue['overdue_line_count'],
|
|
'risk_score': overdue['risk']['score'],
|
|
'risk_band': overdue['risk']['band'],
|
|
})
|
|
except Exception as e:
|
|
_logger.warning("Skipping partner %s in list: %s", p.id, e)
|
|
return {'count': len(rows), 'total': total, 'partners': rows}
|
|
|
|
@http.route('/fusion/followup/get_partner_detail', type='jsonrpc', auth='user')
|
|
def get_partner_detail(self, partner_id):
|
|
partner = request.env['res.partner'].browse(int(partner_id))
|
|
if not partner.exists():
|
|
raise ValidationError(_("Partner %s not found") % partner_id)
|
|
engine = request.env['fusion.followup.engine']
|
|
overdue = engine.get_overdue_for_partner(partner)
|
|
history = engine.snapshot_followup_history(partner, limit=20)
|
|
level = engine.compute_followup_level(partner)
|
|
return {
|
|
'partner': {
|
|
'id': partner.id,
|
|
'name': partner.name,
|
|
'email': partner.email or '',
|
|
'status': partner.fusion_followup_status,
|
|
'paused_until': str(partner.fusion_followup_paused_until)
|
|
if partner.fusion_followup_paused_until else None,
|
|
'last_level_id': partner.fusion_followup_last_level_id.id
|
|
if partner.fusion_followup_last_level_id else None,
|
|
'last_level_name': partner.fusion_followup_last_level_id.name
|
|
if partner.fusion_followup_last_level_id else None,
|
|
'last_run_date': str(partner.fusion_followup_last_run_date)
|
|
if partner.fusion_followup_last_run_date else None,
|
|
'risk_score': partner.fusion_followup_risk_score,
|
|
'risk_band': partner.fusion_followup_risk_band,
|
|
},
|
|
'overdue': overdue,
|
|
'suggested_level': {
|
|
'id': level.id, 'name': level.name, 'tone': level.tone,
|
|
'sequence': level.sequence,
|
|
} if level else None,
|
|
'history': history,
|
|
}
|
|
|
|
@http.route('/fusion/followup/generate_text', type='jsonrpc', auth='user')
|
|
def generate_text(self, partner_id, level_id=None, force_regenerate=False):
|
|
from odoo.addons.fusion_accounting_followup.services.followup_text_generator import (
|
|
generate_followup_text,
|
|
)
|
|
from odoo.addons.fusion_accounting_followup.services.tone_selector import select_tone
|
|
|
|
partner = request.env['res.partner'].browse(int(partner_id))
|
|
engine = request.env['fusion.followup.engine']
|
|
if level_id:
|
|
level = request.env['fusion.followup.level'].browse(int(level_id))
|
|
else:
|
|
level = engine.compute_followup_level(partner)
|
|
if not level:
|
|
return {'status': 'no_level', 'partner_id': partner.id}
|
|
|
|
overdue = engine.get_overdue_for_partner(partner)
|
|
tone = select_tone(
|
|
level_sequence=level.sequence,
|
|
risk_score=overdue['risk']['score'],
|
|
)
|
|
|
|
currency_code = 'USD'
|
|
if partner.company_id and partner.company_id.currency_id:
|
|
currency_code = partner.company_id.currency_id.name or 'USD'
|
|
|
|
text = generate_followup_text(
|
|
request.env,
|
|
partner_name=partner.name,
|
|
total_overdue=overdue['aging']['total_overdue_amount'],
|
|
currency_code=currency_code,
|
|
longest_overdue_days=engine._max_overdue_days_from_aging(overdue['aging']),
|
|
tone=tone,
|
|
invoice_count=overdue['overdue_line_count'],
|
|
risk_drivers=overdue['risk']['drivers'],
|
|
)
|
|
return {
|
|
'status': 'ok',
|
|
'partner_id': partner.id,
|
|
'level_id': level.id,
|
|
'tone': tone,
|
|
'subject': text.get('subject', ''),
|
|
'body': text.get('body', ''),
|
|
'tone_used': text.get('tone_used', tone),
|
|
'key_points': text.get('key_points', []),
|
|
}
|
|
|
|
@http.route('/fusion/followup/send', type='jsonrpc', auth='user')
|
|
def send_followup(self, partner_id, level_id=None, force=False):
|
|
partner = request.env['res.partner'].browse(int(partner_id))
|
|
engine = request.env['fusion.followup.engine']
|
|
level = None
|
|
if level_id:
|
|
level = request.env['fusion.followup.level'].browse(int(level_id))
|
|
return engine.send_followup_email(partner, level=level, force=bool(force))
|
|
|
|
@http.route('/fusion/followup/pause', type='jsonrpc', auth='user')
|
|
def pause(self, partner_id, until_date=None):
|
|
partner = request.env['res.partner'].browse(int(partner_id))
|
|
engine = request.env['fusion.followup.engine']
|
|
return engine.pause_followup(partner, until_date=_parse_date(until_date))
|
|
|
|
@http.route('/fusion/followup/reset', type='jsonrpc', auth='user')
|
|
def reset(self, partner_id):
|
|
partner = request.env['res.partner'].browse(int(partner_id))
|
|
engine = request.env['fusion.followup.engine']
|
|
return engine.reset_followup(partner)
|