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',
|
'name': 'Fusion Accounting Follow-up',
|
||||||
'version': '19.0.1.0.17',
|
'version': '19.0.1.0.18',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.',
|
'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.',
|
||||||
'description': """
|
'description': """
|
||||||
@@ -33,6 +33,7 @@ menu hides; the engine + AI tools remain available for the chat.
|
|||||||
],
|
],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
|
'data/cron.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
'web.assets_backend': [
|
'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 res_partner
|
||||||
from . import account_move_line
|
from . import account_move_line
|
||||||
from . import fusion_followup_engine
|
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_controller
|
||||||
from . import test_followup_adapter
|
from . import test_followup_adapter
|
||||||
from . import test_followup_tools
|
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