import { MENU } from '@basic-erp/sap-types'; import { formatNumber } from '../../core/format'; import type { ListFilter } from '../../core/sl/list-filters'; import type { ListColumn, ListRow, Tone } from '../../shared/list/list.types'; import type { GridColumn } from '../../shared/grid/grid.types'; import type { DocRecord } from './document-model'; import { type DocumentLayout, FIELDS, type FieldColumns, type HeaderField, type LookupColumns, marketingLayout } from './document-layouts'; /** Pantalla de un documento: lista de los últimos ingresados + formulario (nuevo / ver-editar). */ export interface DocumentScreen { /** Ruta de menú (`/ventas/ordenes`); de ella cuelgan `nuevo` y `:docEntry`. */ path: string; /** Recurso del proxy `/api/sl`. */ resource: string; createLabel: string; columns: readonly ListColumn[]; /** Campos necesarios para pintar que no tienen columna propia. */ extraFields?: readonly string[]; filters: readonly ListFilter[]; /** Los más recientes primero. */ orderby: string; /** Filtro fijo de la lista y de la navegación (ej. las boletas son las `Invoices` con `DocumentSubType = bod_Bill`). */ baseFilter?: string; /** Maestro de impuestos de un documento sin socio (la solicitud de compra); los que tienen socio lo traen en `partner`. */ taxResource?: string; /** Formulario de documentos con socio de negocio, líneas y totales (ventas y compras). */ partner?: { label: string; type: 'cCustomer' | 'cSupplier'; /** Maestro de impuestos (el Service Layer no tiene uno propio de compras: en Chile ambos usan `SalesTaxCodes`). */ taxResource: string; }; /** Campos de la cabecera y del pie del formulario, columnas de línea y cómo se guarda. */ layout: DocumentLayout; } export const menuLabel = (path: string): string => MENU.flatMap((m) => m.items).find((i) => i.path === path)?.label ?? path; export function documentStatus(row: ListRow): { label: string; tone: Tone } | undefined { if (row['Cancelled'] === 'tYES') return { label: 'Cancelado', tone: 'err' }; // el documento que SAP crea al cancelar otro (su reverso): también está cerrado, pero no es un documento más if (row['CancelStatus'] === 'csCancellation') return { label: 'Cancelación', tone: 'warn' }; switch (row['DocumentStatus']) { case 'bost_Open': return { label: 'Abierto', tone: 'ok' }; case 'bost_Close': return { label: 'Cerrado', tone: 'muted' }; default: return undefined; } } /** Total en la moneda del documento: SAP guarda en `DocTotalFc` el importe de los documentos en moneda extranjera. */ export function documentTotal(row: ListRow): string { const fc = Number(row['DocTotalFc'] ?? 0); const amount = fc !== 0 ? fc : row['DocTotal']; if (amount === null || amount === undefined) return ''; return `${row['DocCurrency'] ?? ''} ${formatNumber(amount, 2)}`.trim(); } export const statusColumn: ListColumn = { key: 'DocumentStatus', title: 'Estado', width: '100px', format: (r) => documentStatus(r)?.label ?? '', tone: (r) => documentStatus(r)?.tone, }; /** Número de documento: sin separador de miles, y buscable por igualdad (es numérico). */ export const docNum: ListColumn = { key: 'DocNum', title: 'Número', type: 'number', align: 'left', width: '110px', mono: true, searchable: true, format: (r) => String(r['DocNum'] ?? ''), }; /** Referencia del documento y RUT del socio (ofertas y órdenes). */ export const referenceColumns: readonly ListColumn[] = [ { key: 'NumAtCard', title: 'Nº referencia', width: '130px', searchable: true }, { key: 'FederalTaxID', title: 'RUT', width: '110px', mono: true, searchable: true }, ]; /** Indicador del documento (tipo de documento tributario: 33 factura, 39 boleta). */ export const indicatorColumn: ListColumn = { key: 'Indicator', title: 'Indicador', width: '90px', mono: true, searchable: true }; /** Facturas: folio tributario (en el Service Layer `FolioPrefixString` y `FolioNumber`), indicador (tipo de documento: 33 factura, 39 boleta), referencia y RUT. */ export const invoiceColumns: readonly ListColumn[] = [ { key: 'FolioPrefixString', title: 'Folio pref.', width: '90px', mono: true, searchable: true }, { key: 'FolioNumber', title: 'Folio núm.', type: 'number', align: 'left', width: '110px', mono: true, searchable: true, format: (r) => String(r['FolioNumber'] ?? ''), }, indicatorColumn, ...referenceColumns, ]; export const docDate: ListColumn = { key: 'DocDate', title: 'Fecha contab.', type: 'date', align: 'center', width: '115px' }; export const statusFilter: ListFilter = { type: 'select', key: 'DocumentStatus', label: 'Estado', defaultValue: 'bost_Open', options: [ { value: 'bost_Open', label: 'Abierto' }, { value: 'bost_Close', label: 'Cerrado' }, ], }; /** Tipo del documento: de artículos o de servicio (`DocType`). */ export const docTypeColumn: ListColumn = { key: 'DocType', title: 'Tipo', width: '90px', format: (r) => (r['DocType'] === 'dDocument_Service' ? 'Servicio' : r['DocType'] === 'dDocument_Items' ? 'Artículo' : ''), }; export const docTypeFilter: ListFilter = { type: 'select', key: 'DocType', label: 'Tipo', options: [ { value: 'dDocument_Items', label: 'Artículo' }, { value: 'dDocument_Service', label: 'Servicio' }, ], }; /** Cancelaciones: los documentos cancelados y los documentos de cancelación que SAP crea al cancelarlos (por defecto, todos). */ export const cancelFilter: ListFilter = { type: 'select', key: 'CancelStatus', label: 'Cancelación', options: [ { value: 'csYes', label: 'Cancelados' }, { value: 'csCancellation', label: 'Documentos de cancelación' }, ], }; export const dateFilter: ListFilter = { type: 'date', key: 'DocDate', label: 'Fecha contab.' }; export const currencyFilter: ListFilter = { type: 'multi', key: 'DocCurrency', label: 'Moneda', resource: 'Currencies', valueField: 'Code', labelField: 'Name', }; /** Campos que una pantalla de ventas o compras agrega a la cabecera base y columnas extra de su lista. */ export interface MarketingExtras { columns?: readonly ListColumn[]; headerLeft?: readonly HeaderField[]; headerRight?: readonly HeaderField[]; /** Cabecera completa (reemplaza a la base). */ header?: FieldColumns; /** Filtro fijo de la lista y de la navegación entre documentos. */ baseFilter?: string; /** Documento de cobro o pago (factura, nota de crédito): el pie muestra anticipo, importe aplicado y saldo pendiente. */ billing?: boolean; /** Indicador y folio juntos en una fila (`IDENTITY_GROUP`). */ identity?: boolean; /** Valores que se envían siempre al crear (ej. `DocumentSubType`). */ fixed?: DocRecord; /** Columnas de línea de este documento (de `LINE_COLUMNS`); sin ellas, todas. */ lineColumns?: readonly GridColumn[]; /** Columnas de las listas de valores de este documento (cliente, artículo, almacén, impuesto). */ lookups?: LookupColumns; } /** Documentos tributarios: la cabecera suma el folio (en el Service Layer `FolioPrefixString` y `FolioNumber`). */ export const TAX_DOCUMENT: MarketingExtras = { headerRight: [FIELDS.folio] }; /** Facturas y notas de crédito: documento tributario que además se cobra o se paga. */ export const BILLING_DOCUMENT: MarketingExtras = { billing: true, identity: true }; /** Ventas y compras: documentos con socio de negocio y total. */ export const marketing = ( path: string, resource: string, createLabel: string, partner: string, extras: MarketingExtras = {}, ): DocumentScreen => ({ path, layout: marketingLayout({ partnerLabel: partner, taxResource: 'SalesTaxCodes', left: extras.headerLeft, right: extras.headerRight, header: extras.header, lineColumns: extras.lineColumns, lookups: extras.lookups, fixed: extras.fixed, billing: extras.billing, identity: extras.identity, uom: path.startsWith('/ventas') ? 'sales' : 'purchase', }), resource, createLabel, orderby: 'DocEntry desc', baseFilter: extras.baseFilter, partner: path.startsWith('/ventas') ? { label: partner, type: 'cCustomer', taxResource: 'SalesTaxCodes' } : { label: partner, type: 'cSupplier', taxResource: 'SalesTaxCodes' }, extraFields: ['Cancelled', 'CancelStatus', 'DocTotalFc', 'DocCurrency'], columns: [ docNum, statusColumn, docTypeColumn, ...(extras.columns ?? []), { key: 'CardCode', title: partner, width: '130px', mono: true, searchable: true }, { key: 'CardName', title: 'Nombre' }, docDate, { key: 'DocDueDate', title: 'Fecha venc.', type: 'date', align: 'center', width: '115px' }, { key: 'DocTotal', title: 'Total', align: 'right', width: '140px', format: documentTotal }, ], filters: [statusFilter, cancelFilter, docTypeFilter, currencyFilter, dateFilter], }); export const inventory = ( path: string, resource: string, createLabel: string, withStatus: boolean, layout: DocumentLayout, ): DocumentScreen => ({ path, layout, resource, createLabel, orderby: 'DocEntry desc', extraFields: withStatus ? ['Cancelled'] : [], columns: [ docNum, ...(withStatus ? [statusColumn] : []), docDate, ...(resource === 'StockTransfers' || resource === 'InventoryTransferRequests' ? [ { key: 'FromWarehouse', title: 'Almacén origen', width: '130px', mono: true, searchable: true }, { key: 'ToWarehouse', title: 'Almacén destino', width: '130px', mono: true, searchable: true }, ] : []), { key: 'Comments', title: 'Comentarios', searchable: true }, ], filters: withStatus ? [statusFilter, dateFilter] : [dateFilter], });