feat : cycle de vie brouillon/validé du ticket de pesée (ERP-193)
Une pesée (bascule ou manuelle) s'enregistre désormais dès la validation de sa
modale, sans exiger la contrepartie ni l'immatriculation : le ticket naît
« brouillon » (status DRAFT, sans numéro). Le bouton « Valider » finalise quand
les 3 champs du haut (contrepartie + champ associé + immatriculation) ET les 2
pesées sont renseignés : attribution du numéro {siteCode}-TP-{NNNN} et passage
en VALIDATED, puis ouverture du bon de pesée PDF.
Back : counterparty_type/immatriculation/number nullables + colonne status
(migration racine), contraintes strictes déplacées en groupe de validation
finalize, opération PATCH /weighing_tickets/{id}/validate, numéro attribué à la
validation. Front : 4 champs en haut hors blocs, persistance immédiate des
pesées, écrans Ajouter/Modifier refondus, colonne Statut dans la liste, form à
plat pleine largeur. Tests back (lifecycle brouillon/validate) + front à jour.
This commit is contained in:
@@ -48,9 +48,10 @@ const InputStub = defineComponent({
|
||||
},
|
||||
})
|
||||
|
||||
// WeighingBlock stubbe : rend le slot counterparty (présent sur le bloc vide).
|
||||
// WeighingBlock stubbé (Date/Poids/DSD + boutons) — la contrepartie vit désormais
|
||||
// dans les 4 champs du haut, hors bloc (ERP-193).
|
||||
const BlockStub = defineComponent({
|
||||
setup(_, { slots }) { return () => h('div', { 'data-testid': 'block' }, slots.counterparty?.()) },
|
||||
setup() { return () => h('div', { 'data-testid': 'block' }) },
|
||||
})
|
||||
|
||||
const ModalStub = defineComponent({
|
||||
@@ -82,6 +83,7 @@ async function mountPage() {
|
||||
|
||||
const DETAIL = {
|
||||
id: 9,
|
||||
status: 'VALIDATED',
|
||||
number: '86-TP-0001',
|
||||
site: { id: 1, name: 'Chatellerault', code: '86' },
|
||||
counterpartyType: 'CLIENT',
|
||||
@@ -105,11 +107,11 @@ describe('Écran Modification ticket de pesée (page /weighing-tickets/{id}/edit
|
||||
expect(mockFetchTicket).toHaveBeenCalledWith('9')
|
||||
})
|
||||
|
||||
it('bascule des boutons : « Enregistrer » + « Imprimer » présents, pas de « Valider »', async () => {
|
||||
it('ticket validé : action principale « Enregistrer » + « Imprimer » (pas « Valider »)', async () => {
|
||||
const wrapper = await mountPage()
|
||||
// DETAIL.status = VALIDATED → l'action principale s'intitule « Enregistrer ».
|
||||
expect(wrapper.find('[data-label="logistique.weighingTickets.form.save"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-label="logistique.weighingTickets.form.print"]').exists()).toBe(true)
|
||||
// « Valider » est le bouton de l'écran d'AJOUT — absent en modification (RG-5.08).
|
||||
expect(wrapper.find('[data-label="logistique.weighingTickets.form.validate"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
@@ -119,15 +121,24 @@ describe('Écran Modification ticket de pesée (page /weighing-tickets/{id}/edit
|
||||
expect(mockOpen).toHaveBeenCalledWith('/api/weighing_tickets/9/print.pdf', '_blank')
|
||||
})
|
||||
|
||||
it('« Enregistrer » PATCH le ticket puis revient à la liste', async () => {
|
||||
it('« Enregistrer » : PATCH brouillon puis PATCH /validate, retour à la liste', async () => {
|
||||
const wrapper = await mountPage()
|
||||
await wrapper.find('[data-label="logistique.weighingTickets.form.save"]').trigger('click')
|
||||
await flushPromises()
|
||||
// 1. Persistance de l'état courant (brouillon) avec les 2 pesées.
|
||||
expect(mockPatch).toHaveBeenCalledWith(
|
||||
'/weighing_tickets/9',
|
||||
expect.objectContaining({ counterpartyType: 'CLIENT', client: '/api/clients/629', fullWeight: 14300 }),
|
||||
expect.objectContaining({ toast: false }),
|
||||
)
|
||||
// 2. Validation (back autoritaire) — ne porte que les 4 champs du haut.
|
||||
expect(mockPatch).toHaveBeenCalledWith(
|
||||
'/weighing_tickets/9/validate',
|
||||
expect.objectContaining({ counterpartyType: 'CLIENT', immatriculation: 'AB-123-CD' }),
|
||||
expect.objectContaining({ toast: false }),
|
||||
)
|
||||
// « Enregistrer » ouvre aussi le bon de pesée PDF (RG-5.08).
|
||||
expect(mockOpen).toHaveBeenCalledWith('/api/weighing_tickets/9/print.pdf', '_blank')
|
||||
expect(mockPush).toHaveBeenCalledWith('/weighing-tickets')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { defineComponent, h, ref, reactive, Suspense } from 'vue'
|
||||
|
||||
// ── Mocks des composables modules (le form RÉEL est conservé). ────────────────
|
||||
const mockPost = vi.hoisted(() => vi.fn())
|
||||
const mockPatch = vi.hoisted(() => vi.fn())
|
||||
const mockPush = vi.hoisted(() => vi.fn())
|
||||
const mockOpen = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('~/modules/logistique/composables/useWeighingTicketReferentials', () => ({
|
||||
useWeighingTicketReferentials: () => ({ clients: ref([]), suppliers: ref([]), load: vi.fn().mockResolvedValue(undefined) }),
|
||||
}))
|
||||
vi.mock('~/modules/logistique/composables/useWeighbridge', () => ({
|
||||
useWeighbridge: () => ({ triggerAuto: vi.fn(), triggerManual: vi.fn(), extractWeighbridgeError: () => 'err' }),
|
||||
}))
|
||||
|
||||
// ── Auto-imports Nuxt stubbés globalement ───────────────────────────────────
|
||||
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
|
||||
vi.stubGlobal('useHead', () => undefined)
|
||||
vi.stubGlobal('useApi', () => ({ get: vi.fn(), post: mockPost, patch: mockPatch }))
|
||||
vi.stubGlobal('useRouter', () => ({ push: mockPush }))
|
||||
vi.stubGlobal('usePermissions', () => ({ can: () => true }))
|
||||
vi.stubGlobal('navigateTo', vi.fn())
|
||||
vi.stubGlobal('useFormErrors', () => ({ errors: reactive({}), setError: vi.fn(), clearErrors: vi.fn(), handleApiError: vi.fn() }))
|
||||
globalThis.open = mockOpen
|
||||
|
||||
const NewPage = (await import('../weighing-tickets/new.vue')).default
|
||||
|
||||
const ButtonStub = defineComponent({
|
||||
props: { label: { type: String, default: '' }, disabled: { type: Boolean, default: false } },
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
return () => h('button', { 'data-label': props.label, onClick: () => emit('click') }, props.label)
|
||||
},
|
||||
})
|
||||
const InputStub = defineComponent({
|
||||
props: { label: { type: String, default: '' }, modelValue: { default: null } },
|
||||
setup(props) { return () => h('input', { 'data-label': props.label, 'value': props.modelValue as string }) },
|
||||
})
|
||||
const BlockStub = defineComponent({ setup() { return () => h('div', { 'data-testid': 'block' }) } })
|
||||
const ModalStub = defineComponent({
|
||||
props: { modelValue: { type: Boolean, default: false } },
|
||||
setup(_, { slots }) { return () => h('div', {}, [slots.header?.(), slots.default?.(), slots.footer?.()]) },
|
||||
})
|
||||
|
||||
const stubs = {
|
||||
MalioButtonIcon: ButtonStub,
|
||||
MalioButton: ButtonStub,
|
||||
MalioInputText: InputStub,
|
||||
MalioSelect: InputStub,
|
||||
MalioDateTime: InputStub,
|
||||
MalioCheckbox: InputStub,
|
||||
MalioModal: ModalStub,
|
||||
WeighingBlock: BlockStub,
|
||||
}
|
||||
|
||||
async function mountPage() {
|
||||
const wrapper = mount(defineComponent({
|
||||
components: { NewPage },
|
||||
setup: () => () => h(Suspense, null, { default: () => h(NewPage) }),
|
||||
}), { global: { stubs } })
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('Écran Ajouter ticket de pesée (page /weighing-tickets/new)', () => {
|
||||
beforeEach(() => {
|
||||
mockPost.mockReset().mockResolvedValue({ id: 42 })
|
||||
mockPatch.mockReset().mockResolvedValue({})
|
||||
mockPush.mockReset()
|
||||
mockOpen.mockReset()
|
||||
})
|
||||
|
||||
it('un seul bouton « Valider » (pas de « Enregistrer » séparé)', async () => {
|
||||
const wrapper = await mountPage()
|
||||
expect(wrapper.find('[data-label="logistique.weighingTickets.form.validate"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-label="logistique.weighingTickets.form.save"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('« Valider » : POST brouillon (création) puis PATCH /validate, PDF + retour liste', async () => {
|
||||
const wrapper = await mountPage()
|
||||
await wrapper.find('[data-label="logistique.weighingTickets.form.validate"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
// 1. Création du brouillon (POST) → récupère l'id.
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'/weighing_tickets',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ toast: false }),
|
||||
)
|
||||
// 2. Validation (back autoritaire) sur l'id retourné.
|
||||
expect(mockPatch).toHaveBeenCalledWith(
|
||||
'/weighing_tickets/42/validate',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ toast: false }),
|
||||
)
|
||||
// 3. Ouverture du bon de pesée PDF + retour à la liste.
|
||||
expect(mockOpen).toHaveBeenCalledWith('/api/weighing_tickets/42/print.pdf', '_blank')
|
||||
expect(mockPush).toHaveBeenCalledWith('/weighing-tickets')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user