feat(tagumedica): etiquetas de bultos solo en papel Carta precortado (sin parámetro de papel) y vista HTML con los márgenes en pantalla
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0534becc9d
commit
0252717206
4 files changed
+35
-27
No files matched your search
@@ -64,7 +64,7 @@ Menú **Reportes HTML** (`/reportes-html/ejecutar`): reportes que cada cliente d
|
||||
|
||||
## TAGUMEDICA: etiquetas de bultos (solo en `cliente/tagumedica`)
|
||||
|
||||
Primer reporte HTML del cliente (`/reportes-html/ejecutar` → «Etiquetas de bultos»), migrado de `mini-erp` (`templates/reports/stickers.html`, Jinja2 + WeasyPrint): plantilla `apps/api/templates/tagumedica/etiquetas-bultos.njk` (la misma, con `line-height: 1.2` y la fuente Orbitron incrustada con `asset()`), definición en `htmlreports/etiquetas-bultos.ts` y consulta `delivery-labels` (HANA: `ODLN`/`DLN12`/`OCST`, reducida a los 4 datos que usa la plantilla: cliente, dirección, lugar —departamento, provincia y distrito— y documento: el `NumAtCard` o el folio). **Parámetros:** la entrega (ventana de selección de documentos, valor = `DocEntry`), la cantidad de etiquetas (entero de 1 a 500) y el papel (Carta, por defecto, o A4). Sale en hojas horizontales de 4 etiquetas (2 × 2) numeradas `1/6`… Verificado con la entrega 144153 contra `SBO_TAGUMEDICA_21` (consulta + plantilla + PDF de Chrome: 2 páginas, Carta 792 × 612 pt y A4). Los usuarios necesitan el permiso de menú `/reportes-html/*` (o `/reportes-html/ejecutar`). Pendiente: ofrecerlo también en el botón PDF de Entregas.
|
||||
Primer reporte HTML del cliente (`/reportes-html/ejecutar` → «Etiquetas de bultos»), migrado de `mini-erp` (`templates/reports/stickers.html`, Jinja2 + WeasyPrint): plantilla `apps/api/templates/tagumedica/etiquetas-bultos.njk` (la misma, con `line-height: 1.2` y la fuente Orbitron incrustada con `asset()`), definición en `htmlreports/etiquetas-bultos.ts` y consulta `delivery-labels` (HANA: `ODLN`/`DLN12`/`OCST`, reducida a los 4 datos que usa la plantilla: cliente, dirección, lugar —departamento, provincia y distrito— y documento: el `NumAtCard` o el folio). **Parámetros:** la entrega (ventana de selección de documentos, valor = `DocEntry`) y la cantidad de etiquetas (entero de 1 a 500). **El papel es siempre Carta precortado** (no hay parámetro de papel): los márgenes de la plantilla (`1.6cm 1.5cm 1.8cm 2.7cm`) y la rejilla están ajustados a los recortes de la hoja, así que no se tocan sin probar con papel; la salida buena es el **PDF** (botón «Generar PDF»), que se imprime en **«Tamaño real / 100 %»**. Sale en hojas horizontales de 4 etiquetas (2 × 2) numeradas `1/6`… Verificado con la entrega 144153 contra `SBO_TAGUMEDICA_21` (consulta + plantilla + `HtmlPdfService`: PDF de 2 páginas Carta de 792 × 612 pt en 0,6 s; la primera etiqueta cae a 76,5 pt del borde izquierdo y 45,4 pt del superior, más el relleno). La vista HTML lleva los mismos márgenes en pantalla (`@media screen`). Los usuarios necesitan el permiso de menú `/reportes-html/*` (o `/reportes-html/ejecutar`). Pendiente: ofrecerlo también en el botón PDF de Entregas.
|
||||
|
||||
## Ver el nombre del campo de SAP («Campos»)
|
||||
|
||||
|
||||
@@ -9,27 +9,26 @@ import { HTML_REPORTS } from './html-reports.js';
|
||||
|
||||
const ROW = { CardName: 'CLÍNICA <PLASTIC> & CIA S.A.C.', Guia: 'T001-37070', Direccion: 'AV. DOMINGO ORUE NRO. 469', Departamento: 'LIMA', Provincia: 'LIMA', Distrito: 'SURQUILLO' };
|
||||
const render = createRenderer(resolve(import.meta.dirname, '../../templates')).render;
|
||||
const html = (n: number, paper: 'Carta' | 'A4' = 'Carta') => render(ETIQUETAS_BULTOS.template, labelsData(ROW, n, paper));
|
||||
const html = (n: number) => render(ETIQUETAS_BULTOS.template, labelsData(ROW, n));
|
||||
const count = (s: string, re: RegExp) => (s.match(re) ?? []).length;
|
||||
|
||||
describe('etiquetas de bultos', () => {
|
||||
it('está registrado con la entrega, la cantidad y el papel como parámetros', () => {
|
||||
it('está registrado con la entrega y la cantidad como parámetros (el papel es siempre Carta precortado)', () => {
|
||||
expect(HTML_REPORTS.get('etiquetas-bultos')).toBe(ETIQUETAS_BULTOS);
|
||||
expect(ETIQUETAS_BULTOS.params.map((p) => [p.name, p.kind, p.required])).toEqual([['docEntry', 'lookup', true], ['cantidad', 'number', true], ['papel', 'select', true]]);
|
||||
expect(ETIQUETAS_BULTOS.params.map((p) => [p.name, p.kind, p.required])).toEqual([['docEntry', 'lookup', true], ['cantidad', 'number', true]]);
|
||||
expect(ETIQUETAS_BULTOS.params[0].lookup).toBe('doc:DeliveryNotes');
|
||||
// el papel por defecto es Carta
|
||||
expect(parseHtmlValues(ETIQUETAS_BULTOS, { docEntry: '144153', cantidad: '6' })).toMatchObject({ papel: 'Carta' });
|
||||
expect(parseHtmlValues(ETIQUETAS_BULTOS, { docEntry: '144153', cantidad: '6' })).toEqual({ docEntry: '144153', cantidad: '6' });
|
||||
});
|
||||
|
||||
it('reparte las etiquetas de a 4 por hoja y las numera 1/6… 6/6', () => {
|
||||
const d = labelsData(ROW, 6, 'Carta') as { paginas: { numero: number; total: number }[][] };
|
||||
const d = labelsData(ROW, 6) as { paginas: { numero: number; total: number }[][] };
|
||||
expect(d.paginas.map((p) => p.length)).toEqual([4, 2]);
|
||||
expect(d.paginas.flat().map((l) => `${l.numero}/${l.total}`)).toEqual(['1/6', '2/6', '3/6', '4/6', '5/6', '6/6']);
|
||||
expect((labelsData(ROW, 8, 'A4') as { paginas: unknown[] }).paginas).toHaveLength(2);
|
||||
expect((labelsData(ROW, 8) as { paginas: unknown[] }).paginas).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('une departamento, provincia y distrito en el lugar, sin partes vacías', () => {
|
||||
expect((labelsData({ ...ROW, Provincia: null }, 1, 'Carta') as { datos: { lugar: string } }).datos.lugar).toBe('LIMA - SURQUILLO');
|
||||
expect((labelsData({ ...ROW, Provincia: null }, 1) as { datos: { lugar: string } }).datos.lugar).toBe('LIMA - SURQUILLO');
|
||||
});
|
||||
|
||||
it('la plantilla dibuja una etiqueta por cada una, con sus datos, y escapa el HTML del cliente', () => {
|
||||
@@ -44,21 +43,24 @@ describe('etiquetas de bultos', () => {
|
||||
expect(out).not.toContain('<PLASTIC>');
|
||||
});
|
||||
|
||||
it('el papel fija el tamaño de la hoja y su alto útil; la fuente va incrustada', () => {
|
||||
expect(html(1, 'Carta')).toMatch(/size: letter landscape/);
|
||||
expect(html(1, 'Carta')).toContain('18.19cm');
|
||||
expect(html(1, 'A4')).toMatch(/size: A4 landscape/);
|
||||
expect(html(1, 'A4')).toContain('17.6cm');
|
||||
it('la hoja es Carta horizontal con los márgenes ajustados a las etiquetas precortadas; la fuente va incrustada', () => {
|
||||
expect(html(1)).toMatch(/size: letter landscape/);
|
||||
expect(html(1)).toContain('margin: 1.6cm 1.5cm 1.8cm 2.7cm;');
|
||||
expect(html(1)).toContain('18.19cm');
|
||||
expect(html(1)).toContain('data:font/ttf;base64,');
|
||||
expect(html(1)).toContain('line-height: 1.2');
|
||||
});
|
||||
|
||||
it('en pantalla el cuerpo lleva los mismos márgenes para ver las etiquetas como saldrán', () => {
|
||||
expect(html(1)).toMatch(/@media screen\s*{\s*body { margin: 1\.6cm 1\.5cm 1\.8cm 2\.7cm !important; }/);
|
||||
});
|
||||
|
||||
it('pide la guía a la consulta delivery-labels y rechaza cantidades que no valen o una entrega que no existe', async () => {
|
||||
const query = vi.fn(async () => [ROW]);
|
||||
const values = { docEntry: '144153', cantidad: '3', papel: 'A4' };
|
||||
const values = { docEntry: '144153', cantidad: '3' };
|
||||
const data = (await ETIQUETAS_BULTOS.data(values, { query })) as { paper_size: string };
|
||||
expect(query).toHaveBeenCalledWith('delivery-labels', { docEntry: 144153 });
|
||||
expect(data.paper_size).toBe('A4');
|
||||
expect(data.paper_size).toBe('letter');
|
||||
for (const cantidad of ['0', '2.5', String(MAX_LABELS + 1)]) await expect(ETIQUETAS_BULTOS.data({ ...values, cantidad }, { query })).rejects.toThrow(BadRequestException);
|
||||
await expect(ETIQUETAS_BULTOS.data(values, { query: async () => [] })).rejects.toThrow(/No se encontró la entrega 144153/);
|
||||
});
|
||||
|
||||
@@ -3,11 +3,12 @@ import type { GatewayParam } from '@basic-erp/sap-types';
|
||||
import type { SapDbRow } from '../sapdb/sapdb.types.js';
|
||||
import type { HtmlReportDef } from './html-reports.types.js';
|
||||
|
||||
/** Papel: tamaño para `@page` y alto útil de la hoja horizontal (papel menos los márgenes superior e inferior de la plantilla). */
|
||||
const PAPER = {
|
||||
Carta: { size: 'letter', height: '18.19cm' },
|
||||
A4: { size: 'A4', height: '17.6cm' },
|
||||
} as const;
|
||||
/**
|
||||
* El papel de las etiquetas es siempre Carta precortado: tamaño para `@page` y alto útil de la hoja horizontal (papel menos
|
||||
* los márgenes superior e inferior de la plantilla). Los márgenes de la plantilla están ajustados a los recortes de la hoja:
|
||||
* no se tocan sin probar con papel.
|
||||
*/
|
||||
const PAPER = { size: 'letter', height: '18.19cm' } as const;
|
||||
|
||||
/** Tope de etiquetas por pedido (evita un documento enorme por un error de tecleo). */
|
||||
export const MAX_LABELS = 500;
|
||||
@@ -17,7 +18,7 @@ const PER_PAGE = 4;
|
||||
* Lo que ve la plantilla: los datos de la guía y las etiquetas numeradas (`1/6`…), de a 4 por hoja. El lugar une
|
||||
* departamento, provincia y distrito.
|
||||
*/
|
||||
export function labelsData(row: SapDbRow, quantity: number, paper: keyof typeof PAPER): Record<string, unknown> {
|
||||
export function labelsData(row: SapDbRow, quantity: number): Record<string, unknown> {
|
||||
const text = (k: string): string => (row[k] === null || row[k] === undefined ? '' : String(row[k]).trim());
|
||||
const labels = Array.from({ length: quantity }, (_, i) => ({ numero: i + 1, total: quantity }));
|
||||
return {
|
||||
@@ -28,14 +29,14 @@ export function labelsData(row: SapDbRow, quantity: number, paper: keyof typeof
|
||||
documento: text('Guia'),
|
||||
},
|
||||
paginas: Array.from({ length: Math.ceil(labels.length / PER_PAGE) }, (_, i) => labels.slice(i * PER_PAGE, (i + 1) * PER_PAGE)),
|
||||
paper_size: PAPER[paper].size,
|
||||
pagina_alto: PAPER[paper].height,
|
||||
paper_size: PAPER.size,
|
||||
pagina_alto: PAPER.height,
|
||||
};
|
||||
}
|
||||
|
||||
const param = (p: Partial<GatewayParam> & Pick<GatewayParam, 'name' | 'label' | 'kind'>): GatewayParam => ({ type: 'xsd:string', required: false, ...p });
|
||||
|
||||
/** Etiquetas de bultos de una guía de remisión (entrega): recibe la entrega y la cantidad de etiquetas a imprimir. */
|
||||
/** Etiquetas de bultos de una guía de remisión (entrega): recibe la entrega y la cantidad de etiquetas a imprimir (papel Carta precortado). */
|
||||
export const ETIQUETAS_BULTOS: HtmlReportDef = {
|
||||
code: 'etiquetas-bultos',
|
||||
title: 'Etiquetas de bultos',
|
||||
@@ -45,13 +46,12 @@ export const ETIQUETAS_BULTOS: HtmlReportDef = {
|
||||
params: [
|
||||
param({ name: 'docEntry', label: 'Entrega', kind: 'lookup', required: true, lookup: 'doc:DeliveryNotes', type: 'xsd:decimal' }),
|
||||
param({ name: 'cantidad', label: 'Cantidad de etiquetas', kind: 'number', required: true, type: 'xsd:decimal' }),
|
||||
param({ name: 'papel', label: 'Papel', kind: 'select', required: true, defaultValue: 'Carta', options: [{ value: 'Carta', label: 'Carta' }, { value: 'A4', label: 'A4' }] }),
|
||||
],
|
||||
async data(values, ctx) {
|
||||
const quantity = Number(values['cantidad']);
|
||||
if (!Number.isInteger(quantity) || quantity < 1 || quantity > MAX_LABELS) throw new BadRequestException(`La cantidad de etiquetas debe ser un entero entre 1 y ${MAX_LABELS}`);
|
||||
const rows = await ctx.query('delivery-labels', { docEntry: Number(values['docEntry']) });
|
||||
if (!rows[0]) throw new BadRequestException(`No se encontró la entrega ${values['docEntry']}`);
|
||||
return labelsData(rows[0], quantity, values['papel'] as keyof typeof PAPER);
|
||||
return labelsData(rows[0], quantity);
|
||||
},
|
||||
};
|
||||
@@ -20,6 +20,12 @@
|
||||
margin: 1.6cm 1.5cm 1.8cm 2.7cm;
|
||||
}
|
||||
|
||||
/* En pantalla no hay hoja: el cuerpo lleva los mismos márgenes del @page para ver las etiquetas como saldrán. */
|
||||
@media screen {
|
||||
body { margin: 1.6cm 1.5cm 1.8cm 2.7cm !important; }
|
||||
.pagina { margin-bottom: 0.6cm; }
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.2;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
||||
Reference in new issue
Block a user