diff --git a/fusion_accounting_followup/__manifest__.py b/fusion_accounting_followup/__manifest__.py index 4f83692d..3b1a4802 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.25', + 'version': '19.0.1.0.26', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.', 'description': """ @@ -36,6 +36,7 @@ menu hides; the engine + AI tools remain available for the chat. 'data/cron.xml', 'data/followup_levels_data.xml', 'data/mail_templates_data.xml', + 'wizards/batch_followup_wizard_views.xml', ], 'assets': { 'web.assets_backend': [ diff --git a/fusion_accounting_followup/security/ir.model.access.csv b/fusion_accounting_followup/security/ir.model.access.csv index 04690ebc..fa38cd4d 100644 --- a/fusion_accounting_followup/security/ir.model.access.csv +++ b/fusion_accounting_followup/security/ir.model.access.csv @@ -5,3 +5,4 @@ access_fusion_followup_run_user,fusion.followup.run.user,model_fusion_followup_r access_fusion_followup_run_admin,fusion.followup.run.admin,model_fusion_followup_run,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1 access_fusion_followup_text_cache_user,fusion.followup.text.cache.user,model_fusion_followup_text_cache,base.group_user,1,0,0,0 access_fusion_followup_text_cache_admin,fusion.followup.text.cache.admin,model_fusion_followup_text_cache,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1 +access_fusion_batch_followup_wizard_user,fusion.batch.followup.wizard.user,model_fusion_batch_followup_wizard,base.group_user,1,1,1,0 diff --git a/fusion_accounting_followup/tests/__init__.py b/fusion_accounting_followup/tests/__init__.py index 8bb6e435..5e4e01db 100644 --- a/fusion_accounting_followup/tests/__init__.py +++ b/fusion_accounting_followup/tests/__init__.py @@ -17,3 +17,4 @@ from . import test_followup_cron from . import test_engine_property from . import test_followup_full_flow from . import test_performance_benchmarks +from . import test_batch_followup_wizard diff --git a/fusion_accounting_followup/tests/test_batch_followup_wizard.py b/fusion_accounting_followup/tests/test_batch_followup_wizard.py new file mode 100644 index 00000000..dc35f275 --- /dev/null +++ b/fusion_accounting_followup/tests/test_batch_followup_wizard.py @@ -0,0 +1,37 @@ +from odoo.tests.common import TransactionCase +from odoo.tests import tagged +from odoo.exceptions import UserError + + +@tagged('post_install', '-at_install') +class TestBatchFollowupWizard(TransactionCase): + + def test_default_loads_active_ids(self): + partners = self.env['res.partner'].create([ + {'name': 'B1'}, {'name': 'B2'}, + ]) + wizard = self.env['fusion.batch.followup.wizard'].with_context( + active_model='res.partner', active_ids=partners.ids, + ).create({}) + self.assertEqual(set(wizard.partner_ids.ids), set(partners.ids)) + + def test_selected_scope_no_partners_raises(self): + wizard = self.env['fusion.batch.followup.wizard'].create({ + 'scope': 'selected', 'partner_ids': [(6, 0, [])], + }) + with self.assertRaises(UserError): + wizard.action_run() + + def test_run_completes_with_no_overdue_partners(self): + partners = self.env['res.partner'].create([ + {'name': 'NoOverdue1'}, {'name': 'NoOverdue2'}, + ]) + wizard = self.env['fusion.batch.followup.wizard'].create({ + 'scope': 'selected', + 'partner_ids': [(6, 0, partners.ids)], + 'force': True, + }) + wizard.action_run() + self.assertEqual(wizard.state, 'done') + # 2 partners with no overdue → both skipped + self.assertEqual(wizard.skipped_count, 2) diff --git a/fusion_accounting_followup/wizards/__init__.py b/fusion_accounting_followup/wizards/__init__.py index e69de29b..a388a168 100644 --- a/fusion_accounting_followup/wizards/__init__.py +++ b/fusion_accounting_followup/wizards/__init__.py @@ -0,0 +1 @@ +from . import batch_followup_wizard diff --git a/fusion_accounting_followup/wizards/batch_followup_wizard.py b/fusion_accounting_followup/wizards/batch_followup_wizard.py new file mode 100644 index 00000000..44a0ddb6 --- /dev/null +++ b/fusion_accounting_followup/wizards/batch_followup_wizard.py @@ -0,0 +1,91 @@ +"""Batch send follow-ups to selected partners (or all overdue).""" + +from datetime import date + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class FusionBatchFollowupWizard(models.TransientModel): + _name = "fusion.batch.followup.wizard" + _description = "Batch Send Follow-ups Wizard" + + scope = fields.Selection([ + ('selected', 'Selected partners only'), + ('all_overdue', 'All overdue partners'), + ], required=True, default='selected') + partner_ids = fields.Many2many('res.partner', + default=lambda self: self._default_partner_ids()) + force = fields.Boolean(string='Force (override pause + manual review)', + default=False) + auto_resolve_level = fields.Boolean( + string='Auto-resolve level', + default=True, + help="If True, engine picks the appropriate level per partner. " + "If False, use the chosen override level for all.") + override_level_id = fields.Many2one('fusion.followup.level') + + # Results + state = fields.Selection([('draft', 'Draft'), ('done', 'Done')], default='draft') + sent_count = fields.Integer(readonly=True) + skipped_count = fields.Integer(readonly=True) + error_count = fields.Integer(readonly=True) + summary = fields.Text(readonly=True) + + @api.model + def _default_partner_ids(self): + ctx = self.env.context + if ctx.get('active_model') == 'res.partner': + return ctx.get('active_ids', []) + return [] + + def action_run(self): + self.ensure_one() + if self.scope == 'selected' and not self.partner_ids: + raise UserError(_("No partners selected.")) + + partners = self.partner_ids + if self.scope == 'all_overdue': + Line = self.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', '=', self.env.company.id), + ]).mapped('partner_id').ids + partners = self.env['res.partner'].sudo().browse(overdue_partner_ids) + + engine = self.env['fusion.followup.engine'] + sent = 0 + skipped = 0 + errors = [] + for partner in partners: + try: + with self.env.cr.savepoint(): + level = self.override_level_id if not self.auto_resolve_level else None + result = engine.send_followup_email( + partner, level=level, force=self.force) + status = result.get('status', '') + if status == 'sent': + sent += 1 + else: + skipped += 1 + except Exception as e: + errors.append(f"{partner.name}: {e}") + + self.write({ + 'state': 'done', + 'sent_count': sent, + 'skipped_count': skipped, + 'error_count': len(errors), + 'summary': '\n'.join(errors[:20]) if errors else False, + }) + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'new', + } diff --git a/fusion_accounting_followup/wizards/batch_followup_wizard_views.xml b/fusion_accounting_followup/wizards/batch_followup_wizard_views.xml new file mode 100644 index 00000000..538a720b --- /dev/null +++ b/fusion_accounting_followup/wizards/batch_followup_wizard_views.xml @@ -0,0 +1,44 @@ + + + + fusion.batch.followup.wizard.form + fusion.batch.followup.wizard + +
+ + + + + + + + + + + + + + +
+
+ +
+
+ + + Batch Send Follow-ups + fusion.batch.followup.wizard + form + new + + list + +