feat(web): Indicador, prefijo y número del folio juntos en una fila (documentos de facturación y asiento) y las tres fechas de la cabecera en orden

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 14:35:54 -05:00
1 parent 1bb8efa77e
commit a74c19a139
11 files changed
+170 -19

No files matched your search

@@ -6,6 +6,14 @@ import { INDICATOR_LOOKUP, PROJECT_LOOKUP } from './dimension-lookups';
import { SERIES_SUBTYPE } from './document-objects';
import { ANALYTIC_COLUMNS, DOCUMENT_LINE_COLUMNS, lineColumnsFor, lineFieldsOf, newDocumentLine, newServiceLine, recalcLine, serviceLineColumns, uomEntryOf } from './line-columns';
/** Una celda de un grupo de campos: un texto (con ancho y largo máximo) o un código que se elige de una lista. */
export type GroupCell =
| { kind: 'text'; key: string; label: string; width?: string; maxlength?: number }
| { kind: 'lookup'; key: string; label: string; lookup: VhLookup; width?: string };
/** Fila con varios campos juntos, para la cabecera de un documento. */
export const fieldGroup = (label: string, cells: readonly GroupCell[]): HeaderField => ({ type: 'group', label, cells });
/** Valores calculados o del socio que una fila de solo lectura puede mostrar. */
export type ComputedSource = 'docNum' | 'partnerName' | 'partnerTaxId' | 'subtotal' | 'expenses' | 'tax' | 'total' | 'downPayment' | 'applied' | 'balance';
@@ -39,6 +47,11 @@ export type HeaderField =
| { type: 'partner'; key: string; label: string; required?: boolean; missing?: string }
/** Prefijo y número del folio tributario en una misma fila (`FolioPrefixString` y `FolioNumber`). */
| { type: 'folio'; label: string; prefixKey: string; numberKey: string }
/**
* Varios campos juntos en una sola fila, en el orden dado (el indicador, el prefijo y el número del folio). Una empresa
* que los sustituye por campos de usuario arma su propio grupo con `fieldGroup(...)` y oculta el del base.
*/
| { type: 'group'; label: string; cells: readonly GroupCell[] }
/** Serie de numeración y número del documento en una misma fila (`Series` y `DocNum`). */
| { type: 'series'; key: string; label: string }
/** Flete: código del gasto adicional y su importe (`DocumentAdditionalExpenses`). */
@@ -139,6 +152,16 @@ export const FIELDS = {
requester: { type: 'text', key: 'Requester', label: 'Solicitante (usuario)' },
} as const satisfies Record<string, HeaderField>;
/**
* Indicador y folio tributario juntos y en ese orden (en el Service Layer `Indicator`, `FolioPrefixString` y
* `FolioNumber`). El número se edita como texto y se envía como número.
*/
export const IDENTITY_GROUP: HeaderField = fieldGroup('Indicador / Folio', [
{ kind: 'lookup', key: 'Indicator', label: 'Indicador', lookup: INDICATOR_LOOKUP, width: '80px' },
{ kind: 'text', key: 'FolioPrefixString', label: 'Prefijo del folio', width: '90px', maxlength: 10 },
{ kind: 'text', key: 'FolioNumber', label: 'Número de folio' },
]);
export const commentsOnly: FieldColumns = { left: [FIELDS.comments], right: [] };
// --- columnas de línea compartidas ----------------------------------------------------------
@@ -237,6 +260,8 @@ export function marketingLayout(opts: {
uom: 'sales' | 'purchase';
/** Fila del flete en el pie (por defecto sí). */
expenses?: boolean;
/** Indicador y folio juntos en una fila de la derecha (facturas y notas de crédito) en lugar del indicador suelto a la izquierda y el folio. */
identity?: boolean;
/** Facturas y notas de crédito: el pie suma el anticipo total, el importe aplicado y el saldo pendiente. */
billing?: boolean;
/** Valores que se envían siempre al crear (ej. `DocumentSubType: 'bod_Bill'` en las boletas). */
@@ -254,11 +279,12 @@ export function marketingLayout(opts: {
{ type: 'computed', label: 'Nombre', source: 'partnerName' },
{ type: 'computed', label: 'RUT', source: 'partnerTaxId' },
FIELDS.numAtCard,
FIELDS.indicator,
...(opts.identity ? [] : [FIELDS.indicator]),
FIELDS.project,
...(opts.left ?? []),
],
right: [FIELDS.docNumber, FIELDS.docDate, FIELDS.docDueDate, ...(opts.right ?? [])],
// las tres fechas juntas, una debajo de la otra: contabilización, vencimiento y del documento
right: [FIELDS.docNumber, FIELDS.docDate, FIELDS.docDueDate, FIELDS.taxDate, ...(opts.identity ? [IDENTITY_GROUP] : []), ...(opts.right ?? [])],
},
footer: {
left: [...(opts.expenses === false ? [] : [{ type: 'expenses', label: 'Gastos adicionales' } as const]), FIELDS.comments],
@@ -277,7 +303,7 @@ export function marketingLayout(opts: {
],
},
columns,
newHeader: () => ({ DocType: ITEMS_DOC_TYPE, DocDate: today(), DocDueDate: today(), DiscountPercent: 0, Rounding: 'tNO', RoundingDiffAmount: 0 }),
newHeader: () => ({ DocType: ITEMS_DOC_TYPE, DocDate: today(), DocDueDate: today(), TaxDate: today(), DiscountPercent: 0, Rounding: 'tNO', RoundingDiffAmount: 0 }),
newLine: newDocumentLine,
recalc: recalcLine,
service: {
@@ -309,6 +335,8 @@ export const keysOfFields = (fields: readonly HeaderField[]): string[] =>
return [];
case 'folio':
return [f.prefixKey, f.numberKey];
case 'group':
return f.cells.map((c) => c.key);
case 'expenses':
return [];
case 'rounding':
@@ -337,4 +365,4 @@ export const hasSeries = (layout: DocumentLayout): boolean => allFields(layout).
export const hasExpenses = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'expenses');
export const hasFolio = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'folio');
export const hasFolio = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'folio' || (f.type === 'group' && f.cells.some((c) => c.key === 'FolioNumber')));