[#FER-26] Passeport du bovin (!53)
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 - [x] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #53 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #53.
This commit is contained in:
93
src/Command/BackfillBovineMovementsCommand.php
Normal file
93
src/Command/BackfillBovineMovementsCommand.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ use ApiPlatform\Metadata\Post;
|
||||
use App\Repository\BovineRepository;
|
||||
use App\State\Bovin\BovineProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
@@ -135,6 +137,37 @@ class Bovine
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
private ?DateTimeImmutable $exitedAt = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
#[Groups(['bovine:read'])]
|
||||
private ?string $motherNationalNumber = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?BovineType $motherBovineType = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
#[Groups(['bovine:read'])]
|
||||
private ?string $fatherNationalNumber = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?BovineType $fatherBovineType = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, BovineMovement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: BovineMovement::class, mappedBy: 'bovine', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['enteredAt' => 'DESC'])]
|
||||
#[Groups(['bovine:read'])]
|
||||
private Collection $movements;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->movements = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -329,6 +362,79 @@ class Bovine
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMotherNationalNumber(): ?string
|
||||
{
|
||||
return $this->motherNationalNumber;
|
||||
}
|
||||
|
||||
public function setMotherNationalNumber(?string $motherNationalNumber): static
|
||||
{
|
||||
$this->motherNationalNumber = $motherNationalNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMotherBovineType(): ?BovineType
|
||||
{
|
||||
return $this->motherBovineType;
|
||||
}
|
||||
|
||||
public function setMotherBovineType(?BovineType $motherBovineType): static
|
||||
{
|
||||
$this->motherBovineType = $motherBovineType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFatherNationalNumber(): ?string
|
||||
{
|
||||
return $this->fatherNationalNumber;
|
||||
}
|
||||
|
||||
public function setFatherNationalNumber(?string $fatherNationalNumber): static
|
||||
{
|
||||
$this->fatherNationalNumber = $fatherNationalNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFatherBovineType(): ?BovineType
|
||||
{
|
||||
return $this->fatherBovineType;
|
||||
}
|
||||
|
||||
public function setFatherBovineType(?BovineType $fatherBovineType): static
|
||||
{
|
||||
$this->fatherBovineType = $fatherBovineType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, BovineMovement>
|
||||
*/
|
||||
public function getMovements(): Collection
|
||||
{
|
||||
return $this->movements;
|
||||
}
|
||||
|
||||
public function addMovement(BovineMovement $movement): static
|
||||
{
|
||||
if (!$this->movements->contains($movement)) {
|
||||
$this->movements->add($movement);
|
||||
$movement->setBovine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeMovement(BovineMovement $movement): static
|
||||
{
|
||||
$this->movements->removeElement($movement);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
#[ORM\PreUpdate]
|
||||
public function refreshAgeMonths(): void
|
||||
|
||||
129
src/Entity/BovineMovement.php
Normal file
129
src/Entity/BovineMovement.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Repository\BovineMovementRepository;
|
||||
use App\State\Bovin\BovineMovementProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: BovineMovementRepository::class)]
|
||||
#[ORM\Table(name: 'bovine_movement')]
|
||||
#[ORM\Index(name: 'idx_bovine_movement_timeline', columns: ['bovine_id', 'entered_at'])]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Post(
|
||||
denormalizationContext: ['groups' => ['bovine_movement:write']],
|
||||
normalizationContext: ['groups' => ['bovine:read']],
|
||||
processor: BovineMovementProcessor::class,
|
||||
),
|
||||
],
|
||||
security: "is_granted('ROLE_BUREAU')",
|
||||
)]
|
||||
class BovineMovement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['bovine:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'movements')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Groups(['bovine_movement:write'])]
|
||||
private Bovine $bovine;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read', 'bovine_movement:write'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?BuildingCase $buildingCase = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?Building $building = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
#[Groups(['bovine:read', 'bovine_movement:write'])]
|
||||
private DateTimeImmutable $enteredAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
#[Groups(['bovine:read'])]
|
||||
private ?DateTimeImmutable $leftAt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getBovine(): Bovine
|
||||
{
|
||||
return $this->bovine;
|
||||
}
|
||||
|
||||
public function setBovine(Bovine $bovine): static
|
||||
{
|
||||
$this->bovine = $bovine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBuildingCase(): ?BuildingCase
|
||||
{
|
||||
return $this->buildingCase;
|
||||
}
|
||||
|
||||
public function setBuildingCase(?BuildingCase $buildingCase): static
|
||||
{
|
||||
$this->buildingCase = $buildingCase;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBuilding(): ?Building
|
||||
{
|
||||
return $this->building;
|
||||
}
|
||||
|
||||
public function setBuilding(?Building $building): static
|
||||
{
|
||||
$this->building = $building;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnteredAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->enteredAt;
|
||||
}
|
||||
|
||||
public function hasEnteredAt(): bool
|
||||
{
|
||||
return isset($this->enteredAt);
|
||||
}
|
||||
|
||||
public function setEnteredAt(DateTimeImmutable $enteredAt): static
|
||||
{
|
||||
$this->enteredAt = $enteredAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLeftAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->leftAt;
|
||||
}
|
||||
|
||||
public function setLeftAt(?DateTimeImmutable $leftAt): static
|
||||
{
|
||||
$this->leftAt = $leftAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
34
src/Repository/BovineMovementRepository.php
Normal file
34
src/Repository/BovineMovementRepository.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Bovine;
|
||||
use App\Entity\BovineMovement;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<BovineMovement>
|
||||
*/
|
||||
final class BovineMovementRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, BovineMovement::class);
|
||||
}
|
||||
|
||||
public function findOpenMovement(Bovine $bovine): ?BovineMovement
|
||||
{
|
||||
return $this->createQueryBuilder('m')
|
||||
->where('m.bovine = :bovine')
|
||||
->andWhere('m.leftAt IS NULL')
|
||||
->setParameter('bovine', $bovine)
|
||||
->orderBy('m.enteredAt', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
}
|
||||
44
src/State/Bovin/BovineMovementProcessor.php
Normal file
44
src/State/Bovin/BovineMovementProcessor.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State\Bovin;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\BovineMovement;
|
||||
use App\Repository\BovineMovementRepository;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
final class BovineMovementProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BovineMovementRepository $movementRepository,
|
||||
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
||||
private readonly ProcessorInterface $persistProcessor,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
{
|
||||
if (!$data instanceof BovineMovement) {
|
||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
|
||||
$enteredAt = $data->hasEnteredAt() ? $data->getEnteredAt() : new DateTimeImmutable();
|
||||
$data->setEnteredAt($enteredAt);
|
||||
$data->setLeftAt(null);
|
||||
$data->setBuilding(null);
|
||||
|
||||
$bovine = $data->getBovine();
|
||||
|
||||
$openMovement = $this->movementRepository->findOpenMovement($bovine);
|
||||
if (null !== $openMovement) {
|
||||
$openMovement->setLeftAt($enteredAt);
|
||||
}
|
||||
|
||||
$bovine->setBuildingCase($data->getBuildingCase());
|
||||
|
||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,11 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
|
||||
$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;
|
||||
|
||||
Reference in New Issue
Block a user