2026-10-14 15:12:01 -05:00
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' ;
2026-10-14 15:16:22 -05:00
import type { GridColumn } from '../../shared/grid/grid.types' ;
2026-10-15 17:13:33 -05:00
import type { DocRecord } from './document-model' ;
2026-10-14 15:39:53 -05:00
import { type DocumentLayout , FIELDS , type FieldColumns , type HeaderField , type LookupColumns , marketingLayout } from './document-layouts' ;
2026-10-14 15:12:01 -05:00
/** 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 ;
2026-10-15 17:13:33 -05:00
/** Filtro fijo de la lista y de la navegación (ej. las boletas son las `Invoices` con `DocumentSubType = bod_Bill`). */
baseFilter? : string ;
2026-10-14 15:12:01 -05:00
/** 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' };
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' ] ?? '' ),
};
2026-10-14 16:09:41 -05:00
/** 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 },
];
2026-10-19 14:25:28 -05:00
/** 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 };
2026-10-19 14:12:00 -05:00
/** Facturas: folio tributario (en el Service Layer `FolioPrefixString` y `FolioNumber`), indicador (tipo de documento: 33 factura, 39 boleta), referencia y RUT. */
2026-10-14 15:12:01 -05:00
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' ] ?? '' ),
},
2026-10-19 14:25:28 -05:00
indicatorColumn ,
2026-10-14 16:09:41 -05:00
... referenceColumns ,
2026-10-14 15:12:01 -05:00
];
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' },
],
};
2026-10-19 12:31:31 -05:00
/** 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' },
],
};
2026-10-14 15:12:01 -05:00
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 [];
2026-10-14 15:16:22 -05:00
/** Cabecera completa (reemplaza a la base). */
header? : FieldColumns ;
2026-10-15 17:13:33 -05:00
/** Filtro fijo de la lista y de la navegación entre documentos. */
baseFilter? : string ;
2026-10-19 13:03:32 -05:00
/** Documento de cobro o pago (factura, nota de crédito): el pie muestra anticipo, importe aplicado y saldo pendiente. */
billing? : boolean ;
2026-10-19 14:35:54 -05:00
/** Indicador y folio juntos en una fila (`IDENTITY_GROUP`). */
identity? : boolean ;
2026-10-15 17:13:33 -05:00
/** Valores que se envían siempre al crear (ej. `DocumentSubType`). */
fixed? : DocRecord ;
2026-10-14 15:16:22 -05:00
/** Columnas de línea de este documento (de `LINE_COLUMNS`); sin ellas, todas. */
lineColumns? : readonly GridColumn [];
2026-10-14 15:39:53 -05:00
/** Columnas de las listas de valores de este documento (cliente, artículo, almacén, impuesto). */
lookups? : LookupColumns ;
2026-10-14 15:12:01 -05:00
}
/** Documentos tributarios: la cabecera suma el folio (en el Service Layer `FolioPrefixString` y `FolioNumber`). */
export const TAX_DOCUMENT : MarketingExtras = { headerRight : [ FIELDS . folio ] };
2026-10-19 13:03:32 -05:00
/** Facturas y notas de crédito: documento tributario que además se cobra o se paga. */
2026-10-19 14:35:54 -05:00
export const BILLING_DOCUMENT : MarketingExtras = { billing : true , identity : true };
2026-10-19 13:03:32 -05:00
2026-10-14 15:12:01 -05:00
/** 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 ,
2026-10-14 15:16:22 -05:00
layout : marketingLayout ({
partnerLabel : partner ,
taxResource : 'SalesTaxCodes' ,
left : extras.headerLeft ,
right : extras.headerRight ,
header : extras.header ,
lineColumns : extras.lineColumns ,
2026-10-14 15:39:53 -05:00
lookups : extras.lookups ,
2026-10-15 17:13:33 -05:00
fixed : extras.fixed ,
2026-10-19 13:03:32 -05:00
billing : extras.billing ,
2026-10-19 14:35:54 -05:00
identity : extras.identity ,
2026-10-14 16:05:25 -05:00
uom : path.startsWith ( '/ventas' ) ? 'sales' : 'purchase' ,
2026-10-14 15:16:22 -05:00
}),
2026-10-14 15:12:01 -05:00
resource ,
createLabel ,
orderby : 'DocEntry desc' ,
2026-10-15 17:13:33 -05:00
baseFilter : extras.baseFilter ,
2026-10-14 15:12:01 -05:00
partner : path.startsWith ( '/ventas' )
? { label : partner , type : 'cCustomer' , taxResource : 'SalesTaxCodes' }
: { label : partner , type : 'cSupplier' , taxResource : 'SalesTaxCodes' },
extraFields : [ 'Cancelled' , 'DocTotalFc' , 'DocCurrency' ],
columns : [
docNum ,
statusColumn ,
2026-10-19 12:31:31 -05:00
docTypeColumn ,
2026-10-14 15:12:01 -05:00
...( 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 },
],
2026-10-19 12:31:31 -05:00
filters : [ statusFilter , docTypeFilter , currencyFilter , dateFilter ],
2026-10-14 15:12:01 -05:00
});
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 ],
});