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:
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;
|
||||
|
||||
Reference in new issue
Block a user