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';
|
|
|
|
|
import { type DocumentLayout, FIELDS, type FieldColumns, type HeaderField, 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;
|
|
|
|
|
/** 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'] ?? ''),
|
|
|
|
|
};
|
|
|
|
|
/** Facturas: folio tributario (en el Service Layer `FolioPrefixString` y `FolioNumber`), 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'] ?? ''),
|
|
|
|
|
},
|
|
|
|
|
{ key: 'NumAtCard', title: 'Nº referencia', width: '130px', searchable: true },
|
|
|
|
|
{ key: 'FederalTaxID', title: 'RUT', width: '110px', mono: true, searchable: true },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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' },
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
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;
|
|
|
|
|
/** Columnas de línea de este documento (de `LINE_COLUMNS`); sin ellas, todas. */
|
|
|
|
|
lineColumns?: readonly GridColumn[];
|
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] };
|
|
|
|
|
|
|
|
|
|
/** 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:12:01 -05:00
|
|
|
resource,
|
|
|
|
|
createLabel,
|
|
|
|
|
orderby: 'DocEntry desc',
|
|
|
|
|
partner: path.startsWith('/ventas')
|
|
|
|
|
? { label: partner, type: 'cCustomer', taxResource: 'SalesTaxCodes' }
|
|
|
|
|
: { label: partner, type: 'cSupplier', taxResource: 'SalesTaxCodes' },
|
|
|
|
|
extraFields: ['Cancelled', 'DocTotalFc', 'DocCurrency'],
|
|
|
|
|
columns: [
|
|
|
|
|
docNum,
|
|
|
|
|
statusColumn,
|
|
|
|
|
...(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, 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],
|
|
|
|
|
});
|