diff --git a/composer.json b/composer.json index 0e5eb29..4ce5354 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "doctrine/doctrine-bundle": "^3.2", "doctrine/doctrine-migrations-bundle": "^4.0", "doctrine/orm": "^3.6", + "dompdf/dompdf": "^3.0", "lexik/jwt-authentication-bundle": "^3.2", "nelmio/cors-bundle": "^2.6", "nyholm/psr7": "^1.8", diff --git a/composer.lock b/composer.lock index f5fba04..ba44978 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b029c1484227c926d39dfd3ae5cb0699", + "content-hash": "224bae08ec63f217eabf5b2b611deaa0", "packages": [ { "name": "api-platform/doctrine-common", @@ -2520,6 +2520,161 @@ }, "time": "2026-02-08T16:21:46+00:00" }, + { + "name": "dompdf/dompdf", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "shasum": "" + }, + "require": { + "dompdf/php-font-lib": "^1.0.0", + "dompdf/php-svg-lib": "^1.0.0", + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v3.1.5" + }, + "time": "2026-03-03T13:54:37+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "The FontLib Community", + "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/dompdf/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2" + }, + "time": "2026-01-20T14:10:26+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4 || ^9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The SvgLib Community", + "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/dompdf/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2" + }, + "time": "2026-01-02T16:01:13+00:00" + }, { "name": "lcobucci/jwt", "version": "5.6.0", @@ -2894,6 +3049,73 @@ }, "time": "2022-12-02T22:17:43+00:00" }, + { + "name": "masterminds/html5", + "version": "2.10.1", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fd5018f6815fff903946d0564977b44ce8010e29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29", + "reference": "fd5018f6815fff903946d0564977b44ce8010e29", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.10.1" + }, + "time": "2026-06-23T18:43:15+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -3937,6 +4159,86 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v9.4.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.33 || 2.2.2", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.16", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.4.6", + "rector/type-perfect": "1.0.0 || 2.1.3", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.5.x-dev" + } + }, + "autoload": { + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0" + }, + "time": "2026-06-18T15:10:53+00:00" + }, { "name": "symfony/asset", "version": "v8.0.8", @@ -8779,6 +9081,149 @@ ], "time": "2026-03-30T15:14:47+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, { "name": "twig/twig", "version": "v3.24.0", diff --git a/src/Module/Logistique/Domain/Entity/WeighingTicket.php b/src/Module/Logistique/Domain/Entity/WeighingTicket.php index 7e6bf1d..7c27491 100644 --- a/src/Module/Logistique/Domain/Entity/WeighingTicket.php +++ b/src/Module/Logistique/Domain/Entity/WeighingTicket.php @@ -12,6 +12,7 @@ use ApiPlatform\Metadata\Post; use App\Module\Commercial\Domain\Entity\Client; // relation ORM partagee (§ 2.1) use App\Module\Commercial\Domain\Entity\Supplier; // relation ORM partagee (§ 2.1) use App\Module\Logistique\Infrastructure\ApiPlatform\State\Processor\WeighingTicketProcessor; +use App\Module\Logistique\Infrastructure\ApiPlatform\State\Provider\WeighingTicketPrintProvider; use App\Module\Logistique\Infrastructure\ApiPlatform\State\Provider\WeighingTicketProvider; use App\Module\Logistique\Infrastructure\Doctrine\DoctrineWeighingTicketRepository; use App\Module\Sites\Domain\Entity\Site; // relation ORM partagee (§ 2.1) @@ -84,6 +85,18 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; ]], provider: WeighingTicketProvider::class, ), + // Bon de pesee PDF (RG-5.08, spec § 2.12 / § 4.6) : operation dediee qui + // sert un binaire (pas une representation Hydra). Le provider retourne une + // Response -> la serialisation est court-circuitee. Pas de controller + // (decision spec § 4.6). Pas de format API Platform negocie : `.pdf` est + // litteral dans l'URI. + new Get( + uriTemplate: '/weighing_tickets/{id}/print.pdf', + security: "is_granted('logistique.weighing_tickets.view')", + provider: WeighingTicketPrintProvider::class, + output: false, + read: true, + ), new Post( security: "is_granted('logistique.weighing_tickets.manage')", normalizationContext: ['groups' => [ diff --git a/src/Module/Logistique/Infrastructure/ApiPlatform/State/Provider/WeighingTicketPrintProvider.php b/src/Module/Logistique/Infrastructure/ApiPlatform/State/Provider/WeighingTicketPrintProvider.php new file mode 100644 index 0000000..3ad71b6 --- /dev/null +++ b/src/Module/Logistique/Infrastructure/ApiPlatform/State/Provider/WeighingTicketPrintProvider.php @@ -0,0 +1,103 @@ + Dompdf). + * + * Le provider retourne directement une {@see Response} : API Platform court-circuite + * alors la serialisation Hydra (le SerializeListener/RespondListener detectent une + * Response et la renvoient telle quelle). `Content-Type: application/pdf`, + * disposition `inline` (le front ouvre l'apercu — RG-5.08). + * + * Securite & visibilite — miroir de {@see WeighingTicketProvider::provideItem()} : + * - permission `logistique.weighing_tickets.view` portee par l'operation (403) ; + * - 404 si ticket introuvable, soft-delete (non expose au M5 — § 2.13), ou hors + * perimetre du site courant (anti-enumeration, § 2.3 / RG-5.09). + * + * @implements ProviderInterface + */ +final class WeighingTicketPrintProvider implements ProviderInterface +{ + public function __construct( + #[Autowire(service: 'App\Module\Logistique\Infrastructure\Doctrine\DoctrineWeighingTicketRepository')] + private readonly WeighingTicketRepositoryInterface $repository, + private readonly WeighingTicketPdfRenderer $renderer, + private readonly CurrentSiteProviderInterface $currentSiteProvider, + private readonly Security $security, + ) {} + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response + { + $ticket = $this->findVisibleTicket($uriVariables['id'] ?? null); + if (null === $ticket) { + throw new NotFoundHttpException('Ticket de pesée introuvable.'); + } + + $pdf = $this->renderer->render($ticket); + + $response = new Response($pdf); + $response->headers->set('Content-Type', 'application/pdf'); + $response->headers->set( + 'Content-Disposition', + sprintf('inline; filename="bon-pesee-%s.pdf"', $ticket->getNumber() ?? (string) $ticket->getId()), + ); + + return $response; + } + + /** + * Charge le ticket visible par l'utilisateur courant, ou null (-> 404) : + * introuvable, soft-delete, ou hors perimetre du site courant. Logique + * identique a WeighingTicketProvider::provideItem() (cloisonnement § 2.3). + */ + private function findVisibleTicket(mixed $id): ?WeighingTicket + { + if (!is_int($id) && !(is_string($id) && ctype_digit($id))) { + return null; + } + + $ticket = $this->repository->findById((int) $id); + if (null === $ticket || null !== $ticket->getDeletedAt()) { + return null; + } + + $scopeSite = $this->currentScopeSite(); + if (null !== $scopeSite && $ticket->getSite()?->getId() !== $scopeSite->getId()) { + return null; + } + + return $ticket; + } + + /** + * Site servant a cloisonner, ou null si aucun cloisonnement ne s'applique + * (user `sites.bypass_scope`, ou pas de site courant). Miroir de + * WeighingTicketProvider::currentScopeSite(). + */ + private function currentScopeSite(): ?Site + { + if ($this->security->isGranted('sites.bypass_scope')) { + return null; + } + + return $this->currentSiteProvider->get(); + } +} diff --git a/src/Module/Logistique/Infrastructure/Pdf/WeighingTicketPdfRenderer.php b/src/Module/Logistique/Infrastructure/Pdf/WeighingTicketPdfRenderer.php new file mode 100644 index 0000000..179125c --- /dev/null +++ b/src/Module/Logistique/Infrastructure/Pdf/WeighingTicketPdfRenderer.php @@ -0,0 +1,75 @@ + accents FR et « ° » corrects). + */ + public function render(WeighingTicket $ticket): string + { + $html = $this->twig->render('logistique/weighing_ticket_print.html.twig', [ + 'ticket' => $ticket, + 'logoSrc' => $this->logoDataUri(), + ]); + + $options = new Options(); + $options->set('isRemoteEnabled', false); + $options->set('defaultFont', 'DejaVu Sans'); + + $dompdf = new Dompdf($options); + $dompdf->loadHtml($html, 'UTF-8'); + $dompdf->setPaper('A4', 'portrait'); + $dompdf->render(); + + return (string) $dompdf->output(); + } + + /** + * Logo societe encode en data-URI base64, ou null s'il est introuvable (le + * template degrade alors sans bloquer la generation du PDF). + */ + private function logoDataUri(): ?string + { + $binary = @file_get_contents(self::LOGO_PATH); + if (false === $binary) { + return null; + } + + return 'data:image/png;base64,'.base64_encode($binary); + } +} diff --git a/src/Module/Logistique/Infrastructure/Pdf/assets/logo-lpc-liot.png b/src/Module/Logistique/Infrastructure/Pdf/assets/logo-lpc-liot.png new file mode 100644 index 0000000..5ec6928 Binary files /dev/null and b/src/Module/Logistique/Infrastructure/Pdf/assets/logo-lpc-liot.png differ diff --git a/templates/logistique/weighing_ticket_print.html.twig b/templates/logistique/weighing_ticket_print.html.twig new file mode 100644 index 0000000..eaf4b84 --- /dev/null +++ b/templates/logistique/weighing_ticket_print.html.twig @@ -0,0 +1,81 @@ +{# + Ticket de pesée (M5 Logistique) — gabarit imprimable hydraté côté serveur puis + converti en PDF par WeighingTicketPdfRenderer (Dompdf). Cf. spec-back M5 § 2.12 + / § 4.6 (RG-5.08). Reproduit fidèlement le modèle fourni (ticket_pesee.pdf). + + En-tête FIXE (logo + identité société) : le ticket ne change pas en fonction du + site (décision Tristan, ERP-192). Le logo est injecté en data-URI par le renderer + (logoSrc) ; l'identité société est en dur ci-dessous. + + Contraintes Dompdf : CSS2.1 (pas de flexbox/grid), mise en page par tableaux. + Police DejaVu Sans (UTF-8 — accents FR et « ° » rendus correctement). +#} + + + + + + + + {% if logoSrc %} + + {% endif %} + +
SA LIOT Châtellerault
+
Email : lpc.contacts@lpc-liot.fr
+
RCS Châtellerault B 339 505 612
+ +
Ticket de pesée
+ + {# + Référence de pesée affichée au client = un seul numéro, présenté comme un + DSD : en pesée MANUELLE c'est le numéro de pesée saisi (manualNumber), en + pesée AUTO c'est le DSD du pont. « N° pesée » et « DSD » sont la même chose + pour le client (RG-5.04) — on n'expose donc pas le compteur interne du pont + quand une pesée manuelle porte son propre numéro. + #} + {% set emptyRef = (ticket.emptyMode == 'MANUAL' and ticket.emptyManualNumber) ? ticket.emptyManualNumber : ticket.emptyDsd %} + {% set fullRef = (ticket.fullMode == 'MANUAL' and ticket.fullManualNumber) ? ticket.fullManualNumber : ticket.fullDsd %} + + + + + + + + + + + + + + +
Poids à vide{{ ticket.emptyWeight is not null ? ticket.emptyWeight ~ ' kg' : '' }}N° pesée à vide{% if emptyRef is not null %}DSD : {{ emptyRef }}{% endif %}{% if ticket.emptyDate %} {{ ticket.emptyDate|date('d/m/Y H:i:s') }}{% endif %}
Poids à plein{{ ticket.fullWeight is not null ? ticket.fullWeight ~ ' kg' : '' }}N° pesée à plein{% if fullRef is not null %}DSD : {{ fullRef }}{% endif %}{% if ticket.fullDate %} {{ ticket.fullDate|date('d/m/Y H:i:s') }}{% endif %}
+ +
Poids : {{ ticket.netWeight is not null ? ticket.netWeight ~ ' kg' : '—' }}
+ + diff --git a/tests/Module/Logistique/Api/WeighingTicketPrintApiTest.php b/tests/Module/Logistique/Api/WeighingTicketPrintApiTest.php new file mode 100644 index 0000000..5eee1ea --- /dev/null +++ b/tests/Module/Logistique/Api/WeighingTicketPrintApiTest.php @@ -0,0 +1,73 @@ +firstSite(); + $http = $this->authManageOnSite($site); + $client = $this->seedTestClient('Print'); + + $created = $this->postTicket($http, $this->validClientTicketPayload($client)); + self::assertResponseStatusCodeSame(201); + $ticketId = $created->toArray()['id']; + + $response = $http->request('GET', sprintf('/api/weighing_tickets/%d/print.pdf', $ticketId)); + + self::assertResponseIsSuccessful(); + + $headers = $response->getHeaders(false); + self::assertStringContainsString('application/pdf', $headers['content-type'][0] ?? ''); + self::assertStringContainsString('inline', $headers['content-disposition'][0] ?? ''); + + // PDF non vide + signature de fichier PDF (« %PDF-1.x »). + $binary = $response->getContent(false); + self::assertNotSame('', $binary, 'Le PDF du bon de pesée ne doit pas être vide.'); + self::assertStringStartsWith('%PDF', $binary); + } + + public function testForbiddenWithoutViewPermission(): void + { + // On seede un ticket reel via un user habilite, puis on tente l'impression + // avec un user depourvu de `logistique.weighing_tickets.view`. + $site = $this->firstSite(); + $manager = $this->authManageOnSite($site); + $client = $this->seedTestClient('Forbidden'); + + $created = $this->postTicket($manager, $this->validClientTicketPayload($client)); + self::assertResponseStatusCodeSame(201); + $ticketId = $created->toArray()['id']; + + $creds = $this->createUserWithPermission('core.users.view'); + $intrus = $this->authenticatedClient($creds['username'], $creds['password']); + + $intrus->request('GET', sprintf('/api/weighing_tickets/%d/print.pdf', $ticketId)); + + self::assertResponseStatusCodeSame(403); + } + + public function testNotFoundForUnknownTicket(): void + { + $http = $this->authManageOnSite($this->firstSite()); + + $http->request('GET', '/api/weighing_tickets/99999999/print.pdf'); + + self::assertResponseStatusCodeSame(404); + } +}