diff --git a/backend/app/pom.xml b/backend/app/pom.xml index 78ce512..d30246d 100644 --- a/backend/app/pom.xml +++ b/backend/app/pom.xml @@ -108,6 +108,11 @@ hsqldb test + + de.redsix + pdfcompare + test + \ No newline at end of file diff --git a/backend/app/src/main/java/eu/viandeendirect/common/EnumLabelManager.java b/backend/app/src/main/java/eu/viandeendirect/common/EnumLabelManager.java new file mode 100644 index 0000000..e0a8fc0 --- /dev/null +++ b/backend/app/src/main/java/eu/viandeendirect/common/EnumLabelManager.java @@ -0,0 +1,17 @@ +package eu.viandeendirect.common; + +import eu.viandeendirect.model.BeefProduction; + +import java.util.Locale; + +public interface EnumLabelManager { + + default String getLabel(T enumValue, Locale locale) { + return switch (locale.getLanguage()) { + case "fr" -> getFrenchLabel(enumValue); + default -> "undefined"; + }; + } + + String getFrenchLabel(T enumValue); +} diff --git a/backend/app/src/main/java/eu/viandeendirect/common/PDFService.java b/backend/app/src/main/java/eu/viandeendirect/common/PDFService.java new file mode 100644 index 0000000..5e96c74 --- /dev/null +++ b/backend/app/src/main/java/eu/viandeendirect/common/PDFService.java @@ -0,0 +1,57 @@ +package eu.viandeendirect.common; + +import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; +import eu.viandeendirect.model.BeefProduction; +import org.apache.tomcat.util.codec.binary.Base64; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.stream.Collectors; + +public abstract class PDFService { + public ByteArrayOutputStream generatePDF(T arguments) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PdfRendererBuilder builder = new PdfRendererBuilder(); + builder.useFastMode(); + builder.withHtmlContent(getContentAsHtml(arguments), "/"); + builder.toStream(outputStream); + builder.run(); + return outputStream; + } + + protected String getTemplate() throws URISyntaxException, IOException { + return Files.lines(Paths.get(getClass().getClassLoader().getResource(getTemplatePath()).toURI())).collect(Collectors.joining()); + } + + protected String getCSS() throws URISyntaxException, IOException { + return Files.lines(Paths.get(getClass().getClassLoader().getResource(getCSSPath()).toURI())).collect(Collectors.joining()); + } + + protected abstract String getTemplatePath(); + + protected abstract String getCSSPath(); + + protected abstract String getContentAsHtml(T argument); + + protected String getViandeEnDirectLogoAsBase64() { + return getImageAsBase64("images/viande_en_direct.png"); + } + + protected String getLabelRougeLogoAsBase64(BeefProduction beefProduction) { + if (!beefProduction.getLabelRougeCertified()) { + return ""; + } + return getImageAsBase64("images/label_rouge.jpg"); + } + + protected String getImageAsBase64(String filePath) { + try { + return Base64.encodeBase64String(getClass().getClassLoader().getResource(filePath).openStream().readAllBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/backend/app/src/main/java/eu/viandeendirect/domains/production/AnimalTypeLabelManager.java b/backend/app/src/main/java/eu/viandeendirect/domains/production/AnimalTypeLabelManager.java new file mode 100644 index 0000000..8c139f5 --- /dev/null +++ b/backend/app/src/main/java/eu/viandeendirect/domains/production/AnimalTypeLabelManager.java @@ -0,0 +1,19 @@ +package eu.viandeendirect.domains.production; + +import eu.viandeendirect.common.EnumLabelManager; +import eu.viandeendirect.model.BeefProduction; +import org.springframework.stereotype.Component; + +@Component +public class AnimalTypeLabelManager implements EnumLabelManager { + + @Override + public String getFrenchLabel(BeefProduction.AnimalTypeEnum animalType) { + return switch (animalType) { + case COW -> "vache"; + case BULL -> "taureau"; + case VEAL -> "veau"; + case HEIFER -> "génisse"; + }; + } +} diff --git a/backend/app/src/main/java/eu/viandeendirect/domains/production/CattleBreedLabelManager.java b/backend/app/src/main/java/eu/viandeendirect/domains/production/CattleBreedLabelManager.java new file mode 100644 index 0000000..5591079 --- /dev/null +++ b/backend/app/src/main/java/eu/viandeendirect/domains/production/CattleBreedLabelManager.java @@ -0,0 +1,16 @@ +package eu.viandeendirect.domains.production; + +import eu.viandeendirect.common.EnumLabelManager; +import eu.viandeendirect.model.BeefProduction; +import org.springframework.stereotype.Component; + +@Component +public class CattleBreedLabelManager implements EnumLabelManager { + @Override + public String getFrenchLabel(BeefProduction.CattleBreedEnum cattleBreed) { + return switch (cattleBreed) { + case CHAROLAISE -> "charolaise"; + case LIMOUSINE -> "limousine"; + }; + } +} diff --git a/backend/app/src/main/java/eu/viandeendirect/domains/production/PackageElementLabelService.java b/backend/app/src/main/java/eu/viandeendirect/domains/production/PackageElementLabelService.java new file mode 100644 index 0000000..9c8d901 --- /dev/null +++ b/backend/app/src/main/java/eu/viandeendirect/domains/production/PackageElementLabelService.java @@ -0,0 +1,90 @@ +package eu.viandeendirect.domains.production; + +import eu.viandeendirect.common.PDFService; +import eu.viandeendirect.model.BeefProduction; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Locale.FRENCH; + +@Service +public class PackageElementLabelService extends PDFService { + + @Autowired + AnimalTypeLabelManager animalTypeLabelManager; + + @Autowired + CattleBreedLabelManager cattleBreedLabelManager; + + private String productLabelCSS; + private String productLabelTemplate; + + public PackageElementLabelService() throws URISyntaxException, IOException { + productLabelTemplate = getTemplate(); + productLabelCSS = getCSS(); + } + + @Override + protected String getTemplatePath() { + return "html/pdf/package_element_label_template.html"; + } + + @Override + protected String getCSSPath() { + return "html/pdf/package_element_label_template.css"; + } + + @Override + protected String getContentAsHtml(PackageElementLabelService.Arguments arguments) { + var htmlStart = String.format("", productLabelCSS); + var htmlEnd = ""; + return arguments.elementsNames.stream() + .map(elementName -> String.format(productLabelTemplate, getLabelsAsHtml(arguments.beefProduction, elementName))) + .collect(Collectors.joining("", htmlStart, htmlEnd)); + } + + private String getLabelsAsHtml(BeefProduction beefProduction, String elementName) { + return String.format(""" + + + + + + + + + + + + + + + + + +
%1$s race %2$s + +
%3$s
+ %4$s +
+ DLC : %5$td/%5$tm/%5$tY +
+ DLC congelé : %6$td/%6$tm/%6$tY +
""", + StringUtils.capitalize(animalTypeLabelManager.getLabel(beefProduction.getAnimalType(), FRENCH)), + cattleBreedLabelManager.getLabel(beefProduction.getCattleBreed(), FRENCH), + beefProduction.getAnimalIdentifier(), + elementName, + beefProduction.getCuttingDate().plusDays(10), + beefProduction.getCuttingDate().plusYears(1), + getLabelRougeLogoAsBase64(beefProduction)); + } + + public record Arguments(BeefProduction beefProduction, List elementsNames) {} +} diff --git a/backend/app/src/main/resources/html/notification/notification_to_customer_body_template.html b/backend/app/src/main/resources/html/notification/notification_to_customer_body_template.html new file mode 100644 index 0000000..6d7aaf0 --- /dev/null +++ b/backend/app/src/main/resources/html/notification/notification_to_customer_body_template.html @@ -0,0 +1,18 @@ + +
La commande n° %1$s a été enregistrée sur ViandeEnDirect.eu.
+
+
    +
  • Montant de la commande : %3$s €TTC
  • +
  • Quantité commandée : %4$s kg
  • +
  • Date et heure de la livraison : le %5$td/%5$tm/%5$tY entre %5$tR et %6$tR
  • +
  • Adresse de la livraison :
    + %7$s
    + %8$s
    + %9$s
    + %10$s
    + %11$s
    + %12$s
    + %13$s
    +
  • +
+
\ No newline at end of file diff --git a/backend/app/src/main/resources/html/notification/notification_to_producer_body_template.html b/backend/app/src/main/resources/html/notification/notification_to_producer_body_template.html new file mode 100644 index 0000000..cbe3fe9 --- /dev/null +++ b/backend/app/src/main/resources/html/notification/notification_to_producer_body_template.html @@ -0,0 +1,15 @@ + +
La commande n° %s a été enregistrée sur ViandeEnDirect.eu.
+
+
    +
  • Client : +
      +
    • Nom : %s %s
    • +
    • Email : %s
    • +
    • Téléphone : %s
    • +
    +
  • +
  • Montant de la commande : %s €TTC
  • +
  • Quantité commandée : %s kg
  • +
+
\ No newline at end of file diff --git a/backend/app/src/main/resources/html/pdf/invoice_template.css b/backend/app/src/main/resources/html/pdf/invoice_template.css new file mode 100644 index 0000000..8776de4 --- /dev/null +++ b/backend/app/src/main/resources/html/pdf/invoice_template.css @@ -0,0 +1,51 @@ +@page { + size: A4 portrait; +} + +body { + font-family: sans-serif; +} + +.bill { + page-break-after: always; +} + +.full-width-width { + width: 100%; +} + +.articles-table, .articles-table thead, .articles-table tr, .articles-table td { + border-collapse: collapse; + border-color: grey; + border-style: solid; + border-width: thin; + padding: 1mm; +} + +.with-top-margin { + margin-top: 2cm; +} + +.table-head { + background-color: navy; + color: white; + font-weight: bold; +} + +.align-right { + text-align: right; +} + +.payed { + font-weigth: bolder; + font-size: 1cm; + color: red; + border-color: red; + border-style: solid; + border-width: 2mm; + border-radius: 2mm; + padding: 5mm; + text-align: center; + transform: rotate(10deg); + width: 3cm; +} \ No newline at end of file diff --git a/backend/app/src/main/resources/html/pdf/invoice_template.html b/backend/app/src/main/resources/html/pdf/invoice_template.html new file mode 100644 index 0000000..a46e4fa --- /dev/null +++ b/backend/app/src/main/resources/html/pdf/invoice_template.html @@ -0,0 +1,75 @@ +
+
+ + + + + + + +
+
Vendeur
+
%1$s
+ %2$s +
%3$s %4$s
+
SIREN : %5$s
+
Tel. : %6$s
+
+
Client
+
%7$s %8$s
+
Tel. : %9$s
+
+
+
+ + + + + +
+
+ Facture N° %17$tY%17$tm%17$td-%10$s +
+
+ Date : %11$td/%11$tm/%11$tY +
+
+
PAYÉ
+
+
+
+ + + + + + + + %12$s + + + + + + + + + + + + + + + + + + +
DésignationQuantitéPrix Unitaire HTTotal HT
TOTAL HT%13$.2f €
Montant de TVA%14$.2f €
TOTAL TTC%15$.2f €
+
+
+ Date de réglement : %16$td/%16$tm/%16$tY +
+
+ Date de livraison : %17$td/%17$tm/%17$tY +
+
\ No newline at end of file diff --git a/backend/app/src/main/resources/html/pdf/order_label_template.css b/backend/app/src/main/resources/html/pdf/order_label_template.css new file mode 100644 index 0000000..008c81d --- /dev/null +++ b/backend/app/src/main/resources/html/pdf/order_label_template.css @@ -0,0 +1,77 @@ +@page { + size: A4 landscape; +} + +body { + font-family: sans-serif; +} + +article { + page-break-after: always; +} + +thead { + font-weight: bold; +} + +.logo { + width: 40rem; +} + +.logo > img { + height: 3rem; +} + +.order-id { + font-size: 2rem; + text-align: right; +} + +.delivery-description { + font-size: 2rem; + text-align: right; +} + +.customer-info { + font-size: 2rem; + font-weight: bolder; + text-align: center; + margin-top: 1rem; + margin-bottom: 1rem; +} + +.dated-limits-and-animal-description > tbody > tr > td { + vertical-align: top; +} + +.dated-limits, +.product-description, +.animal-description { + margin-bottom: 1rem; +} + +.product-description, +.dated-limits-and-animal-description { + width: 100%; +} + +.dated-limits-and-animal-description > tbody > tr > td { + width: 50%; +} + +.dated-limits-and-animal-description > tbody > tr > td > table { + width: 100%; +} + +.bordered, .bordered thead, .bordered tr, .bordered td { + border: solid thin; + border-collapse: collapse; + padding: 0.3rem; +} + +.certification-images { + margin-left: 1rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; + height: 3rem; +} \ No newline at end of file diff --git a/backend/app/src/main/resources/html/pdf/order_label_template.html b/backend/app/src/main/resources/html/pdf/order_label_template.html new file mode 100644 index 0000000..f8bb6ab --- /dev/null +++ b/backend/app/src/main/resources/html/pdf/order_label_template.html @@ -0,0 +1,81 @@ +
+ + + + + + + +
+ + + + + + + + + +
Commande n° %2$s
Livraison du %1$td/%1$tm/%1$tY
+
+ +
%3$s
+ + + + + + + + + %4$s +
ProduitQuantitéDescriptionPoids net unitaire
+ + + + + + + + +
+ + + + + + + + + + + +
Date limite de consommation fraîche%5$td/%5$tm/%5$tY
Date limite de consommation congelé
(congélation à réception)
%6$td/%6$tm/%6$tY
+
+ + + + + + + + + + + + + + + + + + +
+ %7$s race %8$s n° %9$s +
+ +
Date / lieu de naissance%11$td/%11$tm/%11$tY - %12$s %13$s
Date / lieu d'abattage%14$td/%14$tm/%14$tY - %15$s %16$s
Date / lieu de découpe%17$td/%17$tm/%17$tY - %18$s %19$s
+
+
\ No newline at end of file diff --git a/backend/app/src/main/resources/html/pdf/package_element_label_template.css b/backend/app/src/main/resources/html/pdf/package_element_label_template.css new file mode 100644 index 0000000..f781d78 --- /dev/null +++ b/backend/app/src/main/resources/html/pdf/package_element_label_template.css @@ -0,0 +1,40 @@ +@page { + size: A4 portrait; + margin: 0mm; +} + +body { + margin: 0mm; + font-family: sans-serif; +} + +body > table { + border-spacing: 0mm; +} + +body > table > tbody > tr > td { + height: 42mm; + width: 70mm; + border-style: none; + border-collapse: collapse; + padding: 0mm; +} + +body > table > tbody > tr > td > table { + text-align: center; + width: 100%; +} + +.label-page { + page-break-after: always; +} + +.certification-images { + height: 10mm; +} + +.element-name { + height: 12mm; + font-size: 6mm; + font-weight: bold; +} \ No newline at end of file diff --git a/backend/app/src/main/resources/html/pdf/package_element_label_template.html b/backend/app/src/main/resources/html/pdf/package_element_label_template.html new file mode 100644 index 0000000..3b30944 --- /dev/null +++ b/backend/app/src/main/resources/html/pdf/package_element_label_template.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
%1$s%1$s%1$s
%1$s%1$s%1$s
%1$s%1$s%1$s
%1$s%1$s%1$s
%1$s%1$s%1$s
%1$s%1$s%1$s
%1$s%1$s%1$s
diff --git a/backend/app/src/main/resources/images/label_rouge.jpg b/backend/app/src/main/resources/images/label_rouge.jpg new file mode 100644 index 0000000..5298885 Binary files /dev/null and b/backend/app/src/main/resources/images/label_rouge.jpg differ diff --git a/backend/app/src/main/resources/images/viande_en_direct.png b/backend/app/src/main/resources/images/viande_en_direct.png new file mode 100644 index 0000000..005f34b Binary files /dev/null and b/backend/app/src/main/resources/images/viande_en_direct.png differ diff --git a/backend/app/src/test/java/eu/viandeendirect/domains/production/TestPackageElementLabelService.java b/backend/app/src/test/java/eu/viandeendirect/domains/production/TestPackageElementLabelService.java new file mode 100644 index 0000000..aff6a64 --- /dev/null +++ b/backend/app/src/test/java/eu/viandeendirect/domains/production/TestPackageElementLabelService.java @@ -0,0 +1,66 @@ +package eu.viandeendirect.domains.production; + +import de.redsix.pdfcompare.CompareResult; +import de.redsix.pdfcompare.PdfComparator; +import eu.viandeendirect.model.BeefProduction; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles(value = "test") +@ExtendWith({SpringExtension.class}) +class TestPackageElementLabelService { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestPackageElementLabelService.class); + + @TempDir + File outputFolder; + + @Autowired + PackageElementLabelService service; + + @Test + void generatePDF_should_produce_a_correct_pdf() throws IOException, URISyntaxException { + // given + BeefProduction beefProduction = new BeefProduction(); + beefProduction.setCuttingDate(LocalDate.of(2024, 4, 4)); + beefProduction.setAnimalType(BeefProduction.AnimalTypeEnum.COW); + beefProduction.setCattleBreed(BeefProduction.CattleBreedEnum.LIMOUSINE); + beefProduction.setAnimalIdentifier("FR5705563733"); + beefProduction.setLabelRougeCertified(true); + + // when + ByteArrayOutputStream outputStream = service.generatePDF(new PackageElementLabelService.Arguments(beefProduction, List.of("Bavette (steaks)"))); + + // then + File packageElementLabelFile = new File(outputFolder, "package_element_labels.pdf"); + FileOutputStream fileOutputStream = new FileOutputStream(packageElementLabelFile); + outputStream.writeTo(fileOutputStream); + fileOutputStream.flush(); + LOGGER.info(String.format("PDF file at %s", packageElementLabelFile.getAbsolutePath())); + var expectedProductElementLabelPath = Path.of(ClassLoader.getSystemResource("pdf/expected_package_element_labels.pdf").toURI()); + CompareResult compareResult = new PdfComparator(expectedProductElementLabelPath.toString(), packageElementLabelFile.getAbsolutePath()).compare(); + File compareResultFile = new File(outputFolder, "package_element_labels_compare.pdf"); + compareResult.writeTo(new FileOutputStream(compareResultFile)); + assertThat(compareResult.getDifferences().isEmpty()).isTrue(); + } + +} \ No newline at end of file diff --git a/backend/app/src/test/resources/pdf/expected_package_element_labels.pdf b/backend/app/src/test/resources/pdf/expected_package_element_labels.pdf new file mode 100644 index 0000000..ab79460 Binary files /dev/null and b/backend/app/src/test/resources/pdf/expected_package_element_labels.pdf differ diff --git a/backend/model/src/main/java/eu/viandeendirect/model/BeefProduction.java b/backend/model/src/main/java/eu/viandeendirect/model/BeefProduction.java index 7e8324c..353ba7f 100644 --- a/backend/model/src/main/java/eu/viandeendirect/model/BeefProduction.java +++ b/backend/model/src/main/java/eu/viandeendirect/model/BeefProduction.java @@ -151,7 +151,8 @@ public static CattleBreedEnum fromValue(String value) { private String slaughterHouse; @JsonProperty("cuttingDate") - private String cuttingDate; + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + private LocalDate cuttingDate; @JsonProperty("cuttingPlace") private String cuttingPlace; @@ -371,7 +372,7 @@ public void setSlaughterHouse(String slaughterHouse) { this.slaughterHouse = slaughterHouse; } - public BeefProduction cuttingDate(String cuttingDate) { + public BeefProduction cuttingDate(LocalDate cuttingDate) { this.cuttingDate = cuttingDate; return this; } @@ -380,13 +381,13 @@ public BeefProduction cuttingDate(String cuttingDate) { * date when the animal has been cutted * @return cuttingDate */ - + @Valid @Schema(name = "cuttingDate", description = "date when the animal has been cutted", required = false) - public String getCuttingDate() { + public LocalDate getCuttingDate() { return cuttingDate; } - public void setCuttingDate(String cuttingDate) { + public void setCuttingDate(LocalDate cuttingDate) { this.cuttingDate = cuttingDate; } diff --git a/backend/pom.xml b/backend/pom.xml index 98de358..3b86fb5 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -27,6 +27,7 @@ 0.2.2 1.0.10 2.0.2 + 1.1.60 @@ -77,6 +78,11 @@ openhtmltopdf-pdfbox ${openhtml.version} + + de.redsix + pdfcompare + ${pdfcompare.version} +