Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39f67b3c90 | ||
| eccb8e1fc6 | |||
|
|
de39207102 | ||
| 5da0003c4d | |||
|
|
88f19cbb59 | ||
| df3f86d33f | |||
|
|
5cc9b7855f | ||
| b61321c7b7 |
@@ -66,6 +66,9 @@ Ajouter dans le fichier .env du frontend
|
|||||||
* [#FER-17] Ecran d'ajout de bovin
|
* [#FER-17] Ecran d'ajout de bovin
|
||||||
* [#FER-18] Mise à jour du tableau d'arrivage
|
* [#FER-18] Mise à jour du tableau d'arrivage
|
||||||
* [#FER-26] Passeport du bovin
|
* [#FER-26] Passeport du bovin
|
||||||
|
* [#FER-27] Fix export inventaire bovin
|
||||||
|
* [#FER-25] Ajout un cron pour la synchro de l'inventaire bovin
|
||||||
|
* [#FER-22] Pouvoir exporter les réceptions/expéditions fines en Excel
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.0.100'
|
app.version: '0.0.104'
|
||||||
|
|||||||
@@ -194,7 +194,6 @@ interface BovineMovementData {
|
|||||||
enteredAt: string
|
enteredAt: string
|
||||||
leftAt: string | null
|
leftAt: string | null
|
||||||
buildingCase: BuildingCaseRef | null
|
buildingCase: BuildingCaseRef | null
|
||||||
building: BuildingRef | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BovinePassportData {
|
interface BovinePassportData {
|
||||||
@@ -301,7 +300,7 @@ const movementRows = computed(() => {
|
|||||||
const list = bovine.value?.movements ?? []
|
const list = bovine.value?.movements ?? []
|
||||||
return list.map(m => ({
|
return list.map(m => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
building: m.buildingCase?.building?.label ?? m.building?.label ?? '—',
|
building: m.buildingCase?.building?.label ?? '—',
|
||||||
case: m.buildingCase?.caseNumber != null ? `Case ${m.buildingCase.caseNumber}` : '—',
|
case: m.buildingCase?.caseNumber != null ? `Case ${m.buildingCase.caseNumber}` : '—',
|
||||||
enteredAt: formatDate(m.enteredAt),
|
enteredAt: formatDate(m.enteredAt),
|
||||||
leftAt: m.leftAt ? formatDate(m.leftAt) : null,
|
leftAt: m.leftAt ? formatDate(m.leftAt) : null,
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
{{ formatDate(item.arrivalDate) }}
|
{{ formatDate(item.arrivalDate) }}
|
||||||
</template>
|
</template>
|
||||||
<template #cell-buildingCase.building.label="{ item }">
|
<template #cell-buildingCase.building.label="{ item }">
|
||||||
{{ item.effectiveBuilding?.label ?? '—' }}
|
{{ item.buildingCase?.building?.label ?? '—' }}
|
||||||
</template>
|
</template>
|
||||||
<template #cell-buildingCase.caseNumber="{ item }">
|
<template #cell-buildingCase.caseNumber="{ item }">
|
||||||
{{ item.buildingCase?.caseNumber ?? '—' }}
|
{{ item.buildingCase?.caseNumber ?? '—' }}
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
<div class="flex items-center justify-start gap-10">
|
<div class="flex items-center justify-start gap-10">
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
<h1 class="text-3xl font-bold uppercase text-primary-500">liste des réceptions finies</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">liste des réceptions finies</h1>
|
||||||
|
<div
|
||||||
|
v-if="auth.isBureau"
|
||||||
|
class="bg-primary-500 p-1 rounded-md flex items-center cursor-pointer hover:opacity-80"
|
||||||
|
:class="exporting ? 'cursor-not-allowed opacity-60' : ''"
|
||||||
|
title="Exporter en Excel"
|
||||||
|
@click="exportReceptions"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:file-excel-outline" size="32" class="text-white" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-[86px]">
|
<div class="px-[86px]">
|
||||||
@@ -79,9 +88,35 @@ import type { ReceptionData } from '~/services/dto/reception-data'
|
|||||||
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
||||||
import { getReceptionTypeList } from '~/services/reception-type'
|
import { getReceptionTypeList } from '~/services/reception-type'
|
||||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const api = useApi()
|
||||||
const receptionTypes = ref<ReceptionTypeData[]>([])
|
const receptionTypes = ref<ReceptionTypeData[]>([])
|
||||||
|
const exporting = ref(false)
|
||||||
|
|
||||||
|
const exportReceptions = async () => {
|
||||||
|
if (exporting.value) return
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
const blob = await api.getBlob('receptions/export')
|
||||||
|
const filename = `receptions_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
a.style.display = 'none'
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 60_000)
|
||||||
|
} catch {
|
||||||
|
// toast déjà géré par useApi onResponseError
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const receptionTypeOptions = computed(() =>
|
const receptionTypeOptions = computed(() =>
|
||||||
receptionTypes.value.map(rt => ({ value: rt.id, label: rt.label }))
|
receptionTypes.value.map(rt => ({ value: rt.id, label: rt.label }))
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
<div class="flex items-center justify-start gap-10">
|
<div class="flex items-center justify-start gap-10">
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
<h1 class="text-3xl font-bold uppercase text-primary-500">liste des expéditions finies</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">liste des expéditions finies</h1>
|
||||||
|
<div
|
||||||
|
v-if="auth.isBureau"
|
||||||
|
class="bg-primary-500 p-1 rounded-md flex items-center cursor-pointer hover:opacity-80"
|
||||||
|
:class="exporting ? 'cursor-not-allowed opacity-60' : ''"
|
||||||
|
title="Exporter en Excel"
|
||||||
|
@click="exportShipments"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:file-excel-outline" size="32" class="text-white" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-[86px]">
|
<div class="px-[86px]">
|
||||||
@@ -77,9 +86,35 @@ import type { ShipmentData } from '~/services/dto/shipment-data'
|
|||||||
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
|
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
|
||||||
import { getShipmentTypeList } from '~/services/shipment-type'
|
import { getShipmentTypeList } from '~/services/shipment-type'
|
||||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const api = useApi()
|
||||||
const shipmentTypes = ref<ShipmentTypeData[]>([])
|
const shipmentTypes = ref<ShipmentTypeData[]>([])
|
||||||
|
const exporting = ref(false)
|
||||||
|
|
||||||
|
const exportShipments = async () => {
|
||||||
|
if (exporting.value) return
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
const blob = await api.getBlob('shipments/export')
|
||||||
|
const filename = `expeditions_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
a.style.display = 'none'
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 60_000)
|
||||||
|
} catch {
|
||||||
|
// toast déjà géré par useApi onResponseError
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const shipmentTypeOptions = computed(() =>
|
const shipmentTypeOptions = computed(() =>
|
||||||
shipmentTypes.value.map(st => ({ value: st.id, label: st.label }))
|
shipmentTypes.value.map(st => ({ value: st.id, label: st.label }))
|
||||||
|
|||||||
@@ -9,34 +9,3 @@ export async function createBovine(payload: BovinePayload) {
|
|||||||
toastSuccessKey: 'success.bovine.create'
|
toastSuccessKey: 'success.bovine.create'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBovines(nationalNumbers: string[]): Promise<{ created: BovineData[]; errors: string[] }> {
|
|
||||||
const created: BovineData[] = []
|
|
||||||
const errors: string[] = []
|
|
||||||
|
|
||||||
for (const nationalNumber of nationalNumbers) {
|
|
||||||
try {
|
|
||||||
const bovine = await createBovine({ nationalNumber })
|
|
||||||
if (bovine) {
|
|
||||||
created.push(bovine)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
errors.push(nationalNumber)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { created, errors }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getBovine(id: number) {
|
|
||||||
const api = useApi()
|
|
||||||
return api.get<BovineData>(`bovines/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateBovine(id: number, payload: BovinePayload) {
|
|
||||||
const api = useApi()
|
|
||||||
return api.patch<BovineData>(`bovines/${id}`, payload, {
|
|
||||||
toastErrorKey: 'errors.bovine.update',
|
|
||||||
toastSuccessKey: 'success.bovine.update'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ export interface BovineData {
|
|||||||
arrivalDate: string | null
|
arrivalDate: string | null
|
||||||
exitDate: string | null
|
exitDate: string | null
|
||||||
buildingCase: BovineBuildingCaseRef | null
|
buildingCase: BovineBuildingCaseRef | null
|
||||||
building: BovineBuildingRef | null
|
|
||||||
effectiveBuilding: BovineBuildingRef | null
|
|
||||||
supplier: string | null
|
supplier: string | null
|
||||||
workNumber: string | null
|
workNumber: string | null
|
||||||
birthDate: string | null
|
birthDate: string | null
|
||||||
@@ -29,9 +27,5 @@ export interface BovineData {
|
|||||||
|
|
||||||
export type BovinePayload = {
|
export type BovinePayload = {
|
||||||
nationalNumber?: string
|
nationalNumber?: string
|
||||||
receivedWeight?: number | null
|
|
||||||
pricePerKg?: number | null
|
|
||||||
arrivalDate?: string | null
|
|
||||||
buildingCase?: string | null
|
buildingCase?: string | null
|
||||||
supplier?: string | null
|
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/ApiResource/ReceptionExport.php
Normal file
32
src/ApiResource/ReceptionExport.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\ApiResource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||||
|
use App\State\Reception\ReceptionExportProvider;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/receptions/export',
|
||||||
|
openapi: new OpenApiOperation(
|
||||||
|
summary: 'Export Excel des réceptions terminées.',
|
||||||
|
description: 'Retourne un fichier XLSX listant toutes les réceptions validées (isValid = true), triées par date décroissante.',
|
||||||
|
tags: ['Receptions'],
|
||||||
|
),
|
||||||
|
security: "is_granted('ROLE_BUREAU')",
|
||||||
|
output: false,
|
||||||
|
provider: ReceptionExportProvider::class,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
final class ReceptionExport
|
||||||
|
{
|
||||||
|
#[ApiProperty(identifier: true)]
|
||||||
|
public string $id = 'current';
|
||||||
|
}
|
||||||
32
src/ApiResource/ShipmentExport.php
Normal file
32
src/ApiResource/ShipmentExport.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\ApiResource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||||
|
use App\State\Shipment\ShipmentExportProvider;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/shipments/export',
|
||||||
|
openapi: new OpenApiOperation(
|
||||||
|
summary: 'Export Excel des expéditions terminées.',
|
||||||
|
description: 'Retourne un fichier XLSX listant toutes les expéditions validées (isValid = true), triées par date décroissante.',
|
||||||
|
tags: ['Shipments'],
|
||||||
|
),
|
||||||
|
security: "is_granted('ROLE_BUREAU')",
|
||||||
|
output: false,
|
||||||
|
provider: ShipmentExportProvider::class,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
final class ShipmentExport
|
||||||
|
{
|
||||||
|
#[ApiProperty(identifier: true)]
|
||||||
|
public string $id = 'current';
|
||||||
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Command;
|
|
||||||
|
|
||||||
use App\Entity\Bovine;
|
|
||||||
use App\Entity\BovineMovement;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
use function count;
|
|
||||||
|
|
||||||
#[AsCommand(
|
|
||||||
name: 'app:backfill-bovine-movements',
|
|
||||||
description: 'Crée un mouvement initial pour chaque bovin ayant une case ou un bâtiment mais aucun mouvement enregistré.'
|
|
||||||
)]
|
|
||||||
class BackfillBovineMovementsCommand extends Command
|
|
||||||
{
|
|
||||||
private const FLUSH_EVERY = 100;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly EntityManagerInterface $entityManager,
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
|
|
||||||
$bovines = $this->entityManager->createQueryBuilder()
|
|
||||||
->select('b')
|
|
||||||
->from(Bovine::class, 'b')
|
|
||||||
->where('b.buildingCase IS NOT NULL OR b.building IS NOT NULL')
|
|
||||||
->andWhere('NOT EXISTS (SELECT 1 FROM '.BovineMovement::class.' m WHERE m.bovine = b)')
|
|
||||||
->getQuery()
|
|
||||||
->getResult()
|
|
||||||
;
|
|
||||||
|
|
||||||
$total = count($bovines);
|
|
||||||
if (0 === $total) {
|
|
||||||
$io->success('Aucun bovin à backfiller.');
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->info(sprintf('%d bovin(s) à backfiller.', $total));
|
|
||||||
|
|
||||||
$now = new DateTimeImmutable();
|
|
||||||
$created = 0;
|
|
||||||
$fallback = 0;
|
|
||||||
|
|
||||||
foreach ($bovines as $i => $bovine) {
|
|
||||||
$movement = new BovineMovement();
|
|
||||||
$movement->setBovine($bovine);
|
|
||||||
|
|
||||||
if (null !== $bovine->getBuildingCase()) {
|
|
||||||
$movement->setBuildingCase($bovine->getBuildingCase());
|
|
||||||
} else {
|
|
||||||
$movement->setBuilding($bovine->getBuilding());
|
|
||||||
}
|
|
||||||
|
|
||||||
$enteredAt = $bovine->getArrivalDate();
|
|
||||||
if (null === $enteredAt) {
|
|
||||||
$enteredAt = $now;
|
|
||||||
++$fallback;
|
|
||||||
}
|
|
||||||
$movement->setEnteredAt($enteredAt);
|
|
||||||
|
|
||||||
$this->entityManager->persist($movement);
|
|
||||||
++$created;
|
|
||||||
|
|
||||||
if (0 === ($i + 1) % self::FLUSH_EVERY) {
|
|
||||||
$this->entityManager->flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
$io->success(sprintf('%d mouvement(s) créé(s).', $created));
|
|
||||||
if ($fallback > 0) {
|
|
||||||
$io->warning(sprintf("%d bovin(s) sans date d'arrivée → enteredAt = maintenant.", $fallback));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Command;
|
|
||||||
|
|
||||||
use App\Entity\Bovine;
|
|
||||||
use App\Entity\Building;
|
|
||||||
use App\Entity\Supplier;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
#[AsCommand(
|
|
||||||
name: 'app:feed-bovine-prices',
|
|
||||||
description: 'Met à jour le poids, le prix au kilo et le fournisseur des bovins existants depuis un fichier XLSX.'
|
|
||||||
)]
|
|
||||||
final class FeedBovinePricesCommand extends Command
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private EntityManagerInterface $em,
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this
|
|
||||||
->addArgument('file', InputArgument::REQUIRED, 'Chemin absolu vers le fichier XLSX')
|
|
||||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simule sans persister en BDD')
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
$file = (string) $input->getArgument('file');
|
|
||||||
$dryRun = (bool) $input->getOption('dry-run');
|
|
||||||
|
|
||||||
if (!file_exists($file)) {
|
|
||||||
$io->error(sprintf('Fichier introuvable : %s', $file));
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->title('Feed bovins depuis '.basename($file));
|
|
||||||
if ($dryRun) {
|
|
||||||
$io->warning('Dry-run activé : aucune écriture en BDD.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$spreadsheet = IOFactory::load($file);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$io->error('Impossible de lire le fichier : '.$e->getMessage());
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sheet = $spreadsheet->getActiveSheet();
|
|
||||||
$highestRow = $sheet->getHighestRow();
|
|
||||||
|
|
||||||
// Pré-chargement des fournisseurs pour des lookups rapides (insensible casse).
|
|
||||||
$supplierByName = [];
|
|
||||||
foreach ($this->em->getRepository(Supplier::class)->findAll() as $supplier) {
|
|
||||||
$supplierByName[mb_strtoupper($supplier->getName())] = $supplier;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pré-chargement des bâtiments par code (insensible casse).
|
|
||||||
$buildingByCode = [];
|
|
||||||
foreach ($this->em->getRepository(Building::class)->findAll() as $building) {
|
|
||||||
$buildingByCode[mb_strtoupper($building->getCode())] = $building;
|
|
||||||
}
|
|
||||||
|
|
||||||
$bovineRepo = $this->em->getRepository(Bovine::class);
|
|
||||||
|
|
||||||
$stats = [
|
|
||||||
'total' => 0,
|
|
||||||
'updated' => 0,
|
|
||||||
'notFound' => 0,
|
|
||||||
'invalid' => 0,
|
|
||||||
'supplierMissing' => 0,
|
|
||||||
'buildingMissing' => 0,
|
|
||||||
];
|
|
||||||
$missingNationalNumbers = [];
|
|
||||||
$missingSuppliers = [];
|
|
||||||
$missingBuildings = [];
|
|
||||||
|
|
||||||
$io->progressStart($highestRow);
|
|
||||||
for ($row = 1; $row <= $highestRow; ++$row) {
|
|
||||||
++$stats['total'];
|
|
||||||
|
|
||||||
$rawNationalNumber = (string) ($sheet->getCell([1, $row])->getValue() ?? '');
|
|
||||||
$rawSupplier = (string) ($sheet->getCell([2, $row])->getValue() ?? '');
|
|
||||||
$rawWeight = $sheet->getCell([3, $row])->getValue();
|
|
||||||
$rawPrice = $sheet->getCell([4, $row])->getValue();
|
|
||||||
$rawBuilding = (string) ($sheet->getCell([5, $row])->getValue() ?? '');
|
|
||||||
|
|
||||||
$rawNationalNumber = trim($rawNationalNumber);
|
|
||||||
if ('' === $rawNationalNumber) {
|
|
||||||
++$stats['invalid'];
|
|
||||||
$io->progressAdvance();
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Garde : strip "FR" + espace optionnel uniquement s'il est présent.
|
|
||||||
$nationalNumber = preg_replace('/^FR\s*/i', '', $rawNationalNumber);
|
|
||||||
|
|
||||||
$bovine = $bovineRepo->findOneBy(['nationalNumber' => $nationalNumber]);
|
|
||||||
if (null === $bovine) {
|
|
||||||
++$stats['notFound'];
|
|
||||||
$missingNationalNumbers[] = $nationalNumber;
|
|
||||||
$io->progressAdvance();
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup supplier (peut être null si introuvable ou colonne vide).
|
|
||||||
$supplier = null;
|
|
||||||
$supplierName = mb_strtoupper(trim($rawSupplier));
|
|
||||||
if ('' !== $supplierName) {
|
|
||||||
$supplier = $supplierByName[$supplierName] ?? null;
|
|
||||||
if (null === $supplier) {
|
|
||||||
++$stats['supplierMissing'];
|
|
||||||
$missingSuppliers[$supplierName] = ($missingSuppliers[$supplierName] ?? 0) + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$weight = is_numeric($rawWeight) ? (int) $rawWeight : null;
|
|
||||||
$price = is_numeric($rawPrice) ? (float) $rawPrice : null;
|
|
||||||
|
|
||||||
if (null !== $weight) {
|
|
||||||
$bovine->setReceivedWeight($weight);
|
|
||||||
}
|
|
||||||
if (null !== $price) {
|
|
||||||
$bovine->setPricePerKg($price);
|
|
||||||
}
|
|
||||||
$bovine->setSupplier($supplier);
|
|
||||||
|
|
||||||
// Bâtiment direct : on n'écrase pas une affectation à une case existante.
|
|
||||||
$buildingCode = mb_strtoupper(trim($rawBuilding));
|
|
||||||
if ('' !== $buildingCode && null === $bovine->getBuildingCase()) {
|
|
||||||
$building = $buildingByCode[$buildingCode] ?? null;
|
|
||||||
if (null !== $building) {
|
|
||||||
$bovine->setBuilding($building);
|
|
||||||
} else {
|
|
||||||
++$stats['buildingMissing'];
|
|
||||||
$missingBuildings[$buildingCode] = ($missingBuildings[$buildingCode] ?? 0) + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
++$stats['updated'];
|
|
||||||
$io->progressAdvance();
|
|
||||||
}
|
|
||||||
$io->progressFinish();
|
|
||||||
|
|
||||||
if (!$dryRun) {
|
|
||||||
$this->em->flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->section('Résultats');
|
|
||||||
$io->table(
|
|
||||||
['Métrique', 'Valeur'],
|
|
||||||
[
|
|
||||||
['Lignes totales', $stats['total']],
|
|
||||||
['Bovins mis à jour', $stats['updated']],
|
|
||||||
['Bovins introuvables', $stats['notFound']],
|
|
||||||
['Lignes invalides', $stats['invalid']],
|
|
||||||
['Fournisseurs introuvables (supplier=null)', $stats['supplierMissing']],
|
|
||||||
['Bâtiments introuvables (building non set)', $stats['buildingMissing']],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
if ([] !== $missingNationalNumbers) {
|
|
||||||
$preview = array_slice($missingNationalNumbers, 0, 10);
|
|
||||||
$io->warning(sprintf(
|
|
||||||
'%d bovin(s) introuvable(s). Aperçu : %s%s',
|
|
||||||
count($missingNationalNumbers),
|
|
||||||
implode(', ', $preview),
|
|
||||||
count($missingNationalNumbers) > 10 ? '…' : '',
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([] !== $missingSuppliers) {
|
|
||||||
$list = [];
|
|
||||||
foreach ($missingSuppliers as $name => $count) {
|
|
||||||
$list[] = sprintf('%s (%d)', $name, $count);
|
|
||||||
}
|
|
||||||
$io->warning('Fournisseurs introuvables (bovins rattachés en null) : '.implode(', ', $list));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([] !== $missingBuildings) {
|
|
||||||
$list = [];
|
|
||||||
foreach ($missingBuildings as $code => $count) {
|
|
||||||
$list[] = sprintf('%s (%d)', $code, $count);
|
|
||||||
}
|
|
||||||
$io->warning('Bâtiments introuvables (champ non renseigné) : '.implode(', ', $list));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($dryRun) {
|
|
||||||
$io->success('Dry-run terminé. Relance sans --dry-run pour persister.');
|
|
||||||
} else {
|
|
||||||
$io->success('Feed terminé avec succès.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
src/Command/SyncBovineInventoryCommand.php
Normal file
49
src/Command/SyncBovineInventoryCommand.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Service\BovineInventorySyncer;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:sync-bovine-inventory',
|
||||||
|
description: "Synchronise l'inventaire bovin avec EDNOTIF (équivalent du bouton Rafraîchir de l'interface)."
|
||||||
|
)]
|
||||||
|
final class SyncBovineInventoryCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly BovineInventorySyncer $syncer,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->syncer->sync();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$io->error(sprintf('Échec de la synchronisation : %s', $e->getMessage()));
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->success(sprintf(
|
||||||
|
'Inventaire synchronisé · Créés : %d · Mis à jour : %d · Sortis : %d · Total EDNOTIF : %d',
|
||||||
|
$result->created,
|
||||||
|
$result->updated,
|
||||||
|
$result->exited,
|
||||||
|
$result->total,
|
||||||
|
));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,11 +96,6 @@ class Bovine
|
|||||||
#[ApiProperty(readableLink: true)]
|
#[ApiProperty(readableLink: true)]
|
||||||
private ?BuildingCase $buildingCase = null;
|
private ?BuildingCase $buildingCase = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
|
||||||
#[Groups(['bovine:read'])]
|
|
||||||
#[ApiProperty(readableLink: true)]
|
|
||||||
private ?Building $building = null;
|
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
#[ORM\ManyToOne]
|
||||||
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||||
private ?Supplier $supplier = null;
|
private ?Supplier $supplier = null;
|
||||||
@@ -244,28 +239,6 @@ class Bovine
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBuilding(): ?Building
|
|
||||||
{
|
|
||||||
return $this->building;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setBuilding(?Building $building): static
|
|
||||||
{
|
|
||||||
$this->building = $building;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bâtiment effectif d'un bovin : la case affectée si elle existe (logique
|
|
||||||
* historique), sinon le bâtiment direct (fed depuis l'XLSX initial).
|
|
||||||
*/
|
|
||||||
#[Groups(['bovine:read', 'building_case:read'])]
|
|
||||||
public function getEffectiveBuilding(): ?Building
|
|
||||||
{
|
|
||||||
return $this->buildingCase?->getIdBuilding() ?? $this->building;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSupplier(): ?Supplier
|
public function getSupplier(): ?Supplier
|
||||||
{
|
{
|
||||||
return $this->supplier;
|
return $this->supplier;
|
||||||
|
|||||||
@@ -44,11 +44,6 @@ class BovineMovement
|
|||||||
#[ApiProperty(readableLink: true)]
|
#[ApiProperty(readableLink: true)]
|
||||||
private ?BuildingCase $buildingCase = null;
|
private ?BuildingCase $buildingCase = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
|
||||||
#[Groups(['bovine:read'])]
|
|
||||||
#[ApiProperty(readableLink: true)]
|
|
||||||
private ?Building $building = null;
|
|
||||||
|
|
||||||
#[ORM\Column(type: 'datetime_immutable')]
|
#[ORM\Column(type: 'datetime_immutable')]
|
||||||
#[Groups(['bovine:read', 'bovine_movement:write'])]
|
#[Groups(['bovine:read', 'bovine_movement:write'])]
|
||||||
private DateTimeImmutable $enteredAt;
|
private DateTimeImmutable $enteredAt;
|
||||||
@@ -86,18 +81,6 @@ class BovineMovement
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBuilding(): ?Building
|
|
||||||
{
|
|
||||||
return $this->building;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setBuilding(?Building $building): static
|
|
||||||
{
|
|
||||||
$this->building = $building;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getEnteredAt(): DateTimeImmutable
|
public function getEnteredAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->enteredAt;
|
return $this->enteredAt;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use ApiPlatform\Metadata\Patch;
|
|||||||
use ApiPlatform\Metadata\Post;
|
use ApiPlatform\Metadata\Post;
|
||||||
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||||
use App\Dto\PontBasculeReading;
|
use App\Dto\PontBasculeReading;
|
||||||
|
use App\Repository\ReceptionRepository;
|
||||||
use App\State\Reception\ReceptionReceiptProvider;
|
use App\State\Reception\ReceptionReceiptProvider;
|
||||||
use App\State\Reception\ReceptionWeighingProvider;
|
use App\State\Reception\ReceptionWeighingProvider;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
@@ -28,7 +29,7 @@ use Symfony\Component\Serializer\Attribute\Context;
|
|||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||||
|
|
||||||
#[ORM\Entity]
|
#[ORM\Entity(repositoryClass: ReceptionRepository::class)]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ORM\Table(name: 'reception')]
|
#[ORM\Table(name: 'reception')]
|
||||||
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use ApiPlatform\Metadata\Patch;
|
|||||||
use ApiPlatform\Metadata\Post;
|
use ApiPlatform\Metadata\Post;
|
||||||
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||||
use App\Dto\PontBasculeReading;
|
use App\Dto\PontBasculeReading;
|
||||||
|
use App\Repository\ShipmentRepository;
|
||||||
use App\State\Shipment\ShipmentReceiptProvider;
|
use App\State\Shipment\ShipmentReceiptProvider;
|
||||||
use App\State\Shipment\ShipmentWeighingProvider;
|
use App\State\Shipment\ShipmentWeighingProvider;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
@@ -28,7 +29,7 @@ use Symfony\Component\Serializer\Attribute\Context;
|
|||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||||
|
|
||||||
#[ORM\Entity]
|
#[ORM\Entity(repositoryClass: ShipmentRepository::class)]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ORM\Table(name: 'shipment')]
|
#[ORM\Table(name: 'shipment')]
|
||||||
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
||||||
|
|||||||
52
src/Repository/ReceptionRepository.php
Normal file
52
src/Repository/ReceptionRepository.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Reception;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Reception>
|
||||||
|
*/
|
||||||
|
final class ReceptionRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Reception::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste des réceptions validées pour l'export Excel (de la plus récente à la plus ancienne).
|
||||||
|
*
|
||||||
|
* @return list<Reception>
|
||||||
|
*/
|
||||||
|
public function findValidatedForExport(): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('r')
|
||||||
|
->leftJoin('r.supplier', 'supplier')->addSelect('supplier')
|
||||||
|
->leftJoin('supplier.addresses', 'supplierAddresses')->addSelect('supplierAddresses')
|
||||||
|
->leftJoin('r.address', 'address')->addSelect('address')
|
||||||
|
->leftJoin('r.carrier', 'carrier')->addSelect('carrier')
|
||||||
|
->leftJoin('r.driver', 'driver')->addSelect('driver')
|
||||||
|
->leftJoin('r.truck', 'truck')->addSelect('truck')
|
||||||
|
->leftJoin('r.user', 'user')->addSelect('user')
|
||||||
|
->leftJoin('r.receptionType', 'receptionType')->addSelect('receptionType')
|
||||||
|
->leftJoin('r.merchandiseType', 'merchandiseType')->addSelect('merchandiseType')
|
||||||
|
->leftJoin('r.weights', 'weights')->addSelect('weights')
|
||||||
|
->leftJoin('r.bovines_types', 'bovinesTypes')->addSelect('bovinesTypes')
|
||||||
|
->leftJoin('bovinesTypes.bovineType', 'bovineType')->addSelect('bovineType')
|
||||||
|
->leftJoin('r.pelletBuildings', 'pelletBuildings')->addSelect('pelletBuildings')
|
||||||
|
->leftJoin('pelletBuildings.pelletType', 'pelletType')->addSelect('pelletType')
|
||||||
|
->leftJoin('pelletBuildings.building', 'pelletBuilding')->addSelect('pelletBuilding')
|
||||||
|
->where('r.isValid = :valid')
|
||||||
|
->setParameter('valid', true)
|
||||||
|
->orderBy('r.receptionDate', 'DESC')
|
||||||
|
->addOrderBy('r.id', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/Repository/ShipmentRepository.php
Normal file
46
src/Repository/ShipmentRepository.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Shipment;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Shipment>
|
||||||
|
*/
|
||||||
|
final class ShipmentRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Shipment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste des expéditions validées pour l'export Excel (de la plus récente à la plus ancienne).
|
||||||
|
*
|
||||||
|
* @return list<Shipment>
|
||||||
|
*/
|
||||||
|
public function findValidatedForExport(): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('s')
|
||||||
|
->leftJoin('s.customer', 'customer')->addSelect('customer')
|
||||||
|
->leftJoin('customer.addresses', 'customerAddresses')->addSelect('customerAddresses')
|
||||||
|
->leftJoin('s.address', 'address')->addSelect('address')
|
||||||
|
->leftJoin('s.carrier', 'carrier')->addSelect('carrier')
|
||||||
|
->leftJoin('s.driver', 'driver')->addSelect('driver')
|
||||||
|
->leftJoin('s.truck', 'truck')->addSelect('truck')
|
||||||
|
->leftJoin('s.user', 'user')->addSelect('user')
|
||||||
|
->leftJoin('s.shipmentType', 'shipmentType')->addSelect('shipmentType')
|
||||||
|
->leftJoin('s.weights', 'weights')->addSelect('weights')
|
||||||
|
->where('s.isValid = :valid')
|
||||||
|
->setParameter('valid', true)
|
||||||
|
->orderBy('s.shipmentDate', 'DESC')
|
||||||
|
->addOrderBy('s.id', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/Service/BovineInventorySyncer.php
Normal file
138
src/Service/BovineInventorySyncer.php
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\ApiResource\BovineSyncInventoryResult;
|
||||||
|
use App\Entity\Bovine;
|
||||||
|
use App\Entity\BovineType;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
|
||||||
|
use Malio\EdnotifBundle\Bovin\Dto\AnimalSummaryDto;
|
||||||
|
|
||||||
|
final class BovineInventorySyncer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, BovineType>
|
||||||
|
*/
|
||||||
|
private array $bovineTypeCache = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly BovinApiInterface $bovinApi,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function sync(): BovineSyncInventoryResult
|
||||||
|
{
|
||||||
|
$inventory = $this->bovinApi->getInventory(new DateTimeImmutable('today'));
|
||||||
|
|
||||||
|
$result = new BovineSyncInventoryResult();
|
||||||
|
$result->total = count($inventory->animals);
|
||||||
|
|
||||||
|
$this->bovineTypeCache = [];
|
||||||
|
foreach ($this->em->getRepository(BovineType::class)->findAll() as $bovineType) {
|
||||||
|
if (null !== $bovineType->getCode()) {
|
||||||
|
$this->bovineTypeCache[$bovineType->getCode()] = $bovineType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingByNationalNumber = [];
|
||||||
|
foreach ($this->em->getRepository(Bovine::class)->findAll() as $bovine) {
|
||||||
|
$existingByNationalNumber[$bovine->getNationalNumber()] = $bovine;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen = [];
|
||||||
|
foreach ($inventory->animals as $animal) {
|
||||||
|
$nationalNumber = $animal->identification?->bovin?->nationalNumber;
|
||||||
|
if (null === $nationalNumber || '' === $nationalNumber) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[$nationalNumber] = true;
|
||||||
|
|
||||||
|
if (isset($existingByNationalNumber[$nationalNumber])) {
|
||||||
|
$bovine = $existingByNationalNumber[$nationalNumber];
|
||||||
|
++$result->updated;
|
||||||
|
} else {
|
||||||
|
$bovine = new Bovine();
|
||||||
|
$bovine->setNationalNumber($nationalNumber);
|
||||||
|
$this->em->persist($bovine);
|
||||||
|
++$result->created;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applyEdnotifData($bovine, $animal);
|
||||||
|
$bovine->setExitedAt(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = new DateTimeImmutable();
|
||||||
|
foreach ($existingByNationalNumber as $nationalNumber => $bovine) {
|
||||||
|
if (isset($seen[$nationalNumber])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (null !== $bovine->getExitedAt()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$bovine->setExitedAt($now);
|
||||||
|
++$result->exited;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyEdnotifData(Bovine $bovine, AnimalSummaryDto $animal): void
|
||||||
|
{
|
||||||
|
$identification = $animal->identification;
|
||||||
|
if (null !== $identification) {
|
||||||
|
$bovine->setSex($identification->sex);
|
||||||
|
$bovine->setBovineType($this->resolveBovineType($identification->breedType));
|
||||||
|
$bovine->setWorkNumber($identification->workNumber);
|
||||||
|
$bovine->setBirthDate($identification->birthDate?->date);
|
||||||
|
|
||||||
|
$bovine->setMotherNationalNumber($identification->motherCarrier?->bovin?->nationalNumber);
|
||||||
|
$bovine->setMotherBovineType($this->resolveBovineType($identification->motherCarrier?->breedType));
|
||||||
|
$bovine->setFatherNationalNumber($identification->fatherIpg?->bovin?->nationalNumber);
|
||||||
|
$bovine->setFatherBovineType($this->resolveBovineType($identification->fatherIpg?->breedType));
|
||||||
|
}
|
||||||
|
|
||||||
|
$latestEntry = null;
|
||||||
|
$latestExit = null;
|
||||||
|
foreach ($animal->presencePeriods as $period) {
|
||||||
|
if (null !== $period->entry?->date && (null === $latestEntry || $period->entry->date > $latestEntry)) {
|
||||||
|
$latestEntry = $period->entry->date;
|
||||||
|
}
|
||||||
|
if (null !== $period->exit?->date && (null === $latestExit || $period->exit->date > $latestExit)) {
|
||||||
|
$latestExit = $period->exit->date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$bovine->setArrivalDate($latestEntry);
|
||||||
|
$bovine->setExitDate($latestExit);
|
||||||
|
$bovine->refreshAgeMonths();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trouve un BovineType existant par code, sinon en crée un placeholder
|
||||||
|
* que l'admin pourra renommer dans /admin/bovin/bovin-list.
|
||||||
|
*/
|
||||||
|
private function resolveBovineType(?string $code): ?BovineType
|
||||||
|
{
|
||||||
|
if (null === $code || '' === $code) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->bovineTypeCache[$code])) {
|
||||||
|
return $this->bovineTypeCache[$code];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bovineType = new BovineType();
|
||||||
|
$bovineType->setCode($code);
|
||||||
|
$bovineType->setLabel(sprintf('À renommer (%s)', $code));
|
||||||
|
$this->em->persist($bovineType);
|
||||||
|
|
||||||
|
$this->bovineTypeCache[$code] = $bovineType;
|
||||||
|
|
||||||
|
return $bovineType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,13 +18,15 @@ use PhpOffice\PhpSpreadsheet\Style\Border;
|
|||||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
|
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @implements ProviderInterface<Response>
|
* @implements ProviderInterface<Response>
|
||||||
*/
|
*/
|
||||||
final class BovineInventoryExportProvider implements ProviderInterface
|
final readonly class BovineInventoryExportProvider implements ProviderInterface
|
||||||
{
|
{
|
||||||
private const FARM_NAME = 'FERME SCEA LES NAUDS';
|
private const FARM_NAME = 'FERME SCEA LES NAUDS';
|
||||||
|
|
||||||
@@ -70,6 +72,7 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private BovineRepository $bovineRepository,
|
private BovineRepository $bovineRepository,
|
||||||
private RequestStack $requestStack,
|
private RequestStack $requestStack,
|
||||||
|
private LoggerInterface $logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
||||||
@@ -253,7 +256,16 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
|||||||
// Lignes de données
|
// Lignes de données
|
||||||
$rowNumber = 5;
|
$rowNumber = 5;
|
||||||
foreach ($bovines as $bovine) {
|
foreach ($bovines as $bovine) {
|
||||||
$this->writeBovineRow($sheet, $rowNumber, $bovine);
|
try {
|
||||||
|
$this->writeBovineRow($sheet, $rowNumber, $bovine);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->warning('Export inventaire bovin : ligne ignorée suite à une erreur.', [
|
||||||
|
'bovineId' => $bovine->getId(),
|
||||||
|
'nationalNumber' => $bovine->getNationalNumber(),
|
||||||
|
'row' => $rowNumber,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
}
|
||||||
++$rowNumber;
|
++$rowNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +288,7 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
|||||||
$type = $bovine->getBovineType();
|
$type = $bovine->getBovineType();
|
||||||
$isLim = self::BREED_CODE_LIMOUSINE === $type?->getCode();
|
$isLim = self::BREED_CODE_LIMOUSINE === $type?->getCode();
|
||||||
$isCharo = self::BREED_CODE_CHAROLAISE === $type?->getCode();
|
$isCharo = self::BREED_CODE_CHAROLAISE === $type?->getCode();
|
||||||
$building = $bovine->getBuildingCase()?->getIdBuilding() ?? $bovine->getBuilding();
|
$building = $bovine->getBuildingCase()?->getIdBuilding();
|
||||||
$code = $building?->getCode();
|
$code = $building?->getCode();
|
||||||
|
|
||||||
$sheet->setCellValue('A'.$row, $isLim ? 'X' : '');
|
$sheet->setCellValue('A'.$row, $isLim ? 'X' : '');
|
||||||
@@ -284,22 +296,25 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
|||||||
? (int) $bovine->getWorkNumber()
|
? (int) $bovine->getWorkNumber()
|
||||||
: ($bovine->getWorkNumber() ?? ''));
|
: ($bovine->getWorkNumber() ?? ''));
|
||||||
$sheet->setCellValue('C'.$row, $isCharo ? 'X' : '');
|
$sheet->setCellValue('C'.$row, $isCharo ? 'X' : '');
|
||||||
$sheet->setCellValue('D'.$row, 'FR '.$bovine->getNationalNumber());
|
$national = $bovine->getNationalNumber();
|
||||||
|
$sheet->setCellValue('D'.$row, '' === $national ? '' : 'FR '.$national);
|
||||||
$sheet->setCellValue('E'.$row, 'B1' === $code ? 'X' : '');
|
$sheet->setCellValue('E'.$row, 'B1' === $code ? 'X' : '');
|
||||||
$sheet->setCellValue('F'.$row, 'B2' === $code ? 'X' : '');
|
$sheet->setCellValue('F'.$row, 'B2' === $code ? 'X' : '');
|
||||||
$sheet->setCellValue('G'.$row, 'B3' === $code ? 'X' : '');
|
$sheet->setCellValue('G'.$row, 'B3' === $code ? 'X' : '');
|
||||||
$sheet->setCellValue('H'.$row, $bovine->getBuildingCase()?->getCaseNumber() ?? '');
|
$sheet->setCellValue('H'.$row, $bovine->getBuildingCase()?->getCaseNumber() ?? '');
|
||||||
$sheet->setCellValue('I'.$row, $bovine->getSupplier()?->getName() ?? '');
|
$sheet->setCellValue('I'.$row, $bovine->getSupplier()?->getName() ?? '');
|
||||||
|
|
||||||
$birth = $bovine->getBirthDate();
|
$birth = $bovine->getBirthDate();
|
||||||
$arrival = $bovine->getArrivalDate();
|
$arrival = $bovine->getArrivalDate();
|
||||||
if (null !== $birth) {
|
$birthExcel = $this->safePhpToExcel($birth);
|
||||||
$sheet->setCellValue('J'.$row, ExcelDate::PHPToExcel($birth));
|
$arrivalExcel = $this->safePhpToExcel($arrival);
|
||||||
|
if (null !== $birthExcel) {
|
||||||
|
$sheet->setCellValue('J'.$row, $birthExcel);
|
||||||
}
|
}
|
||||||
if (null !== $arrival) {
|
if (null !== $arrivalExcel) {
|
||||||
$sheet->setCellValue('K'.$row, ExcelDate::PHPToExcel($arrival));
|
$sheet->setCellValue('K'.$row, $arrivalExcel);
|
||||||
}
|
}
|
||||||
if (null !== $birth && null !== $arrival) {
|
if (null !== $birth && null !== $arrival && $birth <= $arrival) {
|
||||||
$diff = $birth->diff($arrival);
|
$diff = $birth->diff($arrival);
|
||||||
$sheet->setCellValue('L'.$row, ($diff->y * 12) + $diff->m);
|
$sheet->setCellValue('L'.$row, ($diff->y * 12) + $diff->m);
|
||||||
}
|
}
|
||||||
@@ -343,6 +358,24 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une date PHP en numéro de série Excel, ou null si la date est absente / hors plage Excel (< 1900).
|
||||||
|
*/
|
||||||
|
private function safePhpToExcel(?DateTimeImmutable $date): ?float
|
||||||
|
{
|
||||||
|
if (null === $date) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$value = ExcelDate::PHPToExcel($date);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_float($value) ? $value : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sous-titre dynamique selon les tranches d'âge cochées.
|
* Sous-titre dynamique selon les tranches d'âge cochées.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ final class BovineMovementProcessor implements ProcessorInterface
|
|||||||
$enteredAt = $data->hasEnteredAt() ? $data->getEnteredAt() : new DateTimeImmutable();
|
$enteredAt = $data->hasEnteredAt() ? $data->getEnteredAt() : new DateTimeImmutable();
|
||||||
$data->setEnteredAt($enteredAt);
|
$data->setEnteredAt($enteredAt);
|
||||||
$data->setLeftAt(null);
|
$data->setLeftAt(null);
|
||||||
$data->setBuilding(null);
|
|
||||||
|
|
||||||
$bovine = $data->getBovine();
|
$bovine = $data->getBovine();
|
||||||
|
|
||||||
|
|||||||
@@ -7,26 +7,15 @@ namespace App\State\Bovin;
|
|||||||
use ApiPlatform\Metadata\Operation;
|
use ApiPlatform\Metadata\Operation;
|
||||||
use ApiPlatform\State\ProcessorInterface;
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
use App\ApiResource\BovineSyncInventoryResult;
|
use App\ApiResource\BovineSyncInventoryResult;
|
||||||
use App\Entity\Bovine;
|
use App\Service\BovineInventorySyncer;
|
||||||
use App\Entity\BovineType;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
|
|
||||||
use Malio\EdnotifBundle\Bovin\Dto\AnimalSummaryDto;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @implements ProcessorInterface<mixed, BovineSyncInventoryResult>
|
* @implements ProcessorInterface<mixed, BovineSyncInventoryResult>
|
||||||
*/
|
*/
|
||||||
final class BovineSyncInventoryProcessor implements ProcessorInterface
|
final readonly class BovineSyncInventoryProcessor implements ProcessorInterface
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var array<string, BovineType>
|
|
||||||
*/
|
|
||||||
private array $bovineTypeCache = [];
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private BovinApiInterface $bovinApi,
|
private BovineInventorySyncer $syncer,
|
||||||
private EntityManagerInterface $em,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function process(
|
public function process(
|
||||||
@@ -35,113 +24,6 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
|
|||||||
array $uriVariables = [],
|
array $uriVariables = [],
|
||||||
array $context = [],
|
array $context = [],
|
||||||
): BovineSyncInventoryResult {
|
): BovineSyncInventoryResult {
|
||||||
$inventory = $this->bovinApi->getInventory(new DateTimeImmutable('today'));
|
return $this->syncer->sync();
|
||||||
|
|
||||||
$result = new BovineSyncInventoryResult();
|
|
||||||
$result->total = count($inventory->animals);
|
|
||||||
|
|
||||||
$this->bovineTypeCache = [];
|
|
||||||
foreach ($this->em->getRepository(BovineType::class)->findAll() as $bovineType) {
|
|
||||||
if (null !== $bovineType->getCode()) {
|
|
||||||
$this->bovineTypeCache[$bovineType->getCode()] = $bovineType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$existingByNationalNumber = [];
|
|
||||||
foreach ($this->em->getRepository(Bovine::class)->findAll() as $bovine) {
|
|
||||||
$existingByNationalNumber[$bovine->getNationalNumber()] = $bovine;
|
|
||||||
}
|
|
||||||
|
|
||||||
$seen = [];
|
|
||||||
foreach ($inventory->animals as $animal) {
|
|
||||||
$nationalNumber = $animal->identification?->bovin?->nationalNumber;
|
|
||||||
if (null === $nationalNumber || '' === $nationalNumber) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$seen[$nationalNumber] = true;
|
|
||||||
|
|
||||||
if (isset($existingByNationalNumber[$nationalNumber])) {
|
|
||||||
$bovine = $existingByNationalNumber[$nationalNumber];
|
|
||||||
++$result->updated;
|
|
||||||
} else {
|
|
||||||
$bovine = new Bovine();
|
|
||||||
$bovine->setNationalNumber($nationalNumber);
|
|
||||||
$this->em->persist($bovine);
|
|
||||||
++$result->created;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->applyEdnotifData($bovine, $animal);
|
|
||||||
$bovine->setExitedAt(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
$now = new DateTimeImmutable();
|
|
||||||
foreach ($existingByNationalNumber as $nationalNumber => $bovine) {
|
|
||||||
if (isset($seen[$nationalNumber])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (null !== $bovine->getExitedAt()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$bovine->setExitedAt($now);
|
|
||||||
++$result->exited;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->em->flush();
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function applyEdnotifData(Bovine $bovine, AnimalSummaryDto $animal): void
|
|
||||||
{
|
|
||||||
$identification = $animal->identification;
|
|
||||||
if (null !== $identification) {
|
|
||||||
$bovine->setSex($identification->sex);
|
|
||||||
$bovine->setBovineType($this->resolveBovineType($identification->breedType));
|
|
||||||
$bovine->setWorkNumber($identification->workNumber);
|
|
||||||
$bovine->setBirthDate($identification->birthDate?->date);
|
|
||||||
|
|
||||||
$bovine->setMotherNationalNumber($identification->motherCarrier?->bovin?->nationalNumber);
|
|
||||||
$bovine->setMotherBovineType($this->resolveBovineType($identification->motherCarrier?->breedType));
|
|
||||||
$bovine->setFatherNationalNumber($identification->fatherIpg?->bovin?->nationalNumber);
|
|
||||||
$bovine->setFatherBovineType($this->resolveBovineType($identification->fatherIpg?->breedType));
|
|
||||||
}
|
|
||||||
|
|
||||||
$latestEntry = null;
|
|
||||||
$latestExit = null;
|
|
||||||
foreach ($animal->presencePeriods as $period) {
|
|
||||||
if (null !== $period->entry?->date && (null === $latestEntry || $period->entry->date > $latestEntry)) {
|
|
||||||
$latestEntry = $period->entry->date;
|
|
||||||
}
|
|
||||||
if (null !== $period->exit?->date && (null === $latestExit || $period->exit->date > $latestExit)) {
|
|
||||||
$latestExit = $period->exit->date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$bovine->setArrivalDate($latestEntry);
|
|
||||||
$bovine->setExitDate($latestExit);
|
|
||||||
$bovine->refreshAgeMonths();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trouve un BovineType existant par code, sinon en crée un placeholder
|
|
||||||
* que l'admin pourra renommer dans /admin/bovin/bovin-list.
|
|
||||||
*/
|
|
||||||
private function resolveBovineType(?string $code): ?BovineType
|
|
||||||
{
|
|
||||||
if (null === $code || '' === $code) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($this->bovineTypeCache[$code])) {
|
|
||||||
return $this->bovineTypeCache[$code];
|
|
||||||
}
|
|
||||||
|
|
||||||
$bovineType = new BovineType();
|
|
||||||
$bovineType->setCode($code);
|
|
||||||
$bovineType->setLabel(sprintf('À renommer (%s)', $code));
|
|
||||||
$this->em->persist($bovineType);
|
|
||||||
|
|
||||||
$this->bovineTypeCache[$code] = $bovineType;
|
|
||||||
|
|
||||||
return $bovineType;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
358
src/State/Reception/ReceptionExportProvider.php
Normal file
358
src/State/Reception/ReceptionExportProvider.php
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State\Reception;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Entity\Address;
|
||||||
|
use App\Entity\Reception;
|
||||||
|
use App\Entity\Weight;
|
||||||
|
use App\Repository\ReceptionRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements ProviderInterface<Response>
|
||||||
|
*/
|
||||||
|
final readonly class ReceptionExportProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
private const FARM_NAME = 'FERME SCEA LES NAUDS';
|
||||||
|
|
||||||
|
private const HEADER_FILL = 'FFCCECFF';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Largeurs de colonnes (A à V).
|
||||||
|
*/
|
||||||
|
private const COLUMN_WIDTHS = [
|
||||||
|
'A' => 12.0,
|
||||||
|
'B' => 11.0,
|
||||||
|
'C' => 7.0,
|
||||||
|
'D' => 14.0,
|
||||||
|
'E' => 12.0,
|
||||||
|
'F' => 22.0,
|
||||||
|
'G' => 30.0,
|
||||||
|
'H' => 30.0,
|
||||||
|
'I' => 18.0,
|
||||||
|
'J' => 8.0,
|
||||||
|
'K' => 18.0,
|
||||||
|
'L' => 14.0,
|
||||||
|
'M' => 12.0,
|
||||||
|
'N' => 16.0,
|
||||||
|
'O' => 22.0,
|
||||||
|
'P' => 11.0,
|
||||||
|
'Q' => 11.0,
|
||||||
|
'R' => 11.0,
|
||||||
|
'S' => 22.0,
|
||||||
|
'T' => 26.0,
|
||||||
|
'U' => 9.0,
|
||||||
|
'V' => 26.0,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ReceptionRepository $receptionRepository,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
||||||
|
{
|
||||||
|
$receptions = $this->receptionRepository->findValidatedForExport();
|
||||||
|
|
||||||
|
$spreadsheet = $this->buildSpreadsheet($receptions);
|
||||||
|
$body = $this->renderXlsx($spreadsheet);
|
||||||
|
$filename = sprintf('receptions_%s.xlsx', new DateTimeImmutable()->format('Y-m-d'));
|
||||||
|
|
||||||
|
$response = new Response($body);
|
||||||
|
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
|
$response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $filename));
|
||||||
|
$response->headers->set('Content-Length', (string) strlen($body));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Reception> $receptions
|
||||||
|
*/
|
||||||
|
private function buildSpreadsheet(array $receptions): Spreadsheet
|
||||||
|
{
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
$spreadsheet->getDefaultStyle()->getFont()->setName('Aptos Narrow')->setSize(11);
|
||||||
|
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->setTitle('Receptions');
|
||||||
|
|
||||||
|
$pageSetup = $sheet->getPageSetup();
|
||||||
|
$pageSetup->setPaperSize(PageSetup::PAPERSIZE_A4);
|
||||||
|
$pageSetup->setOrientation(PageSetup::ORIENTATION_LANDSCAPE);
|
||||||
|
$pageSetup->setFitToWidth(1);
|
||||||
|
$pageSetup->setFitToHeight(0);
|
||||||
|
$pageSetup->setRowsToRepeatAtTopByStartAndEnd(1, 2);
|
||||||
|
$sheet->getPageMargins()->setTop(0.4)->setBottom(0.4)->setLeft(0.3)->setRight(0.3);
|
||||||
|
|
||||||
|
// Ligne 1 : titre + date
|
||||||
|
$sheet->setCellValue('A1', sprintf('%s — RÉCEPTIONS TERMINÉES', self::FARM_NAME));
|
||||||
|
$sheet->mergeCells('A1:U1');
|
||||||
|
$sheet->getStyle('A1:U1')->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'name' => 'Arial Black',
|
||||||
|
'size' => 16,
|
||||||
|
'bold' => true,
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$sheet->setCellValue('V1', ExcelDate::PHPToExcel(new DateTimeImmutable()));
|
||||||
|
$sheet->getStyle('V1')->getNumberFormat()->setFormatCode('dd/mm/yyyy');
|
||||||
|
$sheet->getStyle('V1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT)->setVertical(Alignment::VERTICAL_CENTER);
|
||||||
|
$sheet->getStyle('V1')->getFont()->setSize(12)->setBold(true);
|
||||||
|
$sheet->getRowDimension(1)->setRowHeight(26.0);
|
||||||
|
$sheet->getStyle('A1:V1')->getBorders()->getBottom()->setBorderStyle(Border::BORDER_THICK);
|
||||||
|
|
||||||
|
// Ligne 2 : en-têtes
|
||||||
|
$headers = [
|
||||||
|
'A' => 'N° identification',
|
||||||
|
'B' => 'Date',
|
||||||
|
'C' => 'Heure',
|
||||||
|
'D' => 'Type réception',
|
||||||
|
'E' => 'Utilisateur',
|
||||||
|
'F' => 'Fournisseur',
|
||||||
|
'G' => 'Adresse fournisseur',
|
||||||
|
'H' => 'Adresse réception',
|
||||||
|
'I' => 'Transporteur',
|
||||||
|
'J' => 'Code trans.',
|
||||||
|
'K' => 'Chauffeur',
|
||||||
|
'L' => 'Camion',
|
||||||
|
'M' => 'Plaque',
|
||||||
|
'N' => 'Type marchandise',
|
||||||
|
'O' => 'Détail marchandise',
|
||||||
|
'P' => 'Brut (kg)',
|
||||||
|
'Q' => 'Tare (kg)',
|
||||||
|
'R' => 'Net (kg)',
|
||||||
|
'S' => 'Détail bovins',
|
||||||
|
'T' => 'Bovins par type',
|
||||||
|
'U' => 'Total bovins',
|
||||||
|
'V' => 'Granulés / bâtiments',
|
||||||
|
];
|
||||||
|
foreach ($headers as $col => $value) {
|
||||||
|
$sheet->setCellValue($col.'2', $value);
|
||||||
|
}
|
||||||
|
$sheet->getRowDimension(2)->setRowHeight(32.0);
|
||||||
|
$sheet->getStyle('A2:V2')->applyFromArray([
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
'wrapText' => true,
|
||||||
|
],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['argb' => self::HEADER_FILL],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach (self::COLUMN_WIDTHS as $col => $width) {
|
||||||
|
$sheet->getColumnDimension($col)->setWidth($width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Données
|
||||||
|
$row = 3;
|
||||||
|
foreach ($receptions as $reception) {
|
||||||
|
try {
|
||||||
|
$this->writeReceptionRow($sheet, $row, $reception);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->warning('Export réceptions : ligne ignorée suite à une erreur.', [
|
||||||
|
'receptionId' => $reception->getId(),
|
||||||
|
'identificationNumber' => $reception->getIdentificationNumber(),
|
||||||
|
'row' => $row,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
++$row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastRow = $row - 1;
|
||||||
|
if ($lastRow >= 2) {
|
||||||
|
$sheet->getStyle('A2:V'.$lastRow)->getBorders()->applyFromArray([
|
||||||
|
'allBorders' => ['borderStyle' => Border::BORDER_THIN],
|
||||||
|
'outline' => ['borderStyle' => Border::BORDER_MEDIUM],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->freezePane('A3');
|
||||||
|
|
||||||
|
return $spreadsheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeReceptionRow(Worksheet $sheet, int $row, Reception $reception): void
|
||||||
|
{
|
||||||
|
$sheet->setCellValue('A'.$row, $reception->getIdentificationNumber() ?? '');
|
||||||
|
|
||||||
|
$date = $reception->getReceptionDate();
|
||||||
|
if (null !== $date) {
|
||||||
|
$excelDate = $this->safePhpToExcel($date);
|
||||||
|
if (null !== $excelDate) {
|
||||||
|
$sheet->setCellValue('B'.$row, $excelDate);
|
||||||
|
$sheet->getStyle('B'.$row)->getNumberFormat()->setFormatCode('dd/mm/yyyy');
|
||||||
|
}
|
||||||
|
$sheet->setCellValue('C'.$row, $date->format('H:i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->setCellValue('D'.$row, $reception->getReceptionType()?->getLabel() ?? '');
|
||||||
|
$sheet->setCellValue('E'.$row, $reception->getUser()?->getUsername() ?? '');
|
||||||
|
|
||||||
|
$supplier = $reception->getSupplier();
|
||||||
|
$sheet->setCellValue('F'.$row, $supplier?->getName() ?? '');
|
||||||
|
$sheet->setCellValue('G'.$row, $this->formatAddresses($supplier?->getAddresses()));
|
||||||
|
$sheet->setCellValue('H'.$row, $reception->getAddress()?->getFullAddress() ?? '');
|
||||||
|
|
||||||
|
$carrier = $reception->getCarrier();
|
||||||
|
$sheet->setCellValue('I'.$row, $carrier?->getName() ?? '');
|
||||||
|
$sheet->setCellValue('J'.$row, $carrier?->getCode() ?? '');
|
||||||
|
$sheet->setCellValue('K'.$row, $reception->getDriver()?->getName() ?? '');
|
||||||
|
$sheet->setCellValue('L'.$row, $reception->getTruck()?->getName() ?? '');
|
||||||
|
$sheet->setCellValue('M'.$row, $reception->getLicensePlate() ?? '');
|
||||||
|
|
||||||
|
$sheet->setCellValue('N'.$row, $reception->getMerchandiseType()?->getLabel() ?? '');
|
||||||
|
$sheet->setCellValue('O'.$row, $reception->getMerchandiseDetail() ?? '');
|
||||||
|
|
||||||
|
$gross = $this->extractWeight($reception->getWeights(), 'gross');
|
||||||
|
$tare = $this->extractWeight($reception->getWeights(), 'tare');
|
||||||
|
if (null !== $gross) {
|
||||||
|
$sheet->setCellValue('P'.$row, $gross);
|
||||||
|
}
|
||||||
|
if (null !== $tare) {
|
||||||
|
$sheet->setCellValue('Q'.$row, $tare);
|
||||||
|
}
|
||||||
|
if (null !== $gross && null !== $tare) {
|
||||||
|
$sheet->setCellValue('R'.$row, $gross - $tare);
|
||||||
|
}
|
||||||
|
$sheet->getStyle('P'.$row.':R'.$row)->getNumberFormat()->setFormatCode('#,##0');
|
||||||
|
|
||||||
|
$sheet->setCellValue('S'.$row, $reception->getBovineDetail() ?? '');
|
||||||
|
|
||||||
|
[$bovinesText, $bovinesTotal] = $this->formatBovineTypes($reception);
|
||||||
|
$sheet->setCellValue('T'.$row, $bovinesText);
|
||||||
|
if (null !== $bovinesTotal) {
|
||||||
|
$sheet->setCellValue('U'.$row, $bovinesTotal);
|
||||||
|
}
|
||||||
|
$sheet->setCellValue('V'.$row, $this->formatPelletBuildings($reception));
|
||||||
|
|
||||||
|
// Alignements
|
||||||
|
$sheet->getStyle('A'.$row.':C'.$row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle('J'.$row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle('P'.$row.':R'.$row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
|
||||||
|
$sheet->getStyle('U'.$row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle('G'.$row.':H'.$row)->getAlignment()->setWrapText(true);
|
||||||
|
$sheet->getStyle('O'.$row)->getAlignment()->setWrapText(true);
|
||||||
|
$sheet->getStyle('S'.$row.':V'.$row)->getAlignment()->setWrapText(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: string, 1: ?int} [texte concaténé, total]
|
||||||
|
*/
|
||||||
|
private function formatBovineTypes(Reception $reception): array
|
||||||
|
{
|
||||||
|
$parts = [];
|
||||||
|
$total = 0;
|
||||||
|
$found = false;
|
||||||
|
foreach ($reception->getBovinesTypes() as $rb) {
|
||||||
|
$label = $rb->getBovineType()?->getLabel();
|
||||||
|
$qty = $rb->getQuantity();
|
||||||
|
if (null === $label && null === $qty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parts[] = sprintf('%s : %d', $label ?? '—', $qty ?? 0);
|
||||||
|
$total += $qty ?? 0;
|
||||||
|
$found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [implode(', ', $parts), $found ? $total : null];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatPelletBuildings(Reception $reception): string
|
||||||
|
{
|
||||||
|
$parts = [];
|
||||||
|
foreach ($reception->getPelletBuildings() as $pb) {
|
||||||
|
$pellet = $pb->getPelletType()?->getLabel();
|
||||||
|
$building = $pb->getBuilding()?->getLabel() ?? $pb->getBuilding()?->getCode();
|
||||||
|
if (null === $pellet && null === $building) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parts[] = sprintf('%s (%s)', $pellet ?? '—', $building ?? '—');
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(', ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param null|iterable<Address> $addresses
|
||||||
|
*/
|
||||||
|
private function formatAddresses(?iterable $addresses): string
|
||||||
|
{
|
||||||
|
if (null === $addresses) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
foreach ($addresses as $address) {
|
||||||
|
$full = $address->getFullAddress();
|
||||||
|
if ('' !== $full) {
|
||||||
|
$parts[] = $full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' ; ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param iterable<Weight> $weights
|
||||||
|
*/
|
||||||
|
private function extractWeight(iterable $weights, string $type): ?int
|
||||||
|
{
|
||||||
|
foreach ($weights as $weight) {
|
||||||
|
if ($weight->getType() === $type) {
|
||||||
|
return $weight->getWeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function safePhpToExcel(?DateTimeImmutable $date): ?float
|
||||||
|
{
|
||||||
|
if (null === $date) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$value = ExcelDate::PHPToExcel($date);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_float($value) ? $value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderXlsx(Spreadsheet $spreadsheet): string
|
||||||
|
{
|
||||||
|
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||||
|
ob_start();
|
||||||
|
$writer->save('php://output');
|
||||||
|
$body = ob_get_clean();
|
||||||
|
|
||||||
|
return false !== $body ? $body : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
299
src/State/Shipment/ShipmentExportProvider.php
Normal file
299
src/State/Shipment/ShipmentExportProvider.php
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State\Shipment;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Entity\Address;
|
||||||
|
use App\Entity\Shipment;
|
||||||
|
use App\Entity\Weight;
|
||||||
|
use App\Repository\ShipmentRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements ProviderInterface<Response>
|
||||||
|
*/
|
||||||
|
final readonly class ShipmentExportProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
private const FARM_NAME = 'FERME SCEA LES NAUDS';
|
||||||
|
|
||||||
|
private const HEADER_FILL = 'FFCCECFF';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Largeurs de colonnes (A à Q).
|
||||||
|
*/
|
||||||
|
private const COLUMN_WIDTHS = [
|
||||||
|
'A' => 12.0,
|
||||||
|
'B' => 11.0,
|
||||||
|
'C' => 7.0,
|
||||||
|
'D' => 16.0,
|
||||||
|
'E' => 12.0,
|
||||||
|
'F' => 22.0,
|
||||||
|
'G' => 30.0,
|
||||||
|
'H' => 30.0,
|
||||||
|
'I' => 18.0,
|
||||||
|
'J' => 8.0,
|
||||||
|
'K' => 18.0,
|
||||||
|
'L' => 14.0,
|
||||||
|
'M' => 12.0,
|
||||||
|
'N' => 11.0,
|
||||||
|
'O' => 11.0,
|
||||||
|
'P' => 11.0,
|
||||||
|
'Q' => 13.0,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ShipmentRepository $shipmentRepository,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
||||||
|
{
|
||||||
|
$shipments = $this->shipmentRepository->findValidatedForExport();
|
||||||
|
|
||||||
|
$spreadsheet = $this->buildSpreadsheet($shipments);
|
||||||
|
$body = $this->renderXlsx($spreadsheet);
|
||||||
|
$filename = sprintf('expeditions_%s.xlsx', new DateTimeImmutable()->format('Y-m-d'));
|
||||||
|
|
||||||
|
$response = new Response($body);
|
||||||
|
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
|
$response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $filename));
|
||||||
|
$response->headers->set('Content-Length', (string) strlen($body));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Shipment> $shipments
|
||||||
|
*/
|
||||||
|
private function buildSpreadsheet(array $shipments): Spreadsheet
|
||||||
|
{
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
$spreadsheet->getDefaultStyle()->getFont()->setName('Aptos Narrow')->setSize(11);
|
||||||
|
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->setTitle('Expeditions');
|
||||||
|
|
||||||
|
$pageSetup = $sheet->getPageSetup();
|
||||||
|
$pageSetup->setPaperSize(PageSetup::PAPERSIZE_A4);
|
||||||
|
$pageSetup->setOrientation(PageSetup::ORIENTATION_LANDSCAPE);
|
||||||
|
$pageSetup->setFitToWidth(1);
|
||||||
|
$pageSetup->setFitToHeight(0);
|
||||||
|
$pageSetup->setRowsToRepeatAtTopByStartAndEnd(1, 2);
|
||||||
|
$sheet->getPageMargins()->setTop(0.4)->setBottom(0.4)->setLeft(0.3)->setRight(0.3);
|
||||||
|
|
||||||
|
// Ligne 1 : titre + date
|
||||||
|
$sheet->setCellValue('A1', sprintf('%s — EXPÉDITIONS TERMINÉES', self::FARM_NAME));
|
||||||
|
$sheet->mergeCells('A1:P1');
|
||||||
|
$sheet->getStyle('A1:P1')->applyFromArray([
|
||||||
|
'font' => [
|
||||||
|
'name' => 'Arial Black',
|
||||||
|
'size' => 16,
|
||||||
|
'bold' => true,
|
||||||
|
],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$sheet->setCellValue('Q1', ExcelDate::PHPToExcel(new DateTimeImmutable()));
|
||||||
|
$sheet->getStyle('Q1')->getNumberFormat()->setFormatCode('dd/mm/yyyy');
|
||||||
|
$sheet->getStyle('Q1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT)->setVertical(Alignment::VERTICAL_CENTER);
|
||||||
|
$sheet->getStyle('Q1')->getFont()->setSize(12)->setBold(true);
|
||||||
|
$sheet->getRowDimension(1)->setRowHeight(26.0);
|
||||||
|
$sheet->getStyle('A1:Q1')->getBorders()->getBottom()->setBorderStyle(Border::BORDER_THICK);
|
||||||
|
|
||||||
|
// Ligne 2 : en-têtes
|
||||||
|
$headers = [
|
||||||
|
'A' => 'N° identification',
|
||||||
|
'B' => 'Date',
|
||||||
|
'C' => 'Heure',
|
||||||
|
'D' => "Type d'expédition",
|
||||||
|
'E' => 'Utilisateur',
|
||||||
|
'F' => 'Client',
|
||||||
|
'G' => 'Adresse client',
|
||||||
|
'H' => 'Adresse expédition',
|
||||||
|
'I' => 'Transporteur',
|
||||||
|
'J' => 'Code trans.',
|
||||||
|
'K' => 'Chauffeur',
|
||||||
|
'L' => 'Camion',
|
||||||
|
'M' => 'Plaque',
|
||||||
|
'N' => 'Brut (kg)',
|
||||||
|
'O' => 'Tare (kg)',
|
||||||
|
'P' => 'Net (kg)',
|
||||||
|
'Q' => 'Nb bovins',
|
||||||
|
];
|
||||||
|
foreach ($headers as $col => $value) {
|
||||||
|
$sheet->setCellValue($col.'2', $value);
|
||||||
|
}
|
||||||
|
$sheet->getRowDimension(2)->setRowHeight(32.0);
|
||||||
|
$sheet->getStyle('A2:Q2')->applyFromArray([
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'alignment' => [
|
||||||
|
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||||
|
'vertical' => Alignment::VERTICAL_CENTER,
|
||||||
|
'wrapText' => true,
|
||||||
|
],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['argb' => self::HEADER_FILL],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach (self::COLUMN_WIDTHS as $col => $width) {
|
||||||
|
$sheet->getColumnDimension($col)->setWidth($width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Données
|
||||||
|
$row = 3;
|
||||||
|
foreach ($shipments as $shipment) {
|
||||||
|
try {
|
||||||
|
$this->writeShipmentRow($sheet, $row, $shipment);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->warning('Export expéditions : ligne ignorée suite à une erreur.', [
|
||||||
|
'shipmentId' => $shipment->getId(),
|
||||||
|
'identificationNumber' => $shipment->getIdentificationNumber(),
|
||||||
|
'row' => $row,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
++$row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastRow = $row - 1;
|
||||||
|
if ($lastRow >= 2) {
|
||||||
|
$sheet->getStyle('A2:Q'.$lastRow)->getBorders()->applyFromArray([
|
||||||
|
'allBorders' => ['borderStyle' => Border::BORDER_THIN],
|
||||||
|
'outline' => ['borderStyle' => Border::BORDER_MEDIUM],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->freezePane('A3');
|
||||||
|
|
||||||
|
return $spreadsheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeShipmentRow(Worksheet $sheet, int $row, Shipment $shipment): void
|
||||||
|
{
|
||||||
|
$sheet->setCellValue('A'.$row, $shipment->getIdentificationNumber() ?? '');
|
||||||
|
|
||||||
|
$date = $shipment->getShipmentDate();
|
||||||
|
if (null !== $date) {
|
||||||
|
$excelDate = $this->safePhpToExcel($date);
|
||||||
|
if (null !== $excelDate) {
|
||||||
|
$sheet->setCellValue('B'.$row, $excelDate);
|
||||||
|
$sheet->getStyle('B'.$row)->getNumberFormat()->setFormatCode('dd/mm/yyyy');
|
||||||
|
}
|
||||||
|
$sheet->setCellValue('C'.$row, $date->format('H:i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->setCellValue('D'.$row, $shipment->getShipmentType()?->getLabel() ?? '');
|
||||||
|
$sheet->setCellValue('E'.$row, $shipment->getUser()?->getUsername() ?? '');
|
||||||
|
|
||||||
|
$customer = $shipment->getCustomer();
|
||||||
|
$sheet->setCellValue('F'.$row, $customer?->getName() ?? '');
|
||||||
|
$sheet->setCellValue('G'.$row, $this->formatAddresses($customer?->getAddresses()));
|
||||||
|
$sheet->setCellValue('H'.$row, $shipment->getAddress()?->getFullAddress() ?? '');
|
||||||
|
|
||||||
|
$carrier = $shipment->getCarrier();
|
||||||
|
$sheet->setCellValue('I'.$row, $carrier?->getName() ?? '');
|
||||||
|
$sheet->setCellValue('J'.$row, $carrier?->getCode() ?? '');
|
||||||
|
$sheet->setCellValue('K'.$row, $shipment->getDriver()?->getName() ?? '');
|
||||||
|
$sheet->setCellValue('L'.$row, $shipment->getTruck()?->getName() ?? '');
|
||||||
|
$sheet->setCellValue('M'.$row, $shipment->getLicensePlate() ?? '');
|
||||||
|
|
||||||
|
$gross = $this->extractWeight($shipment->getWeights(), 'gross');
|
||||||
|
$tare = $this->extractWeight($shipment->getWeights(), 'tare');
|
||||||
|
if (null !== $gross) {
|
||||||
|
$sheet->setCellValue('N'.$row, $gross);
|
||||||
|
}
|
||||||
|
if (null !== $tare) {
|
||||||
|
$sheet->setCellValue('O'.$row, $tare);
|
||||||
|
}
|
||||||
|
if (null !== $gross && null !== $tare) {
|
||||||
|
$sheet->setCellValue('P'.$row, $gross - $tare);
|
||||||
|
}
|
||||||
|
$sheet->getStyle('N'.$row.':P'.$row)->getNumberFormat()->setFormatCode('#,##0');
|
||||||
|
|
||||||
|
$sheet->setCellValue('Q'.$row, $shipment->getNbBovinSend());
|
||||||
|
|
||||||
|
// Alignements
|
||||||
|
$sheet->getStyle('A'.$row.':C'.$row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle('J'.$row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle('N'.$row.':P'.$row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
|
||||||
|
$sheet->getStyle('Q'.$row)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle('G'.$row.':H'.$row)->getAlignment()->setWrapText(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param null|iterable<Address> $addresses
|
||||||
|
*/
|
||||||
|
private function formatAddresses(?iterable $addresses): string
|
||||||
|
{
|
||||||
|
if (null === $addresses) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
foreach ($addresses as $address) {
|
||||||
|
$full = $address->getFullAddress();
|
||||||
|
if ('' !== $full) {
|
||||||
|
$parts[] = $full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' ; ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param iterable<Weight> $weights
|
||||||
|
*/
|
||||||
|
private function extractWeight(iterable $weights, string $type): ?int
|
||||||
|
{
|
||||||
|
foreach ($weights as $weight) {
|
||||||
|
if ($weight->getType() === $type) {
|
||||||
|
return $weight->getWeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function safePhpToExcel(?DateTimeImmutable $date): ?float
|
||||||
|
{
|
||||||
|
if (null === $date) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$value = ExcelDate::PHPToExcel($date);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_float($value) ? $value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderXlsx(Spreadsheet $spreadsheet): string
|
||||||
|
{
|
||||||
|
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||||
|
ob_start();
|
||||||
|
$writer->save('php://output');
|
||||||
|
$body = ob_get_clean();
|
||||||
|
|
||||||
|
return false !== $body ? $body : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user