diff --git a/expenses_manager/expenses/migrations/0015_link_legacy_transfers.py b/expenses_manager/expenses/migrations/0015_link_legacy_transfers.py new file mode 100644 index 0000000..f5de187 --- /dev/null +++ b/expenses_manager/expenses/migrations/0015_link_legacy_transfers.py @@ -0,0 +1,72 @@ +"""Convierte en Transfer los traspasos históricos que estaban sueltos. + +Antes de que existiera Transfer, un traspaso eran dos movimientos marcados con +is_transfer pero sin nada que los relacionara: un Expense en la cuenta de origen +y un Income en la de destino. Aquí se emparejan y se les cuelga un Transfer. + +No se pueden emparejar con certeza: nada en la base de datos dice qué gasto va +con qué ingreso. El criterio es mismo owner, mismo importe, misma fecha y cuentas +distintas, y solo cuando en ese grupo hay exactamente un gasto y un ingreso. Los +ambiguos (varios candidatos con el mismo importe y fecha) y los huérfanos (sin +pareja) se dejan tal cual, con su is_transfer=True: siguen contando en los saldos +y fuera de los análisis igual que antes, solo que sin Transfer que los orqueste. +Emparejarlos a ojo sería inventarse datos del usuario. +""" + +from collections import defaultdict + +from django.db import migrations + + +def link_legacy_transfers(apps, schema_editor): + Expense = apps.get_model("expenses", "Expense") + Income = apps.get_model("expenses", "Income") + Transfer = apps.get_model("expenses", "Transfer") + + # Solo las patas que aún no pertenecen a ningún Transfer. + expenses_by_key = defaultdict(list) + for expense in Expense.objects.filter( + is_transfer=True, transfer_out__isnull=True, transfer_fee__isnull=True + ): + expenses_by_key[(expense.owner_id, expense.amount, expense.date)].append( + expense + ) + + incomes_by_key = defaultdict(list) + for income in Income.objects.filter(is_transfer=True, transfer_in__isnull=True): + incomes_by_key[(income.owner_id, income.amount, income.date)].append(income) + + for key, expenses in expenses_by_key.items(): + incomes = incomes_by_key.get(key, []) + + # Uno y solo uno de cada lado: cualquier otra cosa es ambigua. + if len(expenses) != 1 or len(incomes) != 1: + continue + + expense, income = expenses[0], incomes[0] + + if expense.account_id == income.account_id: + continue + + Transfer.objects.create( + owner_id=expense.owner_id, + date=expense.date, + amount=expense.amount, + from_account_id=expense.account_id, + to_account_id=income.account_id, + expense_leg=expense, + income_leg=income, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("expenses", "0014_category_is_system_transfer"), + ] + + operations = [ + # Sin marcha atrás: deshacer esto es deshacer 0014, que se lleva la tabla + # Transfer entera y deja las patas intactas, que es el estado de partida. + migrations.RunPython(link_legacy_transfers, migrations.RunPython.noop), + ]