Skip to content
Snippets Groups Projects
import_nk15.py 19.2 KiB
Newer Older
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
#!/usr/env/bin python3

ynerant's avatar
ynerant committed
import json
import datetime
import re
ynerant's avatar
ynerant committed

import psycopg2 as pg
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
import psycopg2.extras as pge

from django.core.management.base import BaseCommand
from django.core.management import call_command
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
from django.db import transaction
from django.core.exceptions import ValidationError
from django.utils.timezone import make_aware
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
from django.db import IntegrityError
from django.contrib.auth.models import User
from activity.models import ActivityType, Activity, Guest, Entry, GuestTransaction
from note.models import Note
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
from note.models import Alias
from note.models import TemplateCategory, TransactionTemplate, \
    Transaction, RecurrentTransaction, SpecialTransaction
ynerant's avatar
ynerant committed
from member.models import Club, Membership
ynerant's avatar
ynerant committed
from treasury.models import RemittanceType, Remittance, SpecialTransactionProxy
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed

"""
Script d'import de la nk15:
TODO: import transactions
TODO: import adhesion
TODO: import activite
M_START = datetime.date(2019, 8, 31)
M_END = datetime.date(2020, 9, 30)
MAP_IDBDE = {
    -4: 2,  # Carte Bancaire
    -3: 4,  # Virement
    -2: 1,  # Especes
    -1: 3,  # Chèque
    0: 5,  # BDE
MAP_IDACTIVITY = {}
MAP_NAMEACTIVITY = {}
MAP_NAMEGUEST = {}
ynerant's avatar
ynerant committed
MAP_IDSPECIALTRANSACTION = {}


def update_line(n, total, content):
    total = str(total)
    n.rjust(len(total))
    print(f"\r ({n}/{total}) {content:10.10}", end="")

Pierre-antoine Comby's avatar
Pierre-antoine Comby committed

@transaction.atomic
def import_comptes(cur):
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
    cur.execute("SELECT * FROM comptes WHERE idbde > 0 ORDER BY idbde;")
    pkclub = 3
    n = cur.rowcount
    for idx, row in enumerate(cur):
        update_line(idx, n, row["pseudo"])
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        if row["type"] == "personne":
            # sanitize password
            if row["passwd"] != "*|*" and not row["deleted"]:
                passwd_nk15 = "$".join(["custom_nk15", "1", row["passwd"]])
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            else:
                passwd_nk15 = ''
            try:
                obj_dict = {
                    "username": row["pseudo"],
                    "password": passwd_nk15,
                    "first_name": row["nom"],
                    "last_name": row["prenom"],
                    "email": row["mail"],
                    "is_active": True,  # temporary
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
                }
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
                user = User.objects.create(**obj_dict)
                profile = user.profile
                profile.phone_number = row['tel']
                profile.address = row['adresse']
                profile.paid = row['normalien']
                profile.registration_valid = True
                profile.email_confirmed = True
                user.save()
                profile.save()
                # sanitize duplicate aliases (nk12)
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            except ValidationError as e:
                if e.code == 'same_alias':
                    user.username = row["pseudo"] + str(row["idbde"])
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
                else:
            # profile  and note created via signal.
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            note = user.note
            date = row.get("last_negatif", None)
            if date is not None:
                note.last_negative = make_aware(date)
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            note.balance = row["solde"]
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            obj_dict = {
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
                "name": row["pseudo"],
                "email": row["mail"],
                "membership_duration": M_DURATION,
                "membership_start": M_START,
                "membership_end": M_END,
                "membership_fee_paid": 0,
                "membership_fee_unpaid": 0,
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            }
            club, c = Club.objects.get_or_create(**obj_dict)
            pkclub += 1
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            note = club.note
            note.balance = row["solde"]
        MAP_IDBDE[row["idbde"]] = note.note_ptr_id
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
@transaction.atomic
def import_boutons(cur):
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
    cur.execute("SELECT * FROM boutons;")
    n = cur.rowcount
    for idx, row in enumerate(cur):
        update_line(idx, n, row["label"])
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        cat, created = TemplateCategory.objects.get_or_create(name=row["categorie"])
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        obj_dict = {
            "pk": row["id"],
            "name": row["label"],
            "amount": row["montant"],
            "destination_id": MAP_IDBDE[row["destinataire"]],
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            "category": cat,
            "display": row["affiche"],
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            "description": row["description"],
        }
        try:
            with transaction.atomic():  # required for error management
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
                button = TransactionTemplate.objects.create(**obj_dict)
        except IntegrityError as e:
            # button with the same name is not possible in NK20.
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            if "unique" in e.args[0]:
                qs = Club.objects.filter(note__note_ptr=MAP_IDBDE[row["destinataire"]]).values('name')
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
                note_name = qs[0]["name"]
                # rename button name
                obj_dict["name"] = "{} {}".format(obj_dict["name"], note_name)
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
                button = TransactionTemplate.objects.create(**obj_dict)
            else:
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        button.save()

Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
@transaction.atomic
def import_transaction(cur):
    bde = Club.objects.get(name="BDE")
    kfet = Club.objects.get(name="Kfet")
    cur.execute("SELECT t.date AS transac_date, t.type, t.emetteur, t.destinataire, t.quantite, t.montant, t.description, t.valide, t.cantinvalidate, t.categorie\
                   a.idbde,a.annee, a.wei, a.date AS adh_date, a.section
                   FROM transactions AS t\
                   LEFT JOIN adhesions  AS a ON t.id = a.idtransaction\
                   WHERE transactions.id> {}\
                   ORDER BY transactions.id;".format(idmin))
    n = cur.rowcount
    for idx, row in enumerate(cur):
        update_line(idx, n, row["description"])
        try:
            date = make_aware(row["transac_date"])
        except (pytz.NonExistentTimeError, pytz.AmbiguousTimeError):
            date = make_aware(row["transac_date"] + datetime.timedelta(hours=1)
        
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        obj_dict = {
            # "pk": row["id"],
            "destination_id": MAP_IDBDE[row["destinataire"]],
            "source_id": MAP_IDBDE[row["emetteur"]],
            "created_at": make_aware(date),
            "amount": row["montant"],
            "quantity": row["quantite"],
            "reason": row["description"],
            "valid": row["valide"],
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        }
        ttype = row["type"]
        if ttype == "don" or ttype == "transfert":
            Transaction.objects.create(**obj_dict)
        elif ttype == "bouton":
            cat_name = row["categorie"]
            if cat_name is None:
                cat_name = 'None'
            cat, created = TemplateCategory.objects.get_or_create(name=cat_name)
            if created:
                cat.save()
            obj_dict["category"] = cat
            RecurrentTransaction.objects.create(**obj_dict)
        elif ttype == "crédit" or ttype == "retrait":
            field_id = "source_id" if ttype == "crédit" else "destination_id"
            if "espèce" in row["description"]:
                obj_dict[field_id] = 1
            elif "carte" in row["description"]:
                obj_dict[field_id] = 2
            elif "cheques" in row["description"]:
                obj_dict[field_id] = 3
            elif "virement" in row["description"]:
                obj_dict[field_id] = 4
            pk = max(row["destinataire"], row["emetteur"])
            actor = Note.objects.get(id=MAP_IDBDE[pk])
            # custom fields of SpecialTransaction
            if actor.__class__.__name__ == "NoteUser":
                obj_dict["first_name"] = actor.user.first_name
                obj_dict["last_name"] = actor.user.last_name
            elif actor.__class__.__name__ == "NoteClub":
                obj_dict["first_name"] = actor.club.name
                obj_dict["last_name"] = actor.club.name
                raise Exception("Badly formatted Special Transaction You should'nt be there.")
ynerant's avatar
ynerant committed
            tr = SpecialTransaction.objects.create(**obj_dict)
            if "cheques" in row["description"]:
                MAP_IDSPECIALTRANSACTION[row["id"]] = tr
        elif ttype == "adhésion":
            montant = row["montant"]
            m = re.search("Adhésion (\d\d\d\d)( \+ WEI)?", row["description"])
            year = m.group(1)
            wei = m.group(2)
            if montant == 0: # old format for SoGé registration (before )
                montant = 400

ynerant's avatar
ynerant committed
            # Since BDE and Kfet are distinct, don't import membership transaction and use our custom transactions.
            bde_dict = {
                "user": MAP_IDBDE[row["idbde"]],
                "club": bde,
                "date_start": row["date"].date(),  # Only date, not time
                "fee": 500,
            }
            kfet_dict = {
                "user": MAP_IDBDE[row["idbde"]],
                "club": kfet,
                "date_start": row["date"].date(),  # Only date, not time
                "fee": montant - 500
            }
            try:
                with transaction.atomic():
                    bde_membership = Membership.objects.get_or_create(**bde_dict)
                    kfet_membership = Membership.objects.get_or_create(**kfet_dict)
                    bde_membership.transaction.created_at = row["date"]
                    bde_membership.transaction.save()
                    kfet_membership.transaction.created_at = row["date"]
                    kfet_membership.transaction.save()
            except IntegrityError as e:
                raise e
        elif ttype == "invitation":
            m = re.search("Invitation (.*?) \((.*?)\)", row["description"])
            if m is None:
                raise IntegrityError("Invitation is not well formated: {} (must be 'Invitation ACTIVITY_NAME (NAME)')"
                                     .format(row["description"]))

            activity_name = m.group(1)
            guest_name = m.group(2)

            if activity_name not in MAP_NAMEACTIVITY:
                raise IntegrityError("Activity {} is not found".format(activity_name,))
            activity = MAP_NAMEACTIVITY[activity_name]

            if guest_name not in MAP_NAMEGUEST:
                raise IntegrityError("Guest {} is not found".format(guest_name,))

            guest = None
            for g in MAP_NAMEGUEST[guest_name]:
                if g.activity.pk == activity.pk:
                    guest = g
                    break
            if guest is None:
                raise IntegrityError("Guest {} didn't go to the activity {}".format(guest_name, activity_name,))

            obj_dict["guest"] = guest

            GuestTransaction.objects.get_or_create(**obj_dict)
            print("other type not supported yet:", ttype)

Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
@transaction.atomic
def import_aliases(cur):
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
    cur.execute("SELECT * FROM aliases ORDER by id")
    n = cur.rowcount
    for idx, row in enumerate(cur):
        update_line(idx, n, row["titre"])
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        alias_name = row["alias"]
        alias_name_good = (alias_name[:252] + '...') if len(alias_name) > 255 else alias_name
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        obj_dict = {
            "note_id": MAP_IDBDE[row["idbde"]],
            "name": alias_name_good,
            "normalized_name": Alias.normalize(alias_name_good)
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        }
        try:
            with transaction.atomic():
                alias, created = Alias.objects.get_or_create(**obj_dict)
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        except IntegrityError as e:
            if "unique" in e.args[0]:
                continue
            else:
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        alias.save()


@transaction.atomic
def import_activities(cur):
    cur.execute("SELECT * FROM activites ORDER by id")
    n = cur.rowcount
    activity_type = ActivityType.objects.get(name="Pot")  # Need to be fixed manually
    kfet = Club.objects.get(name="Kfet")
    for idx, row in enumerate(cur):
        update_line(idx, n, row["alias"])
        organizer = Club.objects.filter(name=row["signature"])
        if organizer.exists():
            # Try to find the club that organizes the activity. If not founded, assume that is Kfet (fix manually)
            organizer = organizer.get()
        else:
            organizer = kfet
        obj_dict = {
            "name": row["titre"],
            "description": row["description"],
            "activity_type": activity_type,  # By default Pot
            "creater": MAP_IDBDE[row["responsable"]],
            "organizer": organizer,
            "attendees_club": kfet,  # Maybe fix manually
            "date_start": row["debut"],
            "date_end": row["fin"],
            "valid": row["validepar"] is not None,
            "open": row["open"],  # Should be always False
        }
        # WARNING: Fields lieu, liste, listeimprimee are missing
        try:
            with transaction.atomic():
                activity = Activity.objects.get_or_create(**obj_dict)[0]
                MAP_IDACTIVITY[row["id"]] = activity
                MAP_NAMEACTIVITY[activity.name] = activity
        except IntegrityError as e:
            raise e


@transaction.atomic
def import_activity_entries(cur):
    map_idguests = {}

    cur.execute("SELECT * FROM invites ORDER by id")
    n = cur.rowcount
    for idx, row in enumerate(cur):
        update_line(idx, n, row["nom"] + " " + row["prenom"])
        obj_dict = {
            "activity": MAP_IDACTIVITY[row["activity"]],
            "last_name": row["nom"],
            "first_name": row["prenom"],
            "inviter": MAP_IDBDE[row["responsable"]],
        }
        try:
            with transaction.atomic():
                guest = Guest.objects.get_or_create(**obj_dict)[0]
                map_idguests.setdefault(row["responsable"], [])
                map_idguests[row["id"]].append(guest)
                guest_name = guest.first_name + " " + guest.last_name
                MAP_NAMEGUEST.setdefault(guest_name, [])
                MAP_NAMEGUEST[guest_name].append(guest)
        except IntegrityError as e:
            raise e

    cur.execute("SELECT * FROM entree_activites ORDER by id")
    n = cur.rowcount
    for idx, row in enumerate(cur):
        update_line(idx, n, row["nom"] + " " + row["prenom"])
        activity = MAP_IDACTIVITY[row["activity"]]
        guest = None
        if row["est_invite"]:
            for g in map_idguests[row["id"]]:
                if g.activity.pk == activity.pk:
                    guest = g
                    break
            if not guest:
                raise IntegrityError("Guest was not found: " + str(row))
        obj_dict = {
            "activity": activity,
            "time": row["heure_entree"],
            "note": guest.inviter if guest else MAP_IDBDE[row["idbde"]],
            "guest": guest,
        }
        try:
            with transaction.atomic():
                Entry.objects.get_or_create(**obj_dict)
        except IntegrityError as e:
            raise e


ynerant's avatar
ynerant committed

ynerant's avatar
ynerant committed
@transaction.atomic
def import_remittances(cur):
    cur.execute("SELECT * FROM remises ORDER by id")
    map_idremittance = {}
    n = cur.rowcount
    check_type = RemittanceType.objects.get(note__name="Chèque")
    for idx, row in enumerate(cur):
        update_line(idx, n, row["date"])
        obj_dict = {
            "date": row["date"][10:],
            "remittance_type": check_type,
            "comment": row["commentaire"],
            "closed": row["close"],
        }
        try:
            with transaction.atomic():
                remittance = Remittance.objects.get_or_create(**obj_dict)
                map_idremittance[row["id"]] = remittance
        except IntegrityError as e:
            raise e

    print("remittances are imported")
    print("imported checks")

    cur.execute("SELECT * FROM cheques ORDER by id")
    n = cur.rowcount
    for idx, row in enumerate(cur):
        update_line(idx, n, row["date"])
        obj_dict = {
            "date": row["date"][10:],
            "remittance_type": check_type,
            "comment": row["commentaire"],
            "closed": row["close"],
        }
        tr = MAP_IDSPECIALTRANSACTION[row["idtransaction"]]
        proxy = SpecialTransactionProxy.objects.get_or_create(transaction=tr)
        proxy.remittance = map_idremittance[row["idremise"]]
        try:
            with transaction.atomic():
                proxy.save()
        except IntegrityError as e:
            raise e


Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
class Command(BaseCommand):
    """
    Command for importing the database of NK15.
    Need to be run by a user with a registered role in postgres for the database nk15. 
    """

    def print_success(self, to_print):
        return self.stdout.write(self.style.SUCCESS(to_print))

    def add_arguments(self, parser):
        parser.add_argument('-c', '--comptes', action='store_true', help="import accounts")
        parser.add_argument('-b', '--boutons', action='store_true', help="import boutons")
        parser.add_argument('-t', '--transactions', action='store_true', help="import transaction")
        parser.add_argument('-al', '--aliases', action='store_true', help="import aliases")
        parser.add_argument('-ac', '--activities', action='store_true', help="import activities")
ynerant's avatar
ynerant committed
        parser.add_argument('-M', '--memberships', action='store_true', help="import memberships")
ynerant's avatar
ynerant committed
        parser.add_argument('-r', '--remittances', action='store_true', help="import check remittances")
        parser.add_argument('-s', '--save', action='store', help="save mapping of idbde")
        parser.add_argument('-m', '--map', action='store', help="import mapping of idbde")
        parser.add_argument('-d', '--nk15db', action='store', default='nk15', help='NK15 database name')
        parser.add_argument('-u', '--nk15user', action='store', default='nk15_user', help='NK15 database owner')
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
    def handle(self, *args, **kwargs):
        nk15db, nk15user = kwargs['nk15db'], kwargs['nk15user']
        # connecting to nk15 database
        conn = pg.connect(database=nk15db, user=nk15user)
        cur = conn.cursor(cursor_factory=pge.DictCursor)
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed

        if kwargs["comptes"]:
            # reset database.
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            call_command("migrate")
            call_command("loaddata", "initial")
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            self.print_success("reset  nk20 database\n")
            self.print_success("comptes table imported")
        elif kwargs["map"]:
            filename = kwargs["map"]
            with open(filename, 'r') as fp:
                MAP_IDBDE = json.load(fp)
                MAP_IDBDE = {int(k): int(v) for k, v in MAP_IDBDE.items()}
        if kwargs["save"]:
            filename = kwargs["save"]
            with open(filename, 'w') as fp:
                json.dump(MAP_IDBDE, fp, sort_keys=True, indent=2)

        # /!\ need a prober MAP_IDBDE
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        if kwargs["boutons"]:
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            self.print_success("boutons table imported\n")
        if kwargs["activities"]:
            import_activities(cur)
            self.print_success("activities imported\n")
ynerant's avatar
ynerant committed
            import_activity_entries(cur)
            self.print_success("activity entries imported\n")
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
        if kwargs["aliases"]:
Pierre-antoine Comby's avatar
Pierre-antoine Comby committed
            self.print_success("aliases imported\n")
        if kwargs["transactions"]:
            import_transaction(cur)
            self.print_success("transaction imported\n")
ynerant's avatar
ynerant committed
        if kwargs["remittances"]:
            import_remittances(cur)
            self.print_success("remittances imported\n")