feat(logistique) : entité WeighingTicket + dette site.code (ERP-183)
Entité WeighingTicket - Entité métier complète (#[Auditable], TimestampableBlamableTrait, relations ORM Client/Supplier/Site) + contrat de sérialisation à 3 maillons (weighing_ticket:read / :item:read + contextes par opération). - Getters calculés displayDate et plateFreeFormat (#[SerializedName]), sécurité view/manage, pas de Delete/archive. - Validation #[Assert\*] messages FR + #[Assert\Callback] RG-5.03 (->atPath()), libellé i18n audit.entity.logistique_weighingticket. - Repository : interface Domain + DoctrineWeighingTicketRepository (recherche + tri number DESC, deletedAt IS NULL). Dette site.code - Site.code mappé VARCHAR(8) (groupes read/write), dérivation auto au PrePersist (2 premiers chiffres du CP), UniqueConstraint uq_site_code. - Migration Version20260617160000 : ALTER COLUMN code SET NOT NULL + COMMENT. - Fixtures (codes 86/17/82) et SiteApiTest ajustés. Câblage - doctrine.yaml : mapping ORM du module Logistique (absent du scaffold ERP-181). - ColumnCommentsCatalog : site.code + table weighing_ticket. Specs M5 versionnées (spec-back / spec-front / prompts).
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Logistique\Infrastructure\Doctrine;
|
||||
|
||||
use App\Module\Logistique\Domain\Entity\WeighingTicket;
|
||||
use App\Module\Logistique\Domain\Repository\WeighingTicketRepositoryInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<WeighingTicket>
|
||||
*/
|
||||
class DoctrineWeighingTicketRepository extends ServiceEntityRepository implements WeighingTicketRepositoryInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, WeighingTicket::class);
|
||||
}
|
||||
|
||||
public function findById(int $id): ?WeighingTicket
|
||||
{
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
public function save(WeighingTicket $ticket): void
|
||||
{
|
||||
$this->getEntityManager()->persist($ticket);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
public function createListQueryBuilder(?string $search = null): QueryBuilder
|
||||
{
|
||||
// Left-join des contreparties pour la recherche par nom (sans cartesien
|
||||
// dangereux : ManyToOne). Le cloisonnement par site courant est ajoute
|
||||
// par le SiteScopedQueryExtension (§ 2.3). Tri par defaut number DESC.
|
||||
$qb = $this->createQueryBuilder('wt')
|
||||
->leftJoin('wt.client', 'c')
|
||||
->leftJoin('wt.supplier', 's')
|
||||
->andWhere('wt.deletedAt IS NULL')
|
||||
->orderBy('wt.number', 'DESC')
|
||||
;
|
||||
|
||||
$this->applySearch($qb, $search);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche fuzzy insensible a la casse sur le numero, le nom du client /
|
||||
* fournisseur, le libelle « Autre » et l'immatriculation (§ 4.1).
|
||||
* Metacaracteres LIKE (%, _, \) echappes pour rester litteraux.
|
||||
*/
|
||||
private function applySearch(QueryBuilder $qb, ?string $search): void
|
||||
{
|
||||
if (null === $search || '' === trim($search)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$escaped = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], trim($search));
|
||||
$pattern = '%'.mb_strtolower($escaped, 'UTF-8').'%';
|
||||
|
||||
$qb->andWhere(
|
||||
'LOWER(wt.number) LIKE :search '
|
||||
.'OR LOWER(c.companyName) LIKE :search '
|
||||
.'OR LOWER(s.companyName) LIKE :search '
|
||||
.'OR LOWER(wt.otherLabel) LIKE :search '
|
||||
.'OR LOWER(wt.immatriculation) LIKE :search',
|
||||
)->setParameter('search', $pattern);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user