feat(fusion_accounting_followup): 6 JSON-RPC endpoints for OWL widget
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
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from . import followup_controller
|
||||
|
||||
173
fusion_accounting_followup/controllers/followup_controller.py
Normal file
173
fusion_accounting_followup/controllers/followup_controller.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user