diff --git a/fusion_accounting_followup/__manifest__.py b/fusion_accounting_followup/__manifest__.py index 3b1a4802..26540b4e 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.26', + 'version': '19.0.1.0.27', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.', 'description': """ @@ -28,6 +28,7 @@ menu hides; the engine + AI tools remain available for the chat. 'depends': [ 'fusion_accounting_core', 'fusion_accounting_ai', + 'fusion_accounting_migration', 'account', 'mail', ], diff --git a/fusion_accounting_followup/models/__init__.py b/fusion_accounting_followup/models/__init__.py index 216dd595..df3570c7 100644 --- a/fusion_accounting_followup/models/__init__.py +++ b/fusion_accounting_followup/models/__init__.py @@ -5,3 +5,4 @@ from . import res_partner from . import account_move_line from . import fusion_followup_engine from . import fusion_followup_cron +from . import fusion_migration_wizard diff --git a/fusion_accounting_followup/models/fusion_migration_wizard.py b/fusion_accounting_followup/models/fusion_migration_wizard.py new file mode 100644 index 00000000..b2bbea30 --- /dev/null +++ b/fusion_accounting_followup/models/fusion_migration_wizard.py @@ -0,0 +1,80 @@ +"""Followup-specific migration step. + +Backfills fusion.followup.level from Enterprise's account_followup.followup.line +records (if Enterprise account_followup is installed).""" + +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class FusionMigrationWizard(models.TransientModel): + _inherit = "fusion.migration.wizard" + + def _followup_bootstrap_step(self): + """Backfill fusion.followup.level from account_followup.followup.line.""" + result = { + 'step': 'followup_bootstrap', + 'enterprise_module_present': False, + 'created': 0, 'skipped': 0, 'errors': [], + } + + # Enterprise's followup model — name varies by version + EnterpriseLine = self.env.get('account_followup.followup.line') + if EnterpriseLine is None: + EnterpriseLine = self.env.get('account.followup.line') + if EnterpriseLine is None: + result['enterprise_module_present'] = False + return result + result['enterprise_module_present'] = True + + FusionLevel = self.env['fusion.followup.level'].sudo() + try: + ee_records = EnterpriseLine.sudo().search([]) + except Exception as e: + result['errors'].append(f"Enterprise search failed: {e}") + return result + + # Map Enterprise tone-ish fields to ours + for ee in ee_records: + try: + # Idempotency: skip if a level with same sequence + name already exists + seq = getattr(ee, 'sequence', None) or 50 + name = getattr(ee, 'name', None) or f"Migrated Level {seq}" + existing = FusionLevel.search([ + ('sequence', '=', seq), + ('name', '=', name), + ], limit=1) + if existing: + result['skipped'] += 1 + continue + + delay = getattr(ee, 'delay', None) or getattr(ee, 'delay_days', 7) + # Enterprise tone heuristic: scale by sequence + tone = 'gentle' if seq <= 1 else 'firm' if seq <= 2 else 'legal' + + FusionLevel.create({ + 'name': name, + 'sequence': seq + 100, # offset so we don't clash with seeded defaults + 'delay_days': delay, + 'tone': tone, + 'active': True, + }) + result['created'] += 1 + except Exception as e: + result['errors'].append(f"Line {ee.id}: {e}") + + _logger.info( + "fusion_accounting_followup migration: %d created, %d skipped, %d errors", + result['created'], result['skipped'], len(result['errors'])) + return result + + def action_run_migration(self): + result = super().action_run_migration() if hasattr(super(), 'action_run_migration') else None + try: + self._followup_bootstrap_step() + except Exception as e: + _logger.warning("followup_bootstrap_step failed: %s", e) + return result diff --git a/fusion_accounting_followup/tests/__init__.py b/fusion_accounting_followup/tests/__init__.py index 5e4e01db..88f9ca6e 100644 --- a/fusion_accounting_followup/tests/__init__.py +++ b/fusion_accounting_followup/tests/__init__.py @@ -18,3 +18,4 @@ from . import test_engine_property from . import test_followup_full_flow from . import test_performance_benchmarks from . import test_batch_followup_wizard +from . import test_migration_round_trip diff --git a/fusion_accounting_followup/tests/test_migration_round_trip.py b/fusion_accounting_followup/tests/test_migration_round_trip.py new file mode 100644 index 00000000..6dce5510 --- /dev/null +++ b/fusion_accounting_followup/tests/test_migration_round_trip.py @@ -0,0 +1,21 @@ +from odoo.tests.common import TransactionCase +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestFollowupMigrationRoundTrip(TransactionCase): + + def test_bootstrap_step_runs(self): + wizard = self.env['fusion.migration.wizard'].create({}) + result = wizard._followup_bootstrap_step() + self.assertEqual(result['step'], 'followup_bootstrap') + # Either Enterprise present or not — both OK + self.assertIn(result['enterprise_module_present'], [True, False]) + + def test_bootstrap_idempotent(self): + wizard = self.env['fusion.migration.wizard'].create({}) + first = wizard._followup_bootstrap_step() + second = wizard._followup_bootstrap_step() + # Second run skips what first created (or both no-op) + if first['enterprise_module_present']: + self.assertGreaterEqual(second['skipped'], first['created'])