@asteby/metacore-runtime-react 47.1.0 → 49.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/dist/action-modal-dispatcher.d.ts +17 -3
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +80 -13
- package/dist/action-priority.d.ts +34 -0
- package/dist/action-priority.d.ts.map +1 -0
- package/dist/action-priority.js +60 -0
- package/dist/business/catalog-product-search.d.ts +32 -0
- package/dist/business/catalog-product-search.d.ts.map +1 -0
- package/dist/business/catalog-product-search.js +71 -0
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +2 -0
- package/dist/business/document-editor-model.d.ts +119 -0
- package/dist/business/document-editor-model.d.ts.map +1 -0
- package/dist/business/document-editor-model.js +317 -0
- package/dist/business/document-editor-panels.d.ts +64 -0
- package/dist/business/document-editor-panels.d.ts.map +1 -0
- package/dist/business/document-editor-panels.js +58 -0
- package/dist/business/document-editor.d.ts +27 -0
- package/dist/business/document-editor.d.ts.map +1 -0
- package/dist/business/document-editor.js +493 -0
- package/dist/business/document-lines.d.ts.map +1 -1
- package/dist/business/document-lines.js +5 -1
- package/dist/business/index.d.ts +5 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +5 -0
- package/dist/business/line-items.d.ts +16 -1
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +14 -2
- package/dist/business/payment-allocator.d.ts +19 -0
- package/dist/business/payment-allocator.d.ts.map +1 -0
- package/dist/business/payment-allocator.js +31 -0
- package/dist/business/product-search.d.ts +4 -0
- package/dist/business/product-search.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +14 -7
- package/dist/document/document-page.d.ts.map +1 -1
- package/dist/document/document-page.js +8 -1
- package/dist/document/evaluate.d.ts.map +1 -1
- package/dist/document/evaluate.js +17 -1
- package/dist/document/index.d.ts +1 -0
- package/dist/document/index.d.ts.map +1 -1
- package/dist/document/index.js +1 -0
- package/dist/document/secondary-bar.d.ts +20 -0
- package/dist/document/secondary-bar.d.ts.map +1 -0
- package/dist/document/secondary-bar.js +44 -0
- package/dist/document/types.d.ts +8 -0
- package/dist/document/types.d.ts.map +1 -1
- package/dist/document-form-dialog.d.ts +31 -2
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +46 -11
- package/dist/dynamic-columns-helpers.d.ts +9 -0
- package/dist/dynamic-columns-helpers.d.ts.map +1 -1
- package/dist/dynamic-columns-helpers.js +19 -0
- package/dist/dynamic-columns.d.ts +9 -13
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +27 -21
- package/dist/dynamic-crud-page.d.ts +18 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +15 -6
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +15 -3
- package/dist/dynamic-form.d.ts +16 -1
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +78 -20
- package/dist/dynamic-kanban.d.ts +9 -3
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +31 -12
- package/dist/dynamic-relation-helpers.d.ts.map +1 -1
- package/dist/dynamic-relation-helpers.js +8 -0
- package/dist/dynamic-relation.js +1 -1
- package/dist/dynamic-row-actions.d.ts +8 -0
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +12 -2
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +64 -63
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/installed-addons-context.d.ts +15 -0
- package/dist/installed-addons-context.d.ts.map +1 -0
- package/dist/installed-addons-context.js +39 -0
- package/dist/org-runtime-context.d.ts +8 -0
- package/dist/org-runtime-context.d.ts.map +1 -1
- package/dist/org-runtime-context.js +8 -0
- package/dist/org-runtime-provider.d.ts +3 -1
- package/dist/org-runtime-provider.d.ts.map +1 -1
- package/dist/org-runtime-provider.js +3 -3
- package/dist/primitives/aging.d.ts +34 -0
- package/dist/primitives/aging.d.ts.map +1 -0
- package/dist/primitives/aging.js +48 -0
- package/dist/primitives/allocation.d.ts +42 -0
- package/dist/primitives/allocation.d.ts.map +1 -0
- package/dist/primitives/allocation.js +59 -0
- package/dist/primitives/approvals.d.ts +71 -0
- package/dist/primitives/approvals.d.ts.map +1 -0
- package/dist/primitives/approvals.js +48 -0
- package/dist/primitives/contributions.d.ts +93 -0
- package/dist/primitives/contributions.d.ts.map +1 -0
- package/dist/primitives/contributions.js +163 -0
- package/dist/primitives/document-kinds.d.ts +25 -0
- package/dist/primitives/document-kinds.d.ts.map +1 -0
- package/dist/primitives/document-kinds.js +26 -0
- package/dist/primitives/index.d.ts +8 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +23 -0
- package/dist/primitives/panels.d.ts +68 -0
- package/dist/primitives/panels.d.ts.map +1 -0
- package/dist/primitives/panels.js +3 -0
- package/dist/primitives/use-contributions.d.ts +15 -0
- package/dist/primitives/use-contributions.d.ts.map +1 -0
- package/dist/primitives/use-contributions.js +30 -0
- package/dist/record-actions.d.ts +21 -0
- package/dist/record-actions.d.ts.map +1 -0
- package/dist/record-actions.js +76 -0
- package/dist/record-prefill-bar.d.ts +11 -0
- package/dist/record-prefill-bar.d.ts.map +1 -0
- package/dist/record-prefill-bar.js +33 -0
- package/dist/row-actions-menu.d.ts +32 -0
- package/dist/row-actions-menu.d.ts.map +1 -0
- package/dist/row-actions-menu.js +59 -0
- package/dist/slot.d.ts +4 -21
- package/dist/slot.d.ts.map +1 -1
- package/dist/slot.js +14 -36
- package/dist/types.d.ts +134 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/catalog-product-search.test.ts +61 -0
- package/src/__tests__/document-editor-model.test.ts +221 -0
- package/src/__tests__/document-editor.test.tsx +340 -0
- package/src/__tests__/document-form-dialog.test.tsx +158 -3
- package/src/__tests__/dynamic-form-select-edit.test.tsx +155 -0
- package/src/__tests__/dynamic-kanban.test.tsx +24 -3
- package/src/__tests__/dynamic-record-extension-label.test.tsx +88 -0
- package/src/__tests__/installed-addons-context.test.tsx +50 -0
- package/src/__tests__/line-item-totals.test.ts +28 -0
- package/src/__tests__/prefill-scalar-from-record.test.ts +52 -1
- package/src/__tests__/primitives-federated.test.tsx +156 -0
- package/src/__tests__/primitives.test.ts +224 -0
- package/src/__tests__/record-prefill-bar.test.tsx +50 -0
- package/src/__tests__/row-action-link-navigation.test.ts +17 -0
- package/src/__tests__/secondary-actions.test.tsx +261 -0
- package/src/action-modal-dispatcher.tsx +97 -12
- package/src/action-priority.ts +82 -0
- package/src/business/business.test.ts +1 -1
- package/src/business/catalog-product-search.ts +105 -0
- package/src/business/catalog.ts +2 -0
- package/src/business/document-editor-model.ts +367 -0
- package/src/business/document-editor-panels.tsx +164 -0
- package/src/business/document-editor.tsx +781 -0
- package/src/business/document-lines.ts +5 -1
- package/src/business/index.ts +5 -0
- package/src/business/line-items.ts +31 -2
- package/src/business/payment-allocator.tsx +110 -0
- package/src/business/product-search.ts +4 -0
- package/src/dialogs/dynamic-record.tsx +29 -6
- package/src/document/document-page.tsx +15 -0
- package/src/document/evaluate.ts +17 -1
- package/src/document/index.ts +1 -0
- package/src/document/secondary-bar.tsx +92 -0
- package/src/document/types.ts +8 -0
- package/src/document-form-dialog.tsx +96 -12
- package/src/dynamic-columns-helpers.ts +22 -0
- package/src/dynamic-columns.tsx +35 -62
- package/src/dynamic-crud-page.tsx +33 -5
- package/src/dynamic-form-schema.ts +15 -2
- package/src/dynamic-form.tsx +117 -39
- package/src/dynamic-kanban.tsx +34 -7
- package/src/dynamic-relation-helpers.ts +9 -0
- package/src/dynamic-relation.tsx +7 -2
- package/src/dynamic-row-actions.tsx +12 -2
- package/src/dynamic-table.tsx +3 -0
- package/src/index.ts +46 -1
- package/src/installed-addons-context.tsx +58 -0
- package/src/org-runtime-context.ts +10 -0
- package/src/org-runtime-provider.tsx +7 -2
- package/src/primitives/aging.ts +76 -0
- package/src/primitives/allocation.ts +104 -0
- package/src/primitives/approvals.ts +103 -0
- package/src/primitives/contributions.ts +255 -0
- package/src/primitives/document-kinds.ts +62 -0
- package/src/primitives/index.ts +23 -0
- package/src/primitives/panels.ts +59 -0
- package/src/primitives/use-contributions.ts +42 -0
- package/src/record-actions.ts +101 -0
- package/src/record-prefill-bar.tsx +62 -0
- package/src/row-action-stage-gate.test.ts +33 -0
- package/src/row-actions-menu.tsx +159 -0
- package/src/slot.tsx +18 -53
- package/src/types.ts +129 -0
|
@@ -12,10 +12,11 @@ vi.mock('react-i18next', () => {
|
|
|
12
12
|
return { useTranslation: () => value }
|
|
13
13
|
})
|
|
14
14
|
|
|
15
|
-
import { DocumentFormDialog, resolveDocumentForms } from '../document-form-dialog'
|
|
15
|
+
import { DocumentFormDialog, resolveDocumentForms, scopeDocumentFormsToFilter } from '../document-form-dialog'
|
|
16
16
|
import { DynamicCRUDPage } from '../dynamic-crud-page'
|
|
17
17
|
import { useMetadataCache } from '../metadata-cache'
|
|
18
18
|
import { ApiProvider, type ApiClient } from '../api-context'
|
|
19
|
+
import { OrgRuntimeProvider } from '../org-runtime-provider'
|
|
19
20
|
import type { DocumentFormsManifest } from '../types'
|
|
20
21
|
|
|
21
22
|
afterEach(cleanup)
|
|
@@ -128,6 +129,124 @@ describe('DocumentFormDialog', () => {
|
|
|
128
129
|
})
|
|
129
130
|
})
|
|
130
131
|
|
|
132
|
+
// Retest Pitsline 05/10: Facturas → Crear guardaba la factura en $0 porque el
|
|
133
|
+
// host (DynamicCRUDPage / ops) no pasa `searchProducts` y el paso de renglones
|
|
134
|
+
// solo ofrecía «Renglón libre» sin precio.
|
|
135
|
+
describe('DocumentFormDialog: buscador de catálogo por defecto', () => {
|
|
136
|
+
const invoiceOnly: DocumentFormsManifest = { ...forms, types: [forms.types[0]] }
|
|
137
|
+
const catalogRow = {
|
|
138
|
+
id: 'prod-1',
|
|
139
|
+
name: 'Llanta 205/55R16',
|
|
140
|
+
sku: 'LL-205',
|
|
141
|
+
unit_price: '1500.00',
|
|
142
|
+
cost_price: '900',
|
|
143
|
+
unit_of_measure: 'pza',
|
|
144
|
+
product_type: 'product',
|
|
145
|
+
fiscal_data: { mx_clave_prod_serv: '25172504', mx_clave_unidad: 'H87' },
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function mount(opts: { searchProducts?: any; taxRate?: number } = {}) {
|
|
149
|
+
const get = vi.fn().mockResolvedValue({ data: { success: true, data: [catalogRow] } })
|
|
150
|
+
const post = vi.fn().mockResolvedValue({ data: { success: true, data: { id: 'inv-1' } } })
|
|
151
|
+
const client = { get, post, put: vi.fn(), delete: vi.fn() } as unknown as ApiClient
|
|
152
|
+
render(
|
|
153
|
+
<ApiProvider client={client}>
|
|
154
|
+
<OrgRuntimeProvider taxRate={opts.taxRate}>
|
|
155
|
+
<DocumentFormDialog
|
|
156
|
+
open
|
|
157
|
+
onOpenChange={vi.fn()}
|
|
158
|
+
model="customers.Invoice"
|
|
159
|
+
forms={invoiceOnly}
|
|
160
|
+
searchProducts={opts.searchProducts}
|
|
161
|
+
/>
|
|
162
|
+
</OrgRuntimeProvider>
|
|
163
|
+
</ApiProvider>,
|
|
164
|
+
)
|
|
165
|
+
return { get, post }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function pickFirstProduct(query: string, name: RegExp) {
|
|
169
|
+
fireEvent.change(document.querySelectorAll('input')[0], { target: { value: 'ACME' } })
|
|
170
|
+
fireEvent.click(screen.getByRole('button', { name: 'Siguiente' }))
|
|
171
|
+
fireEvent.change(await screen.findByLabelText('Buscar producto'), { target: { value: query } })
|
|
172
|
+
fireEvent.click(await waitFor(() => screen.getByRole('button', { name }), { timeout: 2000 }))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
it('sin searchProducts del host busca en el catálogo y el renglón entra con precio, cantidad 1, unidad, SKU e IVA de la org', async () => {
|
|
176
|
+
const { get, post } = mount({ taxRate: 0.16 })
|
|
177
|
+
await pickFirstProduct('llanta', /Llanta 205\/55R16/)
|
|
178
|
+
expect(get).toHaveBeenCalledWith(
|
|
179
|
+
'/data/products.Product',
|
|
180
|
+
expect.objectContaining({ params: expect.objectContaining({ search: 'llanta' }) }),
|
|
181
|
+
)
|
|
182
|
+
fireEvent.click(screen.getByRole('button', { name: 'Crear' }))
|
|
183
|
+
await waitFor(() => expect(post).toHaveBeenCalled())
|
|
184
|
+
const [line] = (post.mock.calls[0][1] as any).lines
|
|
185
|
+
expect(line).toMatchObject({
|
|
186
|
+
product_id: 'prod-1',
|
|
187
|
+
sku: 'LL-205',
|
|
188
|
+
description: 'Llanta 205/55R16',
|
|
189
|
+
quantity: 1,
|
|
190
|
+
unit_price: 1500,
|
|
191
|
+
tax_rate: 0.16,
|
|
192
|
+
unit: 'pza',
|
|
193
|
+
fiscal_data: { mx_clave_prod_serv: '25172504', mx_clave_unidad: 'H87' },
|
|
194
|
+
})
|
|
195
|
+
expect(line.unit_price).toBeGreaterThan(0)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('si el host pasa searchProducts, se usa ese y no el catálogo', async () => {
|
|
199
|
+
const searchProducts = vi.fn(async () => [{ id: 'h1', name: 'Servicio host', price: 250, tax_rate: 0.08 }])
|
|
200
|
+
const { get, post } = mount({ searchProducts })
|
|
201
|
+
await pickFirstProduct('serv', /Servicio host/)
|
|
202
|
+
expect(searchProducts).toHaveBeenCalled()
|
|
203
|
+
expect(get).not.toHaveBeenCalledWith('/data/products.Product', expect.anything())
|
|
204
|
+
fireEvent.click(screen.getByRole('button', { name: 'Crear' }))
|
|
205
|
+
await waitFor(() => expect(post).toHaveBeenCalled())
|
|
206
|
+
expect((post.mock.calls[0][1] as any).lines[0]).toMatchObject({ product_id: 'h1', quantity: 1, unit_price: 250, tax_rate: 0.08 })
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// Retest Pitsline 2026-10-05 (FAC-00013): el buscador de ops no trae tasa y el
|
|
211
|
+
// renglón entraba sin IVA; el producto sin tasa toma la de la org.
|
|
212
|
+
describe('DocumentFormDialog: IVA de la org con el buscador del host', () => {
|
|
213
|
+
it('producto sin tasa → IVA de la org; con tasa propia → la suya', async () => {
|
|
214
|
+
const searchProducts = vi.fn(async () => [
|
|
215
|
+
{ id: 'p1', name: 'Llanta host', price: 200 },
|
|
216
|
+
{ id: 'p2', name: 'Frontera host', price: 100, tax_rate: 0.08 },
|
|
217
|
+
])
|
|
218
|
+
const post = vi.fn().mockResolvedValue({ data: { success: true, data: { id: 'inv-1' } } })
|
|
219
|
+
const client = { get: vi.fn(), post, put: vi.fn(), delete: vi.fn() } as unknown as ApiClient
|
|
220
|
+
render(
|
|
221
|
+
<ApiProvider client={client}>
|
|
222
|
+
<OrgRuntimeProvider taxRate={0.16}>
|
|
223
|
+
<DocumentFormDialog
|
|
224
|
+
open
|
|
225
|
+
onOpenChange={vi.fn()}
|
|
226
|
+
model="customers.Invoice"
|
|
227
|
+
forms={{ ...forms, lines_field: 'items', types: [forms.types[0]] }}
|
|
228
|
+
searchProducts={searchProducts as any}
|
|
229
|
+
/>
|
|
230
|
+
</OrgRuntimeProvider>
|
|
231
|
+
</ApiProvider>,
|
|
232
|
+
)
|
|
233
|
+
fireEvent.change(document.querySelectorAll('input')[0], { target: { value: 'ACME' } })
|
|
234
|
+
fireEvent.click(screen.getByRole('button', { name: 'Siguiente' }))
|
|
235
|
+
const search = await screen.findByLabelText('Buscar producto')
|
|
236
|
+
fireEvent.change(search, { target: { value: 'host' } })
|
|
237
|
+
fireEvent.click(await waitFor(() => screen.getByRole('button', { name: /Llanta host/ }), { timeout: 2000 }))
|
|
238
|
+
fireEvent.change(await screen.findByLabelText('Buscar producto'), { target: { value: 'fron' } })
|
|
239
|
+
fireEvent.click(await waitFor(() => screen.getByRole('button', { name: /Frontera host/ }), { timeout: 2000 }))
|
|
240
|
+
fireEvent.click(screen.getByRole('button', { name: 'Crear' }))
|
|
241
|
+
await waitFor(() => expect(post).toHaveBeenCalled())
|
|
242
|
+
const body = post.mock.calls[0][1] as any
|
|
243
|
+
expect(body.items).toEqual([
|
|
244
|
+
expect.objectContaining({ product_id: 'p1', quantity: 1, unit_price: 200, tax_rate: 0.16 }),
|
|
245
|
+
expect.objectContaining({ product_id: 'p2', quantity: 1, unit_price: 100, tax_rate: 0.08 }),
|
|
246
|
+
])
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
131
250
|
describe('resolveDocumentForms', () => {
|
|
132
251
|
it('sin manifest (o sin tipos) devuelve undefined: el alta genérica sigue igual', () => {
|
|
133
252
|
expect(resolveDocumentForms(null)).toBeUndefined()
|
|
@@ -157,7 +276,7 @@ describe('DynamicCRUDPage + document_forms', () => {
|
|
|
157
276
|
...extra,
|
|
158
277
|
}) as any
|
|
159
278
|
|
|
160
|
-
const mount = (m: any) => {
|
|
279
|
+
const mount = (m: any, filter?: Record<string, string>) => {
|
|
161
280
|
useMetadataCache.getState().setMetadata('fiscal_documents', m)
|
|
162
281
|
const client = {
|
|
163
282
|
get: vi.fn(async (url: string) =>
|
|
@@ -169,7 +288,7 @@ describe('DynamicCRUDPage + document_forms', () => {
|
|
|
169
288
|
} as unknown as ApiClient
|
|
170
289
|
render(
|
|
171
290
|
<ApiProvider client={client}>
|
|
172
|
-
<DynamicCRUDPage model="fiscal_documents" />
|
|
291
|
+
<DynamicCRUDPage model="fiscal_documents" filter={filter} />
|
|
173
292
|
</ApiProvider>,
|
|
174
293
|
)
|
|
175
294
|
}
|
|
@@ -187,4 +306,40 @@ describe('DynamicCRUDPage + document_forms', () => {
|
|
|
187
306
|
await new Promise((r) => setTimeout(r, 50))
|
|
188
307
|
expect(screen.queryByRole('radiogroup')).toBeNull()
|
|
189
308
|
})
|
|
309
|
+
|
|
310
|
+
// Retest r3 (05/10): «Documentos fiscales → Facturas» (nav filtrado por el
|
|
311
|
+
// type_field) ofrecía Crear y abría el REP, el único tipo declarado.
|
|
312
|
+
it('vista filtrada a un tipo sin formulario: sin botón Crear', async () => {
|
|
313
|
+
mount(meta({ document_forms: forms }), { type: 'P' })
|
|
314
|
+
await screen.findByText(/Nota/)
|
|
315
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
316
|
+
expect(screen.queryByText(/New Nota/)).toBeNull()
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('vista filtrada a un tipo con formulario: Crear abre ese tipo directo', async () => {
|
|
320
|
+
mount(meta({ document_forms: forms }), { type: 'eq:E' })
|
|
321
|
+
fireEvent.click((await screen.findByText(/New Nota/)).closest('button')!)
|
|
322
|
+
expect(await screen.findByText('Factura origen')).toBeTruthy()
|
|
323
|
+
expect(screen.queryByRole('radiogroup')).toBeNull()
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
describe('scopeDocumentFormsToFilter', () => {
|
|
328
|
+
it('sin filtro sobre el type_field no cambia nada', () => {
|
|
329
|
+
expect(scopeDocumentFormsToFilter(forms, undefined)).toEqual({ forms, hideCreate: false })
|
|
330
|
+
expect(scopeDocumentFormsToFilter(forms, { status: 'draft' })).toEqual({ forms, hideCreate: false })
|
|
331
|
+
expect(scopeDocumentFormsToFilter(undefined, { type: 'I' })).toEqual({ forms: undefined, hideCreate: false })
|
|
332
|
+
expect(scopeDocumentFormsToFilter({ ...forms, type_field: undefined }, { type: 'X' }).hideCreate).toBe(false)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('acota al tipo del filtro (valor o key, sin importar mayúsculas ni eq:)', () => {
|
|
336
|
+
expect(scopeDocumentFormsToFilter(forms, { type: 'e' }).forms?.types.map((x) => x.key)).toEqual(['credit_note'])
|
|
337
|
+
expect(scopeDocumentFormsToFilter(forms, { type: 'eq:I' }).forms?.types.map((x) => x.key)).toEqual(['invoice'])
|
|
338
|
+
const noValue: DocumentFormsManifest = { type_field: 'document_type', types: [{ key: 'payment_complement', label: 'REP', fields: [] }] }
|
|
339
|
+
expect(scopeDocumentFormsToFilter(noValue, { document_type: 'payment_complement' }).forms).toBe(noValue)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('filtro a un tipo sin formulario: oculta Crear', () => {
|
|
343
|
+
expect(scopeDocumentFormsToFilter(forms, { type: 'P' })).toEqual({ forms: undefined, hideCreate: true })
|
|
344
|
+
})
|
|
190
345
|
})
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// Editar un registro con un select que YA tiene valor y guardar sin tocarlo debe
|
|
4
|
+
// conservar ese valor en el payload. Antes los `values` del DynamicForm se
|
|
5
|
+
// sembraban en un efecto (primer render con '') y el Select de Radix emitía
|
|
6
|
+
// `onValueChange('')`, que pisaba el valor real y viajaba en el PATCH (p.ej. un
|
|
7
|
+
// renglón «Refacción» quedaba en «-»). Cubre también: `readonly` de columna en
|
|
8
|
+
// `deriveRelationFormFields`, selects vacíos no tocados fuera del PATCH, y el
|
|
9
|
+
// modal de renglón con cuerpo con scroll y footer fijo.
|
|
10
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
11
|
+
import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
12
|
+
|
|
13
|
+
vi.mock('react-i18next', () => ({
|
|
14
|
+
useTranslation: () => ({ t: (k: string, o?: any) => o?.defaultValue ?? k, i18n: { language: 'es' } }),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
import { DynamicForm, guardEmptySelect } from '../dynamic-form'
|
|
18
|
+
import { DynamicRelation } from '../dynamic-relation'
|
|
19
|
+
import { deriveRelationFormFields } from '../dynamic-relation-helpers'
|
|
20
|
+
import { ApiProvider, type ApiClient } from '../api-context'
|
|
21
|
+
import type { ActionFieldDef } from '../types'
|
|
22
|
+
|
|
23
|
+
afterEach(cleanup)
|
|
24
|
+
|
|
25
|
+
const kindField: ActionFieldDef = {
|
|
26
|
+
key: 'kind',
|
|
27
|
+
label: 'Tipo',
|
|
28
|
+
type: 'select',
|
|
29
|
+
options: [
|
|
30
|
+
{ value: 'service', label: 'Servicio' },
|
|
31
|
+
{ value: 'part', label: 'Refacción' },
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('DynamicForm — edición con select', () => {
|
|
36
|
+
it('guardar sin tocar el select conserva su valor en el payload', async () => {
|
|
37
|
+
const onSubmit = vi.fn()
|
|
38
|
+
render(
|
|
39
|
+
<DynamicForm
|
|
40
|
+
fields={[{ key: 'description', label: 'Descripción', type: 'text' }, kindField]}
|
|
41
|
+
initialValues={{ id: 'r1', description: 'Balata', kind: 'part' }}
|
|
42
|
+
onSubmit={onSubmit}
|
|
43
|
+
/>,
|
|
44
|
+
)
|
|
45
|
+
await act(async () => {
|
|
46
|
+
screen.getByText('Guardar').click()
|
|
47
|
+
})
|
|
48
|
+
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
49
|
+
expect(onSubmit.mock.calls[0][0]).toMatchObject({ description: 'Balata', kind: 'part' })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('en edición no manda un select vacío que el usuario no tocó', async () => {
|
|
53
|
+
const onSubmit = vi.fn()
|
|
54
|
+
render(
|
|
55
|
+
<DynamicForm
|
|
56
|
+
fields={[{ key: 'description', label: 'Descripción', type: 'text' }, kindField]}
|
|
57
|
+
initialValues={{ id: 'r1', description: 'Balata' }}
|
|
58
|
+
onSubmit={onSubmit}
|
|
59
|
+
/>,
|
|
60
|
+
)
|
|
61
|
+
await act(async () => {
|
|
62
|
+
screen.getByText('Guardar').click()
|
|
63
|
+
})
|
|
64
|
+
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
65
|
+
expect(onSubmit.mock.calls[0][0]).not.toHaveProperty('kind')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('guardEmptySelect', () => {
|
|
70
|
+
it("ignora '' cuando ya hay valor y '' no es opción", () => {
|
|
71
|
+
const onChange = vi.fn()
|
|
72
|
+
guardEmptySelect('part', onChange, ['service', 'part'])('')
|
|
73
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
it("deja pasar '' si no había valor o si '' es opción válida, y cualquier otro valor", () => {
|
|
76
|
+
const onChange = vi.fn()
|
|
77
|
+
guardEmptySelect('', onChange, ['service', 'part'])('')
|
|
78
|
+
guardEmptySelect('part', onChange, ['', 'part'])('')
|
|
79
|
+
guardEmptySelect('part', onChange, ['service', 'part'])('service')
|
|
80
|
+
expect(onChange.mock.calls).toEqual([[''], [''], ['service']])
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const META = {
|
|
85
|
+
name: 'work_order_item',
|
|
86
|
+
columns: [
|
|
87
|
+
{ key: 'id', label: 'ID', type: 'text', sortable: true, filterable: false, hidden: true },
|
|
88
|
+
{ key: 'work_order_id', label: 'OT', type: 'text', sortable: false, filterable: false },
|
|
89
|
+
{ key: 'description', label: 'Descripción', type: 'text', sortable: true, filterable: true },
|
|
90
|
+
{
|
|
91
|
+
key: 'kind',
|
|
92
|
+
label: 'Tipo',
|
|
93
|
+
type: 'select',
|
|
94
|
+
sortable: true,
|
|
95
|
+
filterable: true,
|
|
96
|
+
options: [
|
|
97
|
+
{ value: 'service', label: 'Servicio' },
|
|
98
|
+
{ value: 'part', label: 'Refacción' },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{ key: 'line_total', label: 'Importe', type: 'number', sortable: false, filterable: false, readonly: true },
|
|
102
|
+
],
|
|
103
|
+
actions: [],
|
|
104
|
+
hasActions: false,
|
|
105
|
+
enableCRUDActions: false,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const DATA = [{ id: 'woi_1', work_order_id: 'wo_1', description: 'Balata', kind: 'part', line_total: 450 }]
|
|
109
|
+
|
|
110
|
+
function mockApi(): ApiClient {
|
|
111
|
+
return {
|
|
112
|
+
get: vi.fn((url: string) => {
|
|
113
|
+
if (url.startsWith('/metadata/table/')) {
|
|
114
|
+
return Promise.resolve({ data: { success: true, data: META } })
|
|
115
|
+
}
|
|
116
|
+
return Promise.resolve({ data: { success: true, data: DATA } })
|
|
117
|
+
}),
|
|
118
|
+
post: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })),
|
|
119
|
+
put: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })),
|
|
120
|
+
delete: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe('DynamicRelation — editar renglón con select', () => {
|
|
125
|
+
it('deriveRelationFormFields copia readonly de la columna', () => {
|
|
126
|
+
const fields = deriveRelationFormFields(META as any, 'work_order_id')
|
|
127
|
+
expect(fields.find((f) => f.key === 'line_total')?.readonly).toBe(true)
|
|
128
|
+
expect(fields.find((f) => f.key === 'kind')?.readonly).toBeUndefined()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('el PATCH conserva el tipo y no manda columnas readonly; el modal hace scroll con footer fijo', async () => {
|
|
132
|
+
const api = mockApi()
|
|
133
|
+
render(
|
|
134
|
+
<ApiProvider client={api}>
|
|
135
|
+
<DynamicRelation kind="one_to_many" model="work_order_item" foreignKey="work_order_id" parentId="wo_1" />
|
|
136
|
+
</ApiProvider>,
|
|
137
|
+
)
|
|
138
|
+
await waitFor(() => expect(screen.getByText('Balata')).toBeTruthy())
|
|
139
|
+
await act(async () => {
|
|
140
|
+
screen.getByLabelText('Editar').click()
|
|
141
|
+
})
|
|
142
|
+
const dialog = await screen.findByRole('dialog')
|
|
143
|
+
expect(dialog.className).toContain('max-h-[90dvh]')
|
|
144
|
+
expect(dialog.querySelector('.overflow-y-auto')).toBeTruthy()
|
|
145
|
+
|
|
146
|
+
await act(async () => {
|
|
147
|
+
screen.getByText('Guardar').click()
|
|
148
|
+
})
|
|
149
|
+
await waitFor(() => expect(api.put).toHaveBeenCalledTimes(1))
|
|
150
|
+
const [url, payload] = (api.put as any).mock.calls[0]
|
|
151
|
+
expect(url).toContain('woi_1')
|
|
152
|
+
expect(payload).toMatchObject({ description: 'Balata', kind: 'part' })
|
|
153
|
+
expect(payload).not.toHaveProperty('line_total')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -293,11 +293,32 @@ describe('withGroupBy (one model, several boards)', () => {
|
|
|
293
293
|
...over,
|
|
294
294
|
}) as any
|
|
295
295
|
|
|
296
|
-
it('is the same object when
|
|
297
|
-
const m = boardMeta()
|
|
296
|
+
it('is the same object when the served group_by is already the stage column', () => {
|
|
297
|
+
const m = boardMeta({ group_by: 'stage' })
|
|
298
298
|
expect(withGroupBy(m, undefined)).toBe(m)
|
|
299
|
-
expect(withGroupBy(m, '
|
|
299
|
+
expect(withGroupBy(m, 'stage')).toBe(m)
|
|
300
300
|
expect(withGroupBy(null, 'stage')).toBeNull()
|
|
301
|
+
const flat = boardMeta({ stages: undefined, transitions: undefined, smart_lanes: undefined })
|
|
302
|
+
expect(withGroupBy(flat, 'assigned_mechanic_id')).toBe(flat)
|
|
303
|
+
expect(withGroupBy(flat, undefined)).toBe(flat)
|
|
304
|
+
})
|
|
305
|
+
// Pitsline r3 (b): WorkOrder serves group_by=service_bay_id (the last kanban
|
|
306
|
+
// nav entry) with the stage lanes. Without ?group_by= the board grouped the
|
|
307
|
+
// stage lanes by the bay: every card in «Sin etapa», each lane count sent
|
|
308
|
+
// f_service_bay_id=diagnosis and a drop PUT service_bay_id='diagnosis' (422).
|
|
309
|
+
it('groups a board without ?group_by= by the stage column, not the served one', () => {
|
|
310
|
+
const out = withGroupBy(boardMeta(), undefined)!
|
|
311
|
+
expect(out.group_by).toBe('stage')
|
|
312
|
+
expect(out.stages).toHaveLength(1)
|
|
313
|
+
expect(out.transitions).toHaveLength(1)
|
|
314
|
+
})
|
|
315
|
+
it('drops the stage machine on the board of the served (non-stage) column', () => {
|
|
316
|
+
const out = withGroupBy(boardMeta(), 'assigned_mechanic_id')!
|
|
317
|
+
expect(out.group_by).toBe('assigned_mechanic_id')
|
|
318
|
+
expect(out.stages).toBeUndefined()
|
|
319
|
+
expect(out.transitions).toBeUndefined()
|
|
320
|
+
expect(out.smart_lanes).toBeUndefined()
|
|
321
|
+
expect(refLaneSource(out)).toEqual({ ref: 'users' })
|
|
301
322
|
})
|
|
302
323
|
it('ignores a column the model does not have', () => {
|
|
303
324
|
const m = boardMeta()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// Retest Pitsline 05/10: the customer and product forms printed the fields that
|
|
4
|
+
// fiscal_mexico adds by extension as `FISCAL_MEXICO.EXT.CUSTOMER.RFC_RECEPTOR`.
|
|
5
|
+
// The server served the label as the raw catalog key and the dialog rendered it
|
|
6
|
+
// verbatim (uppercased by CSS). The dialog now resolves such a key with the
|
|
7
|
+
// client catalog, and humanizes it when the catalog does not have it either.
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
9
|
+
import { cleanup, render, waitFor } from '@testing-library/react'
|
|
10
|
+
import { I18nextProvider } from 'react-i18next'
|
|
11
|
+
import i18next from 'i18next'
|
|
12
|
+
|
|
13
|
+
import { DynamicRecordDialog } from '../dialogs/dynamic-record'
|
|
14
|
+
import { ApiProvider } from '../api-context'
|
|
15
|
+
import { localizeFieldLabel } from '../dynamic-columns-helpers'
|
|
16
|
+
|
|
17
|
+
afterEach(cleanup)
|
|
18
|
+
|
|
19
|
+
function makeI18n() {
|
|
20
|
+
const inst = i18next.createInstance()
|
|
21
|
+
void inst.init({
|
|
22
|
+
lng: 'es',
|
|
23
|
+
fallbackLng: 'es',
|
|
24
|
+
react: { useSuspense: false },
|
|
25
|
+
resources: {
|
|
26
|
+
es: {
|
|
27
|
+
translation: {
|
|
28
|
+
fiscal_mexico: { ext: { customer: { rfc_receptor: 'RFC' } } },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
return inst
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const api = {
|
|
37
|
+
get: vi.fn(async () => ({ data: { data: {} } })),
|
|
38
|
+
post: async () => ({ data: { success: true } }),
|
|
39
|
+
put: async () => ({ data: { success: true } }),
|
|
40
|
+
delete: async () => ({ data: { success: true } }),
|
|
41
|
+
} as never
|
|
42
|
+
|
|
43
|
+
describe('DynamicRecordDialog — extension field labels', () => {
|
|
44
|
+
it('never prints a raw catalog key as the field label', async () => {
|
|
45
|
+
render(
|
|
46
|
+
<I18nextProvider i18n={makeI18n()}>
|
|
47
|
+
<ApiProvider client={api}>
|
|
48
|
+
<DynamicRecordDialog
|
|
49
|
+
open
|
|
50
|
+
onOpenChange={() => {}}
|
|
51
|
+
mode="create"
|
|
52
|
+
model="customers"
|
|
53
|
+
schema={{
|
|
54
|
+
title: 'Nuevo cliente',
|
|
55
|
+
fields: [
|
|
56
|
+
{ key: 'name', label: 'Nombre', type: 'text' },
|
|
57
|
+
{ key: 'fiscal_data.rfc_receptor', label: 'fiscal_mexico.ext.customer.rfc_receptor', type: 'text' },
|
|
58
|
+
{ key: 'fiscal_data.codigo_postal', label: 'fiscal_mexico.ext.customer.codigo_postal', type: 'text' },
|
|
59
|
+
],
|
|
60
|
+
} as never}
|
|
61
|
+
/>
|
|
62
|
+
</ApiProvider>
|
|
63
|
+
</I18nextProvider>,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const label = (key: string) =>
|
|
67
|
+
document.querySelector(`[data-aby-field="${key}"] label`)?.textContent ?? ''
|
|
68
|
+
await waitFor(() => expect(label('fiscal_data.rfc_receptor')).toBe('RFC'))
|
|
69
|
+
expect(label('fiscal_data.codigo_postal')).toBe('Codigo Postal')
|
|
70
|
+
expect(label('name')).toBe('Nombre')
|
|
71
|
+
expect(document.body.textContent).not.toContain('fiscal_mexico.ext')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('localizeFieldLabel', () => {
|
|
76
|
+
it('leaves human labels alone, even single words or acronyms', () => {
|
|
77
|
+
const t = vi.fn((k: string) => `T:${k}`)
|
|
78
|
+
expect(localizeFieldLabel('Nombre', t)).toBe('Nombre')
|
|
79
|
+
expect(localizeFieldLabel('RFC', t)).toBe('RFC')
|
|
80
|
+
expect(localizeFieldLabel('Código postal', t)).toBe('Código postal')
|
|
81
|
+
expect(t).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('resolves catalog keys and humanizes the ones the catalog lacks', () => {
|
|
85
|
+
expect(localizeFieldLabel('a.b', (k: string, o?: { defaultValue?: string }) => (k === 'a.b' ? 'Hola' : o?.defaultValue ?? k))).toBe('Hola')
|
|
86
|
+
expect(localizeFieldLabel('fiscal_mexico.ext.product.mx_clave_unidad', (k: string) => k)).toBe('Mx Clave Unidad')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// InstalledAddonsProvider: sin provider = desconocido (undefined), con provider
|
|
4
|
+
// responde por clave de addon y por capacidad.
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
6
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
7
|
+
import {
|
|
8
|
+
InstalledAddonsProvider,
|
|
9
|
+
useAddonInstalled,
|
|
10
|
+
useCapabilityProvided,
|
|
11
|
+
useInstalledAddons,
|
|
12
|
+
} from '../installed-addons-context'
|
|
13
|
+
|
|
14
|
+
afterEach(() => cleanup())
|
|
15
|
+
|
|
16
|
+
function Probe() {
|
|
17
|
+
const fiscal = useAddonInstalled('fiscal_mexico')
|
|
18
|
+
const issuer = useCapabilityProvided('fiscal.invoice_issuer')
|
|
19
|
+
const all = useInstalledAddons()
|
|
20
|
+
return (
|
|
21
|
+
<span data-testid="probe">
|
|
22
|
+
{String(fiscal)}|{String(issuer)}|{all ? [...all.addons].join(',') : 'null'}
|
|
23
|
+
</span>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('InstalledAddonsProvider', () => {
|
|
28
|
+
it('sin provider devuelve undefined/null (nunca asume ausente)', () => {
|
|
29
|
+
render(<Probe />)
|
|
30
|
+
expect(screen.getByTestId('probe').textContent).toBe('undefined|undefined|null')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('responde por addon y por capacidad', () => {
|
|
34
|
+
render(
|
|
35
|
+
<InstalledAddonsProvider addons={['customers', 'fiscal_mexico']} capabilities={['fiscal.invoice_issuer']}>
|
|
36
|
+
<Probe />
|
|
37
|
+
</InstalledAddonsProvider>,
|
|
38
|
+
)
|
|
39
|
+
expect(screen.getByTestId('probe').textContent).toBe('true|true|customers,fiscal_mexico')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('addon no instalado = false', () => {
|
|
43
|
+
render(
|
|
44
|
+
<InstalledAddonsProvider addons={['customers']}>
|
|
45
|
+
<Probe />
|
|
46
|
+
</InstalledAddonsProvider>,
|
|
47
|
+
)
|
|
48
|
+
expect(screen.getByTestId('probe').textContent).toBe('false|false|customers')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -25,6 +25,34 @@ const journalField = (overrides: Partial<ActionFieldDef> = {}): ActionFieldDef =
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
describe('computeLineItemTotals', () => {
|
|
28
|
+
// Pitsline r3 (g): the work order lines carry qty + a `total` price column
|
|
29
|
+
// and no amount column. qty=2 × 750 footed 750; the saved order was 1500.
|
|
30
|
+
it('suma qty × precio cuando la columna total es el precio unitario', () => {
|
|
31
|
+
const field = {
|
|
32
|
+
key: 'lines',
|
|
33
|
+
type: 'array',
|
|
34
|
+
itemFields: [
|
|
35
|
+
{ key: 'description', type: 'string' },
|
|
36
|
+
{ key: 'qty', type: 'number' },
|
|
37
|
+
{ key: 'price', type: 'number', total: true },
|
|
38
|
+
],
|
|
39
|
+
} as any
|
|
40
|
+
const rows = [{ qty: '2', price: '750' }, { qty: 1, price: 99.9 }]
|
|
41
|
+
expect(computeLineItemTotals(field, rows)).toEqual({ price: 1599.9 })
|
|
42
|
+
})
|
|
43
|
+
it('con columna de importe, el precio unitario se suma tal cual', () => {
|
|
44
|
+
const field = {
|
|
45
|
+
key: 'lines',
|
|
46
|
+
type: 'array',
|
|
47
|
+
itemFields: [
|
|
48
|
+
{ key: 'qty', type: 'number' },
|
|
49
|
+
{ key: 'unit_price', type: 'number', total: true },
|
|
50
|
+
{ key: 'subtotal', type: 'number', total: true },
|
|
51
|
+
],
|
|
52
|
+
} as any
|
|
53
|
+
const rows = [{ qty: 2, unit_price: 10, subtotal: 20 }]
|
|
54
|
+
expect(computeLineItemTotals(field, rows)).toEqual({ unit_price: 10, subtotal: 20 })
|
|
55
|
+
})
|
|
28
56
|
it('suma solo las columnas marcadas con total', () => {
|
|
29
57
|
const rows = [
|
|
30
58
|
{ account_id: 'a', debit: '100', credit: '' },
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
buildFieldDefaults,
|
|
4
|
+
readRecordPath,
|
|
5
|
+
refTargetsModel,
|
|
6
|
+
scalarDefaultFromRecord,
|
|
7
|
+
unwrapRecordScalar,
|
|
8
|
+
} from '../action-modal-dispatcher'
|
|
3
9
|
import type { ActionFieldDef } from '../types'
|
|
4
10
|
|
|
5
11
|
describe('scalarDefaultFromRecord', () => {
|
|
@@ -35,3 +41,48 @@ describe('scalarDefaultFromRecord', () => {
|
|
|
35
41
|
expect(scalarDefaultFromRecord(field, record)).toBe('11111111-1111-4111-8111-111111111111')
|
|
36
42
|
})
|
|
37
43
|
})
|
|
44
|
+
|
|
45
|
+
// Retest Pitsline 2026-10-05: «Registrar pago» desde una factura PPD con saldo
|
|
46
|
+
// abría el modal con «Factura» vacía: el campo es `invoice_id` y la fila (la
|
|
47
|
+
// factura) no tiene esa columna, tiene `id`.
|
|
48
|
+
describe('selector que apunta al modelo de la fila', () => {
|
|
49
|
+
// Campos de customers `register_payment` (target_model Invoice).
|
|
50
|
+
const registerPayment = [
|
|
51
|
+
{ key: 'customer_id', label: 'Cliente', type: 'dynamic_select', required: true, ref: 'Customer' },
|
|
52
|
+
{
|
|
53
|
+
key: 'invoice_id',
|
|
54
|
+
label: 'Factura',
|
|
55
|
+
type: 'dynamic_select',
|
|
56
|
+
ref: 'Invoice',
|
|
57
|
+
options: { source: 'Invoice', value: 'id', label: 'number' },
|
|
58
|
+
},
|
|
59
|
+
{ key: 'amount', label: 'Monto', type: 'number', required: true },
|
|
60
|
+
] as unknown as ActionFieldDef[]
|
|
61
|
+
const invoice = { id: 'inv-13', number: 'FAC-00013', customer_id: { value: 'cus-1', label: 'Demo' }, amount_due: 232 }
|
|
62
|
+
|
|
63
|
+
it('«Registrar pago» abre precargado con la factura y su cliente', () => {
|
|
64
|
+
const d = buildFieldDefaults(registerPayment, invoice, 'invoices')
|
|
65
|
+
expect(d.invoice_id).toBe('inv-13')
|
|
66
|
+
expect(d.customer_id).toBe('cus-1')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('sin el modelo de la fila no adivina', () => {
|
|
70
|
+
expect(buildFieldDefaults(registerPayment, invoice).invoice_id).toBe('')
|
|
71
|
+
expect(buildFieldDefaults(registerPayment, invoice, 'customers').invoice_id).toBe('')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('una columna propia vacía (parent_id sin padre) no se siembra con el id de la fila', () => {
|
|
75
|
+
const parent = { key: 'parent_id', label: 'Padre', type: 'dynamic_select', ref: 'Category' } as ActionFieldDef
|
|
76
|
+
expect(scalarDefaultFromRecord(parent, { id: 'cat-1', parent_id: null }, 'categories')).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('refTargetsModel tolera prefijo de addon, PascalCase y plural de tabla', () => {
|
|
80
|
+
expect(refTargetsModel('customers.Invoice', 'invoices')).toBe(true)
|
|
81
|
+
expect(refTargetsModel('Invoice', 'Invoice')).toBe(true)
|
|
82
|
+
expect(refTargetsModel('CustomerAddress', 'customer_addresses')).toBe(true)
|
|
83
|
+
expect(refTargetsModel('SalesOrder', 'sales_orders')).toBe(true)
|
|
84
|
+
expect(refTargetsModel('Invoice', 'invoice_items')).toBe(false)
|
|
85
|
+
expect(refTargetsModel(undefined, 'invoices')).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|