feat(fusion_accounting_followup): 2 cron jobs (daily scan + weekly risk refresh)
- fusion.followup.cron AbstractModel with two handlers - cron_fusion_followup_daily_scan: walks every overdue partner and delegates to engine.send_followup_email - cron_fusion_followup_risk_refresh: weekly refresh of fusion_followup_risk_score / risk_band on res.partner - V19 ir.cron records (no numbercall field) - 2 smoke tests added (80 total) Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'Fusion Accounting Follow-up',
|
||||
'version': '19.0.1.0.17',
|
||||
'version': '19.0.1.0.18',
|
||||
'category': 'Accounting/Accounting',
|
||||
'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.',
|
||||
'description': """
|
||||
@@ -33,6 +33,7 @@ menu hides; the engine + AI tools remain available for the chat.
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/cron.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
|
||||
24
fusion_accounting_followup/data/cron.xml
Normal file
24
fusion_accounting_followup/data/cron.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="cron_fusion_followup_daily_scan" model="ir.cron">
|
||||
<field name="name">Fusion Follow-up — Daily Scan + Send</field>
|
||||
<field name="model_id" ref="model_fusion_followup_cron"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_daily_scan()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="cron_fusion_followup_risk_refresh" model="ir.cron">
|
||||
<field name="name">Fusion Follow-up — Weekly Risk Refresh</field>
|
||||
<field name="model_id" ref="model_fusion_followup_cron"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_risk_refresh()</field>
|
||||
<field name="interval_number">7</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -4,3 +4,4 @@ from . import fusion_followup_text_cache
|
||||
from . import res_partner
|
||||
from . import account_move_line
|
||||
from . import fusion_followup_engine
|
||||
from . import fusion_followup_cron
|
||||
|
||||
84
fusion_accounting_followup/models/fusion_followup_cron.py
Normal file
84
fusion_accounting_followup/models/fusion_followup_cron.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Cron handlers for fusion_accounting_followup.
|
||||
|
||||
Two scheduled jobs:
|
||||
- Daily scan: walk every partner with an open overdue receivable line and
|
||||
call the engine to send/escalate where appropriate.
|
||||
- Weekly risk refresh: recompute fusion_followup_risk_score on every
|
||||
partner with overdue.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionFollowupCron(models.AbstractModel):
|
||||
_name = "fusion.followup.cron"
|
||||
_description = "Fusion Follow-up Cron Handlers"
|
||||
|
||||
@api.model
|
||||
def _cron_daily_scan(self):
|
||||
"""Scan every partner with overdue and send follow-ups when due."""
|
||||
engine = self.env['fusion.followup.engine']
|
||||
Line = self.env['account.move.line'].sudo()
|
||||
overdue_lines = Line.search([
|
||||
('parent_state', '=', 'posted'),
|
||||
('account_id.account_type', '=', 'asset_receivable'),
|
||||
('reconciled', '=', False),
|
||||
('amount_residual', '>', 0),
|
||||
('date_maturity', '<', date.today()),
|
||||
])
|
||||
partner_ids = list(set(overdue_lines.mapped('partner_id').ids))
|
||||
sent = 0
|
||||
skipped = 0
|
||||
for pid in partner_ids:
|
||||
partner = self.env['res.partner'].sudo().browse(pid)
|
||||
if not partner.exists():
|
||||
continue
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
result = engine.send_followup_email(partner)
|
||||
if result.get('status') == 'sent':
|
||||
sent += 1
|
||||
else:
|
||||
skipped += 1
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
"Cron daily_scan failed for partner %s: %s", pid, e,
|
||||
)
|
||||
skipped += 1
|
||||
_logger.info(
|
||||
"Cron: scanned %d partners, sent %d, skipped %d",
|
||||
len(partner_ids), sent, skipped,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _cron_risk_refresh(self):
|
||||
"""Refresh fusion_followup_risk_score on every partner with overdue."""
|
||||
Partner = self.env['res.partner'].sudo()
|
||||
engine = self.env['fusion.followup.engine']
|
||||
Line = self.env['account.move.line'].sudo()
|
||||
partner_ids = list(set(Line.search([
|
||||
('parent_state', '=', 'posted'),
|
||||
('account_id.account_type', '=', 'asset_receivable'),
|
||||
('reconciled', '=', False),
|
||||
('amount_residual', '>', 0),
|
||||
]).mapped('partner_id').ids))
|
||||
updated = 0
|
||||
for pid in partner_ids:
|
||||
partner = Partner.browse(pid)
|
||||
try:
|
||||
overdue = engine.get_overdue_for_partner(partner)
|
||||
partner.write({
|
||||
'fusion_followup_risk_score': overdue['risk']['score'],
|
||||
'fusion_followup_risk_band': overdue['risk']['band'],
|
||||
})
|
||||
updated += 1
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
"Risk refresh failed for partner %s: %s", pid, e,
|
||||
)
|
||||
_logger.info("Cron: refreshed risk on %d partners", updated)
|
||||
@@ -13,3 +13,4 @@ from . import test_engine_integration
|
||||
from . import test_followup_controller
|
||||
from . import test_followup_adapter
|
||||
from . import test_followup_tools
|
||||
from . import test_followup_cron
|
||||
|
||||
18
fusion_accounting_followup/tests/test_followup_cron.py
Normal file
18
fusion_accounting_followup/tests/test_followup_cron.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Smoke tests for the fusion follow-up cron handlers."""
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestFollowupCron(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.cron = self.env['fusion.followup.cron']
|
||||
|
||||
def test_cron_daily_scan_runs(self):
|
||||
self.cron._cron_daily_scan()
|
||||
|
||||
def test_cron_risk_refresh_runs(self):
|
||||
self.cron._cron_risk_refresh()
|
||||
Reference in New Issue
Block a user