feat(fusion_accounting_followup): per-partner state migration from Enterprise

Migrates Enterprise account_followup per-partner state to Fusion fields:
- res.partner.followup_status -> fusion_followup_status (action_due/no_action)
- res.partner.payment_next_action_date -> fusion_followup_paused_until
  (when future-dated; sets status to 'paused')
- res.partner.followup_line_id -> fusion_followup_last_level_id
  (resolved by name match against migrated levels)

Wired into fusion.migration.wizard.action_run_migration after the
existing _followup_bootstrap_step. Idempotent: skips partners whose
Fusion state is already non-default. Defensive against missing
Enterprise fields (each field probed individually before use).

Closes the per-partner state migration gap that was blocking
Enterprise account_followup uninstall.

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 23:48:22 -04:00
parent b15bf2293e
commit 679dbaa979
2 changed files with 119 additions and 0 deletions

View File

@@ -78,10 +78,120 @@ class FusionMigrationWizard(models.TransientModel):
result['created'], result['skipped'], len(result['errors']))
return result
def _followup_partner_state_bootstrap_step(self):
"""Migration step: copy Enterprise account_followup per-partner state
onto Fusion's fields on res.partner.
Idempotent: only updates partners whose Fusion field is at default
(no_action) and whose Enterprise field has a non-default value.
"""
self.ensure_one()
_logger.info("fusion_accounting_followup partner-state migration starting")
Partner = self.env['res.partner'].sudo()
has_status = 'followup_status' in Partner._fields
has_next_date = 'payment_next_action_date' in Partner._fields
has_line = 'followup_line_id' in Partner._fields
if not (has_status or has_next_date or has_line):
_logger.info(
"Enterprise account_followup partner fields not present \u2014 skipping")
return {
'step': 'followup_partner_state',
'enterprise_module_present': False,
'updated': 0, 'skipped': 0, 'errors': [],
}
result = {
'step': 'followup_partner_state',
'enterprise_module_present': True,
'updated': 0, 'skipped': 0, 'errors': [],
}
domain_terms = []
if has_status:
domain_terms.append(('followup_status', '!=', 'no_action_needed'))
if has_next_date:
domain_terms.append(('payment_next_action_date', '!=', False))
if not domain_terms:
_logger.info("No usable Enterprise follow-up fields \u2014 skipping")
return result
if len(domain_terms) > 1:
domain = ['|'] * (len(domain_terms) - 1) + domain_terms
else:
domain = domain_terms
candidates = Partner.search(domain)
_logger.info(
"Found %d partners with non-default Enterprise follow-up state",
len(candidates))
Level = self.env['fusion.followup.level'].sudo()
today = fields.Date.today()
status_map = {
'in_need_of_action': 'action_due',
'with_overdue_invoices': 'action_due',
'no_action_needed': 'no_action',
}
for partner in candidates:
try:
if partner.fusion_followup_status not in (False, 'no_action'):
result['skipped'] += 1
continue
vals = {}
ent_status = (
getattr(partner, 'followup_status', None)
if has_status else None)
if ent_status and ent_status in status_map:
vals['fusion_followup_status'] = status_map[ent_status]
next_date = (
getattr(partner, 'payment_next_action_date', False)
if has_next_date else False)
if next_date and next_date > today:
vals['fusion_followup_paused_until'] = next_date
vals['fusion_followup_status'] = 'paused'
ent_line = (
getattr(partner, 'followup_line_id', None)
if has_line else None)
if ent_line:
fusion_level = Level.search([
('name', '=', ent_line.name),
], limit=1)
if fusion_level:
vals['fusion_followup_last_level_id'] = fusion_level.id
if vals:
partner.write(vals)
result['updated'] += 1
_logger.debug(
"Migrated partner %s: %s", partner.name, vals)
else:
result['skipped'] += 1
except Exception as e:
result['errors'].append(
f"Partner {partner.id} ({partner.name}): {e}")
_logger.warning(
"Migration failed for partner %s: %s", partner.id, e)
_logger.info(
"fusion_accounting_followup partner-state migration: "
"updated=%d skipped=%d errors=%d",
result['updated'], 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)
try:
self._followup_partner_state_bootstrap_step()
except Exception as e:
_logger.warning("followup_partner_state_bootstrap_step failed: %s", e)
return result

View File

@@ -19,3 +19,12 @@ class TestFollowupMigrationRoundTrip(TransactionCase):
# Second run skips what first created (or both no-op)
if first['enterprise_module_present']:
self.assertGreaterEqual(second['skipped'], first['created'])
def test_partner_state_bootstrap_step(self):
"""Verify the partner-state migration step runs without error."""
wizard = self.env['fusion.migration.wizard'].create({})
result = wizard._followup_partner_state_bootstrap_step()
self.assertEqual(result['step'], 'followup_partner_state')
self.assertIn(result['enterprise_module_present'], [True, False])
self.assertGreaterEqual(result['updated'], 0)
self.assertGreaterEqual(result['skipped'], 0)