feat: initial Spring Boot API - modules Auth, Articles, Tiers, Achats, Ventes, Production, Stock, Dashboard

- 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
This commit is contained in:
Nabil Derouiche 2026-04-19 19:38:43 +01:00
parent 07b7b133fe
commit b53fcf0ab9
61 changed files with 2836 additions and 27 deletions

50
.gitignore vendored
View File

@ -1,45 +1,41 @@
# ---> Java # Java
# Compiled class file
*.class *.class
# Log file
*.log *.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar *.jar
*.war *.war
*.nar *.nar
*.ear *.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
replay_pid* replay_pid*
# ---> Maven # Maven
backend/target/
target/ target/
pom.xml.tag pom.xml.tag
pom.xml.releaseBackup pom.xml.releaseBackup
pom.xml.versionsBackup 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 .mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files # IDE
# Eclipse Core .idea/
*.iml
.vscode/
.project .project
# JDT-specific (Eclipse Java Development Tools)
.classpath .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

104
Livrables/SUIVI-PROJET.md Normal file
View File

@ -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)

21
backend/Dockerfile Normal file
View File

@ -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"]

114
backend/pom.xml Normal file
View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.rayhan</groupId>
<artifactId>erp</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>Rayhan ERP</name>
<description>ERP sur mesure pour SUARL Rayhan — PFE Ali Guennari</description>
<properties>
<java.version>17</java.version>
<jjwt.version>0.12.5</jjwt.version>
</properties>
<dependencies>
<!-- Spring Boot Web (REST API) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA / Hibernate pour la base de données -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Validation des données -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Connecteur MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT — API -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<!-- JWT — Implémentation -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- JWT — Jackson (sérialisation) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok (réduit le boilerplate Java) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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!)");
}
}
}

View File

@ -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();
}
}

View File

@ -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<Article> getAllArticles() {
return articleRepository.findByActifTrue();
}
@GetMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<Article> getArticleById(@PathVariable Long id) {
return articleRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/type/{type}")
@PreAuthorize("isAuthenticated()")
public List<Article> getArticlesByType(@PathVariable Article.TypeArticle type) {
return articleRepository.findByType(type);
}
@GetMapping("/alertes-stock")
@PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_MAGASINIER', 'ROLE_RESPONSABLE_PRODUCTION')")
public List<Article> 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<Article> 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());
}
}

View File

@ -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<String> 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<String> strRoles = signUpRequest.getRoles();
Set<Role> 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 !"));
}
}

View File

@ -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<Client> getAllClients() {
return clientRepository.findByActifTrue();
}
@GetMapping("/search")
@PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
public List<Client> searchClients(@RequestParam String q) {
return clientRepository.findByRaisonSocialeContainingIgnoreCase(q);
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
public ResponseEntity<Client> 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<Client> 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());
}
}

View File

@ -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<String, Object> getDashboard() {
Map<String, Object> 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<String, Object> 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<String, Object> achats = new HashMap<>();
achats.put("commandesEnAttente",
purchaseOrderRepository.countByStatutIn(
List.of(PurchaseOrder.StatutCommande.CONFIRMEE,
PurchaseOrder.StatutCommande.PARTIELLEMENT_RECUE)));
dashboard.put("achats", achats);
// KPIs Production
Map<String, Object> 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<Article> alertesStock = articleRepository.findAll().stream()
.filter(a -> a.isActif() && a.getStockActuel().compareTo(a.getStockMinimum()) <= 0)
.toList();
Map<String, Object> 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;
}
}

View File

@ -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<Fournisseur> getAllFournisseurs() {
return fournisseurRepository.findByActifTrue();
}
@GetMapping("/search")
@PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
public List<Fournisseur> searchFournisseurs(@RequestParam String q) {
return fournisseurRepository.findByRaisonSocialeContainingIgnoreCase(q);
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
public ResponseEntity<Fournisseur> 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<Fournisseur> 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());
}
}

View File

@ -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<BomLine> 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<ProductionOrder> getAllOFs() {
return productionOrderService.getAllOFs();
}
@PostMapping("/orders/plan")
@PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')")
public ResponseEntity<ProductionOrder> planOF(@RequestBody Map<String, Object> 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<ProductionOrder> 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<ProductionOrder> completeOF(@PathVariable Long id,
@RequestBody Map<String, Object> 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));
}
}

View File

@ -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<PurchaseOrder> getAllOrders() {
return purchaseOrderService.getAllPurchaseOrders();
}
@PostMapping
@PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
public ResponseEntity<PurchaseOrder> 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<GoodsReceipt> 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));
}
}

View File

@ -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<SalesOrder> getAllOrders() {
return salesOrderService.getAllSalesOrders();
}
@PostMapping
@PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
public ResponseEntity<SalesOrder> 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<DeliveryNote> 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));
}
}

View File

@ -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<StockMovement> getHistorique(@PathVariable Long articleId) {
return stockService.getHistoriqueArticle(articleId);
}
@PostMapping("/adjust")
@PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_MAGASINIER')")
public ResponseEntity<StockMovement> adjustStock(@RequestBody Map<String, Object> 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);
}
}

View File

@ -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;
}

View File

@ -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<String> roles;
}

View File

@ -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<String> roles;
public JwtResponse(String accessToken, Long id, String username, String email,
String firstName, String lastName, List<String> roles) {
this.token = accessToken;
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
this.roles = roles;
}
}

View File

@ -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;
}

View File

@ -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)
}
}

View File

@ -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é
}

View File

@ -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);
}
}

View File

@ -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<DeliveryNoteLine> lignes = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "created_by")
private User creePar;
public enum StatutLivraison {
EN_PREPARATION, LIVRE, RETOURNE_PARTIEL
}
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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<GoodsReceiptLine> lignes = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "created_by")
private User creePar;
}

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -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<PurchaseOrderLine> lignes = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "created_by")
private User creePar;
public enum StatutCommande {
BROUILLON, CONFIRMEE, PARTIELLEMENT_RECUE, COMPLETEMENT_RECUE, ANNULEE
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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<SalesOrderLine> lignes = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "created_by")
private User creePar;
public enum StatutCommande {
CONFIRMEE, EN_PREPARATION, PARTIELLEMENT_LIVREE, COMPLETEMENT_LIVREE, ANNULEE
}
}

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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<Role> 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;
}
}

View File

@ -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<Article, Long> {
Optional<Article> findByReference(String reference);
Boolean existsByReference(String reference);
List<Article> findByType(Article.TypeArticle type);
List<Article> findByActifTrue();
List<Article> findByStockActuelLessThanEqualAndActifTrue(java.math.BigDecimal seuil);
}

View File

@ -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<BomLine, Long> {
List<BomLine> findByProduitFiniId(Long produitFiniId);
void deleteByProduitFiniId(Long produitFiniId);
}

View File

@ -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<Client, Long> {
List<Client> findByActifTrue();
List<Client> findByRaisonSocialeContainingIgnoreCase(String search);
}

View File

@ -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<DeliveryNote, Long> {
Optional<DeliveryNote> findByReference(String reference);
boolean existsByReference(String reference);
}

View File

@ -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<Fournisseur, Long> {
List<Fournisseur> findByActifTrue();
List<Fournisseur> findByRaisonSocialeContainingIgnoreCase(String search);
}

View File

@ -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<GoodsReceipt, Long> {
Optional<GoodsReceipt> findByReference(String reference);
boolean existsByReference(String reference);
}

View File

@ -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<ProductionOrder, Long> {
Optional<ProductionOrder> findByReference(String reference);
boolean existsByReference(String reference);
List<ProductionOrder> findByStatutOrderByDatePlanifieeDesc(ProductionOrder.StatutOF statut);
long countByStatut(ProductionOrder.StatutOF statut);
}

View File

@ -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<PurchaseOrder, Long> {
Optional<PurchaseOrder> findByReference(String reference);
boolean existsByReference(String reference);
List<PurchaseOrder> findByStatutOrderByDateCommandeDesc(PurchaseOrder.StatutCommande statut);
long countByStatutIn(List<PurchaseOrder.StatutCommande> statuts);
}

View File

@ -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<Role, Integer> {
Optional<Role> findByName(ERole name);
}

View File

@ -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<SalesOrder, Long> {
Optional<SalesOrder> findByReference(String reference);
boolean existsByReference(String reference);
List<SalesOrder> 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);
}

View File

@ -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<StockMovement, Long> {
List<StockMovement> findByArticleIdOrderByDateHeureDesc(Long articleId);
List<StockMovement> findByDateHeureBetweenOrderByDateHeureDesc(LocalDateTime debut, LocalDateTime fin);
}

View File

@ -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<User, Long> {
Optional<User> findByUsername(String username);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
}

View File

@ -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<String, Object> 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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, String email,
String firstName, String lastName,
String password,
Collection<? extends GrantedAuthority> 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<GrantedAuthority> 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<? extends GrantedAuthority> 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; }
}

View File

@ -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);
}
}

View File

@ -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<BomLine> 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<BomLine> 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<ProductionOrder> getAllOFs() {
return productionOrderRepository.findAll();
}
}

View File

@ -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<PurchaseOrder> getAllPurchaseOrders() {
return purchaseOrderRepository.findAll();
}
}

View File

@ -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<SalesOrder> getAllSalesOrders() {
return salesOrderRepository.findAll();
}
}

View File

@ -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<StockMovement> getHistoriqueArticle(Long articleId) {
return stockMovementRepository.findByArticleIdOrderByDateHeureDesc(articleId);
}
}

View File

@ -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

54
docker-compose.yml Normal file
View File

@ -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