feat: solicitudes de compra como documento de marketing (solicitante, válido hasta, fecha necesaria, proveedor y fecha por línea) y API de usuarios con campos limitados

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 14:53:59 -05:00
1 parent a3bd976bbd
commit c1f2cea1e1
19 files changed
+342 -62

No files matched your search

@@ -38,7 +38,7 @@ export type HeaderField =
/** Casilla: en SAP un campo `tYES` / `tNO`. */
| { type: 'checkbox'; key: string; label: string; readonly?: boolean; immutable?: boolean }
/** Campo de código con su ventana de selección (grupo, moneda, lista de precios…). */
| { type: 'lookup'; key: string; label: string; lookup: VhLookup; required?: boolean; missing?: string; immutable?: boolean }
| { type: 'lookup'; key: string; label: string; lookup: VhLookup; required?: boolean; missing?: string; immutable?: boolean; onPick?: (row: Record<string, unknown>) => Record<string, unknown> }
| { type: 'textarea'; key: string; label: string }
/** Lista de valores válidos (típico de un campo de usuario con valores válidos en SAP). */
| { type: 'select'; key: string; label: string; options: readonly { value: string | number; label: string }[]; required?: boolean; missing?: string; immutable?: boolean }
@@ -199,7 +199,62 @@ export const CONTROL_ACCOUNT_LOOKUP: VhLookup = {
baseFilter: `ActiveAccount eq 'tYES' and LockManualTransaction eq 'tYES'`,
};
/** Usuarios de SAP (solicitante de una solicitud de compra): el servidor solo entrega estos campos. */
export const USER_LOOKUP: VhLookup = {
title: 'Usuarios',
resource: 'Users',
orderby: 'UserCode',
valueKey: 'UserCode',
extraFields: ['eMail', 'Department', 'Branch'],
columns: [
{ key: 'UserCode', title: 'Código', width: '140px', mono: true },
{ key: 'UserName', title: 'Nombre' },
],
};
export const DEPARTMENT_LOOKUP: VhLookup = {
title: 'Departamentos',
resource: 'Departments',
orderby: 'Code',
valueType: 'number',
columns: [
{ key: 'Code', title: 'Código', width: '90px', mono: true },
{ key: 'Name', title: 'Nombre' },
],
};
export const BRANCH_LOOKUP: VhLookup = {
title: 'Sucursales',
resource: 'Branches',
orderby: 'Code',
valueType: 'number',
columns: [
{ key: 'Code', title: 'Código', width: '90px', mono: true },
{ key: 'Name', title: 'Nombre' },
],
};
export const FIELDS = {
/** Solicitante de una solicitud de compra: al elegir el usuario se completan su nombre, correo, departamento y sucursal. */
requesterUser: {
type: 'lookup',
key: 'Requester',
label: 'Solicitante (usuario)',
lookup: USER_LOOKUP,
required: true,
missing: 'el solicitante',
onPick: (u: Record<string, unknown>) => ({
RequesterName: u['UserName'] ?? '',
RequesterEmail: u['eMail'] ?? '',
RequesterDepartment: u['Department'] ?? '',
RequesterBranch: u['Branch'] ?? '',
}),
},
requesterName: { type: 'text', key: 'RequesterName', label: 'Nombre de solicitante', readonly: true },
requesterBranch: { type: 'lookup', key: 'RequesterBranch', label: 'Sucursal', lookup: BRANCH_LOOKUP },
requesterDepartment: { type: 'lookup', key: 'RequesterDepartment', label: 'Departamento', lookup: DEPARTMENT_LOOKUP },
requesterNotify: { type: 'checkbox', key: 'SendNotification', label: 'Enviar correo electrónico si se agregó pedido o pedido de compra' },
requesterEmail: { type: 'text', key: 'RequesterEmail', label: 'Dirección de correo electrónico' },
shippingType: { type: 'lookup', key: 'TransportationCode', label: 'Forma de envío', lookup: SHIPPING_TYPE_LOOKUP },
trackingNumber: { type: 'text', key: 'TrackingNumber', label: 'Número de seguimiento' },
blockDunning: { type: 'checkbox', key: 'BlockDunning', label: 'Bloquear reclamaciones' },
@@ -349,6 +404,15 @@ export const blankLine = (): GridLine => ({ ItemCode: '', ItemDescription: '', Q
// --- documentos con socio de negocio (ventas y compras) -------------------------------------
/**
* Cabecera de una solicitud de compra (como en SAP): el solicitante con su sucursal, departamento y correo a la
* izquierda; número, las tres fechas («Válido hasta» en lugar del vencimiento) y la fecha necesaria a la derecha.
*/
const REQUEST_HEADER: FieldColumns = {
left: [FIELDS.docType, FIELDS.requesterUser, FIELDS.requesterName, FIELDS.requesterBranch, FIELDS.requesterDepartment, FIELDS.requesterNotify, FIELDS.requesterEmail, FIELDS.indicator, FIELDS.project],
right: [FIELDS.docNumber, FIELDS.docDate, { ...FIELDS.docDueDate, label: 'Válido hasta' }, FIELDS.taxDate, { type: 'date', key: 'RequriedDate', label: 'Fecha necesaria' }],
};
/**
* Ventas y compras: socio, referencia, fechas; líneas con precio, descuento e impuesto; pie con
* comentarios, descuento y totales. Es el punto de partida; cada pantalla ajusta lo suyo:
@@ -370,6 +434,8 @@ export function marketingLayout(opts: {
uom: 'sales' | 'purchase';
/** Fila del flete en el pie (por defecto sí). */
expenses?: boolean;
/** Solicitud de compra: en lugar del socio de negocio lleva al solicitante (usuario, sucursal, departamento y correo), «Válido hasta» y la fecha necesaria; sin moneda, gastos adicionales ni redondeo. */
requester?: 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. */
@@ -382,7 +448,7 @@ export function marketingLayout(opts: {
);
const layout: DocumentLayout = {
lines: { key: 'DocumentLines', fields: [...lineFieldsOf(columns), 'UoMEntry'], base: true },
header: opts.header ?? {
header: opts.header ?? (opts.requester ? REQUEST_HEADER : {
left: [
FIELDS.docType,
{ type: 'partner', key: 'CardCode', label: opts.partnerLabel, required: true, missing: `el ${opts.partnerLabel.toLowerCase()}` },
@@ -396,8 +462,18 @@ export function marketingLayout(opts: {
],
// 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: {
}),
footer: opts.requester
? {
left: [{ type: 'expenses', label: 'Gastos adicionales' }, FIELDS.comments],
right: [
{ type: 'computed', label: 'Total antes del descuento', source: 'subtotal' },
{ type: 'computed', label: 'Gastos adicionales', source: 'expenses' },
{ type: 'computed', label: 'Impuesto', source: 'tax' },
{ type: 'computed', label: 'Total del documento', source: 'total' },
],
}
: {
left: [...(opts.expenses === false ? [] : [{ type: 'expenses', label: 'Gastos adicionales' } as const]), FIELDS.comments],
right: [
{ type: 'computed', label: 'Total antes del descuento', source: 'subtotal' },
@@ -414,7 +490,7 @@ export function marketingLayout(opts: {
],
},
columns,
newHeader: () => ({ DocType: ITEMS_DOC_TYPE, DocDate: today(), DocDueDate: today(), TaxDate: today(), DiscountPercent: 0, Rounding: 'tNO', RoundingDiffAmount: 0 }),
newHeader: () => (opts.requester ? { DocType: ITEMS_DOC_TYPE, DocDate: today(), DocDueDate: today(), TaxDate: today(), RequriedDate: today(), SendNotification: 'tNO' } : { DocType: ITEMS_DOC_TYPE, DocDate: today(), DocDueDate: today(), TaxDate: today(), DiscountPercent: 0, Rounding: 'tNO', RoundingDiffAmount: 0 }),
newLine: newDocumentLine,
recalc: recalcLine,
service: {
@@ -423,7 +499,7 @@ export function marketingLayout(opts: {
newLine: newServiceLine,
},
// pestañas como en SAP: Logística (destino, facturación, envío) y Finanzas (pago, cuenta asociada, proyecto)
tabs: {
...(opts.requester ? {} : { tabs: {
logistics: {
left: [FIELDS.shipTo, { ...FIELDS.payTo, label: opts.uom === 'sales' ? 'Destinatario de factura' : 'Dirección de facturación' }, FIELDS.shippingType],
right: [FIELDS.trackingNumber, FIELDS.blockDunning],
@@ -443,9 +519,19 @@ export function marketingLayout(opts: {
],
right: [FIELDS.project],
},
},
} }),
seriesSubType: SERIES_SUBTYPE[String(opts.fixed?.['DocumentSubType'] ?? 'bod_None')] ?? '--',
...(opts.fixed ? { beforeCreate: (h: DocRecord) => ({ ...h, ...opts.fixed }) } : {}),
...(opts.requester
? {
// el solicitante es un usuario de SAP (`ReqType` 12); su nombre lo completa SAP
beforeCreate: (h: DocRecord) => {
const { RequesterName: _name, ...rest } = h;
return rest['Requester'] ? { ...rest, ReqType: 12 } : rest;
},
}
: opts.fixed
? { beforeCreate: (h: DocRecord) => ({ ...h, ...opts.fixed }) }
: {}),
requiredLine: [],
};
return opts.lookups ? withLookupColumns(layout, opts.lookups, 'replace') : layout;