Симметричная модель django

Я хочу создать модель, которая симметрична по двум полям. Назовем модель Balance:

class Balance (models.Model):
    payer = models.ForeignKey(auth.User, ...)
    payee = models.ForeignKey(auth.User, ...)
    amount = models.DecimalField(...)

Он должен обладать следующим свойством:

balance_forward = Balance.objects.get(payer=USER_1, payee=USER_2)
balance_backward = Balance.objects.get(payer=USER_2, payee=USER_1)

balance_forward.amount == -1 * balance_backward.amount

Каким образом лучше всего это реализовать?

Итак, я пришел к следующему решению. Не стесняйтесь предлагать другие решения.

class SymmetricPlayersQuerySet (models.query.QuerySet):

    def do_swap(self, obj):
        obj.payer, obj.payee = obj.payee, obj.payer
        obj.amount *= -1

    def get(self, **kwargs):
        swap = False
        if "payer" in kwargs and "payee" in kwargs:
            if kwargs["payer"].id > kwargs["payee"].id:
                swap = True
                kwargs["payer"], kwargs["payee"] = \
                    kwargs["payee"], kwargs["payer"]

        obj = super().get(**kwargs)

        if swap:
            self.do_swap(obj)

        return obj

    def filter(self, *args, **kwargs):
        if (
            ("payer" in kwargs and "payee" not in kwargs) or
            ("payee" in kwargs and "payer" not in kwargs)
        ):
            if "payee" in kwargs:
                key, other = "payee", "payer"
            else:
                key, other = "payer", "payee"

            constraints = (
                models.Q(payer=kwargs[key]) |
                models.Q(payee=kwargs[key])
            )

            queryset = super().filter(constraints)
            for obj in queryset:
                if getattr(obj, other) == kwargs[key]:
                    self.do_swap(obj)

            return queryset

        return super().filter(*args, **kwargs)


class BalanceManager (models.Manager.from_queryset(SymmetricPlayersQuerySet)):
    pass


class Balance (models.Model):

    objects = BalanceManager()

    payer = models.ForeignKey(
        Player,
        on_delete=models.CASCADE,
        related_name='balance_payer',
    )
    payee = models.ForeignKey(
        Player,
        on_delete=models.CASCADE,
        related_name='balance_payee',
    )
    amount = models.DecimalField(decimal_places=2, max_digits=1000, default=0)

    def do_swap(self):
        self.payer, self.payee = self.payee, self.payer
        self.amount *= -1

    def save(self, *args, **kwargs):

        swap = False
        if self.payer.id > self.payee.id:
            swap = True
            self.do_swap()

        result = super().save(*args, **kwargs)

        if swap:
            self.do_swap()

        return result

    def refresh_from_db(self, *args, **kwargs):
        swap = False
        if self.payer.id > self.payee.id:
            swap = True

        super().refresh_from_db(*args, **kwargs)

        if swap:
            self.do_swap()

Вы можете агрегировать объекты Balance с помощью:

from django.db.models import Case, F, Sum, When
from django.conf import settings


class Balance(models.Model):
    payer = models.ForeignKey(settings.AUTH_USER_MODEL)
    payee = models.ForeignKey(settings.AUTH_USER_MODEL)
    amount = models.DecimalField()

    def get_balance(cls, payer, payee):
        return cls.objects.filter(
            Q(payer=payer, payee=payee) | Q(payer=payee, payee=payer)
        ).aggregate(
            total=Sum(
                Case(
                    When(payer=payer, then=F('amount')),
                    otherwise=-F('amount'),
                    output_field=DecimalField(),
                )
            )
        )['total']

В результате будут найдены все Balance между payer и payee, и вычтены те, что в обратном направлении. Таким образом, Balance.get_balance(payer=foo, payee=bar) определит общий поток от foo до bar.

Вернуться на верх