feat: ajout du prix au kilo et de l'age moyen bovin + feed bovin via xlsx (!50)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [x] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #50 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #50.
This commit is contained in:
@@ -19,7 +19,7 @@ use App\State\Bovin\BovineInventoryExportProvider;
|
||||
description: "Retourne un fichier XLSX listant tous les bovins actifs (exitedAt IS NULL) triés par date de naissance croissante, avec colorisation des lignes selon l'âge.",
|
||||
tags: ['Bovines'],
|
||||
),
|
||||
security: "is_granted('ROLE_USER')",
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
output: false,
|
||||
provider: BovineInventoryExportProvider::class,
|
||||
),
|
||||
|
||||
215
src/Command/FeedBovinePricesCommand.php
Normal file
215
src/Command/FeedBovinePricesCommand.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -476,9 +476,12 @@ class SeedCommand extends Command
|
||||
private function seedBovineTypes(): void
|
||||
{
|
||||
$bovineTypes = [
|
||||
['label' => 'Aubrac', 'code' => '14'],
|
||||
['label' => 'Limousine', 'code' => '34'],
|
||||
['label' => 'Charolaise', 'code' => '38'],
|
||||
['label' => 'Croisé', 'code' => '39'],
|
||||
['label' => 'Parthenaise', 'code' => '71'],
|
||||
['label' => "Blonde d'aquitaine", 'code' => '79'],
|
||||
];
|
||||
foreach ($bovineTypes as $type) {
|
||||
$this->upsertByCode(BovineType::class, $type['code'], static function (BovineType $entity) use ($type) {
|
||||
|
||||
@@ -77,9 +77,12 @@ class ReferenceFixtures extends Fixture
|
||||
}
|
||||
|
||||
$bovineTypes = [
|
||||
['label' => 'Aubrac', 'code' => '14'],
|
||||
['label' => 'Limousine', 'code' => '34'],
|
||||
['label' => 'Charolaise', 'code' => '38'],
|
||||
['label' => 'Croisé', 'code' => '39'],
|
||||
['label' => 'Parthenaise', 'code' => '71'],
|
||||
['label' => "Blonde d'aquitaine", 'code' => '79'],
|
||||
];
|
||||
foreach ($bovineTypes as $type) {
|
||||
$bovineType = new BovineType()
|
||||
|
||||
@@ -14,6 +14,7 @@ use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Repository\BovineRepository;
|
||||
use App\State\Bovin\BovineProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -21,17 +22,18 @@ use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Entity(repositoryClass: BovineRepository::class)]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\Table(name: 'bovine')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_bovine_national_number', columns: ['national_number'])]
|
||||
#[ApiFilter(SearchFilter::class, properties: [
|
||||
'nationalNumber' => 'ipartial',
|
||||
'workNumber' => 'ipartial',
|
||||
'breedCode' => 'ipartial',
|
||||
'sex' => 'exact',
|
||||
'buildingCase' => 'exact',
|
||||
'receivedWeight' => 'exact',
|
||||
'nationalNumber' => 'ipartial',
|
||||
'workNumber' => 'ipartial',
|
||||
'bovineType.label' => 'ipartial',
|
||||
'bovineType.code' => 'ipartial',
|
||||
'sex' => 'exact',
|
||||
'buildingCase' => 'exact',
|
||||
'receivedWeight' => 'exact',
|
||||
])]
|
||||
#[ApiFilter(DateFilter::class, properties: ['arrivalDate', 'birthDate', 'exitDate'])]
|
||||
#[ApiFilter(ExistsFilter::class, properties: ['exitedAt'])]
|
||||
@@ -77,6 +79,11 @@ class Bovine
|
||||
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||
private ?int $receivedWeight = null;
|
||||
|
||||
#[ORM\Column(type: 'float', nullable: true)]
|
||||
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN')")]
|
||||
private ?float $pricePerKg = null;
|
||||
|
||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
@@ -87,6 +94,11 @@ class Bovine
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?BuildingCase $buildingCase = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?Building $building = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||
private ?Supplier $supplier = null;
|
||||
@@ -100,9 +112,10 @@ class Bovine
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
private ?DateTimeImmutable $birthDate = null;
|
||||
|
||||
#[ORM\Column(length: 20, nullable: true)]
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read', 'building_case:read'])]
|
||||
private ?string $breedCode = null;
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?BovineType $bovineType = null;
|
||||
|
||||
#[ORM\Column(length: 1, nullable: true)]
|
||||
#[Groups(['bovine:read', 'building_case:read'])]
|
||||
@@ -151,6 +164,29 @@ class Bovine
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPricePerKg(): ?float
|
||||
{
|
||||
return $this->pricePerKg;
|
||||
}
|
||||
|
||||
public function setPricePerKg(?float $pricePerKg): static
|
||||
{
|
||||
$this->pricePerKg = $pricePerKg;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Groups(['bovine:read', 'building_case:read'])]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN')")]
|
||||
public function getFinalPrice(): ?float
|
||||
{
|
||||
if (null === $this->receivedWeight || null === $this->pricePerKg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->receivedWeight * $this->pricePerKg;
|
||||
}
|
||||
|
||||
public function getArrivalDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->arrivalDate;
|
||||
@@ -175,6 +211,28 @@ class Bovine
|
||||
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
|
||||
{
|
||||
return $this->supplier;
|
||||
@@ -211,14 +269,14 @@ class Bovine
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBreedCode(): ?string
|
||||
public function getBovineType(): ?BovineType
|
||||
{
|
||||
return $this->breedCode;
|
||||
return $this->bovineType;
|
||||
}
|
||||
|
||||
public function setBreedCode(?string $breedCode): static
|
||||
public function setBovineType(?BovineType $bovineType): static
|
||||
{
|
||||
$this->breedCode = $breedCode;
|
||||
$this->bovineType = $bovineType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ class BovineType
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 120)]
|
||||
#[Groups(['bovine-type:read', 'bovine-type:write', 'reception:read', 'reception-bovine:read'])]
|
||||
#[Groups(['bovine-type:read', 'bovine-type:write', 'reception:read', 'reception-bovine:read', 'bovine:read', 'building_case:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(length: 50)]
|
||||
#[Groups(['bovine-type:read', 'bovine-type:write', 'reception:read', 'reception-bovine:read'])]
|
||||
#[Groups(['bovine-type:read', 'bovine-type:write', 'reception:read', 'reception-bovine:read', 'bovine:read', 'building_case:read'])]
|
||||
private ?string $code = null;
|
||||
|
||||
public function getId(): ?int
|
||||
|
||||
@@ -16,6 +16,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'building')]
|
||||
#[ApiResource(
|
||||
order: ['displayOrder' => 'ASC', 'id' => 'ASC'],
|
||||
operations: [
|
||||
new Get(
|
||||
requirements: ['id' => '\d+'],
|
||||
@@ -43,6 +44,10 @@ class Building
|
||||
#[Groups(['building:read', 'building:summary', 'reception:read'])]
|
||||
private string $code = '';
|
||||
|
||||
#[ORM\Column(name: 'display_order', type: 'integer', nullable: true)]
|
||||
#[Groups(['building:read', 'building:summary'])]
|
||||
private ?int $displayOrder = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Reception>
|
||||
*/
|
||||
@@ -101,6 +106,18 @@ class Building
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDisplayOrder(): ?int
|
||||
{
|
||||
return $this->displayOrder;
|
||||
}
|
||||
|
||||
public function setDisplayOrder(?int $displayOrder): self
|
||||
{
|
||||
$this->displayOrder = $displayOrder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Reception>
|
||||
*/
|
||||
|
||||
@@ -511,14 +511,10 @@ class Reception
|
||||
$this->identificationNumber = $number;
|
||||
|
||||
$args->getObjectManager()
|
||||
->getConnection()
|
||||
->executeStatement(
|
||||
'UPDATE reception SET identification_number = :number WHERE id = :id',
|
||||
[
|
||||
'number' => $number,
|
||||
'id' => $this->id,
|
||||
]
|
||||
)
|
||||
->createQuery(sprintf('UPDATE %s r SET r.identificationNumber = :number WHERE r.id = :id', self::class))
|
||||
->setParameter('number', $number)
|
||||
->setParameter('id', $this->id)
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -358,14 +358,10 @@ class Shipment
|
||||
$this->identificationNumber = $number;
|
||||
|
||||
$args->getObjectManager()
|
||||
->getConnection()
|
||||
->executeStatement(
|
||||
'UPDATE shipment SET identification_number = :number WHERE id = :id',
|
||||
[
|
||||
'number' => $number,
|
||||
'id' => $this->id,
|
||||
]
|
||||
)
|
||||
->createQuery(sprintf('UPDATE %s s SET s.identificationNumber = :number WHERE s.id = :id', self::class))
|
||||
->setParameter('number', $number)
|
||||
->setParameter('id', $this->id)
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
53
src/Repository/BovineRepository.php
Normal file
53
src/Repository/BovineRepository.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Bovine;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Bovine>
|
||||
*/
|
||||
final class BovineRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Bovine::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compteurs des bovins actifs par tranche d'âge.
|
||||
*
|
||||
* @return array{total: int, over24: int, between22And24: int, between20And22: int}
|
||||
*/
|
||||
public function getInventoryStats(?int $buildingCaseId = null): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('b')
|
||||
->select(
|
||||
'COUNT(b.id) AS total',
|
||||
'SUM(CASE WHEN b.ageMonths >= 24 THEN 1 ELSE 0 END) AS over24',
|
||||
'SUM(CASE WHEN b.ageMonths >= 22 AND b.ageMonths < 24 THEN 1 ELSE 0 END) AS between22And24',
|
||||
'SUM(CASE WHEN b.ageMonths >= 20 AND b.ageMonths < 22 THEN 1 ELSE 0 END) AS between20And22',
|
||||
)
|
||||
->where('b.exitedAt IS NULL')
|
||||
;
|
||||
|
||||
if (null !== $buildingCaseId) {
|
||||
$qb->andWhere('b.buildingCase = :caseId')
|
||||
->setParameter('caseId', $buildingCaseId)
|
||||
;
|
||||
}
|
||||
|
||||
$row = $qb->getQuery()->getSingleResult();
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'over24' => (int) ($row['over24'] ?? 0),
|
||||
'between22And24' => (int) ($row['between22And24'] ?? 0),
|
||||
'between20And22' => (int) ($row['between20And22'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,12 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
||||
{
|
||||
private const HEADER_FILL = 'FFF1F5F9';
|
||||
|
||||
private const COLOR_VIOLET = 'FFC4B5FD';
|
||||
|
||||
private const COLOR_RED = 'FFFCA5A5';
|
||||
|
||||
private const COLOR_ORANGE = 'FFFDBA74';
|
||||
|
||||
private const COLOR_YELLOW = 'FFFDE047';
|
||||
|
||||
private const HEADERS = [
|
||||
'N° National',
|
||||
'N° Travail',
|
||||
@@ -155,7 +155,7 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
||||
$this->formatSex($bovine->getSex()),
|
||||
$this->formatDate($bovine->getBirthDate()),
|
||||
$bovine->getAgeMonths(),
|
||||
$bovine->getBreedCode(),
|
||||
$bovine->getBovineType()?->getLabel(),
|
||||
$bovine->getBuildingCase()?->getIdBuilding()?->getLabel(),
|
||||
$bovine->getBuildingCase()?->getCaseNumber(),
|
||||
$this->formatDate($bovine->getArrivalDate()),
|
||||
@@ -182,14 +182,14 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
||||
return null;
|
||||
}
|
||||
if ($ageMonths >= 24) {
|
||||
return self::COLOR_VIOLET;
|
||||
}
|
||||
if ($ageMonths >= 22) {
|
||||
return self::COLOR_RED;
|
||||
}
|
||||
if ($ageMonths >= 20) {
|
||||
if ($ageMonths >= 22) {
|
||||
return self::COLOR_ORANGE;
|
||||
}
|
||||
if ($ageMonths >= 20) {
|
||||
return self::COLOR_YELLOW;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace App\State\Bovin;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\BovineInventoryStats;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use App\Repository\BovineRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<BovineInventoryStats>
|
||||
@@ -15,26 +16,22 @@ use Doctrine\DBAL\Connection;
|
||||
final class BovineInventoryStatsProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private BovineRepository $bovineRepository,
|
||||
private RequestStack $requestStack,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): BovineInventoryStats
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(<<<'SQL'
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE age_months >= 24) AS over_24,
|
||||
COUNT(*) FILTER (WHERE age_months >= 22 AND age_months < 24) AS between_22_and_24,
|
||||
COUNT(*) FILTER (WHERE age_months >= 20 AND age_months < 22) AS between_20_and_22
|
||||
FROM bovine
|
||||
WHERE exited_at IS NULL
|
||||
SQL);
|
||||
$rawCaseId = $this->requestStack->getCurrentRequest()?->query->get('buildingCaseId');
|
||||
$caseId = null !== $rawCaseId && ctype_digit((string) $rawCaseId) ? (int) $rawCaseId : null;
|
||||
|
||||
$row = $this->bovineRepository->getInventoryStats($caseId);
|
||||
|
||||
$stats = new BovineInventoryStats();
|
||||
$stats->total = (int) ($row['total'] ?? 0);
|
||||
$stats->over24 = (int) ($row['over_24'] ?? 0);
|
||||
$stats->between22And24 = (int) ($row['between_22_and_24'] ?? 0);
|
||||
$stats->between20And22 = (int) ($row['between_20_and_22'] ?? 0);
|
||||
$stats->total = $row['total'];
|
||||
$stats->over24 = $row['over24'];
|
||||
$stats->between22And24 = $row['between22And24'];
|
||||
$stats->between20And22 = $row['between20And22'];
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\ApiResource\BovineSyncInventoryResult;
|
||||
use App\Entity\Bovine;
|
||||
use App\Entity\BovineType;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
|
||||
@@ -18,6 +19,11 @@ use Malio\EdnotifBundle\Bovin\Dto\AnimalSummaryDto;
|
||||
*/
|
||||
final class BovineSyncInventoryProcessor implements ProcessorInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, BovineType>
|
||||
*/
|
||||
private array $bovineTypeCache = [];
|
||||
|
||||
public function __construct(
|
||||
private BovinApiInterface $bovinApi,
|
||||
private EntityManagerInterface $em,
|
||||
@@ -34,6 +40,13 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
|
||||
$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;
|
||||
@@ -83,7 +96,7 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
|
||||
$identification = $animal->identification;
|
||||
if (null !== $identification) {
|
||||
$bovine->setSex($identification->sex);
|
||||
$bovine->setBreedCode($identification->breedType);
|
||||
$bovine->setBovineType($this->resolveBovineType($identification->breedType));
|
||||
$bovine->setWorkNumber($identification->workNumber);
|
||||
$bovine->setBirthDate($identification->birthDate?->date);
|
||||
}
|
||||
@@ -102,4 +115,28 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
|
||||
continue;
|
||||
}
|
||||
|
||||
$breedCode = $bovine->getBreedCode();
|
||||
$breedCode = $bovine->getBovineType()?->getCode();
|
||||
if (null === $headerBreedCode && null !== $breedCode) {
|
||||
$headerBreedCode = $breedCode;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user