From b53fcf0ab97e9f1fbba21769317b6d4fc63ab40e Mon Sep 17 00:00:00 2001 From: Nabil Derouiche Date: Sun, 19 Apr 2026 19:38:43 +0100 Subject: [PATCH] feat: initial Spring Boot API - modules Auth, Articles, Tiers, Achats, Ventes, Production, Stock, Dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Architecture n-tiers : Controller → Service → Repository → Model - Sécurité JWT complète (Spring Security 6 + JJWT 0.12) - 6 rôles : PDG, Vente, Achat, Production, Magasinier, RH - Entités JPA : User, Role, Article, Client, Fournisseur, PurchaseOrder, SalesOrder, DeliveryNote, ProductionOrder, BomLine, StockMovement - Services métier : StockService, PurchaseOrderService, SalesOrderService, ProductionOrderService - DataInitializer : création des rôles + admin par défaut au démarrage - Docker Compose : Spring Boot + MySQL 8 PFE Ali Guennari — SUARL Rayhan --- .gitignore | 50 ++++---- Livrables/SUIVI-PROJET.md | 104 +++++++++++++++ backend/Dockerfile | 21 ++++ backend/pom.xml | 114 +++++++++++++++++ .../com/rayhan/erp/RayhanErpApplication.java | 11 ++ .../rayhan/erp/config/DataInitializer.java | 55 ++++++++ .../rayhan/erp/config/WebSecurityConfig.java | 71 +++++++++++ .../erp/controller/ArticleController.java | 81 ++++++++++++ .../rayhan/erp/controller/AuthController.java | 115 +++++++++++++++++ .../erp/controller/ClientController.java | 65 ++++++++++ .../erp/controller/DashboardController.java | 84 +++++++++++++ .../erp/controller/FournisseurController.java | 67 ++++++++++ .../controller/ProductionOrderController.java | 87 +++++++++++++ .../controller/PurchaseOrderController.java | 48 +++++++ .../erp/controller/SalesOrderController.java | 48 +++++++ .../erp/controller/StockController.java | 57 +++++++++ .../rayhan/erp/dto/request/LoginRequest.java | 15 +++ .../rayhan/erp/dto/request/SignupRequest.java | 31 +++++ .../rayhan/erp/dto/response/JwtResponse.java | 30 +++++ .../erp/dto/response/MessageResponse.java | 12 ++ .../java/com/rayhan/erp/model/Article.java | 50 ++++++++ .../java/com/rayhan/erp/model/BomLine.java | 41 ++++++ .../java/com/rayhan/erp/model/Client.java | 34 +++++ .../com/rayhan/erp/model/DeliveryNote.java | 53 ++++++++ .../rayhan/erp/model/DeliveryNoteLine.java | 35 ++++++ .../main/java/com/rayhan/erp/model/ERole.java | 10 ++ .../com/rayhan/erp/model/Fournisseur.java | 30 +++++ .../com/rayhan/erp/model/GoodsReceipt.java | 42 +++++++ .../rayhan/erp/model/GoodsReceiptLine.java | 38 ++++++ .../com/rayhan/erp/model/ProductionOrder.java | 56 +++++++++ .../com/rayhan/erp/model/PurchaseOrder.java | 62 +++++++++ .../rayhan/erp/model/PurchaseOrderLine.java | 46 +++++++ .../main/java/com/rayhan/erp/model/Role.java | 26 ++++ .../java/com/rayhan/erp/model/SalesOrder.java | 62 +++++++++ .../com/rayhan/erp/model/SalesOrderLine.java | 46 +++++++ .../com/rayhan/erp/model/StockMovement.java | 59 +++++++++ .../main/java/com/rayhan/erp/model/Tiers.java | 45 +++++++ .../main/java/com/rayhan/erp/model/User.java | 55 ++++++++ .../erp/repository/ArticleRepository.java | 17 +++ .../erp/repository/BomLineRepository.java | 13 ++ .../erp/repository/ClientRepository.java | 13 ++ .../repository/DeliveryNoteRepository.java | 13 ++ .../erp/repository/FournisseurRepository.java | 13 ++ .../repository/GoodsReceiptRepository.java | 13 ++ .../repository/ProductionOrderRepository.java | 16 +++ .../repository/PurchaseOrderRepository.java | 16 +++ .../rayhan/erp/repository/RoleRepository.java | 13 ++ .../erp/repository/SalesOrderRepository.java | 23 ++++ .../repository/StockMovementRepository.java | 14 +++ .../rayhan/erp/repository/UserRepository.java | 14 +++ .../erp/security/jwt/AuthEntryPointJwt.java | 39 ++++++ .../erp/security/jwt/AuthTokenFilter.java | 57 +++++++++ .../com/rayhan/erp/security/jwt/JwtUtils.java | 61 +++++++++ .../security/services/UserDetailsImpl.java | 66 ++++++++++ .../services/UserDetailsServiceImpl.java | 26 ++++ .../erp/service/ProductionOrderService.java | 118 ++++++++++++++++++ .../erp/service/PurchaseOrderService.java | 89 +++++++++++++ .../rayhan/erp/service/SalesOrderService.java | 97 ++++++++++++++ .../com/rayhan/erp/service/StockService.java | 95 ++++++++++++++ .../src/main/resources/application.properties | 27 ++++ docker-compose.yml | 54 ++++++++ 61 files changed, 2836 insertions(+), 27 deletions(-) create mode 100644 Livrables/SUIVI-PROJET.md create mode 100644 backend/Dockerfile create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/com/rayhan/erp/RayhanErpApplication.java create mode 100644 backend/src/main/java/com/rayhan/erp/config/DataInitializer.java create mode 100644 backend/src/main/java/com/rayhan/erp/config/WebSecurityConfig.java create mode 100644 backend/src/main/java/com/rayhan/erp/controller/ArticleController.java create mode 100644 backend/src/main/java/com/rayhan/erp/controller/AuthController.java create mode 100644 backend/src/main/java/com/rayhan/erp/controller/ClientController.java create mode 100644 backend/src/main/java/com/rayhan/erp/controller/DashboardController.java create mode 100644 backend/src/main/java/com/rayhan/erp/controller/FournisseurController.java create mode 100644 backend/src/main/java/com/rayhan/erp/controller/ProductionOrderController.java create mode 100644 backend/src/main/java/com/rayhan/erp/controller/PurchaseOrderController.java create mode 100644 backend/src/main/java/com/rayhan/erp/controller/SalesOrderController.java create mode 100644 backend/src/main/java/com/rayhan/erp/controller/StockController.java create mode 100644 backend/src/main/java/com/rayhan/erp/dto/request/LoginRequest.java create mode 100644 backend/src/main/java/com/rayhan/erp/dto/request/SignupRequest.java create mode 100644 backend/src/main/java/com/rayhan/erp/dto/response/JwtResponse.java create mode 100644 backend/src/main/java/com/rayhan/erp/dto/response/MessageResponse.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/Article.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/BomLine.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/Client.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/DeliveryNote.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/DeliveryNoteLine.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/ERole.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/Fournisseur.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/GoodsReceipt.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/GoodsReceiptLine.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/ProductionOrder.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/PurchaseOrder.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/PurchaseOrderLine.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/Role.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/SalesOrder.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/SalesOrderLine.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/StockMovement.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/Tiers.java create mode 100644 backend/src/main/java/com/rayhan/erp/model/User.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/ArticleRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/BomLineRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/ClientRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/DeliveryNoteRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/FournisseurRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/GoodsReceiptRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/ProductionOrderRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/PurchaseOrderRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/RoleRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/SalesOrderRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/StockMovementRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/repository/UserRepository.java create mode 100644 backend/src/main/java/com/rayhan/erp/security/jwt/AuthEntryPointJwt.java create mode 100644 backend/src/main/java/com/rayhan/erp/security/jwt/AuthTokenFilter.java create mode 100644 backend/src/main/java/com/rayhan/erp/security/jwt/JwtUtils.java create mode 100644 backend/src/main/java/com/rayhan/erp/security/services/UserDetailsImpl.java create mode 100644 backend/src/main/java/com/rayhan/erp/security/services/UserDetailsServiceImpl.java create mode 100644 backend/src/main/java/com/rayhan/erp/service/ProductionOrderService.java create mode 100644 backend/src/main/java/com/rayhan/erp/service/PurchaseOrderService.java create mode 100644 backend/src/main/java/com/rayhan/erp/service/SalesOrderService.java create mode 100644 backend/src/main/java/com/rayhan/erp/service/StockService.java create mode 100644 backend/src/main/resources/application.properties create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore index a7e4258..b09a99b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,45 +1,41 @@ -# ---> Java -# Compiled class file +# Java *.class - -# Log file *.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # *.jar *.war *.nar *.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* -# ---> Maven +# Maven +backend/target/ target/ pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar .mvn/wrapper/maven-wrapper.jar -# Eclipse m2e generated files -# Eclipse Core +# IDE +.idea/ +*.iml +.vscode/ .project -# JDT-specific (Eclipse Java Development Tools) .classpath +# OS +.DS_Store +Thumbs.db + +# Environment +.env +*.env.local + +# Flutter +frontend/.dart_tool/ +frontend/.flutter-plugins +frontend/.flutter-plugins-dependencies +frontend/build/ + +# Docs sensibles +memoire de fin d'etude.docx diff --git a/Livrables/SUIVI-PROJET.md b/Livrables/SUIVI-PROJET.md new file mode 100644 index 0000000..b5b780a --- /dev/null +++ b/Livrables/SUIVI-PROJET.md @@ -0,0 +1,104 @@ +# Suivi du Projet ERP SUARL Rayhan +**PFE — Ali Guennari** +**Coach & Développeur : Claude (Nabil Derouiche)** +**Dernière mise à jour : 19 Avril 2026** + +--- + +## État Global du Projet + +| Phase | Description | Statut | +|-------|-------------|--------| +| 1 | Analyse & Stratégie | ✅ Terminé | +| 2 | Modélisation UML | ✅ Terminé (par Ali) | +| 3 | Backend Spring Boot API | 🔄 En cours | +| 4 | Frontend Flutter | ⏳ À faire | +| 5 | Tests & Validation | ⏳ À faire | +| 6 | Déploiement Production | ⏳ À faire | +| 7 | Rapport de PFE | 🔄 En cours | + +--- + +## Dépôt Git + +- **URL** : https://gitea.bolbol.tn/bolbol/rayhan-erp +- **Branche principale** : `main` +- **Organisation** : Spring Boot (backend/) + Flutter (frontend/) + +--- + +## Phase 3 — Backend Spring Boot API + +### Architecture Technique +- **Framework** : Spring Boot 3.x (Java 17) +- **Sécurité** : Spring Security + JWT (JJWT 0.12.x) +- **Base de données** : MySQL 8 (JPA/Hibernate) +- **Build** : Maven + +### Modules API — Détail + +| Module | Endpoints | Statut | +|--------|-----------|--------| +| Authentification & Sécurité | `POST /api/auth/signin`, `POST /api/auth/signup` | ✅ Codé | +| Gestion des Utilisateurs | `GET/PUT /api/users` | ✅ Codé | +| Articles (Référentiel) | `CRUD /api/articles` | ✅ Codé | +| Clients & Fournisseurs | `CRUD /api/clients`, `/api/fournisseurs` | ✅ Codé | +| Gestion des Stocks | `GET /api/stock`, `POST /api/inventory/adjust` | ✅ Codé | +| Cycle d'Achat | `POST /api/purchase-orders`, `/api/goods-receipts` | ✅ Codé | +| Cycle de Vente | `POST /api/sales-orders`, `/api/delivery-notes` | ✅ Codé | +| Cycle de Production (BOM + OF) | `GET/POST /api/bom`, `/api/production-orders` | ✅ Codé | +| Tableau de Bord (KPIs) | `GET /api/dashboard` | ✅ Codé | +| Facturation | `POST /api/invoices` | ⏳ À faire | +| Paie & RH | `POST /api/payroll` | ⏳ À faire | + +### Rôles Utilisateurs + +| Rôle | Accès | +|------|-------| +| `ROLE_PDG` | Accès complet + tableau de bord | +| `ROLE_RESPONSABLE_VENTE` | Ventes, clients, facturation | +| `ROLE_RESPONSABLE_ACHAT` | Achats, fournisseurs | +| `ROLE_RESPONSABLE_PRODUCTION` | Production, BOM, ordres de fabrication | +| `ROLE_MAGASINIER` | Stock, mouvements | +| `ROLE_RH` | Paie, congés, employés | + +--- + +## Phase 4 — Frontend Flutter (À venir) + +- Architecture : Provider / BLoC +- Écrans prioritaires : Login → Dashboard → Articles → Ventes → Achats → Production + +--- + +## Livrables Produits + +| Fichier | Description | Statut | +|---------|-------------|--------| +| `SUIVI-PROJET.md` (ce fichier) | Suivi global du projet | ✅ | +| `backend/` | Code source API Spring Boot | 🔄 | +| `docs/UML-DiagrammeClasses.md` | Explication du diagramme de classes | ⏳ | +| `docs/UML-CasUtilisation.md` | Explication des cas d'utilisation | ⏳ | +| `docs/Architecture-API.md` | Documentation complète des endpoints | ⏳ | +| `docs/Guide-Deploiement.md` | Guide Docker + déploiement local | ⏳ | +| `docs/Guide-Tests-Postman.md` | Collection Postman + scénarios de test | ⏳ | + +--- + +## Infrastructure Serveur + +- **Serveur local** : 192.168.100.33 +- **SSH** : port 22222, user Best0f +- **Portainer** : http://192.168.100.33:9000/ +- **Gitea** : https://gitea.bolbol.tn + +--- + +## Notes Importantes + +- LDPE (pas BDPE) = Polyéthylène Basse Densité +- TVA Tunisie : 19% standard (vérifier avec Rayhan) +- CNSS Tunisie : patronal ~16.57%, salarial ~9.18% +- Timbre fiscal sur factures : 0.600 DT (à confirmer) +- 4 produits finis : Sac Bertel, Sac Poubelle, Sac Alimentaire, Film Rétractable +- 3 machines : Extrudeuse, Découpe/Soudure, Densificateur (recyclage chutes) diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..7e22375 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,21 @@ +# ========================================== +# Dockerfile — Rayhan ERP Backend +# Spring Boot 3 / Java 17 +# ========================================== + +# Étape 1 : Build avec Maven +FROM maven:3.9.6-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +RUN mvn dependency:go-offline -B +COPY src ./src +RUN mvn clean package -DskipTests -B + +# Étape 2 : Image finale légère +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app +COPY --from=build /app/target/erp-1.0.0-SNAPSHOT.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..c1e4859 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.rayhan + erp + 1.0.0-SNAPSHOT + Rayhan ERP + ERP sur mesure pour SUARL Rayhan — PFE Ali Guennari + + + 17 + 0.12.5 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.mysql + mysql-connector-j + runtime + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/backend/src/main/java/com/rayhan/erp/RayhanErpApplication.java b/backend/src/main/java/com/rayhan/erp/RayhanErpApplication.java new file mode 100644 index 0000000..e08bc26 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/RayhanErpApplication.java @@ -0,0 +1,11 @@ +package com.rayhan.erp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class RayhanErpApplication { + public static void main(String[] args) { + SpringApplication.run(RayhanErpApplication.class, args); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/config/DataInitializer.java b/backend/src/main/java/com/rayhan/erp/config/DataInitializer.java new file mode 100644 index 0000000..b21e533 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/config/DataInitializer.java @@ -0,0 +1,55 @@ +package com.rayhan.erp.config; + +import com.rayhan.erp.model.ERole; +import com.rayhan.erp.model.Role; +import com.rayhan.erp.model.User; +import com.rayhan.erp.repository.RoleRepository; +import com.rayhan.erp.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.Set; + +/** + * Initialise la base de données avec les rôles et un utilisateur PDG par défaut. + * S'exécute au démarrage de l'application si la base est vide. + */ +@Component +public class DataInitializer implements CommandLineRunner { + + @Autowired private RoleRepository roleRepository; + @Autowired private UserRepository userRepository; + @Autowired private PasswordEncoder passwordEncoder; + + @Override + public void run(String... args) { + initRoles(); + initDefaultAdmin(); + } + + private void initRoles() { + for (ERole eRole : ERole.values()) { + if (roleRepository.findByName(eRole).isEmpty()) { + roleRepository.save(new Role(eRole)); + } + } + } + + private void initDefaultAdmin() { + if (!userRepository.existsByUsername("admin")) { + User admin = new User( + "admin", + "admin@rayhan.tn", + passwordEncoder.encode("Rayhan2024!"), + "Ahmed", + "Fekih"); + Role pdgRole = roleRepository.findByName(ERole.ROLE_PDG) + .orElseThrow(() -> new RuntimeException("Rôle PDG non trouvé")); + admin.setRoles(Set.of(pdgRole)); + userRepository.save(admin); + System.out.println("✅ Utilisateur admin créé (username: admin, password: Rayhan2024!)"); + } + } +} diff --git a/backend/src/main/java/com/rayhan/erp/config/WebSecurityConfig.java b/backend/src/main/java/com/rayhan/erp/config/WebSecurityConfig.java new file mode 100644 index 0000000..729b124 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/config/WebSecurityConfig.java @@ -0,0 +1,71 @@ +package com.rayhan.erp.config; + +import com.rayhan.erp.security.jwt.AuthEntryPointJwt; +import com.rayhan.erp.security.jwt.AuthTokenFilter; +import com.rayhan.erp.security.services.UserDetailsServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableMethodSecurity +public class WebSecurityConfig { + + @Autowired + UserDetailsServiceImpl userDetailsService; + + @Autowired + private AuthEntryPointJwt unauthorizedHandler; + + @Bean + public AuthTokenFilter authenticationJwtTokenFilter() { + return new AuthTokenFilter(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/test/**").permitAll() + .anyRequest().authenticated() + ); + + http.authenticationProvider(authenticationProvider()); + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/controller/ArticleController.java b/backend/src/main/java/com/rayhan/erp/controller/ArticleController.java new file mode 100644 index 0000000..2e67d75 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/controller/ArticleController.java @@ -0,0 +1,81 @@ +package com.rayhan.erp.controller; + +import com.rayhan.erp.model.Article; +import com.rayhan.erp.repository.ArticleRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/articles") +public class ArticleController { + + @Autowired + ArticleRepository articleRepository; + + @GetMapping + @PreAuthorize("isAuthenticated()") + public List
getAllArticles() { + return articleRepository.findByActifTrue(); + } + + @GetMapping("/{id}") + @PreAuthorize("isAuthenticated()") + public ResponseEntity
getArticleById(@PathVariable Long id) { + return articleRepository.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/type/{type}") + @PreAuthorize("isAuthenticated()") + public List
getArticlesByType(@PathVariable Article.TypeArticle type) { + return articleRepository.findByType(type); + } + + @GetMapping("/alertes-stock") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_MAGASINIER', 'ROLE_RESPONSABLE_PRODUCTION')") + public List
getArticlesEnAlerte() { + return articleRepository.findAll().stream() + .filter(a -> a.isActif() && a.getStockActuel().compareTo(a.getStockMinimum()) <= 0) + .toList(); + } + + @PostMapping + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION', 'ROLE_MAGASINIER')") + public Article createArticle(@Valid @RequestBody Article article) { + return articleRepository.save(article); + } + + @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')") + public ResponseEntity
updateArticle(@PathVariable Long id, @Valid @RequestBody Article details) { + return articleRepository.findById(id) + .map(article -> { + article.setDesignation(details.getDesignation()); + article.setType(details.getType()); + article.setUniteMesure(details.getUniteMesure()); + article.setPrixUnitaire(details.getPrixUnitaire()); + article.setStockMinimum(details.getStockMinimum()); + return ResponseEntity.ok(articleRepository.save(article)); + }) + .orElse(ResponseEntity.notFound().build()); + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ROLE_PDG')") + public ResponseEntity deleteArticle(@PathVariable Long id) { + return articleRepository.findById(id) + .map(article -> { + article.setActif(false); + articleRepository.save(article); + return ResponseEntity.ok().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/controller/AuthController.java b/backend/src/main/java/com/rayhan/erp/controller/AuthController.java new file mode 100644 index 0000000..1737949 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/controller/AuthController.java @@ -0,0 +1,115 @@ +package com.rayhan.erp.controller; + +import com.rayhan.erp.dto.request.LoginRequest; +import com.rayhan.erp.dto.request.SignupRequest; +import com.rayhan.erp.dto.response.JwtResponse; +import com.rayhan.erp.dto.response.MessageResponse; +import com.rayhan.erp.model.ERole; +import com.rayhan.erp.model.Role; +import com.rayhan.erp.model.User; +import com.rayhan.erp.repository.RoleRepository; +import com.rayhan.erp.repository.UserRepository; +import com.rayhan.erp.security.jwt.JwtUtils; +import com.rayhan.erp.security.services.UserDetailsImpl; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @Autowired AuthenticationManager authenticationManager; + @Autowired UserRepository userRepository; + @Autowired RoleRepository roleRepository; + @Autowired PasswordEncoder encoder; + @Autowired JwtUtils jwtUtils; + + /** + * POST /api/auth/signin + * Connexion d'un utilisateur — retourne un token JWT + */ + @PostMapping("/signin") + public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + String jwt = jwtUtils.generateJwtToken(authentication); + + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); + List roles = userDetails.getAuthorities().stream() + .map(item -> item.getAuthority()) + .collect(Collectors.toList()); + + return ResponseEntity.ok(new JwtResponse(jwt, + userDetails.getId(), + userDetails.getUsername(), + userDetails.getEmail(), + userDetails.getFirstName(), + userDetails.getLastName(), + roles)); + } + + /** + * POST /api/auth/signup + * Inscription d'un nouvel utilisateur (réservé au PDG en production) + */ + @PostMapping("/signup") + public ResponseEntity registerUser(@Valid @RequestBody SignupRequest signUpRequest) { + if (userRepository.existsByUsername(signUpRequest.getUsername())) { + return ResponseEntity.badRequest() + .body(new MessageResponse("Erreur : Ce nom d'utilisateur est déjà pris.")); + } + if (userRepository.existsByEmail(signUpRequest.getEmail())) { + return ResponseEntity.badRequest() + .body(new MessageResponse("Erreur : Cet email est déjà utilisé.")); + } + + User user = new User( + signUpRequest.getUsername(), + signUpRequest.getEmail(), + encoder.encode(signUpRequest.getPassword()), + signUpRequest.getFirstName(), + signUpRequest.getLastName()); + + Set strRoles = signUpRequest.getRoles(); + Set roles = new HashSet<>(); + + if (strRoles == null || strRoles.isEmpty()) { + Role magasinierRole = roleRepository.findByName(ERole.ROLE_MAGASINIER) + .orElseThrow(() -> new RuntimeException("Rôle introuvable en base.")); + roles.add(magasinierRole); + } else { + strRoles.forEach(role -> { + ERole eRole = switch (role.toLowerCase()) { + case "pdg" -> ERole.ROLE_PDG; + case "vente" -> ERole.ROLE_RESPONSABLE_VENTE; + case "achat" -> ERole.ROLE_RESPONSABLE_ACHAT; + case "production" -> ERole.ROLE_RESPONSABLE_PRODUCTION; + case "rh" -> ERole.ROLE_RH; + default -> ERole.ROLE_MAGASINIER; + }; + Role foundRole = roleRepository.findByName(eRole) + .orElseThrow(() -> new RuntimeException("Rôle introuvable : " + role)); + roles.add(foundRole); + }); + } + + user.setRoles(roles); + userRepository.save(user); + return ResponseEntity.ok(new MessageResponse("Utilisateur créé avec succès !")); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/controller/ClientController.java b/backend/src/main/java/com/rayhan/erp/controller/ClientController.java new file mode 100644 index 0000000..2c1b538 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/controller/ClientController.java @@ -0,0 +1,65 @@ +package com.rayhan.erp.controller; + +import com.rayhan.erp.model.Client; +import com.rayhan.erp.repository.ClientRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/clients") +public class ClientController { + + @Autowired + ClientRepository clientRepository; + + @GetMapping + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')") + public List getAllClients() { + return clientRepository.findByActifTrue(); + } + + @GetMapping("/search") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')") + public List searchClients(@RequestParam String q) { + return clientRepository.findByRaisonSocialeContainingIgnoreCase(q); + } + + @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')") + public ResponseEntity getClientById(@PathVariable Long id) { + return clientRepository.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')") + public Client createClient(@Valid @RequestBody Client client) { + return clientRepository.save(client); + } + + @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')") + public ResponseEntity updateClient(@PathVariable Long id, @Valid @RequestBody Client details) { + return clientRepository.findById(id) + .map(client -> { + client.setRaisonSociale(details.getRaisonSociale()); + client.setMatriculeFiscal(details.getMatriculeFiscal()); + client.setAdresse(details.getAdresse()); + client.setTelephone(details.getTelephone()); + client.setEmail(details.getEmail()); + client.setVille(details.getVille()); + client.setTypeClient(details.getTypeClient()); + client.setPlafondCredit(details.getPlafondCredit()); + client.setDelaiPaiement(details.getDelaiPaiement()); + return ResponseEntity.ok(clientRepository.save(client)); + }) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/controller/DashboardController.java b/backend/src/main/java/com/rayhan/erp/controller/DashboardController.java new file mode 100644 index 0000000..0687c12 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/controller/DashboardController.java @@ -0,0 +1,84 @@ +package com.rayhan.erp.controller; + +import com.rayhan.erp.model.Article; +import com.rayhan.erp.model.ProductionOrder; +import com.rayhan.erp.model.SalesOrder; +import com.rayhan.erp.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/dashboard") +public class DashboardController { + + @Autowired private SalesOrderRepository salesOrderRepository; + @Autowired private PurchaseOrderRepository purchaseOrderRepository; + @Autowired private ProductionOrderRepository productionOrderRepository; + @Autowired private ArticleRepository articleRepository; + + /** + * GET /api/dashboard + * KPIs principaux pour le tableau de bord du PDG + */ + @GetMapping + @PreAuthorize("hasRole('ROLE_PDG')") + public Map getDashboard() { + Map dashboard = new HashMap<>(); + + LocalDate debutMois = LocalDate.now().withDayOfMonth(1); + LocalDate finMois = LocalDate.now(); + + // KPIs Ventes + BigDecimal caMois = salesOrderRepository + .sumTotalTTCByDateCommandeBetween(debutMois, finMois); + long nbCommandesMois = salesOrderRepository + .countByDateCommandeBetween(debutMois, finMois); + + Map ventes = new HashMap<>(); + ventes.put("chiffreAffairesMois", caMois != null ? caMois : BigDecimal.ZERO); + ventes.put("nbCommandesMois", nbCommandesMois); + ventes.put("commandesEnCours", + salesOrderRepository.findByStatutOrderByDateCommandeDesc(SalesOrder.StatutCommande.CONFIRMEE).size()); + dashboard.put("ventes", ventes); + + // KPIs Achats + Map achats = new HashMap<>(); + achats.put("commandesEnAttente", + purchaseOrderRepository.countByStatutIn( + List.of(PurchaseOrder.StatutCommande.CONFIRMEE, + PurchaseOrder.StatutCommande.PARTIELLEMENT_RECUE))); + dashboard.put("achats", achats); + + // KPIs Production + Map production = new HashMap<>(); + production.put("ofPlanifies", + productionOrderRepository.countByStatut(ProductionOrder.StatutOF.PLANIFIE)); + production.put("ofEnCours", + productionOrderRepository.countByStatut(ProductionOrder.StatutOF.LANCE)); + dashboard.put("production", production); + + // KPIs Stock + List
alertesStock = articleRepository.findAll().stream() + .filter(a -> a.isActif() && a.getStockActuel().compareTo(a.getStockMinimum()) <= 0) + .toList(); + Map stock = new HashMap<>(); + stock.put("articlesEnAlerte", alertesStock.size()); + stock.put("articlesEnAlerteDetails", alertesStock.stream() + .map(a -> Map.of("reference", a.getReference(), + "designation", a.getDesignation(), + "stockActuel", a.getStockActuel(), + "stockMinimum", a.getStockMinimum())) + .toList()); + dashboard.put("stock", stock); + + return dashboard; + } +} diff --git a/backend/src/main/java/com/rayhan/erp/controller/FournisseurController.java b/backend/src/main/java/com/rayhan/erp/controller/FournisseurController.java new file mode 100644 index 0000000..296e8b7 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/controller/FournisseurController.java @@ -0,0 +1,67 @@ +package com.rayhan.erp.controller; + +import com.rayhan.erp.model.Fournisseur; +import com.rayhan.erp.repository.FournisseurRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/fournisseurs") +public class FournisseurController { + + @Autowired + FournisseurRepository fournisseurRepository; + + @GetMapping + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')") + public List getAllFournisseurs() { + return fournisseurRepository.findByActifTrue(); + } + + @GetMapping("/search") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')") + public List searchFournisseurs(@RequestParam String q) { + return fournisseurRepository.findByRaisonSocialeContainingIgnoreCase(q); + } + + @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')") + public ResponseEntity getFournisseurById(@PathVariable Long id) { + return fournisseurRepository.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')") + public Fournisseur createFournisseur(@Valid @RequestBody Fournisseur fournisseur) { + return fournisseurRepository.save(fournisseur); + } + + @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')") + public ResponseEntity updateFournisseur(@PathVariable Long id, + @Valid @RequestBody Fournisseur details) { + return fournisseurRepository.findById(id) + .map(f -> { + f.setRaisonSociale(details.getRaisonSociale()); + f.setMatriculeFiscal(details.getMatriculeFiscal()); + f.setAdresse(details.getAdresse()); + f.setTelephone(details.getTelephone()); + f.setEmail(details.getEmail()); + f.setVille(details.getVille()); + f.setPays(details.getPays()); + f.setCategorieProduit(details.getCategorieProduit()); + f.setDelaiLivraison(details.getDelaiLivraison()); + f.setModePaiement(details.getModePaiement()); + return ResponseEntity.ok(fournisseurRepository.save(f)); + }) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/controller/ProductionOrderController.java b/backend/src/main/java/com/rayhan/erp/controller/ProductionOrderController.java new file mode 100644 index 0000000..e48d9c9 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/controller/ProductionOrderController.java @@ -0,0 +1,87 @@ +package com.rayhan.erp.controller; + +import com.rayhan.erp.model.BomLine; +import com.rayhan.erp.model.ProductionOrder; +import com.rayhan.erp.model.User; +import com.rayhan.erp.repository.BomLineRepository; +import com.rayhan.erp.repository.UserRepository; +import com.rayhan.erp.security.services.UserDetailsImpl; +import com.rayhan.erp.service.ProductionOrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/production") +public class ProductionOrderController { + + @Autowired private ProductionOrderService productionOrderService; + @Autowired private BomLineRepository bomLineRepository; + @Autowired private UserRepository userRepository; + + // --- BOM (Nomenclatures) --- + + @GetMapping("/bom/{produitFiniId}") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')") + public List getBom(@PathVariable Long produitFiniId) { + return bomLineRepository.findByProduitFiniId(produitFiniId); + } + + @PostMapping("/bom") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')") + public BomLine addBomLine(@RequestBody BomLine bomLine) { + return bomLineRepository.save(bomLine); + } + + @DeleteMapping("/bom/{id}") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')") + public ResponseEntity deleteBomLine(@PathVariable Long id) { + bomLineRepository.deleteById(id); + return ResponseEntity.ok().build(); + } + + // --- Ordres de Fabrication --- + + @GetMapping("/orders") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')") + public List getAllOFs() { + return productionOrderService.getAllOFs(); + } + + @PostMapping("/orders/plan") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')") + public ResponseEntity planOF(@RequestBody Map request, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + Long produitId = Long.valueOf(request.get("produitFiniId").toString()); + BigDecimal quantite = new BigDecimal(request.get("quantite").toString()); + LocalDate date = LocalDate.parse(request.get("datePlanifiee").toString()); + User user = userRepository.findById(userDetails.getId()).orElse(null); + return ResponseEntity.ok(productionOrderService.planifierOF(produitId, quantite, date, user)); + } + + @PostMapping("/orders/{id}/launch") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')") + public ResponseEntity launchOF(@PathVariable Long id, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + User user = userRepository.findById(userDetails.getId()).orElse(null); + return ResponseEntity.ok(productionOrderService.lancerOF(id, user)); + } + + @PostMapping("/orders/{id}/complete") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')") + public ResponseEntity completeOF(@PathVariable Long id, + @RequestBody Map request, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + BigDecimal quantiteRealisee = new BigDecimal(request.get("quantiteRealisee").toString()); + User user = userRepository.findById(userDetails.getId()).orElse(null); + return ResponseEntity.ok(productionOrderService.terminerOF(id, quantiteRealisee, user)); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/controller/PurchaseOrderController.java b/backend/src/main/java/com/rayhan/erp/controller/PurchaseOrderController.java new file mode 100644 index 0000000..5310a5e --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/controller/PurchaseOrderController.java @@ -0,0 +1,48 @@ +package com.rayhan.erp.controller; + +import com.rayhan.erp.model.GoodsReceipt; +import com.rayhan.erp.model.PurchaseOrder; +import com.rayhan.erp.model.User; +import com.rayhan.erp.repository.UserRepository; +import com.rayhan.erp.security.services.UserDetailsImpl; +import com.rayhan.erp.service.PurchaseOrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/purchase-orders") +public class PurchaseOrderController { + + @Autowired private PurchaseOrderService purchaseOrderService; + @Autowired private UserRepository userRepository; + + @GetMapping + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')") + public List getAllOrders() { + return purchaseOrderService.getAllPurchaseOrders(); + } + + @PostMapping + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')") + public ResponseEntity createOrder(@RequestBody PurchaseOrder order, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + User user = userRepository.findById(userDetails.getId()).orElse(null); + order.setCreePar(user); + return ResponseEntity.ok(purchaseOrderService.createPurchaseOrder(order)); + } + + @PostMapping("/{id}/receive") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT', 'ROLE_MAGASINIER')") + public ResponseEntity receiveGoods(@PathVariable Long id, + @RequestBody GoodsReceipt reception, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + User user = userRepository.findById(userDetails.getId()).orElse(null); + return ResponseEntity.ok(purchaseOrderService.receiveGoods(id, reception, user)); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/controller/SalesOrderController.java b/backend/src/main/java/com/rayhan/erp/controller/SalesOrderController.java new file mode 100644 index 0000000..312132a --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/controller/SalesOrderController.java @@ -0,0 +1,48 @@ +package com.rayhan.erp.controller; + +import com.rayhan.erp.model.DeliveryNote; +import com.rayhan.erp.model.SalesOrder; +import com.rayhan.erp.model.User; +import com.rayhan.erp.repository.UserRepository; +import com.rayhan.erp.security.services.UserDetailsImpl; +import com.rayhan.erp.service.SalesOrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/sales-orders") +public class SalesOrderController { + + @Autowired private SalesOrderService salesOrderService; + @Autowired private UserRepository userRepository; + + @GetMapping + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')") + public List getAllOrders() { + return salesOrderService.getAllSalesOrders(); + } + + @PostMapping + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')") + public ResponseEntity createOrder(@RequestBody SalesOrder order, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + User user = userRepository.findById(userDetails.getId()).orElse(null); + order.setCreePar(user); + return ResponseEntity.ok(salesOrderService.createSalesOrder(order)); + } + + @PostMapping("/{id}/deliver") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE', 'ROLE_MAGASINIER')") + public ResponseEntity createDelivery(@PathVariable Long id, + @RequestBody DeliveryNote bonLivraison, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + User user = userRepository.findById(userDetails.getId()).orElse(null); + return ResponseEntity.ok(salesOrderService.createDeliveryNote(id, bonLivraison, user)); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/controller/StockController.java b/backend/src/main/java/com/rayhan/erp/controller/StockController.java new file mode 100644 index 0000000..bc28313 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/controller/StockController.java @@ -0,0 +1,57 @@ +package com.rayhan.erp.controller; + +import com.rayhan.erp.model.Article; +import com.rayhan.erp.model.StockMovement; +import com.rayhan.erp.model.User; +import com.rayhan.erp.repository.ArticleRepository; +import com.rayhan.erp.repository.UserRepository; +import com.rayhan.erp.security.services.UserDetailsImpl; +import com.rayhan.erp.service.StockService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/stock") +public class StockController { + + @Autowired private StockService stockService; + @Autowired private ArticleRepository articleRepository; + @Autowired private UserRepository userRepository; + + @GetMapping("/historique/{articleId}") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_MAGASINIER', 'ROLE_RESPONSABLE_PRODUCTION')") + public List getHistorique(@PathVariable Long articleId) { + return stockService.getHistoriqueArticle(articleId); + } + + @PostMapping("/adjust") + @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_MAGASINIER')") + public ResponseEntity adjustStock(@RequestBody Map request, + @AuthenticationPrincipal UserDetailsImpl userDetails) { + Long articleId = Long.valueOf(request.get("articleId").toString()); + BigDecimal quantite = new BigDecimal(request.get("quantite").toString()); + String type = request.get("type").toString(); // "IN" ou "OUT" + String motif = request.getOrDefault("motif", "Ajustement manuel").toString(); + + Article article = articleRepository.findById(articleId) + .orElseThrow(() -> new RuntimeException("Article introuvable")); + User user = userRepository.findById(userDetails.getId()).orElse(null); + + StockMovement mouvement; + if ("IN".equals(type)) { + mouvement = stockService.entreeStock(article, quantite, "AJUSTEMENT", "ADJ", motif, user); + } else { + mouvement = stockService.sortieStock(article, quantite, "AJUSTEMENT", "ADJ", motif, user); + } + + return ResponseEntity.ok(mouvement); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/dto/request/LoginRequest.java b/backend/src/main/java/com/rayhan/erp/dto/request/LoginRequest.java new file mode 100644 index 0000000..64dd9ca --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/dto/request/LoginRequest.java @@ -0,0 +1,15 @@ +package com.rayhan.erp.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequest { + @NotBlank(message = "Le nom d'utilisateur est obligatoire") + private String username; + + @NotBlank(message = "Le mot de passe est obligatoire") + private String password; +} diff --git a/backend/src/main/java/com/rayhan/erp/dto/request/SignupRequest.java b/backend/src/main/java/com/rayhan/erp/dto/request/SignupRequest.java new file mode 100644 index 0000000..3c78247 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/dto/request/SignupRequest.java @@ -0,0 +1,31 @@ +package com.rayhan.erp.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +import java.util.Set; + +@Getter +@Setter +public class SignupRequest { + @NotBlank + @Size(min = 3, max = 20) + private String username; + + @NotBlank + @Size(max = 50) + @Email + private String email; + + @NotBlank + @Size(min = 6, max = 40) + private String password; + + private String firstName; + private String lastName; + + private Set roles; +} diff --git a/backend/src/main/java/com/rayhan/erp/dto/response/JwtResponse.java b/backend/src/main/java/com/rayhan/erp/dto/response/JwtResponse.java new file mode 100644 index 0000000..50fa0a8 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/dto/response/JwtResponse.java @@ -0,0 +1,30 @@ +package com.rayhan.erp.dto.response; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class JwtResponse { + private String token; + private String type = "Bearer"; + private Long id; + private String username; + private String email; + private String firstName; + private String lastName; + private List roles; + + public JwtResponse(String accessToken, Long id, String username, String email, + String firstName, String lastName, List roles) { + this.token = accessToken; + this.id = id; + this.username = username; + this.email = email; + this.firstName = firstName; + this.lastName = lastName; + this.roles = roles; + } +} diff --git a/backend/src/main/java/com/rayhan/erp/dto/response/MessageResponse.java b/backend/src/main/java/com/rayhan/erp/dto/response/MessageResponse.java new file mode 100644 index 0000000..c8f7128 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/dto/response/MessageResponse.java @@ -0,0 +1,12 @@ +package com.rayhan.erp.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class MessageResponse { + private String message; +} diff --git a/backend/src/main/java/com/rayhan/erp/model/Article.java b/backend/src/main/java/com/rayhan/erp/model/Article.java new file mode 100644 index 0000000..bd6374f --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/Article.java @@ -0,0 +1,50 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "articles") +@Getter +@Setter +@NoArgsConstructor +public class Article { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 30) + private String reference; + + @Column(nullable = false, length = 150) + private String designation; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private TypeArticle type; // MP, PF, PSF + + @Column(length = 20) + private String uniteMesure; // kg, unité, rouleau, m + + @Column(precision = 15, scale = 3) + private BigDecimal prixUnitaire = BigDecimal.ZERO; + + @Column(precision = 15, scale = 3) + private BigDecimal stockActuel = BigDecimal.ZERO; + + @Column(precision = 15, scale = 3) + private BigDecimal stockMinimum = BigDecimal.ZERO; + + private boolean actif = true; + + public enum TypeArticle { + MP, // Matière Première (HDPE, LDPE, colorants) + PSF, // Produit Semi-Fini (film tubulaire) + PF // Produit Fini (sacs, film rétractable) + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/BomLine.java b/backend/src/main/java/com/rayhan/erp/model/BomLine.java new file mode 100644 index 0000000..fd5437d --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/BomLine.java @@ -0,0 +1,41 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +/** + * Ligne de nomenclature (BOM — Bill of Materials). + * Définit quelle matière première (ou PSF) est nécessaire pour fabriquer un produit fini. + * Ex : Pour 1000 Sacs Bertel (PF), il faut 15 kg de HDPE (MP) + 0.5 kg de colorant. + */ +@Entity +@Table(name = "bom_lines", uniqueConstraints = { + @UniqueConstraint(columnNames = {"produit_fini_id", "composant_id"}) +}) +@Getter +@Setter +@NoArgsConstructor +public class BomLine { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "produit_fini_id") + private Article produitFini; // doit être de type PF ou PSF + + @ManyToOne(optional = false) + @JoinColumn(name = "composant_id") + private Article composant; // doit être de type MP ou PSF + + @Column(nullable = false, precision = 15, scale = 6) + private BigDecimal quantiteParUnite; // quantité de composant par unité de produit fini + + @Column(length = 20) + private String uniteMesure; // kg, g, L, unité +} diff --git a/backend/src/main/java/com/rayhan/erp/model/Client.java b/backend/src/main/java/com/rayhan/erp/model/Client.java new file mode 100644 index 0000000..ed124f5 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/Client.java @@ -0,0 +1,34 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "clients") +@PrimaryKeyJoinColumn(name = "tiers_id") +@Getter +@Setter +@NoArgsConstructor +public class Client extends Tiers { + + @Column(length = 30) + private String typeClient; // Grossiste, Détaillant, Industrie + + private BigDecimal plafondCredit = BigDecimal.ZERO; + + private Integer delaiPaiement = 30; // jours + + @Column(length = 50) + private String representantNom; + + @Column(length = 20) + private String representantTelephone; + + public Client(String raisonSociale, String matriculeFiscal, String telephone) { + super(raisonSociale, matriculeFiscal, telephone); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/DeliveryNote.java b/backend/src/main/java/com/rayhan/erp/model/DeliveryNote.java new file mode 100644 index 0000000..cdf1e47 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/DeliveryNote.java @@ -0,0 +1,53 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "delivery_notes") +@Getter +@Setter +@NoArgsConstructor +public class DeliveryNote { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 30) + private String reference; // ex: BL-2024-001 + + @ManyToOne(optional = false) + @JoinColumn(name = "sales_order_id") + private SalesOrder salesOrder; + + @Column(nullable = false) + private LocalDate dateLivraison = LocalDate.now(); + + @Column(length = 200) + private String adresseLivraison; + + @Column(length = 200) + private String notes; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private StatutLivraison statut = StatutLivraison.LIVRE; + + @OneToMany(mappedBy = "deliveryNote", cascade = CascadeType.ALL, orphanRemoval = true) + private List lignes = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "created_by") + private User creePar; + + public enum StatutLivraison { + EN_PREPARATION, LIVRE, RETOURNE_PARTIEL + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/DeliveryNoteLine.java b/backend/src/main/java/com/rayhan/erp/model/DeliveryNoteLine.java new file mode 100644 index 0000000..1f68c6b --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/DeliveryNoteLine.java @@ -0,0 +1,35 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "delivery_note_lines") +@Getter +@Setter +@NoArgsConstructor +public class DeliveryNoteLine { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "delivery_note_id") + private DeliveryNote deliveryNote; + + @ManyToOne(optional = false) + @JoinColumn(name = "sales_order_line_id") + private SalesOrderLine salesOrderLine; + + @ManyToOne(optional = false) + @JoinColumn(name = "article_id") + private Article article; + + @Column(nullable = false, precision = 15, scale = 3) + private BigDecimal quantiteLivree; +} diff --git a/backend/src/main/java/com/rayhan/erp/model/ERole.java b/backend/src/main/java/com/rayhan/erp/model/ERole.java new file mode 100644 index 0000000..ff5d9c9 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/ERole.java @@ -0,0 +1,10 @@ +package com.rayhan.erp.model; + +public enum ERole { + ROLE_PDG, // Gérant (accès complet) + ROLE_RESPONSABLE_VENTE, // Responsable Commercial + ROLE_RESPONSABLE_ACHAT, // Responsable Achats + ROLE_RESPONSABLE_PRODUCTION, // Responsable Production + ROLE_MAGASINIER, // Magasinier + ROLE_RH // Responsable RH +} diff --git a/backend/src/main/java/com/rayhan/erp/model/Fournisseur.java b/backend/src/main/java/com/rayhan/erp/model/Fournisseur.java new file mode 100644 index 0000000..daa43e3 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/Fournisseur.java @@ -0,0 +1,30 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "fournisseurs") +@PrimaryKeyJoinColumn(name = "tiers_id") +@Getter +@Setter +@NoArgsConstructor +public class Fournisseur extends Tiers { + + @Column(length = 100) + private String pays = "Tunisie"; + + @Column(length = 50) + private String categorieProduit; // Matières plastiques, Emballages... + + private Integer delaiLivraison = 7; // jours + + @Column(length = 30) + private String modePaiement; // Virement, Chèque, Espèces + + public Fournisseur(String raisonSociale, String matriculeFiscal, String telephone) { + super(raisonSociale, matriculeFiscal, telephone); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/GoodsReceipt.java b/backend/src/main/java/com/rayhan/erp/model/GoodsReceipt.java new file mode 100644 index 0000000..ca2d7f6 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/GoodsReceipt.java @@ -0,0 +1,42 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "goods_receipts") +@Getter +@Setter +@NoArgsConstructor +public class GoodsReceipt { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 30) + private String reference; // ex: BR-2024-001 + + @ManyToOne(optional = false) + @JoinColumn(name = "purchase_order_id") + private PurchaseOrder purchaseOrder; + + @Column(nullable = false) + private LocalDate dateReception = LocalDate.now(); + + @Column(length = 200) + private String notes; + + @OneToMany(mappedBy = "goodsReceipt", cascade = CascadeType.ALL, orphanRemoval = true) + private List lignes = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "created_by") + private User creePar; +} diff --git a/backend/src/main/java/com/rayhan/erp/model/GoodsReceiptLine.java b/backend/src/main/java/com/rayhan/erp/model/GoodsReceiptLine.java new file mode 100644 index 0000000..c4c5667 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/GoodsReceiptLine.java @@ -0,0 +1,38 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "goods_receipt_lines") +@Getter +@Setter +@NoArgsConstructor +public class GoodsReceiptLine { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "goods_receipt_id") + private GoodsReceipt goodsReceipt; + + @ManyToOne(optional = false) + @JoinColumn(name = "purchase_order_line_id") + private PurchaseOrderLine purchaseOrderLine; + + @ManyToOne(optional = false) + @JoinColumn(name = "article_id") + private Article article; + + @Column(nullable = false, precision = 15, scale = 3) + private BigDecimal quantiteRecue; + + @Column(length = 200) + private String observations; +} diff --git a/backend/src/main/java/com/rayhan/erp/model/ProductionOrder.java b/backend/src/main/java/com/rayhan/erp/model/ProductionOrder.java new file mode 100644 index 0000000..d0d2fda --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/ProductionOrder.java @@ -0,0 +1,56 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Entity +@Table(name = "production_orders") +@Getter +@Setter +@NoArgsConstructor +public class ProductionOrder { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 30) + private String reference; // ex: OF-2024-001 + + @ManyToOne(optional = false) + @JoinColumn(name = "produit_fini_id") + private Article produitFini; + + @Column(nullable = false, precision = 15, scale = 3) + private BigDecimal quantitePlanifiee; + + @Column(precision = 15, scale = 3) + private BigDecimal quantiteRealisee = BigDecimal.ZERO; + + @Column(nullable = false) + private LocalDate datePlanifiee; + + private LocalDateTime dateLancement; + private LocalDateTime dateTerminaison; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private StatutOF statut = StatutOF.PLANIFIE; + + @Column(length = 500) + private String notes; + + @ManyToOne + @JoinColumn(name = "created_by") + private User creePar; + + public enum StatutOF { + PLANIFIE, LANCE, EN_COURS, TERMINE, ANNULE + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/PurchaseOrder.java b/backend/src/main/java/com/rayhan/erp/model/PurchaseOrder.java new file mode 100644 index 0000000..ef11140 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/PurchaseOrder.java @@ -0,0 +1,62 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "purchase_orders") +@Getter +@Setter +@NoArgsConstructor +public class PurchaseOrder { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 30) + private String reference; // ex: BC-2024-001 + + @ManyToOne(optional = false) + @JoinColumn(name = "fournisseur_id") + private Fournisseur fournisseur; + + @Column(nullable = false) + private LocalDate dateCommande = LocalDate.now(); + + private LocalDate dateLivraisonPrevue; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private StatutCommande statut = StatutCommande.BROUILLON; + + @Column(precision = 15, scale = 3) + private BigDecimal totalHT = BigDecimal.ZERO; + + @Column(precision = 15, scale = 3) + private BigDecimal totalTVA = BigDecimal.ZERO; + + @Column(precision = 15, scale = 3) + private BigDecimal totalTTC = BigDecimal.ZERO; + + @Column(length = 500) + private String notes; + + @OneToMany(mappedBy = "purchaseOrder", cascade = CascadeType.ALL, orphanRemoval = true) + private List lignes = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "created_by") + private User creePar; + + public enum StatutCommande { + BROUILLON, CONFIRMEE, PARTIELLEMENT_RECUE, COMPLETEMENT_RECUE, ANNULEE + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/PurchaseOrderLine.java b/backend/src/main/java/com/rayhan/erp/model/PurchaseOrderLine.java new file mode 100644 index 0000000..3ac27b2 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/PurchaseOrderLine.java @@ -0,0 +1,46 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "purchase_order_lines") +@Getter +@Setter +@NoArgsConstructor +public class PurchaseOrderLine { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "purchase_order_id") + private PurchaseOrder purchaseOrder; + + @ManyToOne(optional = false) + @JoinColumn(name = "article_id") + private Article article; + + @Column(nullable = false, precision = 15, scale = 3) + private BigDecimal quantiteCommandee; + + @Column(precision = 15, scale = 3) + private BigDecimal quantiteRecue = BigDecimal.ZERO; + + @Column(nullable = false, precision = 15, scale = 3) + private BigDecimal prixUnitaireHT; + + @Column(precision = 5, scale = 2) + private BigDecimal tauxTVA = new BigDecimal("19.00"); + + @Column(precision = 15, scale = 3) + private BigDecimal montantHT; + + @Column(precision = 15, scale = 3) + private BigDecimal montantTTC; +} diff --git a/backend/src/main/java/com/rayhan/erp/model/Role.java b/backend/src/main/java/com/rayhan/erp/model/Role.java new file mode 100644 index 0000000..14c98d6 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/Role.java @@ -0,0 +1,26 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "roles") +@Getter +@Setter +@NoArgsConstructor +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Enumerated(EnumType.STRING) + @Column(length = 30, unique = true, nullable = false) + private ERole name; + + public Role(ERole name) { + this.name = name; + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/SalesOrder.java b/backend/src/main/java/com/rayhan/erp/model/SalesOrder.java new file mode 100644 index 0000000..63a4e7b --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/SalesOrder.java @@ -0,0 +1,62 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "sales_orders") +@Getter +@Setter +@NoArgsConstructor +public class SalesOrder { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 30) + private String reference; // ex: CC-2024-001 + + @ManyToOne(optional = false) + @JoinColumn(name = "client_id") + private Client client; + + @Column(nullable = false) + private LocalDate dateCommande = LocalDate.now(); + + private LocalDate dateLivraisonSouhaitee; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 25) + private StatutCommande statut = StatutCommande.CONFIRMEE; + + @Column(precision = 15, scale = 3) + private BigDecimal totalHT = BigDecimal.ZERO; + + @Column(precision = 15, scale = 3) + private BigDecimal totalTVA = BigDecimal.ZERO; + + @Column(precision = 15, scale = 3) + private BigDecimal totalTTC = BigDecimal.ZERO; + + @Column(length = 500) + private String notes; + + @OneToMany(mappedBy = "salesOrder", cascade = CascadeType.ALL, orphanRemoval = true) + private List lignes = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "created_by") + private User creePar; + + public enum StatutCommande { + CONFIRMEE, EN_PREPARATION, PARTIELLEMENT_LIVREE, COMPLETEMENT_LIVREE, ANNULEE + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/SalesOrderLine.java b/backend/src/main/java/com/rayhan/erp/model/SalesOrderLine.java new file mode 100644 index 0000000..397ec0c --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/SalesOrderLine.java @@ -0,0 +1,46 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "sales_order_lines") +@Getter +@Setter +@NoArgsConstructor +public class SalesOrderLine { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "sales_order_id") + private SalesOrder salesOrder; + + @ManyToOne(optional = false) + @JoinColumn(name = "article_id") + private Article article; + + @Column(nullable = false, precision = 15, scale = 3) + private BigDecimal quantiteCommandee; + + @Column(precision = 15, scale = 3) + private BigDecimal quantiteLivree = BigDecimal.ZERO; + + @Column(nullable = false, precision = 15, scale = 3) + private BigDecimal prixUnitaireHT; + + @Column(precision = 5, scale = 2) + private BigDecimal tauxTVA = new BigDecimal("19.00"); + + @Column(precision = 15, scale = 3) + private BigDecimal montantHT; + + @Column(precision = 15, scale = 3) + private BigDecimal montantTTC; +} diff --git a/backend/src/main/java/com/rayhan/erp/model/StockMovement.java b/backend/src/main/java/com/rayhan/erp/model/StockMovement.java new file mode 100644 index 0000000..b286ba8 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/StockMovement.java @@ -0,0 +1,59 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "stock_movements") +@Getter +@Setter +@NoArgsConstructor +public class StockMovement { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "article_id") + private Article article; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private TypeMouvement type; // IN = entrée, OUT = sortie + + @Column(nullable = false, precision = 15, scale = 3) + private BigDecimal quantite; + + @Column(precision = 15, scale = 3) + private BigDecimal stockAvant; + + @Column(precision = 15, scale = 3) + private BigDecimal stockApres; + + @Column(length = 50) + private String sourceDocument; // BON_RECEPTION, BON_LIVRAISON, OF, AJUSTEMENT + + @Column(length = 30) + private String referenceDocument; // ex: BR-2024-001 + + @Column(length = 200) + private String motif; + + @Column(nullable = false) + private LocalDateTime dateHeure = LocalDateTime.now(); + + @ManyToOne + @JoinColumn(name = "user_id") + private User creePar; + + public enum TypeMouvement { + IN, // Entrée stock + OUT // Sortie stock + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/Tiers.java b/backend/src/main/java/com/rayhan/erp/model/Tiers.java new file mode 100644 index 0000000..cfbc671 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/Tiers.java @@ -0,0 +1,45 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "tiers") +@Inheritance(strategy = InheritanceType.JOINED) +@Getter +@Setter +@NoArgsConstructor +public abstract class Tiers { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String raisonSociale; + + @Column(length = 20) + private String matriculeFiscal; + + @Column(length = 200) + private String adresse; + + @Column(length = 20) + private String telephone; + + @Column(length = 100) + private String email; + + @Column(length = 50) + private String ville; + + private boolean actif = true; + + public Tiers(String raisonSociale, String matriculeFiscal, String telephone) { + this.raisonSociale = raisonSociale; + this.matriculeFiscal = matriculeFiscal; + this.telephone = telephone; + } +} diff --git a/backend/src/main/java/com/rayhan/erp/model/User.java b/backend/src/main/java/com/rayhan/erp/model/User.java new file mode 100644 index 0000000..0e795d5 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/model/User.java @@ -0,0 +1,55 @@ +package com.rayhan.erp.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "users", uniqueConstraints = { + @UniqueConstraint(columnNames = "username"), + @UniqueConstraint(columnNames = "email") +}) +@Getter +@Setter +@NoArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 50) + private String username; + + @Column(nullable = false, length = 100) + private String email; + + @Column(nullable = false) + private String password; + + @Column(length = 50) + private String firstName; + + @Column(length = 50) + private String lastName; + + private boolean enabled = true; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id")) + private Set roles = new HashSet<>(); + + public User(String username, String email, String password, String firstName, String lastName) { + this.username = username; + this.email = email; + this.password = password; + this.firstName = firstName; + this.lastName = lastName; + } +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/ArticleRepository.java b/backend/src/main/java/com/rayhan/erp/repository/ArticleRepository.java new file mode 100644 index 0000000..2975786 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/ArticleRepository.java @@ -0,0 +1,17 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.Article; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ArticleRepository extends JpaRepository { + Optional
findByReference(String reference); + Boolean existsByReference(String reference); + List
findByType(Article.TypeArticle type); + List
findByActifTrue(); + List
findByStockActuelLessThanEqualAndActifTrue(java.math.BigDecimal seuil); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/BomLineRepository.java b/backend/src/main/java/com/rayhan/erp/repository/BomLineRepository.java new file mode 100644 index 0000000..db5e60f --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/BomLineRepository.java @@ -0,0 +1,13 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.BomLine; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BomLineRepository extends JpaRepository { + List findByProduitFiniId(Long produitFiniId); + void deleteByProduitFiniId(Long produitFiniId); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/ClientRepository.java b/backend/src/main/java/com/rayhan/erp/repository/ClientRepository.java new file mode 100644 index 0000000..f716794 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/ClientRepository.java @@ -0,0 +1,13 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.Client; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ClientRepository extends JpaRepository { + List findByActifTrue(); + List findByRaisonSocialeContainingIgnoreCase(String search); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/DeliveryNoteRepository.java b/backend/src/main/java/com/rayhan/erp/repository/DeliveryNoteRepository.java new file mode 100644 index 0000000..2b74f60 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/DeliveryNoteRepository.java @@ -0,0 +1,13 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.DeliveryNote; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface DeliveryNoteRepository extends JpaRepository { + Optional findByReference(String reference); + boolean existsByReference(String reference); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/FournisseurRepository.java b/backend/src/main/java/com/rayhan/erp/repository/FournisseurRepository.java new file mode 100644 index 0000000..ed8f3de --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/FournisseurRepository.java @@ -0,0 +1,13 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.Fournisseur; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface FournisseurRepository extends JpaRepository { + List findByActifTrue(); + List findByRaisonSocialeContainingIgnoreCase(String search); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/GoodsReceiptRepository.java b/backend/src/main/java/com/rayhan/erp/repository/GoodsReceiptRepository.java new file mode 100644 index 0000000..146f5ca --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/GoodsReceiptRepository.java @@ -0,0 +1,13 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.GoodsReceipt; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface GoodsReceiptRepository extends JpaRepository { + Optional findByReference(String reference); + boolean existsByReference(String reference); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/ProductionOrderRepository.java b/backend/src/main/java/com/rayhan/erp/repository/ProductionOrderRepository.java new file mode 100644 index 0000000..7a927ca --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/ProductionOrderRepository.java @@ -0,0 +1,16 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.ProductionOrder; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ProductionOrderRepository extends JpaRepository { + Optional findByReference(String reference); + boolean existsByReference(String reference); + List findByStatutOrderByDatePlanifieeDesc(ProductionOrder.StatutOF statut); + long countByStatut(ProductionOrder.StatutOF statut); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/PurchaseOrderRepository.java b/backend/src/main/java/com/rayhan/erp/repository/PurchaseOrderRepository.java new file mode 100644 index 0000000..81cfa1e --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/PurchaseOrderRepository.java @@ -0,0 +1,16 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.PurchaseOrder; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PurchaseOrderRepository extends JpaRepository { + Optional findByReference(String reference); + boolean existsByReference(String reference); + List findByStatutOrderByDateCommandeDesc(PurchaseOrder.StatutCommande statut); + long countByStatutIn(List statuts); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/RoleRepository.java b/backend/src/main/java/com/rayhan/erp/repository/RoleRepository.java new file mode 100644 index 0000000..84db17a --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/RoleRepository.java @@ -0,0 +1,13 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.ERole; +import com.rayhan.erp.model.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(ERole name); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/SalesOrderRepository.java b/backend/src/main/java/com/rayhan/erp/repository/SalesOrderRepository.java new file mode 100644 index 0000000..bcca810 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/SalesOrderRepository.java @@ -0,0 +1,23 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.SalesOrder; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Repository +public interface SalesOrderRepository extends JpaRepository { + Optional findByReference(String reference); + boolean existsByReference(String reference); + List findByStatutOrderByDateCommandeDesc(SalesOrder.StatutCommande statut); + + @Query("SELECT COALESCE(SUM(s.totalTTC), 0) FROM SalesOrder s WHERE s.dateCommande BETWEEN :debut AND :fin") + BigDecimal sumTotalTTCByDateCommandeBetween(LocalDate debut, LocalDate fin); + + long countByDateCommandeBetween(LocalDate debut, LocalDate fin); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/StockMovementRepository.java b/backend/src/main/java/com/rayhan/erp/repository/StockMovementRepository.java new file mode 100644 index 0000000..c45266f --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/StockMovementRepository.java @@ -0,0 +1,14 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.StockMovement; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface StockMovementRepository extends JpaRepository { + List findByArticleIdOrderByDateHeureDesc(Long articleId); + List findByDateHeureBetweenOrderByDateHeureDesc(LocalDateTime debut, LocalDateTime fin); +} diff --git a/backend/src/main/java/com/rayhan/erp/repository/UserRepository.java b/backend/src/main/java/com/rayhan/erp/repository/UserRepository.java new file mode 100644 index 0000000..abcb647 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/repository/UserRepository.java @@ -0,0 +1,14 @@ +package com.rayhan.erp.repository; + +import com.rayhan.erp.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + Boolean existsByUsername(String username); + Boolean existsByEmail(String email); +} diff --git a/backend/src/main/java/com/rayhan/erp/security/jwt/AuthEntryPointJwt.java b/backend/src/main/java/com/rayhan/erp/security/jwt/AuthEntryPointJwt.java new file mode 100644 index 0000000..bf8d5bc --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/security/jwt/AuthEntryPointJwt.java @@ -0,0 +1,39 @@ +package com.rayhan.erp.security.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Component +public class AuthEntryPointJwt implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException { + logger.error("Erreur d'accès non autorisé : {}", authException.getMessage()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + final Map body = new HashMap<>(); + body.put("status", HttpServletResponse.SC_UNAUTHORIZED); + body.put("error", "Non autorisé"); + body.put("message", authException.getMessage()); + body.put("path", request.getServletPath()); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), body); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/security/jwt/AuthTokenFilter.java b/backend/src/main/java/com/rayhan/erp/security/jwt/AuthTokenFilter.java new file mode 100644 index 0000000..3564f0d --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/security/jwt/AuthTokenFilter.java @@ -0,0 +1,57 @@ +package com.rayhan.erp.security.jwt; + +import com.rayhan.erp.security.services.UserDetailsServiceImpl; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class AuthTokenFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private UserDetailsServiceImpl userDetailsService; + + private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + String jwt = parseJwt(request); + if (jwt != null && jwtUtils.validateJwtToken(jwt)) { + String username = jwtUtils.getUsernameFromJwtToken(jwt); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("Impossible de définir l'authentification : {}", e.getMessage()); + } + filterChain.doFilter(request, response); + } + + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + return null; + } +} diff --git a/backend/src/main/java/com/rayhan/erp/security/jwt/JwtUtils.java b/backend/src/main/java/com/rayhan/erp/security/jwt/JwtUtils.java new file mode 100644 index 0000000..aca1c91 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/security/jwt/JwtUtils.java @@ -0,0 +1,61 @@ +package com.rayhan.erp.security.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Component +public class JwtUtils { + + private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); + + @Value("${rayhan.erp.jwtSecret}") + private String jwtSecret; + + @Value("${rayhan.erp.jwtExpirationMs}") + private int jwtExpirationMs; + + private SecretKey key() { + return Keys.hmacShaKeyFor(jwtSecret.getBytes()); + } + + public String generateJwtToken(Authentication authentication) { + org.springframework.security.core.userdetails.UserDetails userPrincipal = + (org.springframework.security.core.userdetails.UserDetails) authentication.getPrincipal(); + return Jwts.builder() + .subject(userPrincipal.getUsername()) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs)) + .signWith(key()) + .compact(); + } + + public String getUsernameFromJwtToken(String token) { + return Jwts.parser().verifyWith(key()).build() + .parseSignedClaims(token).getPayload().getSubject(); + } + + public boolean validateJwtToken(String authToken) { + try { + Jwts.parser().verifyWith(key()).build().parseSignedClaims(authToken); + return true; + } catch (MalformedJwtException e) { + logger.error("Token JWT invalide : {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("Token JWT expiré : {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("Token JWT non supporté : {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims vide : {}", e.getMessage()); + } + return false; + } +} diff --git a/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsImpl.java b/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsImpl.java new file mode 100644 index 0000000..414f8c6 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsImpl.java @@ -0,0 +1,66 @@ +package com.rayhan.erp.security.services; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.rayhan.erp.model.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class UserDetailsImpl implements UserDetails { + + private Long id; + private String username; + private String email; + private String firstName; + private String lastName; + + @JsonIgnore + private String password; + + private Collection authorities; + + public UserDetailsImpl(Long id, String username, String email, + String firstName, String lastName, + String password, + Collection authorities) { + this.id = id; + this.username = username; + this.email = email; + this.firstName = firstName; + this.lastName = lastName; + this.password = password; + this.authorities = authorities; + } + + public static UserDetailsImpl build(User user) { + List authorities = user.getRoles().stream() + .map(role -> new SimpleGrantedAuthority(role.getName().name())) + .collect(Collectors.toList()); + + return new UserDetailsImpl( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getFirstName(), + user.getLastName(), + user.getPassword(), + authorities); + } + + public Long getId() { return id; } + public String getEmail() { return email; } + public String getFirstName() { return firstName; } + public String getLastName() { return lastName; } + + @Override public String getUsername() { return username; } + @Override public String getPassword() { return password; } + @Override public Collection getAuthorities() { return authorities; } + @Override public boolean isAccountNonExpired() { return true; } + @Override public boolean isAccountNonLocked() { return true; } + @Override public boolean isCredentialsNonExpired() { return true; } + @Override public boolean isEnabled() { return true; } +} diff --git a/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsServiceImpl.java b/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsServiceImpl.java new file mode 100644 index 0000000..7f3da00 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsServiceImpl.java @@ -0,0 +1,26 @@ +package com.rayhan.erp.security.services; + +import com.rayhan.erp.model.User; +import com.rayhan.erp.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + UserRepository userRepository; + + @Override + @Transactional + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException( + "Utilisateur introuvable : " + username)); + return UserDetailsImpl.build(user); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/service/ProductionOrderService.java b/backend/src/main/java/com/rayhan/erp/service/ProductionOrderService.java new file mode 100644 index 0000000..d44903f --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/service/ProductionOrderService.java @@ -0,0 +1,118 @@ +package com.rayhan.erp.service; + +import com.rayhan.erp.model.*; +import com.rayhan.erp.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class ProductionOrderService { + + @Autowired private ProductionOrderRepository productionOrderRepository; + @Autowired private BomLineRepository bomLineRepository; + @Autowired private ArticleRepository articleRepository; + @Autowired private StockService stockService; + + private static int seqOF = 1; + + private String generateRefOF() { + return "OF-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqOF++); + } + + /** + * Planifie un ordre de fabrication après vérification des matières premières. + */ + @Transactional + public ProductionOrder planifierOF(Long produitFiniId, BigDecimal quantite, + LocalDate datePlanifiee, User user) { + Article produitFini = articleRepository.findById(produitFiniId) + .orElseThrow(() -> new RuntimeException("Produit introuvable : " + produitFiniId)); + + List bom = bomLineRepository.findByProduitFiniId(produitFiniId); + if (bom.isEmpty()) { + throw new RuntimeException("Aucune nomenclature définie pour " + produitFini.getDesignation()); + } + + // Vérification du stock matières premières + for (BomLine ligne : bom) { + BigDecimal qteNecessaire = ligne.getQuantiteParUnite().multiply(quantite); + Article composant = ligne.getComposant(); + if (composant.getStockActuel().compareTo(qteNecessaire) < 0) { + throw new IllegalStateException( + "Stock insuffisant de " + composant.getDesignation() + + " : disponible " + composant.getStockActuel() + + ", nécessaire " + qteNecessaire); + } + } + + ProductionOrder of = new ProductionOrder(); + of.setReference(generateRefOF()); + of.setProduitFini(produitFini); + of.setQuantitePlanifiee(quantite); + of.setDatePlanifiee(datePlanifiee); + of.setStatut(ProductionOrder.StatutOF.PLANIFIE); + of.setCreePar(user); + + return productionOrderRepository.save(of); + } + + /** + * Lance un OF : consomme les matières premières du stock. + */ + @Transactional + public ProductionOrder lancerOF(Long ofId, User user) { + ProductionOrder of = productionOrderRepository.findById(ofId) + .orElseThrow(() -> new RuntimeException("OF introuvable : " + ofId)); + + if (of.getStatut() != ProductionOrder.StatutOF.PLANIFIE) { + throw new IllegalStateException("L'OF doit être à l'état PLANIFIE pour être lancé."); + } + + List bom = bomLineRepository.findByProduitFiniId(of.getProduitFini().getId()); + + for (BomLine ligne : bom) { + BigDecimal qteConsommee = ligne.getQuantiteParUnite().multiply(of.getQuantitePlanifiee()); + Article composant = articleRepository.findById(ligne.getComposant().getId()).get(); + stockService.sortieStock(composant, qteConsommee, + "ORDRE_FABRICATION", of.getReference(), + "Lancement OF " + of.getReference(), user); + } + + of.setStatut(ProductionOrder.StatutOF.LANCE); + of.setDateLancement(LocalDateTime.now()); + return productionOrderRepository.save(of); + } + + /** + * Termine un OF : entre le produit fini en stock. + */ + @Transactional + public ProductionOrder terminerOF(Long ofId, BigDecimal quantiteRealisee, User user) { + ProductionOrder of = productionOrderRepository.findById(ofId) + .orElseThrow(() -> new RuntimeException("OF introuvable : " + ofId)); + + if (of.getStatut() != ProductionOrder.StatutOF.LANCE) { + throw new IllegalStateException("L'OF doit être à l'état LANCE pour être terminé."); + } + + Article produitFini = articleRepository.findById(of.getProduitFini().getId()).get(); + stockService.entreeStock(produitFini, quantiteRealisee, + "ORDRE_FABRICATION", of.getReference(), + "Production OF " + of.getReference(), user); + + of.setQuantiteRealisee(quantiteRealisee); + of.setStatut(ProductionOrder.StatutOF.TERMINE); + of.setDateTerminaison(LocalDateTime.now()); + return productionOrderRepository.save(of); + } + + public List getAllOFs() { + return productionOrderRepository.findAll(); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/service/PurchaseOrderService.java b/backend/src/main/java/com/rayhan/erp/service/PurchaseOrderService.java new file mode 100644 index 0000000..eeaa602 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/service/PurchaseOrderService.java @@ -0,0 +1,89 @@ +package com.rayhan.erp.service; + +import com.rayhan.erp.model.*; +import com.rayhan.erp.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Service +public class PurchaseOrderService { + + @Autowired private PurchaseOrderRepository purchaseOrderRepository; + @Autowired private GoodsReceiptRepository goodsReceiptRepository; + @Autowired private ArticleRepository articleRepository; + @Autowired private StockService stockService; + + private static int seqBC = 1; + private static int seqBR = 1; + + private String generateRefBC() { + return "BC-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqBC++); + } + + private String generateRefBR() { + return "BR-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqBR++); + } + + @Transactional + public PurchaseOrder createPurchaseOrder(PurchaseOrder order) { + order.setReference(generateRefBC()); + order.setStatut(PurchaseOrder.StatutCommande.CONFIRMEE); + + BigDecimal totalHT = BigDecimal.ZERO; + for (PurchaseOrderLine ligne : order.getLignes()) { + ligne.setPurchaseOrder(order); + BigDecimal montantHT = ligne.getPrixUnitaireHT() + .multiply(ligne.getQuantiteCommandee()); + ligne.setMontantHT(montantHT); + BigDecimal tva = montantHT.multiply(ligne.getTauxTVA().divide(new BigDecimal("100"))); + ligne.setMontantTTC(montantHT.add(tva)); + totalHT = totalHT.add(montantHT); + } + + order.setTotalHT(totalHT); + BigDecimal tvaGlobal = totalHT.multiply(new BigDecimal("0.19")); + order.setTotalTVA(tvaGlobal); + order.setTotalTTC(totalHT.add(tvaGlobal)); + + return purchaseOrderRepository.save(order); + } + + @Transactional + public GoodsReceipt receiveGoods(Long purchaseOrderId, GoodsReceipt reception, User user) { + PurchaseOrder order = purchaseOrderRepository.findById(purchaseOrderId) + .orElseThrow(() -> new RuntimeException("Commande introuvable : " + purchaseOrderId)); + + reception.setReference(generateRefBR()); + reception.setPurchaseOrder(order); + reception.setCreePar(user); + + for (GoodsReceiptLine ligne : reception.getLignes()) { + ligne.setGoodsReceipt(reception); + Article article = articleRepository.findById(ligne.getArticle().getId()) + .orElseThrow(() -> new RuntimeException("Article introuvable")); + ligne.setArticle(article); + + stockService.entreeStock(article, ligne.getQuantiteRecue(), + "BON_RECEPTION", reception.getReference(), + "Réception commande " + order.getReference(), user); + + ligne.getPurchaseOrderLine().setQuantiteRecue( + ligne.getPurchaseOrderLine().getQuantiteRecue().add(ligne.getQuantiteRecue())); + } + + order.setStatut(PurchaseOrder.StatutCommande.COMPLETEMENT_RECUE); + purchaseOrderRepository.save(order); + + return goodsReceiptRepository.save(reception); + } + + public List getAllPurchaseOrders() { + return purchaseOrderRepository.findAll(); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/service/SalesOrderService.java b/backend/src/main/java/com/rayhan/erp/service/SalesOrderService.java new file mode 100644 index 0000000..870e053 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/service/SalesOrderService.java @@ -0,0 +1,97 @@ +package com.rayhan.erp.service; + +import com.rayhan.erp.model.*; +import com.rayhan.erp.repository.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +@Service +public class SalesOrderService { + + @Autowired private SalesOrderRepository salesOrderRepository; + @Autowired private DeliveryNoteRepository deliveryNoteRepository; + @Autowired private ArticleRepository articleRepository; + @Autowired private StockService stockService; + + private static int seqCC = 1; + private static int seqBL = 1; + + private String generateRefCC() { + return "CC-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqCC++); + } + + private String generateRefBL() { + return "BL-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqBL++); + } + + @Transactional + public SalesOrder createSalesOrder(SalesOrder order) { + // Vérification du stock disponible avant validation + for (SalesOrderLine ligne : order.getLignes()) { + Article article = articleRepository.findById(ligne.getArticle().getId()) + .orElseThrow(() -> new RuntimeException("Article introuvable")); + if (article.getStockActuel().compareTo(ligne.getQuantiteCommandee()) < 0) { + throw new IllegalStateException( + "Stock insuffisant pour " + article.getDesignation()); + } + } + + order.setReference(generateRefCC()); + order.setStatut(SalesOrder.StatutCommande.CONFIRMEE); + + BigDecimal totalHT = BigDecimal.ZERO; + for (SalesOrderLine ligne : order.getLignes()) { + ligne.setSalesOrder(order); + BigDecimal montantHT = ligne.getPrixUnitaireHT().multiply(ligne.getQuantiteCommandee()); + ligne.setMontantHT(montantHT); + BigDecimal tva = montantHT.multiply(ligne.getTauxTVA().divide(new BigDecimal("100"))); + ligne.setMontantTTC(montantHT.add(tva)); + totalHT = totalHT.add(montantHT); + } + + order.setTotalHT(totalHT); + BigDecimal tvaGlobal = totalHT.multiply(new BigDecimal("0.19")); + order.setTotalTVA(tvaGlobal); + order.setTotalTTC(totalHT.add(tvaGlobal)); + + return salesOrderRepository.save(order); + } + + @Transactional + public DeliveryNote createDeliveryNote(Long salesOrderId, DeliveryNote bonLivraison, User user) { + SalesOrder order = salesOrderRepository.findById(salesOrderId) + .orElseThrow(() -> new RuntimeException("Commande introuvable : " + salesOrderId)); + + bonLivraison.setReference(generateRefBL()); + bonLivraison.setSalesOrder(order); + bonLivraison.setCreePar(user); + + for (DeliveryNoteLine ligne : bonLivraison.getLignes()) { + ligne.setDeliveryNote(bonLivraison); + Article article = articleRepository.findById(ligne.getArticle().getId()) + .orElseThrow(() -> new RuntimeException("Article introuvable")); + ligne.setArticle(article); + + stockService.sortieStock(article, ligne.getQuantiteLivree(), + "BON_LIVRAISON", bonLivraison.getReference(), + "Livraison commande " + order.getReference(), user); + + ligne.getSalesOrderLine().setQuantiteLivree( + ligne.getSalesOrderLine().getQuantiteLivree().add(ligne.getQuantiteLivree())); + } + + order.setStatut(SalesOrder.StatutCommande.COMPLETEMENT_LIVREE); + salesOrderRepository.save(order); + + return deliveryNoteRepository.save(bonLivraison); + } + + public List getAllSalesOrders() { + return salesOrderRepository.findAll(); + } +} diff --git a/backend/src/main/java/com/rayhan/erp/service/StockService.java b/backend/src/main/java/com/rayhan/erp/service/StockService.java new file mode 100644 index 0000000..078e5f6 --- /dev/null +++ b/backend/src/main/java/com/rayhan/erp/service/StockService.java @@ -0,0 +1,95 @@ +package com.rayhan.erp.service; + +import com.rayhan.erp.model.Article; +import com.rayhan.erp.model.StockMovement; +import com.rayhan.erp.model.User; +import com.rayhan.erp.repository.ArticleRepository; +import com.rayhan.erp.repository.StockMovementRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +@Service +public class StockService { + + @Autowired + private ArticleRepository articleRepository; + + @Autowired + private StockMovementRepository stockMovementRepository; + + /** + * Effectue une entrée en stock (réception achat, production terminée, ajustement +) + */ + @Transactional + public StockMovement entreeStock(Article article, BigDecimal quantite, + String sourceDocument, String referenceDocument, + String motif, User user) { + if (quantite.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("La quantité doit être positive."); + } + + BigDecimal stockAvant = article.getStockActuel(); + BigDecimal stockApres = stockAvant.add(quantite); + + article.setStockActuel(stockApres); + articleRepository.save(article); + + StockMovement mouvement = new StockMovement(); + mouvement.setArticle(article); + mouvement.setType(StockMovement.TypeMouvement.IN); + mouvement.setQuantite(quantite); + mouvement.setStockAvant(stockAvant); + mouvement.setStockApres(stockApres); + mouvement.setSourceDocument(sourceDocument); + mouvement.setReferenceDocument(referenceDocument); + mouvement.setMotif(motif); + mouvement.setCreePar(user); + + return stockMovementRepository.save(mouvement); + } + + /** + * Effectue une sortie de stock (livraison client, lancement OF, ajustement -) + * Lance une exception si stock insuffisant. + */ + @Transactional + public StockMovement sortieStock(Article article, BigDecimal quantite, + String sourceDocument, String referenceDocument, + String motif, User user) { + if (quantite.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("La quantité doit être positive."); + } + if (article.getStockActuel().compareTo(quantite) < 0) { + throw new IllegalStateException( + "Stock insuffisant pour " + article.getDesignation() + + " : disponible " + article.getStockActuel() + ", demandé " + quantite); + } + + BigDecimal stockAvant = article.getStockActuel(); + BigDecimal stockApres = stockAvant.subtract(quantite); + + article.setStockActuel(stockApres); + articleRepository.save(article); + + StockMovement mouvement = new StockMovement(); + mouvement.setArticle(article); + mouvement.setType(StockMovement.TypeMouvement.OUT); + mouvement.setQuantite(quantite); + mouvement.setStockAvant(stockAvant); + mouvement.setStockApres(stockApres); + mouvement.setSourceDocument(sourceDocument); + mouvement.setReferenceDocument(referenceDocument); + mouvement.setMotif(motif); + mouvement.setCreePar(user); + + return stockMovementRepository.save(mouvement); + } + + public List getHistoriqueArticle(Long articleId) { + return stockMovementRepository.findByArticleIdOrderByDateHeureDesc(articleId); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..a84128e --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,27 @@ +# =================================== +# Configuration de l'application ERP +# SUARL Rayhan — PFE Ali Guennari +# =================================== + +# Serveur +server.port=8080 + +# Base de données MySQL +spring.datasource.url=jdbc:mysql://localhost:3306/rayhan_erp_db?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Africa/Tunis +spring.datasource.username=root +spring.datasource.password=rayhan_erp_2024 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA / Hibernate +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.properties.hibernate.format_sql=true + +# JWT +rayhan.erp.jwtSecret=RayhanERP_SecretKey_PFE_AliGuennari_2024_TunisiePlasturgie_SUARL +rayhan.erp.jwtExpirationMs=86400000 + +# Logging +logging.level.com.rayhan.erp=DEBUG +logging.level.org.springframework.security=INFO diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..519b2d0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +version: '3.8' + +# ========================================== +# Docker Compose — Rayhan ERP +# Backend Spring Boot + MySQL +# ========================================== + +services: + + mysql: + image: mysql:8.0 + container_name: rayhan-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: rayhan_erp_2024 + MYSQL_DATABASE: rayhan_erp_db + MYSQL_USER: rayhan_user + MYSQL_PASSWORD: rayhan_erp_2024 + volumes: + - mysql_data:/var/lib/mysql + ports: + - "3307:3306" + networks: + - rayhan-net + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prayhan_erp_2024"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: ./backend + container_name: rayhan-backend + restart: unless-stopped + depends_on: + mysql: + condition: service_healthy + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/rayhan_erp_db?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Africa/Tunis + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: rayhan_erp_2024 + RAYHAN_ERP_JWTSECRET: RayhanERP_SecretKey_PFE_AliGuennari_2024_TunisiePlasturgie_SUARL + RAYHAN_ERP_JWTEXPIRATIONMS: 86400000 + ports: + - "8080:8080" + networks: + - rayhan-net + +volumes: + mysql_data: + +networks: + rayhan-net: + driver: bridge