import json
from datetime import datetime, timedelta
from decimal import Decimal
from unittest import mock

from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import timezone

from .. import KFET_DELETED_TRIGRAMME
from ..auth import KFET_GENERIC_TRIGRAMME
from ..auth.models import KFetGroup
from ..auth.utils import hash_password
from ..config import kfet_config
from ..models import (
    Account,
    Article,
    ArticleCategory,
    Checkout,
    CheckoutStatement,
    Inventory,
    InventoryArticle,
    Operation,
    OperationGroup,
    Order,
    OrderArticle,
    Supplier,
    SupplierArticle,
    Transfer,
    TransferGroup,
)
from .testcases import ViewTestCaseMixin
from .utils import (
    create_checkout,
    create_checkout_statement,
    create_inventory_article,
    create_operation_group,
    create_team,
    create_user,
    get_perms,
    user_add_perms,
)


class AccountListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account"
    url_expected = "/k-fet/accounts/"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)


class AccountValidFreeTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.is_validandfree.ajax"
    url_expected = "/k-fet/accounts/is_validandfree"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def test_ok_isvalid_isfree(self):
        """Upper case trigramme not taken is valid and free."""
        r = self.client.get(self.url, {"trigramme": "AAA"})
        self.assertDictEqual(
            json.loads(r.content.decode("utf-8")), {"is_valid": True, "is_free": True}
        )

    def test_ok_isvalid_notfree(self):
        """Already taken trigramme is not free, but valid."""
        r = self.client.get(self.url, {"trigramme": "000"})
        self.assertDictEqual(
            json.loads(r.content.decode("utf-8")), {"is_valid": True, "is_free": False}
        )

    def test_ok_notvalid_isfree(self):
        """Lower case if forbidden but free."""
        r = self.client.get(self.url, {"trigramme": "aaa"})
        self.assertDictEqual(
            json.loads(r.content.decode("utf-8")), {"is_valid": False, "is_free": True}
        )


class AccountCreateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.create"
    url_expected = "/k-fet/accounts/new"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    post_data = {
        "trigramme": "AAA",
        "username": "plopplopplop",
        "first_name": "first",
        "last_name": "last",
        "email": "email@domain.net",
    }

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.add_account"])}

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)
        self.assertRedirects(r, reverse("kfet.account.create"))

        account = Account.objects.get(trigramme="AAA")

        self.assertInstanceExpected(
            account,
            {"username": "plopplopplop", "first_name": "first", "last_name": "last"},
        )

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class AccountCreateAjaxViewTests(ViewTestCaseMixin, TestCase):
    urls_conf = [
        {
            "name": "kfet.account.create.fromuser",
            "kwargs": {"username": "user"},
            "expected": "/k-fet/accounts/new/user/user",
        },
        {
            "name": "kfet.account.create.fromclipper",
            "kwargs": {"login_clipper": "myclipper", "fullname": "first last1 last2"},
            "expected": ("/k-fet/accounts/new/clipper/myclipper/first%20last1%20last2"),
        },
        {"name": "kfet.account.create.empty", "expected": "/k-fet/accounts/new/empty"},
    ]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def test_fromuser(self):
        r = self.client.get(self.t_urls[0])
        self.assertEqual(r.status_code, 200)

        user = self.users["user"]

        self.assertEqual(r.context["user_form"].instance, user)
        self.assertEqual(r.context["cof_form"].instance, user.profile)
        self.assertIn("account_form", r.context)

    def test_fromclipper(self):
        r = self.client.get(self.t_urls[1])
        self.assertEqual(r.status_code, 200)

        self.assertIn("user_form", r.context)
        self.assertIn("cof_form", r.context)
        self.assertIn("account_form", r.context)

    def test_empty(self):
        r = self.client.get(self.t_urls[2])
        self.assertEqual(r.status_code, 200)

        self.assertIn("user_form", r.context)
        self.assertIn("cof_form", r.context)
        self.assertIn("account_form", r.context)


class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.create.autocomplete"
    url_expected = "/k-fet/autocomplete/account_new"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def test_ok(self):
        r = self.client.get(self.url, {"q": "first"})
        self.assertEqual(r.status_code, 200)
        self.assertEqual(len(r.context["results"]), 1)
        (res,) = r.context["results"]
        self.assertEqual(res.name, "kfet")

        u = self.users["user"]
        self.assertSetEqual(
            {e.verbose_name for e in res.entries},
            {"{} ({})".format(u, u.profile.account_kfet.trigramme)},
        )


class AccountSearchViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.search.autocomplete"
    url_expected = "/k-fet/autocomplete/account_search"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def test_ok(self):
        r = self.client.get(self.url, {"q": "first"})
        self.assertEqual(r.status_code, 200)

        u = self.users["user"]
        self.assertSetEqual(
            {e.verbose_name for e in r.context["results"][0].entries},
            {"{} ({})".format(u, u.profile.account_kfet.trigramme)},
        )


class AccountReadViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.read"
    url_kwargs = {"trigramme": "001"}
    url_expected = "/k-fet/accounts/001"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    # Users with forbidden access users should get a 404 here, to avoid leaking trigrams
    # See issue #224
    def test_forbidden(self):
        for user in self.auth_forbidden:
            self.assertRedirectsToLoginOr404(user, self.url_expected)
            self.assertRedirectsToLoginOr404(user, "/k-fet/accounts/NEX")

    def assertRedirectsToLoginOr404(self, user, url):
        client = Client()
        if user is None:
            response = client.get(url)
            self.assertRedirects(
                response,
                "/gestion/login?next={}".format(url),
                fetch_redirect_response=False,
            )
        else:
            client.login(username=user, password=user)
            response = client.get(url)
            self.assertEqual(response.status_code, 404)

    def get_users_extra(self):
        return {"user1": create_user("user1", "001")}

    def setUp(self):
        super().setUp()

        user1_acc = self.accounts["user1"]
        team_acc = self.accounts["team"]

        # Dummy operations and operation groups
        checkout = Checkout.objects.create(
            created_by=team_acc,
            name="checkout",
            valid_from=timezone.now(),
            valid_to=timezone.now() + timezone.timedelta(days=365),
        )
        opeg_data = [
            (timezone.now(), Decimal("10")),
            (timezone.now() - timezone.timedelta(days=3), Decimal("3")),
        ]
        OperationGroup.objects.bulk_create(
            [
                OperationGroup(
                    on_acc=user1_acc,
                    checkout=checkout,
                    at=at,
                    is_cof=False,
                    amount=amount,
                )
                for (at, amount) in opeg_data
            ]
        )
        self.operation_groups = OperationGroup.objects.order_by("-amount")
        Operation.objects.create(
            group=self.operation_groups[0],
            type=Operation.PURCHASE,
            amount=Decimal("10"),
        )
        Operation.objects.create(
            group=self.operation_groups[1], type=Operation.PURCHASE, amount=Decimal("3")
        )

    def test_ok(self):
        """We can query the "Account - Read" page."""
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_ok_self(self):
        client = Client()
        client.login(username="user1", password="user1")
        r = client.get(self.url)
        self.assertEqual(r.status_code, 200)


class AccountUpdateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.update"
    url_kwargs = {"trigramme": "100"}
    url_expected = "/k-fet/accounts/100/edit"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    post_data = {
        # User
        "first_name": "The first",
        "last_name": "The last",
        "email": "",
        # Group
        "groups[]": [],
        # Account
        "trigramme": "051",
        "nickname": "",
        "promo": "",
        # 'is_frozen': not checked
        # Account password
        "pwd1": "changed_pwd",
        "pwd2": "changed_pwd",
    }

    def get_users_extra(self):
        return {
            "team1": create_team("team1", "101", perms=["kfet.change_account"]),
            "team2": create_team("team2", "102"),
        }

    def assertRedirectsToLoginOr404(self, user, method, url):
        client = Client()
        meth = getattr(client, method)
        if user is None:
            response = meth(url)
            self.assertRedirects(
                response,
                "/gestion/login?next={}".format(url),
                fetch_redirect_response=False,
            )
        else:
            client.login(username=user, password=user)
            response = meth(url)
            self.assertEqual(response.status_code, 404)

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data, follow=True)
        self.assertRedirects(r, reverse("kfet.account.read", args=["051"]))

        # Comportement attendu : compte modifié,
        # utilisateur/mdp inchangé, warning pour le mdp

        self.accounts["team"].refresh_from_db()
        self.users["team"].refresh_from_db()

        self.assertInstanceExpected(
            self.accounts["team"],
            {"first_name": "team", "last_name": "member", "trigramme": "051"},
        )
        self.assertEqual(self.accounts["team"].password, hash_password("kfetpwd_team"))

        self.assertTrue(
            any("mot de passe" in str(msg).casefold() for msg in r.context["messages"])
        )

    def test_post_ok_self(self):
        r = self.client.post(self.url, self.post_data, follow=True)
        self.assertRedirects(r, reverse("kfet.account.read", args=["051"]))

        self.accounts["team"].refresh_from_db()
        self.users["team"].refresh_from_db()

        # Comportement attendu : compte/mdp modifié, utilisateur inchangé

        self.assertInstanceExpected(
            self.accounts["team"],
            {"first_name": "team", "last_name": "member", "trigramme": "051"},
        )
        self.assertEqual(self.accounts["team"].password, hash_password("changed_pwd"))

    def test_post_forbidden(self):
        client = Client()
        client.login(username="team2", password="team2")
        r = client.post(self.url, self.post_data)

        self.assertTrue(
            any(
                "permission refusée" in str(msg).casefold()
                for msg in r.context["messages"]
            )
        )


class AccountDeleteViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.delete"
    url_kwargs = {"trigramme": "001"}
    url_expected = "/k-fet/accounts/001/delete"

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]
    http_methods = ["GET", "POST"]
    with_liq = True

    def get_users_extra(self):
        return {
            "user1": create_user("user1", "001"),
            "team1": create_team("team1", "101", perms=["kfet.delete_account"]),
            "trez": create_user("trez", "#13"),
        }

    def test_get_405(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 405)

    def test_post_ok(self):
        r = self.client.post(self.url, {})
        self.assertRedirects(r, reverse("kfet.account"))

        with self.assertRaises(Account.DoesNotExist):
            self.accounts["user1"].refresh_from_db()

    def test_protected_accounts(self):
        for trigramme in ["LIQ", "#13", KFET_GENERIC_TRIGRAMME, KFET_DELETED_TRIGRAMME]:
            if Account.objects.get(trigramme=trigramme).readable:
                expected_code = 200
            else:
                expected_code = 404
            r = self.client.post(
                reverse(self.url_name, kwargs={"trigramme": trigramme}), {}
            )
            self.assertRedirects(
                r,
                reverse("kfet.account.read", kwargs={"trigramme": trigramme}),
                target_status_code=expected_code,
            )
            # Devrait être redondant avec le précédent, mais on sait jamais
            self.assertTrue(Account.objects.filter(trigramme=trigramme).exists())

    def test_nonempty_accounts(self):
        self.accounts["user1"].balance = 1
        self.accounts["user1"].save()

        r = self.client.post(self.url, {})
        self.assertRedirects(r, reverse("kfet.account.read", kwargs=self.url_kwargs))
        # Shouldn't throw an error
        self.accounts["user1"].refresh_from_db()


class AccountGroupListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.group"
    url_expected = "/k-fet/accounts/groups"

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.manage_perms"])}

    def setUp(self):
        super().setUp()
        self.group1 = KFetGroup.objects.create(name="Group1")
        self.group2 = KFetGroup.objects.create(name="Group2")

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        self.assertQuerysetEqual(
            r.context["groups"],
            [self.group1.pk, self.group2.pk],
            transform=lambda group: group.pk,
            ordered=False,
        )


class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.group.create"
    url_expected = "/k-fet/accounts/groups/new"

    http_methods = ["GET", "POST"]

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.manage_perms"])}

    @property
    def post_data(self):
        return {
            "name": "The Group",
            "permissions": [
                str(self.perms["kfet.is_team"].pk),
                str(self.perms["kfet.manage_perms"].pk),
            ],
        }

    def setUp(self):
        super().setUp()
        self.perms = get_perms("kfet.is_team", "kfet.manage_perms")

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        r = self.client.post(self.url, self.post_data)
        self.assertRedirects(r, reverse("kfet.account.group"))

        group = KFetGroup.objects.get(name="The Group")

        self.assertQuerysetEqual(
            group.permissions.all(),
            map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]),
            transform=repr,
            ordered=False,
        )


class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.group.update"

    http_methods = ["GET", "POST"]

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    @property
    def url_kwargs(self):
        return {"pk": self.group.pk}

    @property
    def url_expected(self):
        return "/k-fet/accounts/groups/{}/edit".format(self.group.pk)

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.manage_perms"])}

    @property
    def post_data(self):
        return {
            "name": "The Group",
            "permissions": [
                str(self.perms["kfet.is_team"].pk),
                str(self.perms["kfet.manage_perms"].pk),
            ],
        }

    def setUp(self):
        super().setUp()
        self.perms = get_perms("kfet.is_team", "kfet.manage_perms")
        self.group = KFetGroup.objects.create(name="Group")
        self.group.permissions.set(self.perms.values())

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        r = self.client.post(self.url, self.post_data)
        self.assertRedirects(r, reverse("kfet.account.group"))

        self.group.refresh_from_db()

        self.assertEqual(self.group.name, "The Group")
        self.assertQuerysetEqual(
            self.group.permissions.all(),
            map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]),
            transform=repr,
            ordered=False,
        )


class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.negative"
    url_expected = "/k-fet/accounts/negatives"

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.view_negs"])}

    def setUp(self):
        super().setUp()
        account = self.accounts["user"]
        account.balance = -5
        account.save()
        account.update_negative()

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)
        self.assertQuerysetEqual(
            r.context["negatives"],
            map(repr, [self.accounts["user"].negative]),
            transform=repr,
            ordered=False,
        )


class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.stat.operation.list"
    url_kwargs = {"trigramme": "001"}
    url_expected = "/k-fet/accounts/001/stat/operations/list"

    auth_user = "user1"
    auth_forbidden = [None, "user", "team"]

    def get_users_extra(self):
        return {"user1": create_user("user1", "001")}

    # Users with forbidden access users should get a 404 here, to avoid leaking trigrams
    # See issue #224
    def test_forbidden(self):
        for user in self.auth_forbidden:
            self.assertRedirectsToLoginOr404(user, self.url_expected)
            self.assertRedirectsToLoginOr404(
                user, "/k-fet/accounts/NEX/stat/operations/list"
            )

    def assertRedirectsToLoginOr404(self, user, url):
        client = Client()
        if user is None:
            response = client.get(url)
            self.assertRedirects(
                response,
                "/gestion/login?next={}".format(url),
                fetch_redirect_response=False,
            )
        else:
            client.login(username=user, password=user)
            response = client.get(url)
            self.assertEqual(response.status_code, 404)

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        content = json.loads(r.content.decode("utf-8"))

        base_url = reverse("kfet.account.stat.operation", args=["001"])

        expected_stats = [
            {
                "label": "Tout le temps",
                "url": {
                    "path": base_url,
                    "query": {
                        "scale-name": ["month"],
                        "scale-last": ["True"],
                        "scale-begin": [
                            self.accounts["user1"]
                            .created_at.replace(tzinfo=None)
                            .isoformat(" ")
                        ],
                    },
                },
            },
            {
                "label": "1 an",
                "url": {
                    "path": base_url,
                    "query": {
                        "scale-n_steps": ["12"],
                        "scale-name": ["month"],
                        "scale-last": ["True"],
                    },
                },
            },
            {
                "label": "3 mois",
                "url": {
                    "path": base_url,
                    "query": {
                        "scale-n_steps": ["13"],
                        "scale-name": ["week"],
                        "scale-last": ["True"],
                    },
                },
            },
            {
                "label": "2 semaines",
                "url": {
                    "path": base_url,
                    "query": {
                        "scale-n_steps": ["14"],
                        "scale-name": ["day"],
                        "scale-last": ["True"],
                    },
                },
            },
        ]

        for stat, expected in zip(content["stats"], expected_stats):
            expected_url = expected.pop("url")
            self.assertUrlsEqual(stat["url"], expected_url)
            self.assertEqual(stat, {**stat, **expected})


class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.stat.operation"
    url_kwargs = {"trigramme": "001"}
    url_expected = "/k-fet/accounts/001/stat/operations"

    auth_user = "user1"
    auth_forbidden = [None, "user", "team"]

    # Users with forbidden access users should get a 404 here, to avoid leaking trigrams
    # See issue #224
    def test_forbidden(self):
        for user in self.auth_forbidden:
            self.assertRedirectsToLoginOr404(user, self.url_expected)
            self.assertRedirectsToLoginOr404(
                user, "/k-fet/accounts/NEX/stat/operations"
            )

    def assertRedirectsToLoginOr404(self, user, url):
        client = Client()
        if user is None:
            response = client.get(url)
            self.assertRedirects(
                response,
                "/gestion/login?next={}".format(url),
                fetch_redirect_response=False,
            )
        else:
            client.login(username=user, password=user)
            response = client.get(url)
            self.assertEqual(response.status_code, 404)

    def get_users_extra(self):
        return {"user1": create_user("user1", "001")}

    def test_ok(self):
        r = self.client.get(
            self.url, {"scale-name": "day", "scale-n_steps": 7, "scale-last": True}
        )
        self.assertEqual(r.status_code, 200)


class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.stat.balance.list"
    url_kwargs = {"trigramme": "001"}
    url_expected = "/k-fet/accounts/001/stat/balance/list"

    auth_user = "user1"
    auth_forbidden = [None, "user", "team"]

    # Users with forbidden access users should get a 404 here, to avoid leaking trigrams
    # See issue #224
    def test_forbidden(self):
        for user in self.auth_forbidden:
            self.assertRedirectsToLoginOr404(user, self.url_expected)
            self.assertRedirectsToLoginOr404(
                user, "/k-fet/accounts/NEX/stat/balance/list"
            )

    def assertRedirectsToLoginOr404(self, user, url):
        client = Client()
        if user is None:
            response = client.get(url)
            self.assertRedirects(
                response,
                "/gestion/login?next={}".format(url),
                fetch_redirect_response=False,
            )
        else:
            client.login(username=user, password=user)
            response = client.get(url)
            self.assertEqual(response.status_code, 404)

    def get_users_extra(self):
        return {"user1": create_user("user1", "001")}

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        content = json.loads(r.content.decode("utf-8"))

        base_url = reverse("kfet.account.stat.balance", args=["001"])

        expected_stats = [
            {"label": "Tout le temps", "url": base_url},
            {
                "label": "1 an",
                "url": {"path": base_url, "query": {"last_days": ["365"]}},
            },
            {
                "label": "6 mois",
                "url": {"path": base_url, "query": {"last_days": ["183"]}},
            },
            {
                "label": "3 mois",
                "url": {"path": base_url, "query": {"last_days": ["90"]}},
            },
            {
                "label": "30 jours",
                "url": {"path": base_url, "query": {"last_days": ["30"]}},
            },
        ]

        for stat, expected in zip(content["stats"], expected_stats):
            expected_url = expected.pop("url")
            self.assertUrlsEqual(stat["url"], expected_url)
            self.assertEqual(stat, {**stat, **expected})


class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.stat.balance"
    url_kwargs = {"trigramme": "001"}
    url_expected = "/k-fet/accounts/001/stat/balance"

    auth_user = "user1"
    auth_forbidden = [None, "user", "team"]

    # Users with forbidden access users should get a 404 here, to avoid leaking trigrams
    # See issue #224
    def test_forbidden(self):
        for user in self.auth_forbidden:
            self.assertRedirectsToLoginOr404(user, self.url_expected)
            self.assertRedirectsToLoginOr404(user, "/k-fet/accounts/NEX/stat/balance")

    def assertRedirectsToLoginOr404(self, user, url):
        client = Client()
        if user is None:
            response = client.get(url)
            self.assertRedirects(
                response,
                "/gestion/login?next={}".format(url),
                fetch_redirect_response=False,
            )
        else:
            client.login(username=user, password=user)
            response = client.get(url)
            self.assertEqual(response.status_code, 404)

    def get_users_extra(self):
        return {"user1": create_user("user1", "001")}

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)


class CheckoutListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.checkout"
    url_expected = "/k-fet/checkouts/"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def setUp(self):
        super().setUp()
        self.checkout1 = Checkout.objects.create(
            name="Checkout 1",
            created_by=self.accounts["team"],
            valid_from=self.now,
            valid_to=self.now + timedelta(days=5),
        )
        self.checkout2 = Checkout.objects.create(
            name="Checkout 2",
            created_by=self.accounts["team"],
            valid_from=self.now + timedelta(days=10),
            valid_to=self.now + timedelta(days=15),
        )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)
        self.assertQuerysetEqual(
            r.context["checkouts"],
            map(repr, [self.checkout1, self.checkout2]),
            transform=repr,
            ordered=False,
        )


class CheckoutCreateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.checkout.create"
    url_expected = "/k-fet/checkouts/new"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    post_data = {
        "name": "Checkout",
        "valid_from": "2017-10-08 17:45:00",
        "valid_to": "2017-11-08 16:00:00",
        "balance": "3.14",
        # 'is_protected': not checked
    }

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.add_checkout"])}

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)

        checkout = Checkout.objects.get(name="Checkout")
        self.assertRedirects(r, checkout.get_absolute_url())

        self.assertInstanceExpected(
            checkout,
            {
                "name": "Checkout",
                "valid_from": timezone.make_aware(datetime(2017, 10, 8, 17, 45)),
                "valid_to": timezone.make_aware(datetime(2017, 11, 8, 16, 00)),
                "balance": Decimal("3.14"),
                "is_protected": False,
            },
        )

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class CheckoutReadViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.checkout.read"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.checkout.pk}

    @property
    def url_expected(self):
        return "/k-fet/checkouts/{}".format(self.checkout.pk)

    def setUp(self):
        super().setUp()

        with mock.patch("django.utils.timezone.now") as mock_now:
            mock_now.return_value = self.now

            self.checkout = Checkout.objects.create(
                name="Checkout",
                balance=Decimal("10"),
                created_by=self.accounts["team"],
                valid_from=self.now,
                valid_to=self.now + timedelta(days=1),
            )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)
        self.assertEqual(r.context["checkout"], self.checkout)


class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.checkout.update"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    post_data = {
        "name": "Checkout updated",
        "valid_from": "2018-01-01 08:00:00",
        "valid_to": "2018-07-01 16:00:00",
    }

    @property
    def url_kwargs(self):
        return {"pk": self.checkout.pk}

    @property
    def url_expected(self):
        return "/k-fet/checkouts/{}/edit".format(self.checkout.pk)

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.change_checkout"])}

    def setUp(self):
        super().setUp()
        self.checkout = Checkout.objects.create(
            name="Checkout",
            valid_from=self.now,
            valid_to=self.now + timedelta(days=5),
            balance=Decimal("3.14"),
            is_protected=False,
            created_by=self.accounts["team"],
        )

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)
        self.assertRedirects(r, self.checkout.get_absolute_url())

        self.checkout.refresh_from_db()

        self.assertInstanceExpected(
            self.checkout,
            {
                "name": "Checkout updated",
                "valid_from": timezone.make_aware(datetime(2018, 1, 1, 8, 0, 0)),
                "valid_to": timezone.make_aware(datetime(2018, 7, 1, 16, 0, 0)),
            },
        )

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.checkoutstatement"
    url_expected = "/k-fet/checkouts/statements/"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def setUp(self):
        super().setUp()
        self.checkout1 = Checkout.objects.create(
            created_by=self.accounts["team"],
            name="Checkout 1",
            valid_from=self.now,
            valid_to=self.now + timedelta(days=5),
        )
        self.checkout2 = Checkout.objects.create(
            created_by=self.accounts["team"],
            name="Checkout 2",
            valid_from=self.now + timedelta(days=10),
            valid_to=self.now + timedelta(days=15),
        )
        self.statement1 = CheckoutStatement.objects.create(
            checkout=self.checkout1,
            by=self.accounts["team"],
            balance_old=5,
            balance_new=0,
            amount_taken=5,
        )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        expected_statements = list(self.checkout1.statements.all()) + list(
            self.checkout2.statements.all()
        )

        self.assertQuerysetEqual(
            r.context["checkoutstatements"],
            map(repr, expected_statements),
            transform=repr,
            ordered=False,
        )


class CheckoutStatementCreateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.checkoutstatement.create"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    post_data = {
        # Let
        "balance_001": 0,
        "balance_002": 0,
        "balance_005": 0,
        "balance_01": 0,
        "balance_02": 0,
        "balance_05": 0,
        "balance_1": 1,
        "balance_2": 0,
        "balance_5": 0,
        "balance_10": 1,
        "balance_20": 0,
        "balance_50": 0,
        "balance_100": 1,
        "balance_200": 0,
        "balance_500": 0,
        # Taken
        "taken_001": 0,
        "taken_002": 0,
        "taken_005": 0,
        "taken_01": 0,
        "taken_02": 0,
        "taken_05": 0,
        "taken_1": 2,
        "taken_2": 0,
        "taken_5": 0,
        "taken_10": 2,
        "taken_20": 0,
        "taken_50": 0,
        "taken_100": 2,
        "taken_200": 0,
        "taken_500": 0,
        "taken_cheque": 0,
        # 'not_count': not checked
    }

    @property
    def url_kwargs(self):
        return {"pk_checkout": self.checkout.pk}

    @property
    def url_expected(self):
        return "/k-fet/checkouts/{}/statements/add".format(self.checkout.pk)

    def get_users_extra(self):
        return {
            "team1": create_team("team1", "001", perms=["kfet.add_checkoutstatement"])
        }

    def setUp(self):
        super().setUp()
        self.checkout = Checkout.objects.create(
            name="Checkout",
            created_by=self.accounts["team"],
            balance=5,
            valid_from=self.now,
            valid_to=self.now + timedelta(days=5),
        )

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    @mock.patch("django.utils.timezone.now")
    def test_post_ok(self, mock_now):
        self.now += timedelta(days=2)
        mock_now.return_value = self.now

        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)
        self.assertRedirects(r, self.checkout.get_absolute_url())

        statement = CheckoutStatement.objects.get(at=self.now)

        self.assertInstanceExpected(
            statement,
            {
                "by": self.accounts["team1"],
                "checkout": self.checkout,
                "balance_old": Decimal("5"),
                "balance_new": Decimal("111"),
                "amount_taken": Decimal("222"),
                "amount_error": Decimal("328"),
                "at": self.now,
                "not_count": False,
            },
        )

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class CheckoutStatementUpdateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.checkoutstatement.update"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    post_data = {
        "amount_taken": 3,
        "amount_error": 2,
        "balance_old": 8,
        "balance_new": 5,
        # Taken
        "taken_001": 0,
        "taken_002": 0,
        "taken_005": 0,
        "taken_01": 0,
        "taken_02": 0,
        "taken_05": 0,
        "taken_1": 1,
        "taken_2": 1,
        "taken_5": 0,
        "taken_10": 0,
        "taken_20": 0,
        "taken_50": 0,
        "taken_100": 0,
        "taken_200": 0,
        "taken_500": 0,
        "taken_cheque": 0,
    }

    @property
    def url_kwargs(self):
        return {"pk_checkout": self.checkout.pk, "pk": self.statement.pk}

    @property
    def url_expected(self):
        return "/k-fet/checkouts/{pk_checkout}/statements/{pk}/edit".format(
            pk_checkout=self.checkout.pk, pk=self.statement.pk
        )

    def get_users_extra(self):
        return {
            "team1": create_team(
                "team1", "101", perms=["kfet.change_checkoutstatement"]
            )
        }

    def setUp(self):
        super().setUp()
        self.checkout = Checkout.objects.create(
            name="Checkout",
            created_by=self.accounts["team"],
            balance=5,
            valid_from=self.now,
            valid_to=self.now + timedelta(days=5),
        )
        self.statement = CheckoutStatement.objects.create(
            by=self.accounts["team"],
            checkout=self.checkout,
            balance_new=5,
            balance_old=8,
            amount_error=2,
            amount_taken=5,
        )

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    @mock.patch("django.utils.timezone.now")
    def test_post_ok(self, mock_now):
        self.now += timedelta(days=2)
        mock_now.return_value = self.now

        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)
        self.assertRedirects(r, self.checkout.get_absolute_url())

        self.statement.refresh_from_db()

        self.assertInstanceExpected(
            self.statement,
            {
                "taken_1": 1,
                "taken_2": 1,
                "balance_new": 5,
                "balance_old": 8,
                "amount_error": 0,
                "amount_taken": 3,
            },
        )

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class ArticleCategoryListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.category"
    url_expected = "/k-fet/categories/"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def setUp(self):
        super().setUp()
        self.category1 = ArticleCategory.objects.create(name="Category 1")
        self.category2 = ArticleCategory.objects.create(name="Category 2")

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        self.assertQuerysetEqual(
            r.context["categories"],
            map(repr, [self.category1, self.category2]),
            transform=repr,
        )


class ArticleCategoryUpdateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.category.update"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.category.pk}

    @property
    def url_expected(self):
        return "/k-fet/categories/{}/edit".format(self.category.pk)

    def get_users_extra(self):
        return {
            "team1": create_team("team1", "101", perms=["kfet.change_articlecategory"])
        }

    @property
    def post_data(self):
        return {
            "name": "The Category",
            # 'has_addcost': not checked
        }

    def setUp(self):
        super().setUp()
        self.category = ArticleCategory.objects.create(name="Category")

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)
        self.assertRedirects(r, reverse("kfet.category"))

        self.category.refresh_from_db()

        self.assertInstanceExpected(
            self.category, {"name": "The Category", "has_addcost": False}
        )

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class ArticleListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.article"
    url_expected = "/k-fet/articles/"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def setUp(self):
        super().setUp()
        category = ArticleCategory.objects.create(name="Category")
        self.article1 = Article.objects.create(name="Article 1", category=category)
        self.article2 = Article.objects.create(name="Article 2", category=category)

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)
        self.assertQuerysetEqual(
            r.context["articles"],
            map(repr, [self.article1, self.article2]),
            transform=repr,
        )


class ArticleCreateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.article.create"
    url_expected = "/k-fet/articles/new"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.add_article"])}

    @property
    def post_data(self):
        return {
            "name": "Article",
            "category": self.category.pk,
            "stock": 5,
            "price": "2.5",
        }

    def setUp(self):
        super().setUp()
        self.category = ArticleCategory.objects.create(name="Category")

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)

        article = Article.objects.get(name="Article")

        self.assertRedirects(r, article.get_absolute_url())

        self.assertInstanceExpected(
            article, {"name": "Article", "category": self.category}
        )

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class ArticleReadViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.article.read"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.article.pk}

    @property
    def url_expected(self):
        return "/k-fet/articles/{}".format(self.article.pk)

    def setUp(self):
        super().setUp()
        self.article = Article.objects.create(
            name="Article",
            category=ArticleCategory.objects.create(name="Category"),
            stock=5,
            price=Decimal("2.5"),
        )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)
        self.assertEqual(r.context["article"], self.article)


class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.article.update"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.article.pk}

    @property
    def url_expected(self):
        return "/k-fet/articles/{}/edit".format(self.article.pk)

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.change_article"])}

    @property
    def post_data(self):
        return {
            "name": "The Article",
            "category": self.article.category.pk,
            "is_sold": "1",
            "price": "3.5",
            "box_type": "carton",
            # 'hidden': not checked
        }

    def setUp(self):
        super().setUp()
        self.category = ArticleCategory.objects.create(name="Category")
        self.article = Article.objects.create(
            name="Article", category=self.category, stock=5, price=Decimal("2.5")
        )

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)

        self.assertRedirects(r, self.article.get_absolute_url())

        self.article.refresh_from_db()

        self.assertInstanceExpected(
            self.article, {"name": "The Article", "price": Decimal("3.5")}
        )

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class ArticleDeleteViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.article.delete"

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    @property
    def url_kwargs(self):
        return {"pk": self.article.pk}

    @property
    def url_expected(self):
        return "/k-fet/articles/{}/delete".format(self.article.pk)

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.delete_article"])}

    def setUp(self):
        super().setUp()
        self.category = ArticleCategory.objects.create(name="Category")
        self.article = Article.objects.create(
            name="Article", category=self.category, stock=5, price=Decimal("2.5")
        )

    def test_get_redirects(self):
        r = self.client.get(self.url)
        self.assertRedirects(r, reverse("kfet.article.read", kwargs=self.url_kwargs))

    def test_post_ok(self):
        r = self.client.post(self.url, {})
        self.assertRedirects(r, reverse("kfet.article"))

        with self.assertRaises(Article.DoesNotExist):
            self.article.refresh_from_db()


class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.article.stat.sales.list"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.article.pk}

    @property
    def url_expected(self):
        return "/k-fet/articles/{}/stat/sales/list".format(self.article.pk)

    def setUp(self):
        super().setUp()
        self.article = Article.objects.create(
            name="Article", category=ArticleCategory.objects.create(name="Category")
        )
        checkout = Checkout.objects.create(
            name="Checkout",
            created_by=self.accounts["team"],
            balance=5,
            valid_from=self.now,
            valid_to=self.now + timedelta(days=5),
        )

        self.opegroup = create_operation_group(
            on_acc=self.accounts["user"],
            checkout=checkout,
            content=[
                {"type": Operation.PURCHASE, "article": self.article, "article_nb": 2},
            ],
        )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        content = json.loads(r.content.decode("utf-8"))

        base_url = reverse("kfet.article.stat.sales", args=[self.article.pk])

        expected_stats = [
            {
                "label": "Tout le temps",
                "url": {
                    "path": base_url,
                    "query": {
                        "scale-name": ["month"],
                        "scale-last": ["True"],
                        "scale-begin": [self.opegroup.at.strftime("%Y-%m-%d %H:%M:%S")],
                    },
                },
            },
            {
                "label": "1 an",
                "url": {
                    "path": base_url,
                    "query": {
                        "scale-n_steps": ["12"],
                        "scale-name": ["month"],
                        "scale-last": ["True"],
                    },
                },
            },
            {
                "label": "3 mois",
                "url": {
                    "path": base_url,
                    "query": {
                        "scale-n_steps": ["13"],
                        "scale-name": ["week"],
                        "scale-last": ["True"],
                    },
                },
            },
            {
                "label": "2 semaines",
                "url": {
                    "path": base_url,
                    "query": {
                        "scale-n_steps": ["14"],
                        "scale-name": ["day"],
                        "scale-last": ["True"],
                    },
                },
            },
        ]

        for stat, expected in zip(content["stats"], expected_stats):
            expected_url = expected.pop("url")
            self.assertUrlsEqual(stat["url"], expected_url)
            self.assertEqual(stat, {**stat, **expected})


class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.article.stat.sales"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.article.pk}

    @property
    def url_expected(self):
        return "/k-fet/articles/{}/stat/sales".format(self.article.pk)

    def setUp(self):
        super().setUp()
        self.article = Article.objects.create(
            name="Article", category=ArticleCategory.objects.create(name="Category")
        )

    def test_ok(self):
        r = self.client.get(
            self.url, {"scale-name": "day", "scale-n_steps": 7, "scale-last": True}
        )
        self.assertEqual(r.status_code, 200)


class KPsulViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.kpsul"
    url_expected = "/k-fet/k-psul/"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)


class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.kpsul.checkout_data"
    url_expected = "/k-fet/k-psul/checkout_data"

    http_methods = ["POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def setUp(self):
        super().setUp()
        self.checkout = Checkout.objects.create(
            name="Checkout",
            balance=Decimal("10"),
            created_by=self.accounts["team"],
            valid_from=self.now,
            valid_to=self.now + timedelta(days=5),
        )

    def test_ok(self):
        r = self.client.post(self.url, {"pk": self.checkout.pk})
        self.assertEqual(r.status_code, 200)

        content = json.loads(r.content.decode("utf-8"))

        expected = {"name": "Checkout", "balance": "10.00"}

        self.assertEqual(content, {**content, **expected})

        self.assertSetEqual(
            set(content.keys()),
            set(
                [
                    "balance",
                    "id",
                    "name",
                    "valid_from",
                    "valid_to",
                    "last_statement_at",
                    "last_statement_balance",
                    "last_statement_by_first_name",
                    "last_statement_by_last_name",
                    "last_statement_by_trigramme",
                ]
            ),
        )


class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
    """
    Test cases for kpsul_perform_operations view.

    Below is the test ordering, try to keep this organized ;-)

    * OperationGroup:
        - test_group...
        - test_invalid_group...
    * Operation:
        - test_purchase...
        - test_invalid_purchase...
        - test_deposit...
        - test_invalid_deposit...
        - test_withdraw...
        - test_invalid_withdraw...
        - test_edit...
        - test_invalid_edit...
    * Addcost:
        - test_addcost...
    * Negative:
        - test_negative...
        - test_invalid_negative...
    * More concrete examples:
        - test_multi...

    To test valid requests, one should use '_assertResponseOk(response)' to get
    hints about failure reasons, if any.

    At least one test per operation type should test the complete response and
    behavior (HTTP, WebSocket, object updates, and object creations)
    Other tests of the same operation type can only assert the specific
    behavior differences.

    For invalid requests, response errors should be tested.

    """

    url_name = "kfet.kpsul.perform_operations"
    url_expected = "/k-fet/k-psul/perform_operations"

    http_methods = ["POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    with_liq = True

    def setUp(self):
        super(KPsulPerformOperationsViewTests, self).setUp()

        # A Checkout, curently usable, balance=100
        self.checkout = Checkout.objects.create(
            created_by=self.accounts["team"],
            name="Checkout",
            valid_from=timezone.now() - timedelta(days=7),
            valid_to=timezone.now() + timedelta(days=7),
            balance=Decimal("100.00"),
        )
        # An Article, price=2.5, stock=20
        self.article = Article.objects.create(
            category=ArticleCategory.objects.create(name="Category"),
            name="Article",
            price=Decimal("2.5"),
            stock=20,
        )
        # Another Article, price=2.5, stock=20, no COF reduction
        self.article_no_reduction = Article.objects.create(
            category=ArticleCategory.objects.create(
                name="Category_no_reduction",
                has_reduction=False,
            ),
            name="Article_no_reduction",
            price=Decimal("2.5"),
            stock=20,
        )
        # An Account, trigramme=000, balance=50
        # Do not assume user is cof, nor not cof.
        self.account = self.accounts["user"]
        self.account.balance = Decimal("50.00")
        self.account.save()

        # Create a channel to listen to KPsul's messages
        channel_layer = get_channel_layer()
        self.channel = async_to_sync(channel_layer.new_channel)()

        async_to_sync(channel_layer.group_add)("kfet.kpsul", self.channel)

        self.receive_msg = lambda: async_to_sync(channel_layer.receive)(self.channel)

        # Reset cache of kfet config
        kfet_config._conf_init = False

    def _assertResponseOk(self, response):
        """
        Asserts that status code of 'response' is 200, and returns the
        deserialized content of the JSONResponse.

        In case status code is not 200, it prints the content of "errors" of
        the response.

        """
        json_data = json.loads(getattr(response, "content", b"{}").decode("utf-8"))
        try:
            self.assertEqual(response.status_code, 200)
        except AssertionError as exc:
            msg = "Expected response is 200, got {}. Errors: {}".format(
                response.status_code, json_data.get("errors")
            )
            raise AssertionError(msg) from exc
        return json_data

    def get_base_post_data(self):
        return {
            # OperationGroup form
            "on_acc": str(self.account.pk),
            "checkout": str(self.checkout.pk),
            # Operation formset
            "form-TOTAL_FORMS": "0",
            "form-INITIAL_FORMS": "0",
            "form-MIN_NUM_FORMS": "1",
            "form-MAX_NUM_FORMS": "1000",
        }

    base_post_data = property(get_base_post_data)

    def test_invalid_group_on_acc(self):
        data = dict(self.base_post_data, **{"on_acc": "GNR"})
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_on_acc", "invalid_formset"],
        )

    def test_group_on_acc_expects_comment(self):
        user_add_perms(self.users["team"], ["kfet.perform_commented_operations"])
        self.account.trigramme = "#13"
        self.account.save()
        self.assertTrue(self.account.need_comment)

        data = dict(
            self.base_post_data,
            **{
                "comment": "A comment to explain it",
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

    def test_invalid_group_on_acc_expects_comment(self):
        user_add_perms(self.users["team"], ["kfet.perform_commented_operations"])
        self.account.trigramme = "#13"
        self.account.save()
        self.assertTrue(self.account.need_comment)

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertEqual(json_data["need_comment"], True)

    def test_invalid_group_on_acc_needs_comment_requires_perm(self):
        self.account.trigramme = "#13"
        self.account.save()
        self.assertTrue(self.account.need_comment)

        data = dict(
            self.base_post_data,
            **{
                "comment": "A comment to explain it",
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 403)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertEqual(
            json_data["missing_perms"],
            ["Enregistrer des commandes avec commentaires"],
        )

    def test_error_on_acc_frozen(self):
        self.account.is_frozen = True
        self.account.save()

        data = dict(
            self.base_post_data,
            **{
                "comment": "A comment to explain it",
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertEqual([e["code"] for e in json_data["errors"]], ["frozen_acc"])

    def test_invalid_group_checkout(self):
        self.checkout.valid_from -= timedelta(days=300)
        self.checkout.valid_to -= timedelta(days=300)
        self.checkout.save()

        data = dict(self.base_post_data)
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_checkout", "invalid_formset"],
        )

    def test_invalid_group_expects_one_operation(self):
        data = dict(self.base_post_data)
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_purchase_with_user_is_nof_cof(self):
        self.account.cofprofile.is_cof = False
        self.account.cofprofile.save()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        # Check response status
        json_data = self._assertResponseOk(resp)

        # Check object creations
        operation_group = OperationGroup.objects.get()
        self.assertDictEqual(
            operation_group.__dict__,
            {
                "_state": mock.ANY,
                "at": mock.ANY,
                "amount": Decimal("-5.00"),
                "checkout_id": self.checkout.pk,
                "comment": "",
                "id": mock.ANY,
                "is_cof": False,
                "on_acc_id": self.account.pk,
                "valid_by_id": None,
            },
        )
        operation = Operation.objects.get()
        self.assertDictEqual(
            operation.__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("-5.00"),
                "article_id": self.article.pk,
                "article_nb": 2,
                "canceled_at": None,
                "canceled_by_id": None,
                "group_id": operation_group.pk,
                "id": mock.ANY,
                "type": "purchase",
            },
        )

        # Check response content
        self.assertDictEqual(
            json_data,
            {"errors": []},
        )

        # Check object updates
        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("45.00"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 18)

        # Check websocket data
        ws_data = self.receive_msg()

        self.assertDictEqual(
            ws_data,
            {
                "type": "kpsul",
                "groups": [
                    {
                        "add": True,
                        "type": "operation",
                        "at": mock.ANY,
                        "amount": Decimal("-5.00"),
                        "checkout__name": "Checkout",
                        "comment": "",
                        "id": operation_group.pk,
                        "is_cof": False,
                        "on_acc__trigramme": "000",
                        "valid_by__trigramme": None,
                        "entries": [
                            {
                                "id": operation.pk,
                                "addcost_amount": None,
                                "addcost_for__trigramme": None,
                                "amount": Decimal("-5.00"),
                                "article__name": "Article",
                                "article_nb": 2,
                                "canceled_at": None,
                                "canceled_by__trigramme": None,
                                "group_id": operation_group.pk,
                                "type": "purchase",
                            }
                        ],
                    }
                ],
                "checkouts": [{"id": self.checkout.pk, "balance": Decimal("100.00")}],
                "articles": [{"id": self.article.pk, "stock": 18}],
            },
        )

    def test_purchase_with_user_is_cof(self):
        kfet_config.set(kfet_reduction_cof=Decimal("20"))
        self.account.cofprofile.is_cof = True
        self.account.cofprofile.save()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertEqual(operation_group.amount, Decimal("-4.00"))
        self.assertEqual(operation_group.is_cof, True)
        operation = Operation.objects.get()
        self.assertEqual(operation.amount, Decimal("-4.00"))

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("46.00"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 18)

    def test_purchase_with_cash(self):
        data = dict(
            self.base_post_data,
            **{
                "on_acc": str(self.accounts["liq"].pk),
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertEqual(operation_group.on_acc, self.accounts["liq"])
        self.assertEqual(operation_group.is_cof, False)

        self.accounts["liq"].refresh_from_db()
        self.assertEqual(self.accounts["liq"].balance, 0)
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("105.00"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 18)

    def test_purchase_no_reduction(self):
        kfet_config.set(kfet_reduction_cof=Decimal("20"))
        self.account.cofprofile.is_cof = True
        self.account.cofprofile.save()
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "2",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article_no_reduction.pk),
                "form-0-article_nb": "1",
                "form-1-type": "purchase",
                "form-1-amount": "",
                "form-1-article": str(self.article.pk),
                "form-1-article_nb": "1",
            }
        )

        resp = self.client.post(self.url, data)
        self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertEqual(operation_group.amount, Decimal("-4.50"))
        operation = Operation.objects.get(article=self.article)
        self.assertEqual(operation.amount, Decimal("-2.00"))
        operation = Operation.objects.get(article=self.article_no_reduction)
        self.assertEqual(operation.amount, Decimal("-2.50"))

    def test_invalid_purchase_expects_article(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": "",
                "form-0-article_nb": "1",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_invalid_purchase_expects_article_nb(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_invalid_purchase_expects_article_nb_greater_than_1(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "-1",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_invalid_operation_not_purchase_with_cash(self):
        data = dict(
            self.base_post_data,
            **{
                "on_acc": str(self.accounts["liq"].pk),
                "form-TOTAL_FORMS": "1",
                "form-0-type": "deposit",
                "form-0-amount": "10.00",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_liq"],
        )

    def test_deposit(self):
        user_add_perms(self.users["team"], ["kfet.perform_deposit"])
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "deposit",
                "form-0-amount": "10.75",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertDictEqual(
            operation_group.__dict__,
            {
                "_state": mock.ANY,
                "at": mock.ANY,
                "amount": Decimal("10.75"),
                "checkout_id": self.checkout.pk,
                "comment": "",
                "id": mock.ANY,
                "is_cof": False,
                "on_acc_id": self.account.pk,
                "valid_by_id": self.accounts["team"].pk,
            },
        )
        operation = Operation.objects.get()
        self.assertDictEqual(
            operation.__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("10.75"),
                "article_id": None,
                "article_nb": None,
                "canceled_at": None,
                "canceled_by_id": None,
                "group_id": operation_group.pk,
                "id": mock.ANY,
                "type": "deposit",
            },
        )

        self.assertDictEqual(
            json_data,
            {"errors": []},
        )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("60.75"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("110.75"))

        ws_data = self.receive_msg()

        self.assertDictEqual(
            ws_data,
            {
                "type": "kpsul",
                "groups": [
                    {
                        "add": True,
                        "type": "operation",
                        "at": mock.ANY,
                        "amount": Decimal("10.75"),
                        "checkout__name": "Checkout",
                        "comment": "",
                        "id": operation_group.pk,
                        "is_cof": False,
                        "on_acc__trigramme": "000",
                        "valid_by__trigramme": "100",
                        "entries": [
                            {
                                "id": operation.pk,
                                "addcost_amount": None,
                                "addcost_for__trigramme": None,
                                "amount": Decimal("10.75"),
                                "article__name": None,
                                "article_nb": None,
                                "canceled_at": None,
                                "canceled_by__trigramme": None,
                                "group_id": operation_group.pk,
                                "type": "deposit",
                            }
                        ],
                    }
                ],
                "checkouts": [{"id": self.checkout.pk, "balance": Decimal("110.75")}],
                "articles": [],
            },
        )

    def test_invalid_deposit_expects_amount(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "deposit",
                "form-0-amount": "",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_invalid_deposit_too_many_params(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "deposit",
                "form-0-amount": "10",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "3",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_invalid_deposit_expects_positive_amount(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "deposit",
                "form-0-amount": "-10",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_invalid_deposit_requires_perm(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "deposit",
                "form-0-amount": "10.75",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 403)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertEqual(json_data["missing_perms"], ["Effectuer une charge"])

    def test_withdraw(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "withdraw",
                "form-0-amount": "-10.75",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertDictEqual(
            operation_group.__dict__,
            {
                "_state": mock.ANY,
                "at": mock.ANY,
                "amount": Decimal("-10.75"),
                "checkout_id": self.checkout.pk,
                "comment": "",
                "id": mock.ANY,
                "is_cof": False,
                "on_acc_id": self.account.pk,
                "valid_by_id": None,
            },
        )
        operation = Operation.objects.get()
        self.assertDictEqual(
            operation.__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("-10.75"),
                "article_id": None,
                "article_nb": None,
                "canceled_at": None,
                "canceled_by_id": None,
                "group_id": operation_group.pk,
                "id": mock.ANY,
                "type": "withdraw",
            },
        )

        self.assertDictEqual(
            json_data,
            {"errors": []},
        )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("39.25"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("89.25"))

        ws_data = self.receive_msg()

        self.assertDictEqual(
            ws_data,
            {
                "type": "kpsul",
                "groups": [
                    {
                        "add": True,
                        "type": "operation",
                        "at": mock.ANY,
                        "amount": Decimal("-10.75"),
                        "checkout__name": "Checkout",
                        "comment": "",
                        "id": operation_group.pk,
                        "is_cof": False,
                        "on_acc__trigramme": "000",
                        "valid_by__trigramme": None,
                        "entries": [
                            {
                                "id": operation.pk,
                                "addcost_amount": None,
                                "addcost_for__trigramme": None,
                                "amount": Decimal("-10.75"),
                                "article__name": None,
                                "article_nb": None,
                                "canceled_at": None,
                                "canceled_by__trigramme": None,
                                "group_id": operation_group.pk,
                                "type": "withdraw",
                            }
                        ],
                    }
                ],
                "checkouts": [{"id": self.checkout.pk, "balance": Decimal("89.25")}],
                "articles": [],
            },
        )

    def test_invalid_withdraw_expects_amount(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "withdraw",
                "form-0-amount": "",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_invalid_withdraw_too_many_params(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "withdraw",
                "form-0-amount": "-10",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "3",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_invalid_withdraw_expects_negative_amount(self):
        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "withdraw",
                "form-0-amount": "10",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_formset"],
        )

    def test_edit(self):
        user_add_perms(self.users["team"], ["kfet.edit_balance_account"])

        data = dict(
            self.base_post_data,
            **{
                "comment": "A comment to explain it",
                "form-TOTAL_FORMS": "1",
                "form-0-type": "edit",
                "form-0-amount": "10.75",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertDictEqual(
            operation_group.__dict__,
            {
                "_state": mock.ANY,
                "at": mock.ANY,
                "amount": Decimal("10.75"),
                "checkout_id": self.checkout.pk,
                "comment": "A comment to explain it",
                "id": mock.ANY,
                "is_cof": False,
                "on_acc_id": self.account.pk,
                "valid_by_id": self.accounts["team"].pk,
            },
        )
        operation = Operation.objects.get()
        self.assertDictEqual(
            operation.__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("10.75"),
                "article_id": None,
                "article_nb": None,
                "canceled_at": None,
                "canceled_by_id": None,
                "group_id": operation_group.pk,
                "id": mock.ANY,
                "type": "edit",
            },
        )

        self.assertDictEqual(
            json_data,
            {"errors": []},
        )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("60.75"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))

        ws_data = self.receive_msg()

        self.assertDictEqual(
            ws_data,
            {
                "type": "kpsul",
                "groups": [
                    {
                        "add": True,
                        "type": "operation",
                        "at": mock.ANY,
                        "amount": Decimal("10.75"),
                        "checkout__name": "Checkout",
                        "comment": "A comment to explain it",
                        "id": operation_group.pk,
                        "is_cof": False,
                        "on_acc__trigramme": "000",
                        "valid_by__trigramme": "100",
                        "entries": [
                            {
                                "id": operation.pk,
                                "addcost_amount": None,
                                "addcost_for__trigramme": None,
                                "amount": Decimal("10.75"),
                                "article__name": None,
                                "article_nb": None,
                                "canceled_at": None,
                                "canceled_by__trigramme": None,
                                "group_id": operation_group.pk,
                                "type": "edit",
                            }
                        ],
                    }
                ],
                "checkouts": [{"id": self.checkout.pk, "balance": Decimal("100.00")}],
                "articles": [],
            },
        )

    def test_invalid_edit_requires_perm(self):
        data = dict(
            self.base_post_data,
            **{
                "comment": "A comment to explain it",
                "form-TOTAL_FORMS": "1",
                "form-0-type": "edit",
                "form-0-amount": "10.75",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 403)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertEqual(
            json_data["missing_perms"],
            ["Modifier la balance d'un compte"],
        )

    def test_invalid_edit_expects_comment(self):
        user_add_perms(self.users["team"], ["kfet.edit_balance_account"])

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "edit",
                "form-0-amount": "10.75",
                "form-0-article": "",
                "form-0-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertEqual(json_data["need_comment"], True)

    def _setup_addcost(self):
        self.register_user("addcost", create_user("addcost", "ADD"))
        kfet_config.set(
            addcost_amount=Decimal("0.50"), addcost_for=self.accounts["addcost"]
        )

    def test_addcost_user_is_not_cof(self):
        self.account.cofprofile.is_cof = False
        self.account.cofprofile.save()
        self._setup_addcost()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertEqual(operation_group.amount, Decimal("-6.00"))
        operation = Operation.objects.get()
        self.assertEqual(operation.addcost_for, self.accounts["addcost"])
        self.assertEqual(operation.addcost_amount, Decimal("1.00"))
        self.assertEqual(operation.amount, Decimal("-6.00"))

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("44.00"))
        self.accounts["addcost"].refresh_from_db()
        self.assertEqual(self.accounts["addcost"].balance, Decimal("1.00"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))

        ws_data = self.receive_msg()
        ws_data_ope = ws_data["groups"][0]["entries"][0]

        self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00"))
        self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD")

    def test_addcost_user_is_cof(self):
        kfet_config.set(reduction_cof=Decimal("20"))
        self.account.cofprofile.is_cof = True
        self.account.cofprofile.save()
        self._setup_addcost()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertEqual(operation_group.amount, Decimal("-4.80"))
        operation = Operation.objects.get()
        self.assertEqual(operation.addcost_for, self.accounts["addcost"])
        self.assertEqual(operation.addcost_amount, Decimal("0.80"))
        self.assertEqual(operation.amount, Decimal("-4.80"))

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("45.20"))
        self.accounts["addcost"].refresh_from_db()
        self.assertEqual(self.accounts["addcost"].balance, Decimal("0.80"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))

        ws_data = self.receive_msg()
        ws_data_ope = ws_data["groups"][0]["entries"][0]

        self.assertEqual(ws_data_ope["addcost_amount"], Decimal("0.80"))
        self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD")

    def test_addcost_user_is_cash(self):
        self.account.cofprofile.is_cof = True
        self.account.cofprofile.save()
        self._setup_addcost()

        data = dict(
            self.base_post_data,
            **{
                "on_acc": str(self.accounts["liq"].pk),
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertEqual(operation_group.amount, Decimal("-6.00"))
        operation = Operation.objects.get()
        self.assertEqual(operation.addcost_for, self.accounts["addcost"])
        self.assertEqual(operation.addcost_amount, Decimal("1.00"))
        self.assertEqual(operation.amount, Decimal("-6.00"))

        self.accounts["addcost"].refresh_from_db()
        self.assertEqual(self.accounts["addcost"].balance, Decimal("1.00"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("106.00"))

        ws_data = self.receive_msg()
        ws_data_ope = ws_data["groups"][0]["entries"][0]

        self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00"))
        self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD")

    def test_addcost_to_self(self):
        self._setup_addcost()
        self.accounts["addcost"].balance = Decimal("20.00")
        self.accounts["addcost"].save()

        data = dict(
            self.base_post_data,
            **{
                "on_acc": str(self.accounts["addcost"].pk),
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertEqual(operation_group.amount, Decimal("-5.00"))
        operation = Operation.objects.get()
        self.assertEqual(operation.addcost_for, None)
        self.assertEqual(operation.addcost_amount, None)
        self.assertEqual(operation.amount, Decimal("-5.00"))

        self.accounts["addcost"].refresh_from_db()
        self.assertEqual(self.accounts["addcost"].balance, Decimal("15.00"))

        ws_data = self.receive_msg()
        ws_data_ope = ws_data["groups"][0]["entries"][0]

        self.assertEqual(ws_data_ope["addcost_amount"], None)
        self.assertEqual(ws_data_ope["addcost_for__trigramme"], None)

    def test_addcost_category_disabled(self):
        self._setup_addcost()
        self.article.category.has_addcost = False
        self.article.category.save()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        operation_group = OperationGroup.objects.get()
        self.assertEqual(operation_group.amount, Decimal("-5.00"))
        operation = Operation.objects.get()
        self.assertEqual(operation.addcost_for, None)
        self.assertEqual(operation.addcost_amount, None)
        self.assertEqual(operation.amount, Decimal("-5.00"))

        self.accounts["addcost"].refresh_from_db()
        self.assertEqual(self.accounts["addcost"].balance, Decimal("0.00"))

        ws_data = self.receive_msg()
        ws_data_ope = ws_data["groups"][0]["entries"][0]

        self.assertEqual(ws_data_ope["addcost_amount"], None)
        self.assertEqual(ws_data_ope["addcost_for__trigramme"], None)

    def test_negative_new(self):
        user_add_perms(self.users["team"], ["kfet.perform_negative_operations"])
        self.account.balance = Decimal("1.00")
        self.account.save()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("-4.00"))

    def test_negative_exists(self):
        user_add_perms(self.users["team"], ["kfet.perform_negative_operations"])
        self.account.balance = Decimal("-10.00")
        self.account.save()
        self.account.update_negative()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("-15.00"))

    def test_negative_exists_balance_higher_than_initial(self):
        user_add_perms(self.users["team"], ["kfet.perform_deposit"])
        self.account.balance = Decimal("-10.00")
        self.account.save()
        self.account.update_negative()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "2",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "1",
                "form-1-type": "deposit",
                "form-1-amount": "5.00",
                "form-1-article": "",
                "form-1-article_nb": "",
            }
        )
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("-7.50"))

    def test_invalid_negative_new_requires_perm(self):
        self.account.balance = Decimal("1.00")
        self.account.save()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 403)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertEqual(
            json_data["missing_perms"],
            ["Enregistrer des commandes en négatif"],
        )

    def test_invalid_negative_exceeds_amount_allowed_from_config(self):
        user_add_perms(self.users["team"], ["kfet.perform_negative_operations"])
        kfet_config.set(overdraft_amount=Decimal("-1.00"))
        self.account.balance = Decimal("1.00")
        self.account.save()
        self.account.update_negative()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "1",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
            }
        )
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["negative"],
        )

    def test_multi_0(self):
        article2 = Article.objects.create(
            name="Article 2",
            price=Decimal("4"),
            stock=-5,
            category=ArticleCategory.objects.first(),
        )
        self.account.cofprofile.is_cof = False
        self.account.cofprofile.save()

        data = dict(
            self.base_post_data,
            **{
                "form-TOTAL_FORMS": "2",
                "form-0-type": "purchase",
                "form-0-amount": "",
                "form-0-article": str(self.article.pk),
                "form-0-article_nb": "2",
                "form-1-type": "purchase",
                "form-1-amount": "",
                "form-1-article": str(article2.pk),
                "form-1-article_nb": "1",
            }
        )
        resp = self.client.post(self.url, data)

        # Check response status
        json_data = self._assertResponseOk(resp)

        # Check object creations
        operation_group = OperationGroup.objects.get()
        self.assertDictEqual(
            operation_group.__dict__,
            {
                "_state": mock.ANY,
                "at": mock.ANY,
                "amount": Decimal("-9.00"),
                "checkout_id": self.checkout.pk,
                "comment": "",
                "id": mock.ANY,
                "is_cof": False,
                "on_acc_id": self.account.pk,
                "valid_by_id": None,
            },
        )
        operation_list = Operation.objects.all()
        self.assertEqual(len(operation_list), 2)
        self.assertDictEqual(
            operation_list[0].__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("-5.00"),
                "article_id": self.article.pk,
                "article_nb": 2,
                "canceled_at": None,
                "canceled_by_id": None,
                "group_id": operation_group.pk,
                "id": mock.ANY,
                "type": "purchase",
            },
        )
        self.assertDictEqual(
            operation_list[1].__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("-4.00"),
                "article_id": article2.pk,
                "article_nb": 1,
                "canceled_at": None,
                "canceled_by_id": None,
                "group_id": operation_group.pk,
                "id": mock.ANY,
                "type": "purchase",
            },
        )

        # Check response content
        self.assertDictEqual(
            json_data,
            {"errors": []},
        )

        # Check object updates
        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("41.00"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 18)
        article2.refresh_from_db()
        self.assertEqual(article2.stock, -6)

        # Check websocket data
        ws_data = self.receive_msg()

        self.assertDictEqual(
            ws_data,
            {
                "type": "kpsul",
                "groups": [
                    {
                        "add": True,
                        "type": "operation",
                        "at": mock.ANY,
                        "amount": Decimal("-9.00"),
                        "checkout__name": "Checkout",
                        "comment": "",
                        "id": operation_group.pk,
                        "is_cof": False,
                        "on_acc__trigramme": "000",
                        "valid_by__trigramme": None,
                        "entries": [
                            {
                                "id": operation_list[0].pk,
                                "addcost_amount": None,
                                "addcost_for__trigramme": None,
                                "amount": Decimal("-5.00"),
                                "article__name": "Article",
                                "article_nb": 2,
                                "canceled_at": None,
                                "canceled_by__trigramme": None,
                                "group_id": operation_group.pk,
                                "type": "purchase",
                            },
                            {
                                "id": operation_list[1].pk,
                                "addcost_amount": None,
                                "addcost_for__trigramme": None,
                                "amount": Decimal("-4.00"),
                                "article__name": "Article 2",
                                "article_nb": 1,
                                "canceled_at": None,
                                "canceled_by__trigramme": None,
                                "group_id": operation_group.pk,
                                "type": "purchase",
                            },
                        ],
                    }
                ],
                "checkouts": [{"id": self.checkout.pk, "balance": Decimal("100.00")}],
                "articles": [
                    {"id": self.article.pk, "stock": 18},
                    {"id": article2.pk, "stock": -6},
                ],
            },
        )


class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
    """
    Test cases for kpsul_cancel_operations view.

    To test valid requests, one should use '_assertResponseOk(response)' to get
    hints about failure reasons, if any.

    At least one test per operation type should test the complete response and
    behavior (HTTP, WebSocket, object updates, and object creations)
    Other tests of the same operation type can only assert the specific
    behavior differences.

    For invalid requests, response errors should be tested.

    """

    url_name = "kfet.operations.cancel"
    url_expected = "/k-fet/k-psul/cancel_operations"

    http_methods = ["POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    with_liq = True

    def setUp(self):
        super(KPsulCancelOperationsViewTests, self).setUp()

        self.checkout = create_checkout(balance=Decimal("100.00"))
        # An Article, price=2.5, stock=20
        self.article = Article.objects.create(
            category=ArticleCategory.objects.create(name="Category"),
            name="Article",
            price=Decimal("2.5"),
            stock=20,
        )
        # An Account, trigramme=000, balance=50
        # Do not assume user is cof, nor not cof.
        self.account = self.accounts["user"]
        self.account.balance = Decimal("50.00")
        self.account.save()

        # Create a channel to listen to KPsul's messages
        channel_layer = get_channel_layer()

        self.channel = async_to_sync(channel_layer.new_channel)()

        async_to_sync(channel_layer.group_add)("kfet.kpsul", self.channel)

        self.receive_msg = lambda: async_to_sync(channel_layer.receive)(self.channel)

    def _assertResponseOk(self, response):
        """
        Asserts that status code of 'response' is 200, and returns the
        deserialized content of the JSONResponse.

        In case status code is not 200, it prints the content of "errors" of
        the response.

        """
        json_data = json.loads(getattr(response, "content", b"{}").decode("utf-8"))
        try:
            self.assertEqual(response.status_code, 200)
        except AssertionError as exc:
            msg = "Expected response is 200, got {}. Errors: {}".format(
                response.status_code, json_data.get("errors")
            )
            raise AssertionError(msg) from exc
        return json_data

    def test_invalid_operation_not_int(self):
        data = {"operations[]": ["a"]}
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["invalid_request"],
        )

    def test_invalid_operation_not_exist(self):
        data = {"operations[]": ["1000"]}
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["cancel_missing"],
        )

    @mock.patch("django.utils.timezone.now")
    def test_purchase(self, now_mock):
        now_mock.return_value = self.now
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[
                {
                    "type": Operation.PURCHASE,
                    "article": self.article,
                    "article_nb": 2,
                }
            ],
        )
        operation = group.opes.get()
        now_mock.return_value += timedelta(seconds=15)

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        group = OperationGroup.objects.get()
        self.assertDictEqual(
            group.__dict__,
            {
                "_state": mock.ANY,
                "amount": Decimal("0.00"),
                "at": mock.ANY,
                "checkout_id": self.checkout.pk,
                "comment": "",
                "id": mock.ANY,
                "is_cof": False,
                "on_acc_id": self.account.pk,
                "valid_by_id": None,
            },
        )
        operation = Operation.objects.get()
        self.assertDictEqual(
            operation.__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("-5.00"),
                "article_id": self.article.pk,
                "article_nb": 2,
                "canceled_at": self.now + timedelta(seconds=15),
                "canceled_by_id": None,
                "group_id": group.pk,
                "id": mock.ANY,
                "type": Operation.PURCHASE,
            },
        )

        self.assertDictEqual(
            json_data,
            {
                "canceled": [
                    {
                        "id": operation.id,
                        # l'encodage des dates en JSON est relou...
                        "canceled_at": mock.ANY,
                        "canceled_by__trigramme": None,
                    }
                ],
                "errors": [],
                "warnings": {},
                "opegroups_to_update": [
                    {
                        "id": group.pk,
                        "amount": str(group.amount),
                        "is_cof": group.is_cof,
                    }
                ],
            },
        )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("55.00"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 22)
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))

        ws_data = self.receive_msg()

        self.assertDictEqual(
            ws_data,
            {
                "type": "kpsul",
                "checkouts": [],
                "articles": [{"id": self.article.pk, "stock": 22}],
            },
        )

    def test_purchase_with_addcost(self):
        # TODO(AD): L'état de la balance du compte destinataire de la majoration ne
        # devrait pas empêcher l'annulation d'une opération.
        addcost_user = create_user(
            "addcost", "ADD", account_attrs={"balance": Decimal("10.00")}
        )
        addcost_account = addcost_user.profile.account_kfet
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[
                {
                    "type": Operation.PURCHASE,
                    "article": self.article,
                    "article_nb": 2,
                    "amount": Decimal("-6.00"),
                    "addcost_amount": Decimal("1.00"),
                    "addcost_for": addcost_account,
                }
            ],
        )
        operation = group.opes.get()

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("56.00"))
        addcost_account.refresh_from_db()
        self.assertEqual(addcost_account.balance, Decimal("9.00"))

    def test_purchase_cash(self):
        group = create_operation_group(
            on_acc=self.accounts["liq"],
            checkout=self.checkout,
            content=[
                {
                    "type": Operation.PURCHASE,
                    "article": self.article,
                    "article_nb": 2,
                    "amount": Decimal("-5.00"),
                }
            ],
        )
        operation = group.opes.get()

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        self.assertEqual(self.accounts["liq"].balance, Decimal("0.00"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("95.00"))

        ws_data = self.receive_msg()

        self.assertListEqual(
            ws_data["checkouts"],
            [{"id": self.checkout.pk, "balance": Decimal("95.00")}],
        )

    def test_purchase_cash_with_addcost(self):
        # TODO(AD): L'état de la balance du compte destinataire de la majoration ne
        # devrait pas empêcher l'annulation d'une opération.
        addcost_user = create_user(
            "addcost", "ADD", account_attrs={"balance": Decimal("10.00")}
        )
        addcost_account = addcost_user.profile.account_kfet
        group = create_operation_group(
            on_acc=self.accounts["liq"],
            checkout=self.checkout,
            content=[
                {
                    "type": Operation.PURCHASE,
                    "article": self.article,
                    "article_nb": 2,
                    "amount": Decimal("-6.00"),
                    "addcost_amount": Decimal("1.00"),
                    "addcost_for": addcost_account,
                }
            ],
        )
        operation = group.opes.get()

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        self._assertResponseOk(resp)

        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("94.00"))
        addcost_account.refresh_from_db()
        self.assertEqual(addcost_account.balance, Decimal("9.00"))

        ws_data = self.receive_msg()

        self.assertListEqual(
            ws_data["checkouts"],
            [{"id": self.checkout.pk, "balance": Decimal("94.00")}],
        )

    @mock.patch("django.utils.timezone.now")
    def test_deposit(self, now_mock):
        now_mock.return_value = self.now
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[{"type": Operation.DEPOSIT, "amount": Decimal("10.75")}],
        )
        operation = group.opes.get()
        now_mock.return_value += timedelta(seconds=15)

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        group = OperationGroup.objects.get()
        self.assertDictEqual(
            group.__dict__,
            {
                "_state": mock.ANY,
                "amount": Decimal("0.00"),
                "at": mock.ANY,
                "checkout_id": self.checkout.pk,
                "comment": "",
                "id": mock.ANY,
                "is_cof": False,
                "on_acc_id": self.account.pk,
                "valid_by_id": None,
            },
        )
        operation = Operation.objects.get()
        self.assertDictEqual(
            operation.__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("10.75"),
                "article_id": None,
                "article_nb": None,
                "canceled_at": self.now + timedelta(seconds=15),
                "canceled_by_id": None,
                "group_id": group.pk,
                "id": mock.ANY,
                "type": Operation.DEPOSIT,
            },
        )

        self.assertDictEqual(
            json_data,
            {
                "canceled": [
                    {
                        "id": operation.id,
                        # l'encodage des dates en JSON est relou...
                        "canceled_at": mock.ANY,
                        "canceled_by__trigramme": None,
                    }
                ],
                "errors": [],
                "warnings": {},
                "opegroups_to_update": [
                    {
                        "id": group.pk,
                        "amount": str(group.amount),
                        "is_cof": group.is_cof,
                    }
                ],
            },
        )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("39.25"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 20)
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("89.25"))

        ws_data = self.receive_msg()

        self.assertDictEqual(
            ws_data,
            {
                "type": "kpsul",
                "checkouts": [{"id": self.checkout.pk, "balance": Decimal("89.25")}],
                "articles": [],
            },
        )

    @mock.patch("django.utils.timezone.now")
    def test_withdraw(self, now_mock):
        now_mock.return_value = self.now
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[{"type": Operation.WITHDRAW, "amount": Decimal("-10.75")}],
        )
        operation = group.opes.get()
        now_mock.return_value += timedelta(seconds=15)

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        group = OperationGroup.objects.get()
        self.assertDictEqual(
            group.__dict__,
            {
                "_state": mock.ANY,
                "amount": Decimal("0.00"),
                "at": mock.ANY,
                "checkout_id": self.checkout.pk,
                "comment": "",
                "id": mock.ANY,
                "is_cof": False,
                "on_acc_id": self.account.pk,
                "valid_by_id": None,
            },
        )
        operation = Operation.objects.get()
        self.assertDictEqual(
            operation.__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("-10.75"),
                "article_id": None,
                "article_nb": None,
                "canceled_at": self.now + timedelta(seconds=15),
                "canceled_by_id": None,
                "group_id": group.pk,
                "id": mock.ANY,
                "type": Operation.WITHDRAW,
            },
        )

        self.assertDictEqual(
            json_data,
            {
                "canceled": [
                    {
                        "id": operation.id,
                        # l'encodage des dates en JSON est relou...
                        "canceled_at": mock.ANY,
                        "canceled_by__trigramme": None,
                    }
                ],
                "errors": [],
                "warnings": {},
                "opegroups_to_update": [
                    {
                        "id": group.pk,
                        "amount": str(group.amount),
                        "is_cof": group.is_cof,
                    }
                ],
            },
        )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("60.75"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 20)
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("110.75"))

        ws_data = self.receive_msg()

        self.assertDictEqual(
            ws_data,
            {
                "type": "kpsul",
                "checkouts": [{"id": self.checkout.pk, "balance": Decimal("110.75")}],
                "articles": [],
            },
        )

    @mock.patch("django.utils.timezone.now")
    def test_edit(self, now_mock):
        now_mock.return_value = self.now
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[{"type": Operation.EDIT, "amount": Decimal("-10.75")}],
        )
        operation = group.opes.get()
        now_mock.return_value += timedelta(seconds=15)

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        group = OperationGroup.objects.get()
        self.assertDictEqual(
            group.__dict__,
            {
                "_state": mock.ANY,
                "amount": Decimal("0.00"),
                "at": mock.ANY,
                "checkout_id": self.checkout.pk,
                "comment": "",
                "id": mock.ANY,
                "is_cof": False,
                "on_acc_id": self.account.pk,
                "valid_by_id": None,
            },
        )
        operation = Operation.objects.get()
        self.assertDictEqual(
            operation.__dict__,
            {
                "_state": mock.ANY,
                "addcost_amount": None,
                "addcost_for_id": None,
                "amount": Decimal("-10.75"),
                "article_id": None,
                "article_nb": None,
                "canceled_at": self.now + timedelta(seconds=15),
                "canceled_by_id": None,
                "group_id": group.pk,
                "id": mock.ANY,
                "type": Operation.EDIT,
            },
        )

        self.assertDictEqual(
            json_data,
            {
                "canceled": [
                    {
                        "id": operation.id,
                        # l'encodage des dates en JSON est relou...
                        "canceled_at": mock.ANY,
                        "canceled_by__trigramme": None,
                    }
                ],
                "errors": [],
                "warnings": {},
                "opegroups_to_update": [
                    {
                        "id": group.pk,
                        "amount": str(group.amount),
                        "is_cof": group.is_cof,
                    }
                ],
            },
        )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("60.75"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 20)
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))

        ws_data = self.receive_msg()

        self.assertDictEqual(
            ws_data,
            {"type": "kpsul", "checkouts": [], "articles": []},
        )

    @mock.patch("django.utils.timezone.now")
    def test_old_operations(self, now_mock):
        kfet_config.set(cancel_duration=timedelta(minutes=10))
        user_add_perms(self.users["team"], ["kfet.cancel_old_operations"])
        now_mock.return_value = self.now
        group = create_operation_group(
            at=self.now,
            on_acc=self.account,
            checkout=self.checkout,
            content=[{"type": Operation.WITHDRAW, "amount": Decimal("-10.75")}],
        )
        operation = group.opes.get()
        now_mock.return_value += timedelta(minutes=10, seconds=1)

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)
        self.assertEqual(len(json_data["canceled"]), 1)

    @mock.patch("django.utils.timezone.now")
    def test_invalid_old_operations_requires_perm(self, now_mock):
        kfet_config.set(cancel_duration=timedelta(minutes=10))
        now_mock.return_value = self.now
        group = create_operation_group(
            at=self.now,
            on_acc=self.account,
            checkout=self.checkout,
            content=[{"type": Operation.WITHDRAW, "amount": Decimal("-10.75")}],
        )
        operation = group.opes.get()
        now_mock.return_value += timedelta(minutes=10, seconds=1)

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 403)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertEqual(
            json_data["missing_perms"],
            ["Annuler des commandes non récentes"],
        )

    def test_already_canceled(self):
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[
                {
                    "type": Operation.WITHDRAW,
                    "amount": Decimal("-10.75"),
                    "canceled_at": timezone.now(),
                }
            ],
        )
        operation = group.opes.get()

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        self.assertDictEqual(
            json_data["warnings"], {"already_canceled": [operation.pk]}
        )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("50.00"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))

    @mock.patch("django.utils.timezone.now")
    def test_checkout_before_last_statement(self, now_mock):
        now_mock.return_value = self.now
        group = create_operation_group(
            at=self.now,
            on_acc=self.account,
            checkout=self.checkout,
            content=[{"type": Operation.WITHDRAW, "amount": Decimal("-10.75")}],
        )
        operation = group.opes.get()
        now_mock.return_value += timedelta(seconds=30)
        create_checkout_statement(checkout=self.checkout)
        now_mock.return_value += timedelta(seconds=30)

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        self.assertEqual(len(json_data["canceled"]), 1)

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("60.75"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))

    @mock.patch("django.utils.timezone.now")
    def test_article_before_last_inventory(self, now_mock):
        now_mock.return_value = self.now
        group = create_operation_group(
            at=self.now,
            on_acc=self.account,
            checkout=self.checkout,
            content=[
                {"type": Operation.PURCHASE, "article": self.article, "article_nb": 2}
            ],
        )
        operation = group.opes.get()
        now_mock.return_value += timedelta(seconds=30)
        create_inventory_article(article=self.article)
        now_mock.return_value += timedelta(seconds=30)

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        self.assertEqual(len(json_data["canceled"]), 1)

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("55.00"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 20)

    def test_negative(self):
        kfet_config.set(overdraft_amount=Decimal("40.00"))
        user_add_perms(self.users["team"], ["kfet.perform_negative_operations"])
        self.account.balance = Decimal("-20.00")
        self.account.save()
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[{"type": Operation.DEPOSIT, "amount": Decimal("10.75")}],
        )
        operation = group.opes.get()

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        self.assertEqual(len(json_data["canceled"]), 1)

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("-30.75"))
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("89.25"))

    def test_invalid_negative_above_thresholds(self):
        kfet_config.set(overdraft_amount=Decimal("5.00"))
        user_add_perms(self.users["team"], ["kfet.perform_negative_operations"])
        self.account.balance = Decimal("-20.00")
        self.account.save()
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[{"type": Operation.DEPOSIT, "amount": Decimal("10.75")}],
        )
        operation = group.opes.get()

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 400)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertCountEqual(
            [e["code"] for e in json_data["errors"]],
            ["negative"],
        )

    def test_invalid_negative_requires_perms(self):
        kfet_config.set(overdraft_amount=Decimal("40.00"))
        self.account.balance = Decimal("-20.00")
        self.account.save()
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[{"type": Operation.DEPOSIT, "amount": Decimal("10.75")}],
        )
        operation = group.opes.get()

        data = {"operations[]": [str(operation.pk)]}
        resp = self.client.post(self.url, data)

        self.assertEqual(resp.status_code, 403)
        json_data = json.loads(resp.content.decode("utf-8"))
        self.assertEqual(
            json_data["missing_perms"],
            ["Enregistrer des commandes en négatif"],
        )

    def test_partial_0(self):
        group = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[
                {"type": Operation.PURCHASE, "article": self.article, "article_nb": 2},
                {"type": Operation.DEPOSIT, "amount": Decimal("10.75")},
                {"type": Operation.EDIT, "amount": Decimal("-6.00")},
                {
                    "type": Operation.WITHDRAW,
                    "amount": Decimal("-10.75"),
                    "canceled_at": timezone.now(),
                },
            ],
        )
        operation1 = group.opes.get(type=Operation.PURCHASE)
        operation2 = group.opes.get(type=Operation.EDIT)
        operation3 = group.opes.get(type=Operation.WITHDRAW)

        data = {
            "operations[]": [str(operation1.pk), str(operation2.pk), str(operation3.pk)]
        }
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        group.refresh_from_db()
        self.assertEqual(group.amount, Decimal("10.75"))
        self.assertEqual(group.opes.exclude(canceled_at=None).count(), 3)
        self.maxDiff = None
        self.assertDictEqual(
            json_data,
            {
                "canceled": [
                    {
                        "id": operation1.id,
                        # l'encodage des dates en JSON est relou...
                        "canceled_at": mock.ANY,
                        "canceled_by__trigramme": None,
                    },
                    {
                        "id": operation2.id,
                        # l'encodage des dates en JSON est relou...
                        "canceled_at": mock.ANY,
                        "canceled_by__trigramme": None,
                    },
                ],
                "errors": [],
                "warnings": {"already_canceled": [operation3.pk]},
                "opegroups_to_update": [
                    {
                        "id": group.pk,
                        "amount": str(group.amount),
                        "is_cof": group.is_cof,
                    }
                ],
            },
        )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("61.00"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 22)
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("100.00"))

    def test_multi_0(self):
        group1 = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[
                {"type": Operation.PURCHASE, "article": self.article, "article_nb": 2},
                {"type": Operation.DEPOSIT, "amount": Decimal("10.75")},
                {"type": Operation.EDIT, "amount": Decimal("-6.00")},
            ],
        )
        operation11 = group1.opes.get(type=Operation.PURCHASE)
        group2 = create_operation_group(
            on_acc=self.account,
            checkout=self.checkout,
            content=[
                {"type": Operation.PURCHASE, "article": self.article, "article_nb": 5},
                {"type": Operation.DEPOSIT, "amount": Decimal("3.00")},
            ],
        )
        operation21 = group2.opes.get(type=Operation.PURCHASE)
        operation22 = group2.opes.get(type=Operation.DEPOSIT)

        data = {
            "operations[]": [
                str(operation11.pk),
                str(operation21.pk),
                str(operation22.pk),
            ]
        }
        resp = self.client.post(self.url, data)

        json_data = self._assertResponseOk(resp)

        group1.refresh_from_db()
        self.assertEqual(group1.amount, Decimal("4.75"))
        self.assertEqual(group1.opes.exclude(canceled_at=None).count(), 1)
        group2.refresh_from_db()
        self.assertEqual(group2.amount, Decimal(0))
        self.assertEqual(group2.opes.exclude(canceled_at=None).count(), 2)

        self.assertEqual(len(json_data["canceled"]), 3)

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, Decimal("64.50"))
        self.article.refresh_from_db()
        self.assertEqual(self.article.stock, 27)
        self.checkout.refresh_from_db()
        self.assertEqual(self.checkout.balance, Decimal("97.00"))


class KPsulArticlesData(ViewTestCaseMixin, TestCase):
    url_name = "kfet.kpsul.articles_data"
    url_expected = "/k-fet/k-psul/articles_data"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def setUp(self):
        super().setUp()
        category = ArticleCategory.objects.create(name="Catégorie")
        self.article1 = Article.objects.create(category=category, name="Article 1")
        self.article2 = Article.objects.create(
            category=category, name="Article 2", price=Decimal("2.5")
        )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        content = json.loads(r.content.decode("utf-8"))

        articles = content["articles"]

        expected_list = [
            {"category__name": "Catégorie", "name": "Article 1", "price": "0.00"},
            {"category__name": "Catégorie", "name": "Article 2", "price": "2.50"},
        ]

        for expected, article in zip(expected_list, articles):
            self.assertEqual(article, {**article, **expected})
            self.assertSetEqual(
                set(article.keys()),
                set(
                    [
                        "id",
                        "name",
                        "price",
                        "stock",
                        "category_id",
                        "category__name",
                        "category__has_addcost",
                        "category__has_reduction",
                    ]
                ),
            )


class KPsulUpdateAddcost(ViewTestCaseMixin, TestCase):
    url_name = "kfet.kpsul.update_addcost"
    url_expected = "/k-fet/k-psul/update_addcost"

    http_methods = ["POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    post_data = {"trigramme": "000", "amount": "0.5"}

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.manage_addcosts"])}

    def test_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)
        self.assertEqual(r.status_code, 200)

        self.assertEqual(kfet_config.addcost_for, Account.objects.get(trigramme="000"))
        self.assertEqual(kfet_config.addcost_amount, Decimal("0.5"))

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbidden(r)


class KPsulGetSettings(ViewTestCaseMixin, TestCase):
    url_name = "kfet.kpsul.get_settings"
    url_expected = "/k-fet/k-psul/get_settings"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)


class HistoryJSONViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.history.json"
    url_expected = "/k-fet/history.json"

    auth_user = "team"
    auth_forbidden = [None, "user", "noaccount"]

    def test_ok(self):
        r = self.client.post(self.url)
        self.assertEqual(r.status_code, 200)

    def get_users_extra(self):
        noaccount = User.objects.create(username="noaccount")
        noaccount.set_password("noaccount")
        noaccount.save()
        return {"noaccount": noaccount}


class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.account.read.json"

    http_methods = ["GET"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"trigramme": self.accounts["user"].trigramme}

    @property
    def url_expected(self):
        return "/k-fet/accounts/{}/.json".format(self.accounts["user"].trigramme)

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        content = json.loads(r.content.decode("utf-8"))

        expected = {"name": "first last", "trigramme": "000", "balance": "0.00"}
        self.assertEqual(content, {**content, **expected})

        self.assertSetEqual(
            set(content.keys()),
            set(
                [
                    "balance",
                    "departement",
                    "email",
                    "id",
                    "is_cof",
                    "is_frozen",
                    "name",
                    "nickname",
                    "promo",
                    "trigramme",
                ]
            ),
        )


class SettingsListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.settings"
    url_expected = "/k-fet/settings/"

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.see_config"])}

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)


class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.settings.update"
    url_expected = "/k-fet/settings/edit"

    http_methods = ["GET", "POST"]

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    @property
    def post_data(self):
        return {
            "kfet_reduction_cof": "25",
            "kfet_addcost_amount": "0.5",
            "kfet_addcost_for": self.accounts["user"].pk,
            "kfet_overdraft_duration": "2 00:00:00",
            "kfet_overdraft_amount": "25",
            "kfet_cancel_duration": "00:20:00",
        }

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.change_config"])}

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        r = self.client.post(self.url, self.post_data)
        # Redirect is skipped because client may lack permissions.
        self.assertRedirects(r, reverse("kfet.settings"), fetch_redirect_response=False)

        expected_config = {
            "reduction_cof": Decimal("25"),
            "addcost_amount": Decimal("0.5"),
            "addcost_for": self.accounts["user"],
            "overdraft_duration": timedelta(days=2),
            "overdraft_amount": Decimal("25"),
            "cancel_duration": timedelta(minutes=20),
        }

        for key, expected in expected_config.items():
            self.assertEqual(getattr(kfet_config, key), expected)


class TransferListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.transfers"
    url_expected = "/k-fet/transfers/"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)


class TransferCreateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.transfers.create"
    url_expected = "/k-fet/transfers/new"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)


class TransferPerformViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.transfers.perform"
    url_expected = "/k-fet/transfers/perform"

    http_methods = ["POST"]

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    def get_users_extra(self):
        return {
            "team1": create_team(
                "team1",
                "101",
                perms=[
                    # Required
                    "kfet.add_transfer",
                    # Convenience
                    "kfet.perform_negative_operations",
                ],
            )
        }

    @property
    def post_data(self):
        return {
            # General
            "comment": "",
            # Formset management
            "form-TOTAL_FORMS": "10",
            "form-INITIAL_FORMS": "0",
            "form-MIN_NUM_FORMS": "1",
            "form-MAX_NUM_FORMS": "1000",
            # Transfer 1
            "form-0-from_acc": str(self.accounts["user"].pk),
            "form-0-to_acc": str(self.accounts["team"].pk),
            "form-0-amount": "3.5",
            # Transfer 2
            "form-1-from_acc": str(self.accounts["team"].pk),
            "form-1-to_acc": str(self.accounts["team1"].pk),
            "form-1-amount": "2.4",
        }

    def test_ok(self):
        r = self.client.post(self.url, self.post_data)
        self.assertEqual(r.status_code, 200)

        user = self.accounts["user"]
        user.refresh_from_db()
        self.assertEqual(user.balance, Decimal("-3.5"))

        team = self.accounts["team"]
        team.refresh_from_db()
        self.assertEqual(team.balance, Decimal("1.1"))

        team1 = self.accounts["team1"]
        team1.refresh_from_db()
        self.assertEqual(team1.balance, Decimal("2.4"))


class TransferCancelViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.transfers.cancel"
    url_expected = "/k-fet/transfers/cancel"

    http_methods = ["POST"]

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    def get_users_extra(self):
        return {
            "team1": create_team(
                "team1",
                "101",
                perms=[
                    # Convenience
                    "kfet.perform_negative_operations"
                ],
            )
        }

    @property
    def post_data(self):
        return {"transfers[]": [self.transfer1.pk, self.transfer2.pk]}

    def setUp(self):
        super().setUp()
        group = TransferGroup.objects.create()
        self.transfer1 = Transfer.objects.create(
            group=group,
            from_acc=self.accounts["user"],
            to_acc=self.accounts["team"],
            amount="3.5",
        )
        self.transfer2 = Transfer.objects.create(
            group=group,
            from_acc=self.accounts["team"],
            to_acc=self.accounts["root"],
            amount="2.4",
        )

    def test_ok(self):
        r = self.client.post(self.url, self.post_data)
        self.assertEqual(r.status_code, 200)

        user = self.accounts["user"]
        user.refresh_from_db()
        self.assertEqual(user.balance, Decimal("3.5"))

        team = self.accounts["team"]
        team.refresh_from_db()
        self.assertEqual(team.balance, Decimal("-1.1"))

        root = self.accounts["root"]
        root.refresh_from_db()
        self.assertEqual(root.balance, Decimal("-2.4"))


class InventoryListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.inventory"
    url_expected = "/k-fet/inventaires/"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def setUp(self):
        super().setUp()
        self.inventory = Inventory.objects.create(by=self.accounts["team"])
        category = ArticleCategory.objects.create(name="Category")
        article = Article.objects.create(name="Article", category=category)
        InventoryArticle.objects.create(
            inventory=self.inventory, article=article, stock_old=5, stock_new=0
        )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        inventories = r.context["inventories"]
        self.assertQuerysetEqual(
            inventories, map(repr, [self.inventory]), transform=repr
        )


class InventoryCreateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.inventory.create"
    url_expected = "/k-fet/inventaires/new"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.add_inventory"])}

    @property
    def post_data(self):
        return {
            # Formset management
            "form-TOTAL_FORMS": "2",
            "form-INITIAL_FORMS": "2",
            "form-MIN_NUM_FORMS": "0",
            "form-MAX_NUM_FORMS": "1000",
            # Article 1
            "form-0-article": str(self.article1.pk),
            "form-0-stock_new": "5",
            # Article 2
            "form-1-article": str(self.article2.pk),
            "form-1-stock_new": "10",
        }

    def setUp(self):
        super().setUp()
        category = ArticleCategory.objects.create(name="Category")
        self.article1 = Article.objects.create(category=category, name="Article 1")
        self.article2 = Article.objects.create(category=category, name="Article 2")

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)
        self.assertRedirects(r, reverse("kfet.inventory"))

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class InventoryReadViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.inventory.read"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.inventory.pk}

    @property
    def url_expected(self):
        return "/k-fet/inventaires/{}".format(self.inventory.pk)

    def setUp(self):
        super().setUp()
        self.inventory = Inventory.objects.create(by=self.accounts["team"])
        category = ArticleCategory.objects.create(name="Category")
        article = Article.objects.create(name="Article", category=category)
        InventoryArticle.objects.create(
            inventory=self.inventory, article=article, stock_old=5, stock_new=0
        )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)


class InventoryDeleteViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.inventory.delete"

    auth_user = "team1"
    auth_forbidden = [None, "user", "team"]

    def get_users_extra(self):
        return {
            "user1": create_user("user1", "001"),
            "team1": create_team("team1", "101", perms=["kfet.delete_inventory"]),
        }

    @property
    def url_kwargs(self):
        return {"pk": self.inventory1.pk}

    @property
    def url_expected(self):
        return "/k-fet/inventaires/{}/delete".format(self.inventory1.pk)

    def setUp(self):
        super().setUp()
        # Deux inventaires : un avec article 1 + 2, l'autre avec 1 + 3
        self.inventory1 = Inventory.objects.create(
            by=self.accounts["team"], at=self.now
        )
        self.inventory2 = Inventory.objects.create(
            by=self.accounts["team"], at=self.now + timedelta(days=1)
        )
        category = ArticleCategory.objects.create(name="Category")
        # Le stock des articles correspond à leur dernier inventaire
        self.article1 = Article.objects.create(
            name="Article1", category=category, stock=51
        )
        self.article2 = Article.objects.create(
            name="Article2", category=category, stock=42
        )
        self.article3 = Article.objects.create(
            name="Article3", category=category, stock=42
        )

        InventoryArticle.objects.create(
            inventory=self.inventory1,
            article=self.article1,
            stock_old=23,
            stock_new=42,
        )
        InventoryArticle.objects.create(
            inventory=self.inventory1,
            article=self.article2,
            stock_old=23,
            stock_new=42,
        )
        InventoryArticle.objects.create(
            inventory=self.inventory2,
            article=self.article1,
            stock_old=42,
            stock_new=51,
        )
        InventoryArticle.objects.create(
            inventory=self.inventory2,
            article=self.article3,
            stock_old=23,
            stock_new=42,
        )

    def test_ok(self):
        r = self.client.post(self.url)
        self.assertRedirects(r, reverse("kfet.inventory"))

        # On vérifie que l'inventaire n'existe plus
        self.assertFalse(Inventory.objects.filter(pk=self.inventory1.pk).exists())
        # On check les stocks
        self.article1.refresh_from_db()
        self.article2.refresh_from_db()
        self.article3.refresh_from_db()
        self.assertEqual(self.article1.stock, 51)
        self.assertEqual(self.article2.stock, 23)
        self.assertEqual(self.article3.stock, 42)


class OrderListViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.order"
    url_expected = "/k-fet/orders/"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    def setUp(self):
        super().setUp()
        category = ArticleCategory.objects.create(name="Category")
        article = Article.objects.create(name="Article", category=category)

        supplier = Supplier.objects.create(name="Supplier")
        SupplierArticle.objects.create(supplier=supplier, article=article)

        self.order = Order.objects.create(supplier=supplier)
        OrderArticle.objects.create(
            order=self.order, article=article, quantity_ordered=24
        )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

        orders = r.context["orders"]
        self.assertQuerysetEqual(orders, map(repr, [self.order]), transform=repr)


class OrderReadViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.order.read"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.order.pk}

    @property
    def url_expected(self):
        return "/k-fet/orders/{}".format(self.order.pk)

    def setUp(self):
        super().setUp()
        category = ArticleCategory.objects.create(name="Category")
        article = Article.objects.create(name="Article", category=category)

        supplier = Supplier.objects.create(name="Supplier")
        SupplierArticle.objects.create(supplier=supplier, article=article)

        self.order = Order.objects.create(supplier=supplier)
        OrderArticle.objects.create(
            order=self.order, article=article, quantity_ordered=24
        )

    def test_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)


class SupplierUpdateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.order.supplier.update"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.supplier.pk}

    @property
    def url_expected(self):
        return "/k-fet/orders/suppliers/{}/edit".format(self.supplier.pk)

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.change_supplier"])}

    @property
    def post_data(self):
        return {
            "name": "The Supplier",
            "phone": "",
            "comment": "",
            "address": "",
            "email": "",
        }

    def setUp(self):
        super().setUp()
        self.supplier = Supplier.objects.create(name="Supplier")

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    def test_post_ok(self):
        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)
        self.assertRedirects(r, reverse("kfet.order"))

        self.supplier.refresh_from_db()
        self.assertEqual(self.supplier.name, "The Supplier")

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class OrderCreateViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.order.new"

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.supplier.pk}

    @property
    def url_expected(self):
        return "/k-fet/orders/suppliers/{}/new-order".format(self.supplier.pk)

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.add_order"])}

    @property
    def post_data(self):
        return {
            # Formset management
            "form-TOTAL_FORMS": "1",
            "form-INITIAL_FORMS": "1",
            "form-MIN_NUM_FORMS": "0",
            "form-MAX_NUM_FORMS": "1000",
            # Article
            "form-0-article": self.article.pk,
            "form-0-quantity_ordered": "20",
        }

    def setUp(self):
        super().setUp()
        category = ArticleCategory.objects.create(name="Category")
        self.article = Article.objects.create(name="Article", category=category)

        self.supplier = Supplier.objects.create(name="Supplier")
        SupplierArticle.objects.create(supplier=self.supplier, article=self.article)

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    @mock.patch("django.utils.timezone.now")
    def test_post_ok(self, mock_now):
        mock_now.return_value = self.now

        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)

        order = Order.objects.get(at=self.now)

        self.assertRedirects(r, reverse("kfet.order.read", args=[order.pk]))

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)


class OrderToInventoryViewTests(ViewTestCaseMixin, TestCase):
    url_name = "kfet.order.to_inventory"

    http_methods = ["GET", "POST"]

    auth_user = "team"
    auth_forbidden = [None, "user"]

    @property
    def url_kwargs(self):
        return {"pk": self.order.pk}

    @property
    def url_expected(self):
        return "/k-fet/orders/{}/to_inventory".format(self.order.pk)

    def get_users_extra(self):
        return {"team1": create_team("team1", "101", perms=["kfet.order_to_inventory"])}

    @property
    def post_data(self):
        return {
            # Formset mangaement
            "form-TOTAL_FORMS": "1",
            "form-INITIAL_FORMS": "1",
            "form-MIN_NUM_FORMS": "0",
            "form-MAX_NUM_FORMS": "1000",
            # Article 1
            "form-0-article": self.article.pk,
            "form-0-quantity_received": "20",
            "form-0-price_HT": "",
            "form-0-TVA": "",
            "form-0-rights": "",
        }

    def setUp(self):
        super().setUp()
        category = ArticleCategory.objects.create(name="Category")
        self.article = Article.objects.create(name="Article", category=category)

        supplier = Supplier.objects.create(name="Supplier")
        SupplierArticle.objects.create(supplier=supplier, article=self.article)

        self.order = Order.objects.create(supplier=supplier)
        OrderArticle.objects.create(
            order=self.order, article=self.article, quantity_ordered=24
        )

    def test_get_ok(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 200)

    @mock.patch("django.utils.timezone.now")
    def test_post_ok(self, mock_now):
        mock_now.return_value = self.now

        client = Client()
        client.login(username="team1", password="team1")

        r = client.post(self.url, self.post_data)
        self.assertRedirects(r, reverse("kfet.order"))

        inventory = Inventory.objects.first()

        self.assertInstanceExpected(
            inventory,
            {"by": self.accounts["team1"], "at": self.now, "order": self.order},
        )
        self.assertQuerysetEqual(
            inventory.articles.all(), map(repr, [self.article]), transform=repr
        )

        compte = InventoryArticle.objects.get(article=self.article)

        self.assertInstanceExpected(
            compte, {"stock_old": 0, "stock_new": 20, "stock_error": 0}
        )

    def test_post_forbidden(self):
        r = self.client.post(self.url, self.post_data)
        self.assertForbiddenKfet(r)
