diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 02ce9dd..2e31409 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,15 +4,15 @@ - @@ -651,7 +659,8 @@ - diff --git a/AGENTS.md b/AGENTS.md index 1b20d25..3aa6687 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ Backend conventions - API Platform operations are defined on Doctrine entities. - Reception entity is in `src/Entity/Reception.php`, with custom weigh endpoint `/receptions/weigh`. - Reception fields: `date_reception`, `license_plate`, `current_step` (default 0), `is_valid` (default false). +- Reception also has `identification_number` (auto `N-BR-####`), `merchandise_type`, `merchandise_detail`, `buildings` (M2M), and `pellet_buildings` (via `reception_pellet_building`). - `date_reception` is set by the UI, stored as `DateTimeImmutable`, serialized as `Y-m-d`. - Weight entity (`src/Entity/Weight.php`) is 1–N with Reception, each row stores `type` (`gross` or `tare`), `dsd`, `weight`, `weighed_at` (all nullable except `type`). - Weigh endpoint `/receptions/weigh` returns `PontBasculeReading` with `dsd`, `weight`, `weighedAt` (formatted `Y-m-d`). @@ -29,6 +30,7 @@ Frontend conventions - Zod is used for form validation (e.g. `frontend/components/reception/reception-form.vue`); shared helpers live in `frontend/utils/zod-errors.ts`. - Weighing logic is shared via `frontend/composables/useWeighing.ts`. - Reception step UI uses store state (`currentStep`) in `frontend/pages/reception/[[id]].vue`. +- Step 2 uses `frontend/components/reception/reception-product-received.vue` for merchandise selection; type codes in `frontend/utils/constants.ts`. - Active nav styles in header use `NuxtLink` with `custom` slot. - Reusable UI components live under `frontend/components/ui/` and are auto-imported with `Ui` prefix (e.g. `UiLoadingDots`). - Service layer lives in `frontend/services/` with typed DTOs in `frontend/services/dto/`. @@ -47,6 +49,8 @@ Notes - Keep endpoints in plural (API Platform convention). - New reference data added: - Reception types (`reception_type`, fields: `label`, `code`), selectable on reception form. + - Merchandise types (`merchandise_type`, fields: `label`, `code`) and pellet types (`pellet_type`, fields: `label`, `code`). + - Buildings (`building`, fields: `label`, `code`) and reception allocations (`reception_building` M2M, `reception_pellet_building` unique on reception/pellet/building). - Suppliers (`supplier`) with addresses (`address`, fields: `label`, `street`, `postal_code`, `city`, `country_code` ISO2), via `supplier_address` join table. - Trucks (`truck`, field: `name`), linked to receptions. - Carriers (`carrier`, fields: `name`, nullable `code`), Drivers (`driver`, fields: `name`, `carrier_id`), Vehicles (`vehicle`, fields: `plate`, `carrier_id`, `truck_id`) used for LIOT logic. diff --git a/frontend/components/reception/reception-product-received.vue b/frontend/components/reception/reception-product-received.vue new file mode 100644 index 0000000..4d8ed20 --- /dev/null +++ b/frontend/components/reception/reception-product-received.vue @@ -0,0 +1,232 @@ + + + diff --git a/frontend/components/reception/reception-unloading.vue b/frontend/components/reception/reception-unloading.vue deleted file mode 100644 index 4e51886..0000000 --- a/frontend/components/reception/reception-unloading.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/frontend/composables/usePdfPrinter.ts b/frontend/composables/usePdfPrinter.ts index 747c684..b920f1e 100644 --- a/frontend/composables/usePdfPrinter.ts +++ b/frontend/composables/usePdfPrinter.ts @@ -6,21 +6,26 @@ export const usePdfPrinter = () => { const currentReception = receptionStore.current const printPdf = async (url: string): Promise => { - if (!import.meta.client) { - return - } + const blob = await api.getBlob(url); + + const pdfBlob = blob.type === 'application/pdf' + ? blob + : new Blob([blob], { type: 'application/pdf' }); + + const blobUrl = URL.createObjectURL(pdfBlob); + + const filename = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}.pdf`; - const blob = await api.getBlob(url) - const blobUrl = URL.createObjectURL(blob) const a = document.createElement('a'); - a.href = url; - // nom du fichier à changer par les infos du store pinia - a.download = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}`; + a.href = blobUrl; + a.download = filename; + a.style.display = 'none'; document.body.appendChild(a); a.click(); a.remove(); - window.open(blobUrl, '_blank', 'noopener,noreferrer') - setTimeout(() => URL.revokeObjectURL(blobUrl), 60000) + + window.open(blobUrl, '_blank', 'noopener,noreferrer'); + setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000); } return { diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index e241aba..6f9026f 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -17,6 +17,20 @@ "receptionType": { "list": "Impossible de récupérer la liste des types de réception." }, + "merchandiseType": { + "list": "Impossible de récupérer la liste des types de marchandises." + }, + "building": { + "list": "Impossible de récupérer la liste des bâtiments." + }, + "pelletType": { + "list": "Impossible de récupérer la liste des types de granulés." + }, + "receptionPelletBuilding": { + "list": "Impossible de récupérer la liste des dépôts de granulés.", + "create": "Impossible d'enregistrer le dépôt de granulés.", + "delete": "Impossible de supprimer le dépôt de granulés." + }, "supplier": { "list": "Impossible de récupérer la liste des fournisseurs." }, diff --git a/frontend/pages/reception/[[id]].vue b/frontend/pages/reception/[[id]].vue index 062217e..9be5d9a 100644 --- a/frontend/pages/reception/[[id]].vue +++ b/frontend/pages/reception/[[id]].vue @@ -16,7 +16,7 @@ - + diff --git a/frontend/services/building.ts b/frontend/services/building.ts new file mode 100644 index 0000000..67f764e --- /dev/null +++ b/frontend/services/building.ts @@ -0,0 +1,23 @@ +import { useApi } from '~/composables/useApi' +import type { BuildingData } from '~/services/dto/building-data' + +export type BuildingListResponse = + | BuildingData[] + | { 'hydra:member'?: BuildingData[] } + +export async function getBuildingList(): Promise { + const api = useApi() + const response = await api.get('buildings', {}, { + toastErrorKey: 'errors.building.list' + }) + + if (Array.isArray(response)) { + return response + } + + if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) { + return response['hydra:member'] + } + + return [] +} diff --git a/frontend/services/dto/building-data.ts b/frontend/services/dto/building-data.ts new file mode 100644 index 0000000..4993219 --- /dev/null +++ b/frontend/services/dto/building-data.ts @@ -0,0 +1,5 @@ +export interface BuildingData { + id: number + label: string + code: string +} diff --git a/frontend/services/dto/merchandise-type-data.ts b/frontend/services/dto/merchandise-type-data.ts new file mode 100644 index 0000000..76aaf64 --- /dev/null +++ b/frontend/services/dto/merchandise-type-data.ts @@ -0,0 +1,5 @@ +export interface MerchandiseTypeData { + id: number + label: string + code: string +} diff --git a/frontend/services/dto/pellet-type-data.ts b/frontend/services/dto/pellet-type-data.ts new file mode 100644 index 0000000..fdbaaed --- /dev/null +++ b/frontend/services/dto/pellet-type-data.ts @@ -0,0 +1,5 @@ +export interface PelletTypeData { + id: number + label: string + code: string +} diff --git a/frontend/services/dto/reception-data.ts b/frontend/services/dto/reception-data.ts index 6b831b5..857fa07 100644 --- a/frontend/services/dto/reception-data.ts +++ b/frontend/services/dto/reception-data.ts @@ -1,4 +1,7 @@ import type { ReceptionTypeData } from '~/services/dto/reception-type-data' +import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data' +import type { BuildingData } from '~/services/dto/building-data' +import type { ReceptionPelletBuildingData } from '~/services/dto/reception-pellet-building-data' import type { UserData } from '~/services/dto/user-data' import type { SupplierData } from '~/services/dto/supplier-data' import type { AddressData } from '~/services/dto/address-data' @@ -15,6 +18,10 @@ export interface ReceptionData { currentStep: number isValid: boolean receptionType?: ReceptionTypeData | null + merchandiseType?: MerchandiseTypeData | null + merchandiseDetail?: string | null + buildings?: BuildingData[] | null + pelletBuildings?: ReceptionPelletBuildingData[] | null user?: UserData | null supplier?: SupplierData | null address?: AddressData | null @@ -37,6 +44,9 @@ export type ReceptionPayload = { currentStep?: number isValid?: boolean receptionType?: string | null + merchandiseType?: string | null + merchandiseDetail?: string | null + buildings?: string[] | null user?: string | null supplier?: string | null address?: string | null diff --git a/frontend/services/dto/reception-pellet-building-data.ts b/frontend/services/dto/reception-pellet-building-data.ts new file mode 100644 index 0000000..9b78c44 --- /dev/null +++ b/frontend/services/dto/reception-pellet-building-data.ts @@ -0,0 +1,9 @@ +import type { BuildingData } from '~/services/dto/building-data' +import type { PelletTypeData } from '~/services/dto/pellet-type-data' + +export interface ReceptionPelletBuildingData { + id: number + reception?: string + building: BuildingData + pelletType: PelletTypeData +} diff --git a/frontend/services/merchandise-type.ts b/frontend/services/merchandise-type.ts new file mode 100644 index 0000000..715969a --- /dev/null +++ b/frontend/services/merchandise-type.ts @@ -0,0 +1,23 @@ +import { useApi } from '~/composables/useApi' +import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data' + +export type MerchandiseTypeListResponse = + | MerchandiseTypeData[] + | { 'hydra:member'?: MerchandiseTypeData[] } + +export async function getMerchandiseTypeList(): Promise { + const api = useApi() + const response = await api.get('merchandise_types', {}, { + toastErrorKey: 'errors.merchandiseType.list' + }) + + if (Array.isArray(response)) { + return response + } + + if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) { + return response['hydra:member'] + } + + return [] +} diff --git a/frontend/services/pellet-type.ts b/frontend/services/pellet-type.ts new file mode 100644 index 0000000..06915b2 --- /dev/null +++ b/frontend/services/pellet-type.ts @@ -0,0 +1,23 @@ +import { useApi } from '~/composables/useApi' +import type { PelletTypeData } from '~/services/dto/pellet-type-data' + +export type PelletTypeListResponse = + | PelletTypeData[] + | { 'hydra:member'?: PelletTypeData[] } + +export async function getPelletTypeList(): Promise { + const api = useApi() + const response = await api.get('pellet_types', {}, { + toastErrorKey: 'errors.pelletType.list' + }) + + if (Array.isArray(response)) { + return response + } + + if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) { + return response['hydra:member'] + } + + return [] +} diff --git a/frontend/services/reception-pellet-building.ts b/frontend/services/reception-pellet-building.ts new file mode 100644 index 0000000..1cf02ef --- /dev/null +++ b/frontend/services/reception-pellet-building.ts @@ -0,0 +1,51 @@ +import { useApi } from '~/composables/useApi' +import type { ReceptionPelletBuildingData } from '~/services/dto/reception-pellet-building-data' + +export type ReceptionPelletBuildingListResponse = + | ReceptionPelletBuildingData[] + | { 'hydra:member'?: ReceptionPelletBuildingData[] } + +export type ReceptionPelletBuildingPayload = { + reception: string + pelletType: string + building: string +} + +export async function getReceptionPelletBuildingList( + receptionIri: string +): Promise { + const api = useApi() + const response = await api.get( + 'reception_pellet_buildings', + { reception: receptionIri }, + { + toastErrorKey: 'errors.receptionPelletBuilding.list' + } + ) + + if (Array.isArray(response)) { + return response + } + + if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) { + return response['hydra:member'] + } + + return [] +} + +export async function createReceptionPelletBuilding( + payload: ReceptionPelletBuildingPayload +): Promise { + const api = useApi() + return api.post('reception_pellet_buildings', payload, { + toastErrorKey: 'errors.receptionPelletBuilding.create' + }) +} + +export async function deleteReceptionPelletBuilding(id: number): Promise { + const api = useApi() + await api.delete(`reception_pellet_buildings/${id}`, {}, { + toastErrorKey: 'errors.receptionPelletBuilding.delete' + }) +} diff --git a/frontend/utils/constants.ts b/frontend/utils/constants.ts new file mode 100644 index 0000000..00d3a3e --- /dev/null +++ b/frontend/utils/constants.ts @@ -0,0 +1,8 @@ +export const RECEPTION_TYPE_CODES = { + MERCHANDISES: 'MARCHANDISES' +} as const + +export const MERCHANDISE_TYPE_CODES = { + GRANULE: 'GRANULE', + AUTRES: 'AUTRES' +} as const diff --git a/migrations/Version20260128000200.php b/migrations/Version20260128000200.php new file mode 100644 index 0000000..3af4806 --- /dev/null +++ b/migrations/Version20260128000200.php @@ -0,0 +1,32 @@ +addSql('CREATE TABLE merchandise_type (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(120) NOT NULL, code VARCHAR(50) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('ALTER TABLE reception ADD merchandise_type_id INT DEFAULT NULL'); + $this->addSql('CREATE INDEX IDX_83DC02E3BCAAA7C0 ON reception (merchandise_type_id)'); + $this->addSql('ALTER TABLE reception ADD CONSTRAINT FK_83DC02E3BCAAA7C0 FOREIGN KEY (merchandise_type_id) REFERENCES merchandise_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE reception DROP CONSTRAINT FK_83DC02E3BCAAA7C0'); + $this->addSql('DROP INDEX IDX_83DC02E3BCAAA7C0'); + $this->addSql('ALTER TABLE reception DROP merchandise_type_id'); + $this->addSql('DROP TABLE merchandise_type'); + } +} diff --git a/migrations/Version20260128000300.php b/migrations/Version20260128000300.php new file mode 100644 index 0000000..d646187 --- /dev/null +++ b/migrations/Version20260128000300.php @@ -0,0 +1,48 @@ +addSql('CREATE TABLE building (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(120) NOT NULL, code VARCHAR(50) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE pellet_type (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(120) NOT NULL, code VARCHAR(50) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE reception_building (reception_id INT NOT NULL, building_id INT NOT NULL, PRIMARY KEY(reception_id, building_id))'); + $this->addSql('CREATE INDEX IDX_46E7F9F23E4A2E34 ON reception_building (reception_id)'); + $this->addSql('CREATE INDEX IDX_46E7F9F24D2A7E12 ON reception_building (building_id)'); + $this->addSql('ALTER TABLE reception_building ADD CONSTRAINT FK_46E7F9F23E4A2E34 FOREIGN KEY (reception_id) REFERENCES reception (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE reception_building ADD CONSTRAINT FK_46E7F9F24D2A7E12 FOREIGN KEY (building_id) REFERENCES building (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE TABLE reception_pellet_building (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, reception_id INT NOT NULL, pellet_type_id INT NOT NULL, building_id INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX uniq_reception_pellet_building ON reception_pellet_building (reception_id, pellet_type_id, building_id)'); + $this->addSql('CREATE INDEX IDX_5DF3AA933E4A2E34 ON reception_pellet_building (reception_id)'); + $this->addSql('CREATE INDEX IDX_5DF3AA93955258D ON reception_pellet_building (pellet_type_id)'); + $this->addSql('CREATE INDEX IDX_5DF3AA934D2A7E12 ON reception_pellet_building (building_id)'); + $this->addSql('ALTER TABLE reception_pellet_building ADD CONSTRAINT FK_5DF3AA933E4A2E34 FOREIGN KEY (reception_id) REFERENCES reception (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE reception_pellet_building ADD CONSTRAINT FK_5DF3AA93955258D FOREIGN KEY (pellet_type_id) REFERENCES pellet_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE reception_pellet_building ADD CONSTRAINT FK_5DF3AA934D2A7E12 FOREIGN KEY (building_id) REFERENCES building (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE reception_pellet_building DROP CONSTRAINT FK_5DF3AA933E4A2E34'); + $this->addSql('ALTER TABLE reception_pellet_building DROP CONSTRAINT FK_5DF3AA93955258D'); + $this->addSql('ALTER TABLE reception_pellet_building DROP CONSTRAINT FK_5DF3AA934D2A7E12'); + $this->addSql('DROP TABLE reception_pellet_building'); + $this->addSql('ALTER TABLE reception_building DROP CONSTRAINT FK_46E7F9F23E4A2E34'); + $this->addSql('ALTER TABLE reception_building DROP CONSTRAINT FK_46E7F9F24D2A7E12'); + $this->addSql('DROP TABLE reception_building'); + $this->addSql('DROP TABLE pellet_type'); + $this->addSql('DROP TABLE building'); + } +} diff --git a/migrations/Version20260128000400.php b/migrations/Version20260128000400.php new file mode 100644 index 0000000..5ba2fce --- /dev/null +++ b/migrations/Version20260128000400.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE reception ADD merchandise_detail VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE reception DROP merchandise_detail'); + } +} diff --git a/src/Entity/Building.php b/src/Entity/Building.php new file mode 100644 index 0000000..92caa14 --- /dev/null +++ b/src/Entity/Building.php @@ -0,0 +1,92 @@ + '\d+'], + normalizationContext: ['groups' => ['building:read']], + ), + new GetCollection( + normalizationContext: ['groups' => ['building:read']], + ), + ], + security: "is_granted('ROLE_USER')", +)] +class Building +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['building:read', 'reception:read'])] + private ?int $id = null; + + #[ORM\Column(length: 120)] + #[Groups(['building:read', 'reception:read'])] + private string $label = ''; + + #[ORM\Column(length: 50)] + #[Groups(['building:read', 'reception:read'])] + private string $code = ''; + + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Reception::class, mappedBy: 'buildings')] + private Collection $receptions; + + public function __construct() + { + $this->receptions = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): string + { + return $this->label; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function getCode(): string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } + + /** + * @return Collection + */ + public function getReceptions(): Collection + { + return $this->receptions; + } +} diff --git a/src/Entity/MerchandiseType.php b/src/Entity/MerchandiseType.php new file mode 100644 index 0000000..20bfcde --- /dev/null +++ b/src/Entity/MerchandiseType.php @@ -0,0 +1,92 @@ + '\d+'], + normalizationContext: ['groups' => ['merchandise-type:read']], + ), + new GetCollection( + normalizationContext: ['groups' => ['merchandise-type:read']], + ), + ], + security: "is_granted('ROLE_USER')", +)] +class MerchandiseType +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['reception:read', 'merchandise-type:read'])] + private ?int $id = null; + + #[ORM\Column(length: 120)] + #[Groups(['reception:read', 'merchandise-type:read'])] + private string $label = ''; + + #[ORM\Column(length: 50)] + #[Groups(['reception:read', 'merchandise-type:read'])] + private string $code = ''; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'merchandiseType', targetEntity: Reception::class)] + private Collection $receptions; + + public function __construct() + { + $this->receptions = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): string + { + return $this->label; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function getCode(): string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } + + /** + * @return Collection + */ + public function getReceptions(): Collection + { + return $this->receptions; + } +} diff --git a/src/Entity/PelletType.php b/src/Entity/PelletType.php new file mode 100644 index 0000000..8a2acff --- /dev/null +++ b/src/Entity/PelletType.php @@ -0,0 +1,71 @@ + '\d+'], + normalizationContext: ['groups' => ['pellet-type:read']], + ), + new GetCollection( + normalizationContext: ['groups' => ['pellet-type:read']], + ), + ], + security: "is_granted('ROLE_USER')", +)] +class PelletType +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['pellet-type:read', 'reception:read'])] + private ?int $id = null; + + #[ORM\Column(length: 120)] + #[Groups(['pellet-type:read', 'reception:read'])] + private string $label = ''; + + #[ORM\Column(length: 50)] + #[Groups(['pellet-type:read', 'reception:read'])] + private string $code = ''; + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): string + { + return $this->label; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function getCode(): string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } +} diff --git a/src/Entity/Reception.php b/src/Entity/Reception.php index 11c58c6..7a90ff8 100644 --- a/src/Entity/Reception.php +++ b/src/Entity/Reception.php @@ -96,6 +96,10 @@ class Reception #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] private ?DateTimeImmutable $receptionDate = null; + #[ORM\Column(length: 255, nullable: true)] + #[Groups(['reception:read', 'reception:write'])] + private ?string $merchandiseDetail = null; + #[ORM\OneToMany(targetEntity: Weight::class, mappedBy: 'reception', cascade: ['persist', 'remove'], orphanRemoval: true)] #[Groups(['reception:read'])] private Collection $weights; @@ -106,6 +110,28 @@ class Reception #[ApiProperty(readableLink: true)] private ?ReceptionType $receptionType = null; + #[ORM\ManyToOne(inversedBy: 'receptions')] + #[ORM\JoinColumn(nullable: true)] + #[Groups(['reception:read', 'reception:write'])] + #[ApiProperty(readableLink: true)] + private ?MerchandiseType $merchandiseType = null; + + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Building::class, inversedBy: 'receptions')] + #[ORM\JoinTable(name: 'reception_building')] + #[Groups(['reception:read', 'reception:write'])] + #[ApiProperty(readableLink: true)] + private Collection $buildings; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: ReceptionPelletBuilding::class, mappedBy: 'reception', cascade: ['persist', 'remove'], orphanRemoval: true)] + #[Groups(['reception:read'])] + private Collection $pelletBuildings; + #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: true)] #[Groups(['reception:read', 'reception:write'])] @@ -145,8 +171,10 @@ class Reception public function __construct( ?DateTimeImmutable $receptionDate = null, ) { - $this->receptionDate = $receptionDate; - $this->weights = new ArrayCollection(); + $this->receptionDate = $receptionDate; + $this->weights = new ArrayCollection(); + $this->buildings = new ArrayCollection(); + $this->pelletBuildings = new ArrayCollection(); } public function getId(): ?int @@ -218,6 +246,18 @@ class Reception return $this; } + public function getMerchandiseDetail(): ?string + { + return $this->merchandiseDetail; + } + + public function setMerchandiseDetail(?string $merchandiseDetail): self + { + $this->merchandiseDetail = $merchandiseDetail; + + return $this; + } + /** * @return Collection */ @@ -238,6 +278,71 @@ class Reception return $this; } + public function getMerchandiseType(): ?MerchandiseType + { + return $this->merchandiseType; + } + + public function setMerchandiseType(?MerchandiseType $merchandiseType): self + { + $this->merchandiseType = $merchandiseType; + + return $this; + } + + /** + * @return Collection + */ + public function getBuildings(): Collection + { + return $this->buildings; + } + + public function addBuilding(Building $building): self + { + if (!$this->buildings->contains($building)) { + $this->buildings->add($building); + } + + return $this; + } + + public function removeBuilding(Building $building): self + { + $this->buildings->removeElement($building); + + return $this; + } + + /** + * @return Collection + */ + public function getPelletBuildings(): Collection + { + return $this->pelletBuildings; + } + + public function addPelletBuilding(ReceptionPelletBuilding $pelletBuilding): self + { + if (!$this->pelletBuildings->contains($pelletBuilding)) { + $this->pelletBuildings->add($pelletBuilding); + $pelletBuilding->setReception($this); + } + + return $this; + } + + public function removePelletBuilding(ReceptionPelletBuilding $pelletBuilding): self + { + if ($this->pelletBuildings->removeElement($pelletBuilding)) { + if ($pelletBuilding->getReception() === $this) { + $pelletBuilding->setReception(null); + } + } + + return $this; + } + public function getUser(): ?User { return $this->user; diff --git a/src/Entity/ReceptionPelletBuilding.php b/src/Entity/ReceptionPelletBuilding.php new file mode 100644 index 0000000..23bd5d3 --- /dev/null +++ b/src/Entity/ReceptionPelletBuilding.php @@ -0,0 +1,101 @@ + '\d+'], + normalizationContext: ['groups' => ['reception-pellet-building:read']], + ), + new GetCollection( + normalizationContext: ['groups' => ['reception-pellet-building:read']], + ), + new Post( + normalizationContext: ['groups' => ['reception-pellet-building:read']], + denormalizationContext: ['groups' => ['reception-pellet-building:write']], + ), + new Delete(), + ], + security: "is_granted('ROLE_USER')", +)] +#[ApiFilter(SearchFilter::class, properties: ['reception' => 'exact'])] +class ReceptionPelletBuilding +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['reception-pellet-building:read', 'reception:read'])] + private ?int $id = null; + + #[ORM\ManyToOne(inversedBy: 'pelletBuildings')] + #[ORM\JoinColumn(nullable: false)] + #[Groups(['reception-pellet-building:read', 'reception-pellet-building:write'])] + private ?Reception $reception = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: false)] + #[Groups(['reception-pellet-building:read', 'reception-pellet-building:write', 'reception:read'])] + private ?PelletType $pelletType = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: false)] + #[Groups(['reception-pellet-building:read', 'reception-pellet-building:write', 'reception:read'])] + private ?Building $building = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getReception(): ?Reception + { + return $this->reception; + } + + public function setReception(?Reception $reception): self + { + $this->reception = $reception; + + return $this; + } + + public function getPelletType(): ?PelletType + { + return $this->pelletType; + } + + public function setPelletType(?PelletType $pelletType): self + { + $this->pelletType = $pelletType; + + return $this; + } + + public function getBuilding(): ?Building + { + return $this->building; + } + + public function setBuilding(?Building $building): self + { + $this->building = $building; + + return $this; + } +} diff --git a/templates/reception_voucher.html.twig b/templates/reception_voucher.html.twig index c1c0553..9e2f7b8 100644 --- a/templates/reception_voucher.html.twig +++ b/templates/reception_voucher.html.twig @@ -16,12 +16,10 @@ p{ margin:0; } em{ font-style: normal; } - .red{ color:red; } - .company-block{ font-size:14px; text-align:left; - line-height:1.25; + line-height:1.3; } .box{ @@ -36,7 +34,7 @@ text-align:center; font-size: 18pt; font-weight: 700; - margin: 64px 0 15px 0; + margin: 64px 0 20px 0; } .info-table { @@ -47,7 +45,7 @@ } .info-table th { - font-size: 16px; + font-size: 16px; } table{ @@ -63,30 +61,60 @@ } th{ text-align:center; font-weight:700; } - /* tables de layout (sans bordures) */ .layout, .layout td{ border:none !important; padding:0; } - /* GRAND TABLEAU : verrouillage dompdf */ + .bigtable-wrap{ + border: 1px solid #000; + height: 425px; + margin-bottom: 10px; + } + .bigtable{ + width: 100%; + height: 100%; + border: none; + border-collapse: collapse; table-layout: fixed; } + .bigtable th, .bigtable td{ font-size: 16px; + border: 1px solid #333; } + + .bigtable thead th{ border-top: 0; } + .bigtable tbody tr:last-child td{ border-bottom: 0; } + + .bigtable tr th:first-child, + .bigtable tr td:first-child{ border-left: 0; } + + .bigtable tr th:last-child, + .bigtable tr td:last-child{ border-right: 0; } + + .bigtable thead th{ border-bottom: 0; } + .bigtable tbody tr:first-child td{ border-top: 1px solid #333; } + .bigtable-notes{ font-size: 14px; line-height: 1.25; } - /* ligne “filler” pour forcer la hauteur comme l'exemple */ - .fill td{ - border-top:none; - height: 75mm; /* ajuste si besoin */ + .border-bottom { + border-bottom: 1px solid #000; } + .footer-block{ page-break-inside: avoid; } + .signature-title{ font-size:12px; margin-bottom:2mm; } - .signature-box{ height: 22mm; margin-bottom: 4mm; } + .signature-box { + height: 22mm; + margin-bottom: 4mm; + border: 0.5px solid #000; + + padding: 6px 10px; + } + .meta{ font-size: 16px; line-height: 1.35; } @@ -104,7 +132,7 @@ 14 Allée d’Argenson
Z.I Nord – Secteur Est
86100 CHATELLERAULT
- TEL : 05 49 20 09 10
+ Tel. : 05 49 20 09 10
Email : lpc.contacts@lpc-liot.fr
RCS Châtellerault B 444 262 455 @@ -113,7 +141,7 @@ -
+
{{ reception.supplier.name }}
{{ reception.address.street }}
{{ reception.address.postalCode }} {{ reception.address.city }} @@ -140,57 +168,100 @@ - - - - - - - - +
+
DésignationQté livrée (kg)
+ + + + + + - - - + + + + + + + + + +

- - +
+ {% if reception.merchandiseType and reception.merchandiseType.code == 'AUTRES' and reception.merchandiseDetail %} +

Précision : {{ reception.merchandiseDetail }}

+ {% endif %} - -
- - - - -
DésignationQté livrée (kg)
- MAÏS sec

+
+ {{ reception.receptionType.label }}

-
- {% set grossWeight = null %} - {% set tareWeight = null %} +
+ {% set grossWeight = null %} + {% set tareWeight = null %} - {% for weight in reception.weights %} - {% if weight.type == 'gross' %} - {% set grossWeight = weight %} -

Poids à plein : {{ grossWeight.weight }}kg (pesée n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})

- {% elseif weight.type == 'tare' %} - {% set tareWeight = weight %} -

Poids à vide : {{ tareWeight.weight }}kg (pesée n°{{ tareWeight.dsd }} {{ tareWeight.weighedAt|date('d/m/Y H:i:s') }})

+ {% for weight in reception.weights %} + {% if weight.type == 'gross' %} + {% set grossWeight = weight %} +

Poids à plein : {{ grossWeight.weight }}kg (pesée n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})

+ {% elseif weight.type == 'tare' %} + {% set tareWeight = weight %} +

Poids à vide : {{ tareWeight.weight }}kg (pesée n°{{ tareWeight.dsd }} {{ tareWeight.weighedAt|date('d/m/Y H:i:s') }})

+ {% endif %} + {% endfor %} +
+
+ {% if grossWeight and tareWeight %} + {{ grossWeight.weight - tareWeight.weight }} + {% else %} + 0 + {% endif %} +
+ + {% if reception.merchandiseType %} + {{ reception.merchandiseType.label }} + {% else %} + - {% endif %} - {% endfor %} - - - {% if grossWeight and tareWeight %} - {{ grossWeight.weight - tareWeight.weight }} - {% else %} - 0 - {% endif %} -
+ {% if reception.merchandiseType and reception.merchandiseType.code == 'GRANULE' %} + {% set pelletGroups = {} %} + {% for selection in reception.pelletBuildings %} + {% set pelletLabel = selection.pelletType.label %} + {% if pelletGroups[pelletLabel] is not defined %} + {% set pelletGroups = pelletGroups|merge({ (pelletLabel): [] }) %} + {% endif %} + {% set pelletGroups = pelletGroups|merge({ + (pelletLabel): pelletGroups[pelletLabel]|merge([selection.building.label]) + }) %} + {% endfor %} + {% for pelletLabel, buildingLabels in pelletGroups %} +

{{ pelletLabel }} : {{ buildingLabels|join(', ') }}

+ {% else %} +

Aucun dépôt de granulés renseigné.

+ {% endfor %} + {% else %} + {% set buildingLabels = [] %} + {% for building in reception.buildings %} + {% set buildingLabels = buildingLabels|merge([building.label]) %} + {% endfor %} + {% if buildingLabels %} +

Ferme : {{ buildingLabels|join(', ') }}

+ {% else %} +

Aucun bâtiment renseigné.

+ {% endif %} + {% endif %} +
+ + + + + +
- - + +