From 042dcf80673c2bcad8cb06f154ef21a04fd46848 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 21:04:37 -0400 Subject: [PATCH] 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 --- fusion_accounting_followup/__manifest__.py | 3 +- fusion_accounting_followup/data/cron.xml | 24 ++++++ fusion_accounting_followup/models/__init__.py | 1 + .../models/fusion_followup_cron.py | 84 +++++++++++++++++++ fusion_accounting_followup/tests/__init__.py | 1 + .../tests/test_followup_cron.py | 18 ++++ 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_followup/data/cron.xml create mode 100644 fusion_accounting_followup/models/fusion_followup_cron.py create mode 100644 fusion_accounting_followup/tests/test_followup_cron.py diff --git a/fusion_accounting_followup/__manifest__.py b/fusion_accounting_followup/__manifest__.py index f95be0ad..14adf203 100644 --- a/fusion_accounting_followup/__manifest__.py +++ b/fusion_accounting_followup/__manifest__.py @@ -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': [ diff --git a/fusion_accounting_followup/data/cron.xml b/fusion_accounting_followup/data/cron.xml new file mode 100644 index 00000000..b28cee68 --- /dev/null +++ b/fusion_accounting_followup/data/cron.xml @@ -0,0 +1,24 @@ + + + + + Fusion Follow-up — Daily Scan + Send + + code + model._cron_daily_scan() + 1 + days + + + + + Fusion Follow-up — Weekly Risk Refresh + + code + model._cron_risk_refresh() + 7 + days + + + + diff --git a/fusion_accounting_followup/models/__init__.py b/fusion_accounting_followup/models/__init__.py index ec9d216e..216dd595 100644 --- a/fusion_accounting_followup/models/__init__.py +++ b/fusion_accounting_followup/models/__init__.py @@ -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 diff --git a/fusion_accounting_followup/models/fusion_followup_cron.py b/fusion_accounting_followup/models/fusion_followup_cron.py new file mode 100644 index 00000000..f0c825b7 --- /dev/null +++ b/fusion_accounting_followup/models/fusion_followup_cron.py @@ -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) diff --git a/fusion_accounting_followup/tests/__init__.py b/fusion_accounting_followup/tests/__init__.py index 94fa47b3..76912615 100644 --- a/fusion_accounting_followup/tests/__init__.py +++ b/fusion_accounting_followup/tests/__init__.py @@ -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 diff --git a/fusion_accounting_followup/tests/test_followup_cron.py b/fusion_accounting_followup/tests/test_followup_cron.py new file mode 100644 index 00000000..13d815c9 --- /dev/null +++ b/fusion_accounting_followup/tests/test_followup_cron.py @@ -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()