Skip to content

Référence technique de l'API

This page contains technical documentation generated automatically from the project's docstrings.

Model

The logical core and entities of the application.

Main entities

Account Management Module. Handles controller to database link for accounts.

Compte

Represents a bank account.

Source code in src/gestion_bancaire/Modele/compte.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Compte:
    """
    Represents a bank account.
    """

    def __init__(
        self,
        account_id: int | None,
        type_compte: TypeCompte,
        id_client: int,
        initial_amount: int = 0,
    ) -> None:
        self._type_compte = type_compte
        self._id_client = id_client
        self._id = account_id
        # On ne crée en base QUE si l'id n'existe pas encore
        if self._id is None:
            # Note : Assurez-vous que SQLCompte.creer accepte un montant initial
            new = SQLCompte.creer(self._type_compte, self._id_client, initial_amount)
            self._id = new.id

    def get_id(self):
        """Get the the id"""
        return self._id

    def get_type_compte(self):
        """Get the account type"""
        return self._type_compte

    @property
    def solde(self) -> int:
        """Calcule le solde actuel en sommant toutes les opérations."""
        total_credits, total_debits = SQLCompte.get_credits_and_debits(self._id)  # type: ignore
        return total_credits - total_debits

    @classmethod
    def load(cls, account_id):
        """
        Load an account based on his id
        """
        account = SQLCompte.get(account_id)
        if not account:
            logger.error("Account not found")
            return None
        loaded_account = cls(
            account_id=account.id,  # type: ignore
            type_compte=account.type_compte,  # type: ignore
            id_client=account.id_client,  # type: ignore
        )
        return loaded_account

    def __repr__(self):
        return f"<Compte(id={self._id}, type={self._type_compte.name})>"  # noqa: E501

solde property

Calcule le solde actuel en sommant toutes les opérations.

get_id()

Get the the id

Source code in src/gestion_bancaire/Modele/compte.py
35
36
37
def get_id(self):
    """Get the the id"""
    return self._id

get_type_compte()

Get the account type

Source code in src/gestion_bancaire/Modele/compte.py
39
40
41
def get_type_compte(self):
    """Get the account type"""
    return self._type_compte

load(account_id) classmethod

Load an account based on his id

Source code in src/gestion_bancaire/Modele/compte.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@classmethod
def load(cls, account_id):
    """
    Load an account based on his id
    """
    account = SQLCompte.get(account_id)
    if not account:
        logger.error("Account not found")
        return None
    loaded_account = cls(
        account_id=account.id,  # type: ignore
        type_compte=account.type_compte,  # type: ignore
        id_client=account.id_client,  # type: ignore
    )
    return loaded_account

Customer

Source code in src/gestion_bancaire/Modele/customer.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
class Customer:
    def __init__(
        self,
        personal_info: CustomerPersonalInfo,
        contact_info: CustomerContactInfo,
        card_info: CustomerCardInfo,
        address: str,
        customer_id: Optional[int],
    ):
        self.customer_id = customer_id
        self.personal_info = personal_info
        self.contact_info = contact_info
        self.card_info = card_info
        self.address = address

    def save(self, session):
        """
        Crée un nouveau Customer si l'ID est absent, ou met à jour l'existante
        """
        from Modele.SQL.sql_customer import Customer as CustomerSQL

        # Import local pour éviter une erreur ImportError ou AttributeError
        # car classe Customer_SQL importe aussi Customer

        customer_storage_model = CustomerSQL(
            customer_id=self.customer_id,
            first_name=self.personal_info.first_name,
            last_name=self.personal_info.last_name,
            phone=self.contact_info.phone,
            email=self.contact_info.email,
            card_number=self.card_info.card_number,
            address=self.address,
        )

        if self.customer_id is None:
            action_name = "créé"
        else:
            action_name = "mis à jour"

        try:
            customer_storage_model = session.merge(customer_storage_model)
            session.commit()

            self.__synchro_id_from_storage__(customer_storage_model)
            print(
                f"Le client {self.personal_info.first_name} a été {action_name} avec succès (ID: {self.customer_id})."
            )

        except SQLAlchemyError as database_error:
            session.rollback()
            raise RuntimeError(
                f"Échec de la sauvegarde SQL : {database_error}"
            ) from database_error

    def __synchro_id_from_storage__(self, customer_storage_model: CustomerSQL):
        """Met à jour l'ID de l'objet Customer avec celui généré par la BDD"""
        self.customer_id = customer_storage_model.customer_id

    @classmethod
    def obtain(cls, session, customer_id_to_obtain: int) -> Customer | None:
        """
        Récupère un client dans la bdd et reconstruit l'objet Customer
        """
        from Modele.SQL.sql_customer import Customer as CustomerSQL

        try:
            customer_storage_model: CustomerSQL | None = (
                session.query(CustomerSQL)
                .filter_by(customer_id=customer_id_to_obtain)
                .first()
            )
        except SQLAlchemyError as database_error:
            raise RuntimeError(
                f"Erreur lors de la récupération du client {customer_id_to_obtain} : {database_error}"
            ) from database_error

        if customer_storage_model:
            personal: CustomerPersonalInfo = CustomerPersonalInfo(
                first_name=customer_storage_model.first_name,
                last_name=customer_storage_model.last_name,
            )
            contact: CustomerContactInfo = CustomerContactInfo(
                phone=customer_storage_model.phone, email=customer_storage_model.email
            )
            card: CustomerCardInfo = CustomerCardInfo(
                card_number=customer_storage_model.card_number,
            )
            return cls(
                personal_info=personal,
                contact_info=contact,
                card_info=card,
                address=customer_storage_model.address,
                customer_id=customer_storage_model.customer_id,
            )
        return None

    def remove(self, session):
        """
        Supprime le client actuel de la base de données
        """
        from Modele.SQL.sql_customer import Customer as CustomerSQL

        if not self.customer_id:
            print(
                "Action impossible : Ce client n'a pas d'ID (il n'est pas enregistré dans la base)."
            )
            return

        try:
            customer_storage_model: CustomerSQL | None = (
                session.query(CustomerSQL)
                .filter_by(customer_id=self.customer_id)
                .first()
            )
        except SQLAlchemyError as search_error:
            raise RuntimeError(
                f"Erreur lors de la suppression du client {self.customer_id}"
            ) from search_error

        if not customer_storage_model:
            print(f"Le client {self.customer_id} n'existe pas en base.")
            return

        try:
            session.delete(customer_storage_model)
            session.commit()
        except SQLAlchemyError as delete_error:
            session.rollback()
            raise RuntimeError(
                f"Erreur lors de la suppression : {delete_error}"
            ) from delete_error

        print(
            f"Le client {self.personal_info.first_name} a été supprimé avec succès (ID: {self.customer_id})."
        )
        self.reset_id()

    def reset_id(self):
        """
        Détache l'objet Customer de son id en base de données
        Utile après une suppression ou pour cloner un client
        """
        self.customer_id = None

__synchro_id_from_storage__(customer_storage_model)

Met à jour l'ID de l'objet Customer avec celui généré par la BDD

Source code in src/gestion_bancaire/Modele/customer.py
85
86
87
def __synchro_id_from_storage__(self, customer_storage_model: CustomerSQL):
    """Met à jour l'ID de l'objet Customer avec celui généré par la BDD"""
    self.customer_id = customer_storage_model.customer_id

obtain(session, customer_id_to_obtain) classmethod

Récupère un client dans la bdd et reconstruit l'objet Customer

Source code in src/gestion_bancaire/Modele/customer.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
@classmethod
def obtain(cls, session, customer_id_to_obtain: int) -> Customer | None:
    """
    Récupère un client dans la bdd et reconstruit l'objet Customer
    """
    from Modele.SQL.sql_customer import Customer as CustomerSQL

    try:
        customer_storage_model: CustomerSQL | None = (
            session.query(CustomerSQL)
            .filter_by(customer_id=customer_id_to_obtain)
            .first()
        )
    except SQLAlchemyError as database_error:
        raise RuntimeError(
            f"Erreur lors de la récupération du client {customer_id_to_obtain} : {database_error}"
        ) from database_error

    if customer_storage_model:
        personal: CustomerPersonalInfo = CustomerPersonalInfo(
            first_name=customer_storage_model.first_name,
            last_name=customer_storage_model.last_name,
        )
        contact: CustomerContactInfo = CustomerContactInfo(
            phone=customer_storage_model.phone, email=customer_storage_model.email
        )
        card: CustomerCardInfo = CustomerCardInfo(
            card_number=customer_storage_model.card_number,
        )
        return cls(
            personal_info=personal,
            contact_info=contact,
            card_info=card,
            address=customer_storage_model.address,
            customer_id=customer_storage_model.customer_id,
        )
    return None

remove(session)

Supprime le client actuel de la base de données

Source code in src/gestion_bancaire/Modele/customer.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def remove(self, session):
    """
    Supprime le client actuel de la base de données
    """
    from Modele.SQL.sql_customer import Customer as CustomerSQL

    if not self.customer_id:
        print(
            "Action impossible : Ce client n'a pas d'ID (il n'est pas enregistré dans la base)."
        )
        return

    try:
        customer_storage_model: CustomerSQL | None = (
            session.query(CustomerSQL)
            .filter_by(customer_id=self.customer_id)
            .first()
        )
    except SQLAlchemyError as search_error:
        raise RuntimeError(
            f"Erreur lors de la suppression du client {self.customer_id}"
        ) from search_error

    if not customer_storage_model:
        print(f"Le client {self.customer_id} n'existe pas en base.")
        return

    try:
        session.delete(customer_storage_model)
        session.commit()
    except SQLAlchemyError as delete_error:
        session.rollback()
        raise RuntimeError(
            f"Erreur lors de la suppression : {delete_error}"
        ) from delete_error

    print(
        f"Le client {self.personal_info.first_name} a été supprimé avec succès (ID: {self.customer_id})."
    )
    self.reset_id()

reset_id()

Détache l'objet Customer de son id en base de données Utile après une suppression ou pour cloner un client

Source code in src/gestion_bancaire/Modele/customer.py
168
169
170
171
172
173
def reset_id(self):
    """
    Détache l'objet Customer de son id en base de données
    Utile après une suppression ou pour cloner un client
    """
    self.customer_id = None

save(session)

Crée un nouveau Customer si l'ID est absent, ou met à jour l'existante

Source code in src/gestion_bancaire/Modele/customer.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def save(self, session):
    """
    Crée un nouveau Customer si l'ID est absent, ou met à jour l'existante
    """
    from Modele.SQL.sql_customer import Customer as CustomerSQL

    # Import local pour éviter une erreur ImportError ou AttributeError
    # car classe Customer_SQL importe aussi Customer

    customer_storage_model = CustomerSQL(
        customer_id=self.customer_id,
        first_name=self.personal_info.first_name,
        last_name=self.personal_info.last_name,
        phone=self.contact_info.phone,
        email=self.contact_info.email,
        card_number=self.card_info.card_number,
        address=self.address,
    )

    if self.customer_id is None:
        action_name = "créé"
    else:
        action_name = "mis à jour"

    try:
        customer_storage_model = session.merge(customer_storage_model)
        session.commit()

        self.__synchro_id_from_storage__(customer_storage_model)
        print(
            f"Le client {self.personal_info.first_name} a été {action_name} avec succès (ID: {self.customer_id})."
        )

    except SQLAlchemyError as database_error:
        session.rollback()
        raise RuntimeError(
            f"Échec de la sauvegarde SQL : {database_error}"
        ) from database_error

Operations Management Module. Defines the data model for transactions between accounts.

Operation

Represent operations

Source code in src/gestion_bancaire/Modele/operation.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class Operation:
    """
    Represent operations
    """

    def __init__(
        self, id_source_account: int, id_target_account: int, amount: int
    ) -> None:
        self.id_source_account = id_source_account
        self.id_target_account = id_target_account
        self.amount = amount
        self.date_operation = datetime.now()
        self._id = 0

    def execute(self) -> None:
        """
        Execute the operation
        """
        operation = SQLOperation.execute_transfer(
            self.id_source_account, self.id_target_account, self.amount
        )
        if operation is not None:
            self._id = operation.get_id()
            self.date_operation = operation.date_operation  # sync the 2 objects
        else:
            logger.error("The transfert wasn't committed, please retry")
            raise OperationException

    def get_id(self):
        """
        return the id of the operation
        """
        return self._id

    def set_id(self, new_id):
        """
        Set a new id to the operation (for test only)
        """
        self._id = new_id

    @classmethod
    def get_account_history(cls, account_id: int) -> list["Operation"]:
        """Gives all the transactions associated with an account"""
        sql_ops = SQLOperation.get_by_account(account_id)
        history = []
        for sql_op in sql_ops:
            op = cls(
                id_source_account=sql_op.id_compte_source,  # type: ignore
                id_target_account=sql_op.id_compte_cible,  # type: ignore
                amount=sql_op.montant,  # type: ignore
            )
            op._id = sql_op.id
            op.date_operation = sql_op.date_operation
            history.append(op)
        return history

    def __repr__(self):
        return f"""<Operation(id={self._id},
                from={self.id_source_account}
                to={self.id_target_account},
                amount={self.amount}€)>"""  # noqa : E501

execute()

Execute the operation

Source code in src/gestion_bancaire/Modele/operation.py
34
35
36
37
38
39
40
41
42
43
44
45
46
def execute(self) -> None:
    """
    Execute the operation
    """
    operation = SQLOperation.execute_transfer(
        self.id_source_account, self.id_target_account, self.amount
    )
    if operation is not None:
        self._id = operation.get_id()
        self.date_operation = operation.date_operation  # sync the 2 objects
    else:
        logger.error("The transfert wasn't committed, please retry")
        raise OperationException

get_account_history(account_id) classmethod

Gives all the transactions associated with an account

Source code in src/gestion_bancaire/Modele/operation.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@classmethod
def get_account_history(cls, account_id: int) -> list["Operation"]:
    """Gives all the transactions associated with an account"""
    sql_ops = SQLOperation.get_by_account(account_id)
    history = []
    for sql_op in sql_ops:
        op = cls(
            id_source_account=sql_op.id_compte_source,  # type: ignore
            id_target_account=sql_op.id_compte_cible,  # type: ignore
            amount=sql_op.montant,  # type: ignore
        )
        op._id = sql_op.id
        op.date_operation = sql_op.date_operation
        history.append(op)
    return history

get_id()

return the id of the operation

Source code in src/gestion_bancaire/Modele/operation.py
48
49
50
51
52
def get_id(self):
    """
    return the id of the operation
    """
    return self._id

set_id(new_id)

Set a new id to the operation (for test only)

Source code in src/gestion_bancaire/Modele/operation.py
54
55
56
57
58
def set_id(self, new_id):
    """
    Set a new id to the operation (for test only)
    """
    self._id = new_id

OperationException

Bases: Exception

Operation Exception class to handle error specific to this module

Source code in src/gestion_bancaire/Modele/operation.py
83
84
85
86
87
88
89
class OperationException(Exception):
    """
    Operation Exception class to handle error specific to this module
    """

    def __repr__(self) -> str:
        return "The operation couldn't be committed to the database please retry"

Data Access (SQL)

These modules manage persistence with the database.

Create the engine and the session of sqlalchemy for communicating with the database. Create the database if not present.

Database Initialization Module. Provides functions to set up the database schema and initial state.

initialiser_bdd()

Drops all existing tables and recreates the database schema from scratch.

Source code in src/gestion_bancaire/Modele/SQL/db_setup.py
14
15
16
17
18
19
20
21
def initialiser_bdd() -> None:
    """
    Drops all existing tables and recreates the database schema from scratch.
    """
    Base.metadata.drop_all(bind=engine)
    logger.debug("Database emptied")
    Base.metadata.create_all(bind=engine)
    logger.info("Database Initialized")

initialiser_coffre_fort()

Initializes the bank's master account (ID 0) if it does not already exist.

Source code in src/gestion_bancaire/Modele/SQL/db_setup.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def initialiser_coffre_fort() -> None:
    """
    Initializes the bank's master account (ID 0) if it does not already exist.
    """
    with SESSIONLOCAL() as session:
        coffre = session.query(SQLCompte).filter_by(id=0).first()

        if not coffre:
            # Create the master account (The Safe) with ID 0
            coffre = SQLCompte(id=0, type_compte=TypeCompte.COURANT, id_client=0)
            session.add(coffre)
            try:
                session.commit()
                logger.info("The safe was initialized")
            except SQLAlchemyError as e:
                session.rollback()
                logger.error(
                    "Failed to initialize the safe. "
                    "ID 0 may be reserved or there is a constraint conflict: %s",
                    e,
                )

SQL Account Management Module. Handles database operations for bank accounts.

SQLCompte

Bases: Base

Represents a bank account in the database.

Source code in src/gestion_bancaire/Modele/SQL/sql_comptes.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
class SQLCompte(Base):
    """
    Represents a bank account in the database.
    """

    __tablename__ = "comptes"

    id = Column(Integer, primary_key=True, autoincrement=True)
    type_compte = Column(Enum(TypeCompte), default=TypeCompte.COURANT, nullable=False)
    id_client = Column(Integer, ForeignKey("customers.customer_id"), nullable=False)

    @classmethod
    def get_credits_and_debits(cls, account_id: int) -> tuple[int, int]:
        """
        Calculates the total amount of credits and debits for a specific account.
        """
        from Modele.SQL.sql_operations import SQLOperation

        with SESSIONLOCAL() as session:
            credit_ops = (
                session.query(SQLOperation).filter_by(id_compte_cible=account_id).all()
            )
            total_credits = sum(op.montant for op in credit_ops)

            debit_ops = (
                session.query(SQLOperation).filter_by(id_compte_source=account_id).all()
            )
            total_debits = sum(op.montant for op in debit_ops)

            return total_credits, total_debits  # type: ignore

    @classmethod
    def creer(cls, type_enum, id_client, initial_amount: int = 0):
        """
        Creates the account and, if an initial balance is provided,
        generates an initial deposit transaction.
        """
        with SESSIONLOCAL() as session:
            nouveau = cls(type_compte=type_enum, id_client=id_client)
            session.add(nouveau)
            session.commit()
            session.refresh(nouveau)

            if initial_amount != 0:
                # pylint: disable=import-outside-toplevel
                from Modele.operation import Operation

                op_initiale = Operation(
                    id_source_account=0,  # Bank internal account
                    id_target_account=nouveau.id,  # type: ignore
                    amount=initial_amount,
                )
                Operation.execute(op_initiale)

            return nouveau

    @classmethod
    def get(cls, compte_id):
        """
        Retrieves an account by its unique identifier.
        """
        with SESSIONLOCAL() as session:
            return session.query(cls).filter_by(id=compte_id).first()

    def sauvegarder(self):
        """
        Updates the account record in the database.
        """
        with SESSIONLOCAL() as session:
            session.merge(self)
            session.commit()
            logger.debug("Account %s updated", self.id)

    def supprimer(self):
        """
        Supprime le compte et toutes ses opérations associées de la base de données.
        """
        from Modele.SQL.sql_operations import SQLOperation

        with SESSIONLOCAL() as session:
            session.query(SQLOperation).filter(
                (SQLOperation.id_compte_source == self.id)
                | (SQLOperation.id_compte_cible == self.id)
            ).delete(synchronize_session=False)

            compte_a_supprimer = session.query(SQLCompte).get(self.id)

            if compte_a_supprimer:
                session.delete(compte_a_supprimer)
                session.commit()
                logger.debug(f"Account {self.id} and its history deleted.")
            else:
                logger.warning(f"Account {self.id} not found during deletion.")

    def __repr__(self):
        return f"<Compte(id={self.id}, type={self.type_compte.name})>"

creer(type_enum, id_client, initial_amount=0) classmethod

Creates the account and, if an initial balance is provided, generates an initial deposit transaction.

Source code in src/gestion_bancaire/Modele/SQL/sql_comptes.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@classmethod
def creer(cls, type_enum, id_client, initial_amount: int = 0):
    """
    Creates the account and, if an initial balance is provided,
    generates an initial deposit transaction.
    """
    with SESSIONLOCAL() as session:
        nouveau = cls(type_compte=type_enum, id_client=id_client)
        session.add(nouveau)
        session.commit()
        session.refresh(nouveau)

        if initial_amount != 0:
            # pylint: disable=import-outside-toplevel
            from Modele.operation import Operation

            op_initiale = Operation(
                id_source_account=0,  # Bank internal account
                id_target_account=nouveau.id,  # type: ignore
                amount=initial_amount,
            )
            Operation.execute(op_initiale)

        return nouveau

get(compte_id) classmethod

Retrieves an account by its unique identifier.

Source code in src/gestion_bancaire/Modele/SQL/sql_comptes.py
72
73
74
75
76
77
78
@classmethod
def get(cls, compte_id):
    """
    Retrieves an account by its unique identifier.
    """
    with SESSIONLOCAL() as session:
        return session.query(cls).filter_by(id=compte_id).first()

get_credits_and_debits(account_id) classmethod

Calculates the total amount of credits and debits for a specific account.

Source code in src/gestion_bancaire/Modele/SQL/sql_comptes.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@classmethod
def get_credits_and_debits(cls, account_id: int) -> tuple[int, int]:
    """
    Calculates the total amount of credits and debits for a specific account.
    """
    from Modele.SQL.sql_operations import SQLOperation

    with SESSIONLOCAL() as session:
        credit_ops = (
            session.query(SQLOperation).filter_by(id_compte_cible=account_id).all()
        )
        total_credits = sum(op.montant for op in credit_ops)

        debit_ops = (
            session.query(SQLOperation).filter_by(id_compte_source=account_id).all()
        )
        total_debits = sum(op.montant for op in debit_ops)

        return total_credits, total_debits  # type: ignore

sauvegarder()

Updates the account record in the database.

Source code in src/gestion_bancaire/Modele/SQL/sql_comptes.py
80
81
82
83
84
85
86
87
def sauvegarder(self):
    """
    Updates the account record in the database.
    """
    with SESSIONLOCAL() as session:
        session.merge(self)
        session.commit()
        logger.debug("Account %s updated", self.id)

supprimer()

Supprime le compte et toutes ses opérations associées de la base de données.

Source code in src/gestion_bancaire/Modele/SQL/sql_comptes.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def supprimer(self):
    """
    Supprime le compte et toutes ses opérations associées de la base de données.
    """
    from Modele.SQL.sql_operations import SQLOperation

    with SESSIONLOCAL() as session:
        session.query(SQLOperation).filter(
            (SQLOperation.id_compte_source == self.id)
            | (SQLOperation.id_compte_cible == self.id)
        ).delete(synchronize_session=False)

        compte_a_supprimer = session.query(SQLCompte).get(self.id)

        if compte_a_supprimer:
            session.delete(compte_a_supprimer)
            session.commit()
            logger.debug(f"Account {self.id} and its history deleted.")
        else:
            logger.warning(f"Account {self.id} not found during deletion.")

Customer

Bases: Base

Source code in src/gestion_bancaire/Modele/SQL/sql_customer.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Customer(Base):
    __tablename__ = "customers"

    customer_id = Column(Integer, primary_key=True, autoincrement=True)
    first_name = Column(String(50), nullable=False)
    last_name = Column(String(50), nullable=False)
    phone = Column(String(12), nullable=False)
    email = Column(String(50), nullable=False)
    card_number = Column(String(16), nullable=False)
    address = Column(String(200))

    def to_domain(self) -> "CustomerStorageModel":
        """
        Converts SQL parameters into a Customer object that can be used by the application.
        That is, divide into personal_info, contact_info, card_info, etc.
        """
        from Modele.customer import (
            Customer,
            CustomerCardInfo,
            CustomerContactInfo,
            CustomerPersonalInfo,
        )

        personal = CustomerPersonalInfo(self.first_name, self.last_name)
        contact = CustomerContactInfo(self.phone, self.email)
        card = CustomerCardInfo(self.card_number)

        return Customer(
            personal_info=personal,
            contact_info=contact,
            card_info=card,
            address=self.address,
            customer_id=self.customer_id,
        )

to_domain()

Converts SQL parameters into a Customer object that can be used by the application. That is, divide into personal_info, contact_info, card_info, etc.

Source code in src/gestion_bancaire/Modele/SQL/sql_customer.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def to_domain(self) -> "CustomerStorageModel":
    """
    Converts SQL parameters into a Customer object that can be used by the application.
    That is, divide into personal_info, contact_info, card_info, etc.
    """
    from Modele.customer import (
        Customer,
        CustomerCardInfo,
        CustomerContactInfo,
        CustomerPersonalInfo,
    )

    personal = CustomerPersonalInfo(self.first_name, self.last_name)
    contact = CustomerContactInfo(self.phone, self.email)
    card = CustomerCardInfo(self.card_number)

    return Customer(
        personal_info=personal,
        contact_info=contact,
        card_info=card,
        address=self.address,
        customer_id=self.customer_id,
    )

SQL Operations Management Module. Defines the data model for transactions between accounts.

SQLOperation

Bases: Base

Represents a banking operation between two accounts in the database.

Source code in src/gestion_bancaire/Modele/SQL/sql_operations.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class SQLOperation(Base):
    """
    Represents a banking operation between two accounts in the database.
    """

    __tablename__ = "operations"
    id = Column(Integer, primary_key=True, autoincrement=True)

    id_compte_source = Column(Integer, ForeignKey("comptes.id"), nullable=False)
    id_compte_cible = Column(Integer, ForeignKey("comptes.id"), nullable=False)
    montant = Column(Float, nullable=False)
    date_operation = Column(DateTime, default=datetime.now)

    def get_id(self):
        """Returns the operation unique identifier."""
        return self.id

    @classmethod
    def execute_transfer(cls, source_id, target_id, amount):
        """
        Creates a transaction in the database and executes it.
        """
        with SESSIONLOCAL() as session:
            # pylint: disable=import-outside-toplevel
            from Modele.SQL.sql_comptes import SQLCompte

            source = session.query(SQLCompte).get(source_id)
            cible = session.query(SQLCompte).get(target_id)

            if not source or not cible:
                logger.error("Transfer error: One of the accounts does not exist.")
                return None

            nouvelle_operation = cls(
                id_compte_source=source_id, id_compte_cible=target_id, montant=amount
            )

            session.add(nouvelle_operation)
            session.commit()

            logger.debug(
                "Transfer of %s€ successful from %s to %s.",
                amount,
                source_id,
                target_id,
            )
            return nouvelle_operation

    @classmethod
    def get_by_account(cls, account_id: int):
        """
        Retrieves all transactions associated with a specific account.
        """
        with SESSIONLOCAL() as session:
            return (
                session.query(cls)
                .filter(
                    (cls.id_compte_source == account_id)
                    | (cls.id_compte_cible == account_id)
                )
                .order_by(cls.date_operation.desc())
                .all()
            )

    def __repr__(self):
        return (
            f"<Operation(id={self.id}, from={self.id_compte_source} "
            f"to={self.id_compte_cible}, amount={self.montant}€)>"
        )

execute_transfer(source_id, target_id, amount) classmethod

Creates a transaction in the database and executes it.

Source code in src/gestion_bancaire/Modele/SQL/sql_operations.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@classmethod
def execute_transfer(cls, source_id, target_id, amount):
    """
    Creates a transaction in the database and executes it.
    """
    with SESSIONLOCAL() as session:
        # pylint: disable=import-outside-toplevel
        from Modele.SQL.sql_comptes import SQLCompte

        source = session.query(SQLCompte).get(source_id)
        cible = session.query(SQLCompte).get(target_id)

        if not source or not cible:
            logger.error("Transfer error: One of the accounts does not exist.")
            return None

        nouvelle_operation = cls(
            id_compte_source=source_id, id_compte_cible=target_id, montant=amount
        )

        session.add(nouvelle_operation)
        session.commit()

        logger.debug(
            "Transfer of %s€ successful from %s to %s.",
            amount,
            source_id,
            target_id,
        )
        return nouvelle_operation

get_by_account(account_id) classmethod

Retrieves all transactions associated with a specific account.

Source code in src/gestion_bancaire/Modele/SQL/sql_operations.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@classmethod
def get_by_account(cls, account_id: int):
    """
    Retrieves all transactions associated with a specific account.
    """
    with SESSIONLOCAL() as session:
        return (
            session.query(cls)
            .filter(
                (cls.id_compte_source == account_id)
                | (cls.id_compte_cible == account_id)
            )
            .order_by(cls.date_operation.desc())
            .all()
        )

get_id()

Returns the operation unique identifier.

Source code in src/gestion_bancaire/Modele/SQL/sql_operations.py
29
30
31
def get_id(self):
    """Returns the operation unique identifier."""
    return self.id

Controller

The link between the Model and the View.

Controller

Source code in src/gestion_bancaire/Controleur/controleur.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
class Controller:
    # --- UTILITAIRES DE CONVERSION ---
    def _euros_vers_centimes(self, montant_euros: float) -> int:
        """Convertit 10.50€ en 1050 cts"""
        return int(round(float(montant_euros) * 100))

    # --- LECTURE ---
    def get_client_details(self, client_id):
        with SessionLocal() as session:
            client_obj = Customer.obtain(session, client_id)
            if not client_obj:
                return None
            details = {
                "nom": client_obj.personal_info.last_name,
                "prenom": client_obj.personal_info.first_name,
                "telephone": client_obj.contact_info.phone,
                "email": client_obj.contact_info.email,
                "adresse": client_obj.address,
            }
            return details

    def get_tous_les_clients(self):
        with SessionLocal() as session:
            clients_sql = session.query(CustomerSQL).all()
            liste_affichage = []
            for c in clients_sql:
                liste_affichage.append(
                    {
                        "id": c.customer_id,
                        "nom": f"{c.last_name.upper()} {c.first_name.capitalize()}",
                    }
                )
            return liste_affichage

    def get_comptes_client(self, client_id) -> list:
        with SessionLocal() as session:
            try:
                comptes_sql = (
                    session.query(SQLCompte).filter_by(id_client=client_id).all()
                )
            except Exception as e:
                print(f"Erreur SQL : {e}")
                return []

            data_comptes = []
            for c_sql in comptes_sql:
                compte_metier = Compte.load(c_sql.id)

                if compte_metier:
                    valeur_solde = compte_metier.solde
                    nom_type = compte_metier.get_type_compte().name

                    if client_id == 0:
                        solde_str = "∞"
                    else:
                        solde_str = f"{(valeur_solde / 100):.2f} €"
                    # --------------------------------------------

                    data_comptes.append(
                        {
                            "id": compte_metier.get_id(),
                            "type": nom_type,
                            "solde": solde_str,
                        }
                    )
            return data_comptes

    # --- ECRITURE ---

    def creer_nouveau_client(self, nom, prenom, email, telephone, adresse):
        if not nom or not prenom:
            return False, "Le nom et le prénom sont obligatoires."

        faux_numero_carte = (
            f"FR76{''.join([str(random.randint(0, 9)) for _ in range(12)])}"
        )

        infos_perso = CustomerPersonalInfo(first_name=prenom, last_name=nom)
        infos_contact = CustomerContactInfo(phone=telephone, email=email)
        infos_carte = CustomerCardInfo(card_number=faux_numero_carte)

        nouveau_client = Customer(
            personal_info=infos_perso,
            contact_info=infos_contact,
            card_info=infos_carte,
            address=adresse,
            customer_id=None,
        )

        with SessionLocal() as session:
            try:
                nouveau_client.save(session)
                return (
                    True,
                    f"Client créé avec succès (ID: {nouveau_client.customer_id})",
                )
            except Exception as e:
                return False, f"Erreur : {e}"

    def mettre_a_jour_client(self, client_id, nom, prenom, email, telephone, adresse):
        try:
            with SessionLocal() as session:
                client = (
                    session.query(CustomerSQL).filter_by(customer_id=client_id).first()
                )
                if not client:
                    return False, "Client introuvable."

                client.first_name = prenom
                client.last_name = nom
                client.email = email
                client.phone = telephone
                client.address = adresse

                session.commit()
                return True, "Mise à jour réussie."
        except Exception as e:
            return False, f"Erreur SQL : {e}"

    def ajouter_compte_client(self, client_id, type_compte_str, solde_initial_euros):
        try:
            type_enum = TypeCompte[type_compte_str]
        except KeyError:
            return False, f"Type de compte invalide : {type_compte_str}"

        try:
            # CONVERSION : Euros (Input) -> Centimes (BDD)
            solde_initial_centimes = self._euros_vers_centimes(solde_initial_euros)

            Compte(
                account_id=None,
                type_compte=type_enum,
                id_client=client_id,
                initial_amount=solde_initial_centimes,  # On passe des centimes
            )
            return True, "Compte créé avec succès."

        except Exception as e:
            return False, f"Erreur lors de la création : {e}"

    def effectuer_depot(self, compte_id, montant_euros):
        try:
            # CONVERSION
            montant_centimes = self._euros_vers_centimes(montant_euros)
            if montant_centimes <= 0:
                return False, "Le montant doit être positif."

            compte = Compte.load(compte_id)
            if not compte:
                return False, "Compte introuvable"

            op = Operation(0, compte_id, montant_centimes)
            op.execute()

            solde_act = Compte.load(compte_id).solde
            return True, f"Dépôt effectué. Nouveau solde : {solde_act / 100:.2f} €"
        except Exception as e:
            return False, str(e)

    def effectuer_retrait(self, compte_id, montant_euros):
        try:
            # CONVERSION
            montant_centimes = self._euros_vers_centimes(montant_euros)
            if montant_centimes <= 0:
                return False, "Le montant doit être positif."

            compte = Compte.load(compte_id)
            if not compte:
                return False, "Compte introuvable"

            solde_init = compte.solde  # Déjà en centimes

            if (solde_init - montant_centimes) >= -DECOUVERT_MAX:
                # Compte vers 0 (Banque/Cash)
                op = Operation(compte_id, 0, montant_centimes)
                op.execute()

                solde_act = Compte.load(compte_id).solde
                return (
                    True,
                    f"Retrait effectué. Nouveau solde : {solde_act / 100:.2f} €",
                )
            else:
                return False, "Solde insuffisant"
        except Exception as e:
            return False, str(e)

    def effectuer_virement(self, id_source, id_cible, montant_euros):
        try:
            # CONVERSION
            montant_centimes = self._euros_vers_centimes(montant_euros)
            if montant_centimes <= 0:
                return False, "Le montant doit être positif."

            source = Compte.load(id_source)
            if not source:
                return False, "Compte source introuvable"

            solde_init = source.solde  # En centimes

            # Cas spécial Banque (ID 0)
            if id_source == 0:
                Operation(id_source, id_cible, montant_centimes).execute()
                return True, "Virement Banque effectué."

            if (solde_init - montant_centimes) >= -DECOUVERT_MAX:
                op = Operation(id_source, id_cible, montant_centimes)
                op.execute()
                return True, "Virement effectué."
            else:
                return False, "Solde insuffisant"
        except Exception as e:
            return False, f"Erreur virement: {e}"

    def cloturer_compte(self, id_compte):
        try:
            compte_metier = Compte.load(id_compte)
            if not compte_metier:
                return False, "Compte introuvable."

            if compte_metier.solde != 0:
                solde_euros = compte_metier.solde / 100
                return (
                    False,
                    f"Solde non nul ({solde_euros:.2f} €). Impossible de clôturer.",
                )

            sql_compte = SQLCompte.get(id_compte)
            if sql_compte:
                sql_compte.supprimer()
                return True, "Compte clôturé avec succès."
            return False, "Erreur technique lors de la suppression."

        except Exception as e:
            return False, f"Erreur lors de la clôture : {e}"

View

The graphical user interface.

MainWindow

Bases: QMainWindow

Source code in src/gestion_bancaire/Vue/vue_principale.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.controller = Controller()

        self.setWindowTitle("Gestion Bancaire - Finale")
        self.resize(900, 600)

        self.selected_user = None

        self.right_panel_widget = QWidget()
        # self.right_panel_widget.setStyleSheet("background-color: white;")
        self.right_panel_layout = QVBoxLayout(self.right_panel_widget)
        self.right_panel_layout.addWidget(
            QLabel("Sélectionnez un client ou une action.")
        )

        self.createToolBar()
        content_area = QWidget()
        self.setCentralWidget(content_area)
        self.content_layout = QHBoxLayout(content_area)
        self.side_client_bar = self.createSideClientBar()

        self.content_layout.addLayout(self.side_client_bar, 1)
        self.content_layout.addWidget(self.right_panel_widget, 4)

        self.reload_client_list()

    def createToolBar(self):
        """Création de la barre de menu en haut"""
        toolbar = self.addToolBar("Menu Principal")
        toolbar.setMovable(False)

        action_comptes = QAction("Comptes", self)
        action_comptes.triggered.connect(lambda: self.show_account(self.selected_user))
        toolbar.addAction(action_comptes)

        action_virements = QAction("Virements", self)
        action_virements.triggered.connect(lambda: operations.show_transfer(self))
        toolbar.addAction(action_virements)

        action_depot = QAction("Depot", self)
        action_depot.triggered.connect(lambda: operations.show_deposit(self))
        toolbar.addAction(action_depot)

        action_retrait = QAction("Retrait", self)
        action_retrait.triggered.connect(lambda: operations.show_retrait(self))
        toolbar.addAction(action_retrait)

        action_modif = QAction("Modifier infos", self)
        action_modif.triggered.connect(
            lambda: account_operations.show_modify_client_popup(self)
        )
        toolbar.addAction(action_modif)

    def createSideClientBar(self):
        """Création de la colonne de gauche (Recherche + Liste vide)"""
        sidebar_layout = QVBoxLayout()

        search_row = QHBoxLayout()
        self.barre_recherche_client = QLineEdit()
        self.barre_recherche_client.setPlaceholderText("Nom client")
        self.barre_recherche_client.textChanged.connect(self.filtrer_clients)

        self.bouton_refresh_clients = QPushButton("Refresh")
        self.bouton_refresh_clients.clicked.connect(self.reload_client_list)

        search_row.addWidget(self.barre_recherche_client)
        search_row.addWidget(self.bouton_refresh_clients)
        sidebar_layout.addLayout(search_row)

        # Liste vide
        self.client_list = QListWidget()
        self.client_list.setAlternatingRowColors(True)
        self.client_list.itemClicked.connect(self.show_account)
        sidebar_layout.addWidget(self.client_list)

        # Bouton création
        self.bouton_create_new_client = QPushButton("Créer un client")
        self.bouton_create_new_client.clicked.connect(
            lambda: account_operations.show_create_client_popup(self)
        )
        sidebar_layout.addWidget(self.bouton_create_new_client)

        return sidebar_layout

    def reload_client_list(self):
        """Récupère les clients depuis la BDD et remplit la liste"""
        self.barre_recherche_client.clear()
        self.selected_user = None
        self.client_list.clear()

        clients = self.controller.get_tous_les_clients()

        for client in clients:
            item = QListWidgetItem(client["nom"])
            # On cache l'ID SQL dans l'item
            item.setData(Qt.UserRole, client["id"])
            self.client_list.addItem(item)

    def filtrer_clients(self):
        """Filtre visuel de la liste"""
        texte_recherche = self.barre_recherche_client.text().lower()
        for i in range(self.client_list.count()):
            item = self.client_list.item(i)
            if item:
                correspondance = item.text().lower().startswith(texte_recherche)
                item.setHidden(not correspondance)

    def show_account(self, item):
        """Affiche les détails du client et ses comptes avec actions"""
        if item is None:
            return

        new_widget = QWidget()
        # new_widget.setStyleSheet("background-color: white;")
        new_layout = QVBoxLayout(new_widget)

        if hasattr(self, "right_panel_widget") and self.right_panel_widget:
            self.content_layout.replaceWidget(self.right_panel_widget, new_widget)
            self.right_panel_widget.deleteLater()

        self.right_panel_widget = new_widget
        self.right_panel_layout = new_layout

        client_id = item.data(Qt.UserRole)
        self.selected_user = item

        infos = self.controller.get_client_details(client_id)
        comptes = self.controller.get_comptes_client(client_id)

        if infos:
            self.right_panel_layout.addWidget(
                QLabel(f"<h2>Client : {infos['nom']}</h2>")
            )
            self.right_panel_layout.addWidget(
                QLabel(f"<b>Email :</b> {infos.get('email', 'N/A')}")
            )
            self.right_panel_layout.addWidget(
                QLabel(f"<b>Tél :</b> {infos.get('telephone', 'N/A')}")
            )

        self.right_panel_layout.addWidget(QLabel("<hr>"))

        header_layout = QHBoxLayout()
        header_layout.addWidget(QLabel("<h3>Comptes Bancaires :</h3>"))

        btn_add = QPushButton("+ Ouvrir un compte")
        btn_add.setStyleSheet(
            "background-color: #27ae60; color: white; font-weight: bold; padding: 5px;"
        )
        btn_add.clicked.connect(lambda: self.ouvrir_compte_dialog(client_id))
        header_layout.addWidget(btn_add)

        self.right_panel_layout.addLayout(header_layout)

        if not comptes:
            self.right_panel_layout.addWidget(QLabel("<i>Aucun compte ouvert.</i>"))
        else:
            for cpt in comptes:
                frame = QFrame()
                frame.setStyleSheet(
                    "border: 1px solid #ccc; border-radius: 5px; margin-bottom: 5px;"
                )
                row_layout = QHBoxLayout(frame)

                lbl_info = QLabel(f"<b>{cpt['type']}</b> (N°{cpt['id']})")
                lbl_solde = QLabel(f"{cpt['solde']}")
                lbl_solde.setStyleSheet(
                    "font-weight: bold; color: #2c3e50; font-size: 14px;"
                )

                row_layout.addWidget(lbl_info)
                row_layout.addStretch()  # Pousse le reste à droite
                row_layout.addWidget(lbl_solde)

                btn_close = QPushButton("Clôturer")
                btn_close.setStyleSheet(
                    "background-color: #e74c3c; color: white; border: none; padding: 5px;"
                )

                btn_close.clicked.connect(
                    lambda checked, cid=cpt["id"]: self.cloturer_compte_dialog(cid)
                )

                row_layout.addWidget(btn_close)

                self.right_panel_layout.addWidget(frame)

        self.right_panel_layout.addStretch()

    def ouvrir_compte_dialog(self, client_id):
        """Ouvre une popup pour choisir le type de compte à créer"""
        types = ["COURANT", "LIVRET_A", "PEL"]
        type_choisi, ok = QInputDialog.getItem(
            self, "Nouveau Compte", "Type de compte :", types, 0, False
        )

        if ok and type_choisi:
            # On crée avec 0€ par défaut
            succes, msg = self.controller.ajouter_compte_client(
                client_id, type_choisi, 0.0
            )

            if succes:
                QMessageBox.information(self, "Succès", "Compte ouvert avec succès.")
                self.show_account(self.selected_user)  # Rafraîchir l'affichage
            else:
                QMessageBox.critical(self, "Erreur", msg)

    def cloturer_compte_dialog(self, compte_id):
        """Demande confirmation et clôture le compte"""
        reply = QMessageBox.question(
            self,
            "Confirmation",
            "Êtes-vous sûr de vouloir clôturer ce compte ?\nCette action est irréversible.",
            QMessageBox.Yes | QMessageBox.No,
        )

        if reply == QMessageBox.Yes:
            succes, msg = self.controller.cloturer_compte(compte_id)

            if succes:
                QMessageBox.information(self, "Succès", msg)
                self.show_account(self.selected_user)  # Rafraîchir
            else:
                QMessageBox.warning(self, "Impossible", msg)

cloturer_compte_dialog(compte_id)

Demande confirmation et clôture le compte

Source code in src/gestion_bancaire/Vue/vue_principale.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
def cloturer_compte_dialog(self, compte_id):
    """Demande confirmation et clôture le compte"""
    reply = QMessageBox.question(
        self,
        "Confirmation",
        "Êtes-vous sûr de vouloir clôturer ce compte ?\nCette action est irréversible.",
        QMessageBox.Yes | QMessageBox.No,
    )

    if reply == QMessageBox.Yes:
        succes, msg = self.controller.cloturer_compte(compte_id)

        if succes:
            QMessageBox.information(self, "Succès", msg)
            self.show_account(self.selected_user)  # Rafraîchir
        else:
            QMessageBox.warning(self, "Impossible", msg)

createSideClientBar()

Création de la colonne de gauche (Recherche + Liste vide)

Source code in src/gestion_bancaire/Vue/vue_principale.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def createSideClientBar(self):
    """Création de la colonne de gauche (Recherche + Liste vide)"""
    sidebar_layout = QVBoxLayout()

    search_row = QHBoxLayout()
    self.barre_recherche_client = QLineEdit()
    self.barre_recherche_client.setPlaceholderText("Nom client")
    self.barre_recherche_client.textChanged.connect(self.filtrer_clients)

    self.bouton_refresh_clients = QPushButton("Refresh")
    self.bouton_refresh_clients.clicked.connect(self.reload_client_list)

    search_row.addWidget(self.barre_recherche_client)
    search_row.addWidget(self.bouton_refresh_clients)
    sidebar_layout.addLayout(search_row)

    # Liste vide
    self.client_list = QListWidget()
    self.client_list.setAlternatingRowColors(True)
    self.client_list.itemClicked.connect(self.show_account)
    sidebar_layout.addWidget(self.client_list)

    # Bouton création
    self.bouton_create_new_client = QPushButton("Créer un client")
    self.bouton_create_new_client.clicked.connect(
        lambda: account_operations.show_create_client_popup(self)
    )
    sidebar_layout.addWidget(self.bouton_create_new_client)

    return sidebar_layout

createToolBar()

Création de la barre de menu en haut

Source code in src/gestion_bancaire/Vue/vue_principale.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def createToolBar(self):
    """Création de la barre de menu en haut"""
    toolbar = self.addToolBar("Menu Principal")
    toolbar.setMovable(False)

    action_comptes = QAction("Comptes", self)
    action_comptes.triggered.connect(lambda: self.show_account(self.selected_user))
    toolbar.addAction(action_comptes)

    action_virements = QAction("Virements", self)
    action_virements.triggered.connect(lambda: operations.show_transfer(self))
    toolbar.addAction(action_virements)

    action_depot = QAction("Depot", self)
    action_depot.triggered.connect(lambda: operations.show_deposit(self))
    toolbar.addAction(action_depot)

    action_retrait = QAction("Retrait", self)
    action_retrait.triggered.connect(lambda: operations.show_retrait(self))
    toolbar.addAction(action_retrait)

    action_modif = QAction("Modifier infos", self)
    action_modif.triggered.connect(
        lambda: account_operations.show_modify_client_popup(self)
    )
    toolbar.addAction(action_modif)

filtrer_clients()

Filtre visuel de la liste

Source code in src/gestion_bancaire/Vue/vue_principale.py
124
125
126
127
128
129
130
131
def filtrer_clients(self):
    """Filtre visuel de la liste"""
    texte_recherche = self.barre_recherche_client.text().lower()
    for i in range(self.client_list.count()):
        item = self.client_list.item(i)
        if item:
            correspondance = item.text().lower().startswith(texte_recherche)
            item.setHidden(not correspondance)

ouvrir_compte_dialog(client_id)

Ouvre une popup pour choisir le type de compte à créer

Source code in src/gestion_bancaire/Vue/vue_principale.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def ouvrir_compte_dialog(self, client_id):
    """Ouvre une popup pour choisir le type de compte à créer"""
    types = ["COURANT", "LIVRET_A", "PEL"]
    type_choisi, ok = QInputDialog.getItem(
        self, "Nouveau Compte", "Type de compte :", types, 0, False
    )

    if ok and type_choisi:
        # On crée avec 0€ par défaut
        succes, msg = self.controller.ajouter_compte_client(
            client_id, type_choisi, 0.0
        )

        if succes:
            QMessageBox.information(self, "Succès", "Compte ouvert avec succès.")
            self.show_account(self.selected_user)  # Rafraîchir l'affichage
        else:
            QMessageBox.critical(self, "Erreur", msg)

reload_client_list()

Récupère les clients depuis la BDD et remplit la liste

Source code in src/gestion_bancaire/Vue/vue_principale.py
110
111
112
113
114
115
116
117
118
119
120
121
122
def reload_client_list(self):
    """Récupère les clients depuis la BDD et remplit la liste"""
    self.barre_recherche_client.clear()
    self.selected_user = None
    self.client_list.clear()

    clients = self.controller.get_tous_les_clients()

    for client in clients:
        item = QListWidgetItem(client["nom"])
        # On cache l'ID SQL dans l'item
        item.setData(Qt.UserRole, client["id"])
        self.client_list.addItem(item)

show_account(item)

Affiche les détails du client et ses comptes avec actions

Source code in src/gestion_bancaire/Vue/vue_principale.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def show_account(self, item):
    """Affiche les détails du client et ses comptes avec actions"""
    if item is None:
        return

    new_widget = QWidget()
    # new_widget.setStyleSheet("background-color: white;")
    new_layout = QVBoxLayout(new_widget)

    if hasattr(self, "right_panel_widget") and self.right_panel_widget:
        self.content_layout.replaceWidget(self.right_panel_widget, new_widget)
        self.right_panel_widget.deleteLater()

    self.right_panel_widget = new_widget
    self.right_panel_layout = new_layout

    client_id = item.data(Qt.UserRole)
    self.selected_user = item

    infos = self.controller.get_client_details(client_id)
    comptes = self.controller.get_comptes_client(client_id)

    if infos:
        self.right_panel_layout.addWidget(
            QLabel(f"<h2>Client : {infos['nom']}</h2>")
        )
        self.right_panel_layout.addWidget(
            QLabel(f"<b>Email :</b> {infos.get('email', 'N/A')}")
        )
        self.right_panel_layout.addWidget(
            QLabel(f"<b>Tél :</b> {infos.get('telephone', 'N/A')}")
        )

    self.right_panel_layout.addWidget(QLabel("<hr>"))

    header_layout = QHBoxLayout()
    header_layout.addWidget(QLabel("<h3>Comptes Bancaires :</h3>"))

    btn_add = QPushButton("+ Ouvrir un compte")
    btn_add.setStyleSheet(
        "background-color: #27ae60; color: white; font-weight: bold; padding: 5px;"
    )
    btn_add.clicked.connect(lambda: self.ouvrir_compte_dialog(client_id))
    header_layout.addWidget(btn_add)

    self.right_panel_layout.addLayout(header_layout)

    if not comptes:
        self.right_panel_layout.addWidget(QLabel("<i>Aucun compte ouvert.</i>"))
    else:
        for cpt in comptes:
            frame = QFrame()
            frame.setStyleSheet(
                "border: 1px solid #ccc; border-radius: 5px; margin-bottom: 5px;"
            )
            row_layout = QHBoxLayout(frame)

            lbl_info = QLabel(f"<b>{cpt['type']}</b> (N°{cpt['id']})")
            lbl_solde = QLabel(f"{cpt['solde']}")
            lbl_solde.setStyleSheet(
                "font-weight: bold; color: #2c3e50; font-size: 14px;"
            )

            row_layout.addWidget(lbl_info)
            row_layout.addStretch()  # Pousse le reste à droite
            row_layout.addWidget(lbl_solde)

            btn_close = QPushButton("Clôturer")
            btn_close.setStyleSheet(
                "background-color: #e74c3c; color: white; border: none; padding: 5px;"
            )

            btn_close.clicked.connect(
                lambda checked, cid=cpt["id"]: self.cloturer_compte_dialog(cid)
            )

            row_layout.addWidget(btn_close)

            self.right_panel_layout.addWidget(frame)

    self.right_panel_layout.addStretch()

OperationWidget

Bases: QWidget

Classe de base pour tous les widgets d'opérations

Source code in src/gestion_bancaire/Vue/operations/base.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class OperationWidget(QWidget):
    """Classe de base pour tous les widgets d'opérations"""

    def __init__(self, main_window):
        super().__init__()
        self.main_window = main_window
        self.current_user = main_window.selected_user
        # self.setStyleSheet("background-color: white;")

    def update_right_panel(self):
        """Remplace le widget de droite"""
        if self.main_window.right_panel_widget:
            self.main_window.right_panel_widget.deleteLater()

        self.main_window.content_layout.addWidget(self, 4)
        self.main_window.right_panel_widget = self

    def show_if_user_selected(self):
        if not self.current_user:
            self.show_selection_error()
            return False
        return True

    def show_selection_error(self):
        error_widget = QWidget()
        error_widget.setStyleSheet("background-color: #ffe6e6;")
        layout = QVBoxLayout(error_widget)
        error_label = QLabel("<b>Veuillez sélectionner un client dans la liste.</b>")
        error_label.setAlignment(Qt.AlignCenter)
        layout.addStretch()
        layout.addWidget(error_label)
        layout.addStretch()

        if self.main_window.right_panel_widget:
            self.main_window.right_panel_widget.deleteLater()
        self.main_window.content_layout.addWidget(error_widget, 4)
        self.main_window.right_panel_widget = error_widget

    def get_account_list(self, client_item=None):
        """Retourne la vraie liste des comptes via le contrôleur"""
        target_item = client_item if client_item else self.current_user
        if not target_item:
            return []

        client_id = (
            target_item
            if isinstance(target_item, int)
            else target_item.data(Qt.UserRole)
        )

        comptes = self.main_window.controller.get_comptes_client(client_id)

        formatted_accounts = []
        for c in comptes:
            formatted_accounts.append(
                {"id": c["id"], "nom": f"{c['type']} (Solde: {c['solde']})"}
            )
        return formatted_accounts

    def validate_amount(self, amount_text):
        if not amount_text or amount_text.strip() == "":
            QMessageBox.warning(self, "Erreur", "Veuillez entrer un montant.")
            return False
        try:
            val = float(amount_text)
            if val <= 0:
                raise ValueError
            return True
        except ValueError:
            QMessageBox.warning(self, "Erreur", "Montant invalide (doit être > 0).")
            return False

get_account_list(client_item=None)

Retourne la vraie liste des comptes via le contrôleur

Source code in src/gestion_bancaire/Vue/operations/base.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def get_account_list(self, client_item=None):
    """Retourne la vraie liste des comptes via le contrôleur"""
    target_item = client_item if client_item else self.current_user
    if not target_item:
        return []

    client_id = (
        target_item
        if isinstance(target_item, int)
        else target_item.data(Qt.UserRole)
    )

    comptes = self.main_window.controller.get_comptes_client(client_id)

    formatted_accounts = []
    for c in comptes:
        formatted_accounts.append(
            {"id": c["id"], "nom": f"{c['type']} (Solde: {c['solde']})"}
        )
    return formatted_accounts

update_right_panel()

Remplace le widget de droite

Source code in src/gestion_bancaire/Vue/operations/base.py
14
15
16
17
18
19
20
def update_right_panel(self):
    """Remplace le widget de droite"""
    if self.main_window.right_panel_widget:
        self.main_window.right_panel_widget.deleteLater()

    self.main_window.content_layout.addWidget(self, 4)
    self.main_window.right_panel_widget = self