Índice de contenido
Contenido
1. Introducción
2. Pre-requisitos del sistema
2.1. Identificación de la aplicación en SAP Fiori Library
2.2. Verificación del nodo ICF en la transacción SICF
2.3. Verificación del servicio OData en /IWFND/MAINT_SERVICE
3. Desarrollo
3.1. Custom Fields: campos adicionales sin código ABAP
3.1.1. Concepto y propósito
3.1.2. Creación de los campos desde la aplicación Fiori
3.1.3. Publicación y activación en servicios OData
3.1.4. Verificación en el metadata del servicio
3.2. Custom Business Object: datos maestros configurables
3.2.1. Por qué un CBO y no otras alternativas
3.2.2. Creación y configuración del objeto
3.2.3. Carga de datos mediante SAP Gateway Client
3.3. Extension Project: la extensión de la interfaz de usuario
3.3.1. Qué es un Extension Project y cómo se diferencia de otras técnicas
3.3.2. Creación del proyecto desde SAP Business Application Studio
3.3.3. Estructura de archivos generada
3.3.4. Migración a UI5 CLI v4
3.3.5. Resolución del error de batch (504)
3.4. Incorporación del servicio OData del CBO
3.4.1. Registro del DataSource en el manifest
3.4.2. Configuración del modelo OData
3.5. Construcción del Fragment XML
3.5.1. Diseño del fragment con ComboBox y campo de días
3.5.2. Binding directo al modelo del CBO
3.6. Controller Extension: lógica de inyección y eventos
3.6.1. Registro del controller en el manifest
3.6.2. Patrón de controller extension en UI5 1.78
3.6.3. Inyección dinámica del fragment como ObjectPageSection
3.6.4. Lógica del evento onPaymentMethodChange
3.7. Internacionalización con i18n
3.7.1. Por qué usar i18nExt en lugar de i18n
3.7.2. Configuración del ResourceModel
4. Conclusión
1. Introducción
En el ecosistema SAP, la aplicación Create Supplier Invoice (App ID: F0859) es una de las transacciones más utilizadas por los contadores de cuentas por pagar. Esta aplicación Fiori, cuyo nombre técnico es MM_SUPPIV_MANS1 y cuyo componente UI5 corresponde a ui.s2p.mm.supplinvoice.manage.s1, permite registrar facturas de proveedores en el módulo de gestión de materiales (MM). Su interfaz está construida sobre un sap.uxap.ObjectPageLayout que organiza la información en pestañas: General Information, Payment, Tax, entre otras. Cada pestaña agrupa los campos relevantes para un aspecto del documento, y el servicio OData MM_SUPPLIER_INVOICE_MANAGE se encarga de la comunicación entre la interfaz y el backend. La aplicación forma parte de la línea de negocio de Purchasing, Sourcing and Procurement, está diseñada para dispositivos de escritorio y tablet, y requiere SAP S/4HANA como producto de backend con base de datos HANA exclusiva.
Sin embargo, el flujo estándar de la aplicación no contempla un escenario que muchas empresas necesitan: registrar el método de pago real acordado con el proveedor (transferencia bancaria, cheque, letra de cambio, pagaré o efectivo) y calcular automáticamente los días de plazo asociados a cada método. El campo estándar de condiciones de pago no cubre esta necesidad porque responde a la lógica contable del sistema, no al acuerdo comercial específico entre la empresa y el proveedor. Esta diferencia entre lo que el sistema ofrece y lo que el negocio requiere es precisamente el tipo de brecha que la estrategia Clean Core está diseñada para cerrar.
Clean Core no es una herramienta ni un producto; es una filosofía de desarrollo que SAP promueve como estándar para S/4HANA. Su premisa es directa: el núcleo del sistema permanece intacto, y todas las extensiones se construyen en capas externas que el sistema reconoce y gestiona sin interferir con el código original. Esta filosofía se apoya en dos pilares de extensibilidad. El primero, denominado Key User Extensibility, permite que usuarios de negocio con perfil técnico creen campos personalizados, objetos de negocio y lógica sencilla desde aplicaciones Fiori del Launchpad, sin necesidad de acceder al entorno de desarrollo ABAP. El segundo pilar, Developer Extensibility, permite que los desarrolladores construyan extensiones más sofisticadas utilizando herramientas como SAP Business Application Studio o Visual Studio Code; los Extension Projects son el mecanismo principal de este pilar para la capa de interfaz de usuario.
Para entender por qué esta separación es tan importante, conviene pensar en lo que ocurre cuando un equipo de desarrollo interviene directamente el código fuente de una aplicación estándar. Es comparable a modificar el motor de un vehículo que aún recibe actualizaciones del fabricante: cada vez que el fabricante libera una revisión del motor, el taller debe verificar que las piezas propias no entren en conflicto con las nuevas. En el mundo SAP, ese “taller” es el equipo de desarrollo, y las “revisiones del fabricante” son los Support Packages y Feature Packs que SAP libera periódicamente. La estrategia Clean Core elimina ese conflicto de raíz, porque las extensiones nunca tocan el motor original.
Este artículo presenta el desarrollo completo de una extensión para la aplicación Create Supplier Invoice en SAP S/4HANA 2020 On-Premise. El recorrido abarca tres capas: la creación de Custom Fields en la cabecera de la factura, la construcción de un Custom Business Object que almacena los métodos de pago con sus plazos por defecto, y el desarrollo de un Extension Project que inyecta un ComboBox conectado al objeto de negocio y actualiza los campos personalizados en tiempo real. El resultado final es una extensión funcional, transportable y upgrade-safe que no requiere una sola línea de código ABAP.
2. Pre-requisitos del sistema
Antes de iniciar el desarrollo de cualquier Extension Project, es indispensable verificar que la aplicación estándar y su servicio OData están correctamente configurados en el sistema. Esta verificación evita errores en etapas posteriores que pueden ser difíciles de diagnosticar. Toda la información técnica necesaria para estas verificaciones se obtiene de la SAP Fiori Apps Library, el catálogo oficial donde SAP documenta cada aplicación Fiori con sus requisitos de instalación y configuración.
2.1. Identificación de la aplicación en SAP Fiori Library
La SAP Fiori Apps Library es el punto de partida obligatorio antes de trabajar con cualquier aplicación Fiori estándar. Para la aplicación Create Supplier Invoice, la ficha técnica proporciona los siguientes datos de configuración:
| Dato técnico | Valor |
| App ID | F0859 |
| Application Type | Transactional (SAP Fiori / SAPUI5) |
| SAPUI5 Application | MM_SUPPIV_MANS1 |
| UI5 Component | ui.s2p.mm.supplinvoice.manage.s1 |
| OData Service | MM_SUPPLIER_INVOICE_MANAGE (versión 0001) |
| Software Component | S4CORE 105 |
| Database | HANA DB exclusive |
| Device Types | Desktop, Tablet |
| Required Back-End Product | SAP S/4HANA |
Además, la ficha revela información de configuración del SAP Fiori Launchpad que resulta útil para comprender cómo se accede a la aplicación:
| Configuración Launchpad | Valor |
| Semantic Object | SupplierInvoice |
| Semantic Action | create (Mode = 01) |
| Technical Catalog | SAP_TC_PRC_COMMON |
| Business Catalog | SAP_PRC_BC_INVOICER |
| Business Role | SAP_BR_AP_ACCOUNTANT_PROCUREMT |
Estos datos son fundamentales porque permiten identificar con precisión qué nodo ICF debe estar activo, qué servicio OData debe estar registrado y qué autorizaciones PFCG necesita el usuario.
2.2. Verificación del nodo ICF en la transacción SICF
La transacción SICF (SAP Internet Communication Framework) gestiona los nodos de servicio HTTP del sistema. Cada aplicación SAPUI5 se despliega como un repositorio BSP que se sirve a través de un nodo ICF. Si el nodo no está activo, la aplicación no se carga en el navegador.
Para verificar el nodo de la aplicación Create Supplier Invoice, se ejecuta la transacción SICF en el SAP GUI y se navega por la siguiente ruta del árbol:
/default_host/sap/bc/ui5_ui5/sap/mm_suppiv_mans1
El nodo mm_suppiv_mans1 debe mostrar el estado activo (icono verde). Si el nodo aparece inactivo (icono gris), se activa haciendo clic derecho sobre el nodo y seleccionando la opción Activate Service. Es importante también verificar los nodos de los reuse components que la aplicación utiliza, ya que si alguno de estos nodos está inactivo, la aplicación puede cargar parcialmente o mostrar errores en secciones específicas. Según la información de la SAP Fiori Library, la aplicación tiene una dependencia con el componente GLO_TXI_REUSE (sap.gs.fin.lib.reusetaxinvc) que se agrega automáticamente por dependencias.
2.3. Verificación del servicio OData en /IWFND/MAINT_SERVICE
La transacción /IWFND/MAINT_SERVICE es el panel de administración de los servicios OData del SAP Gateway. Desde esta transacción se verifica que el servicio MM_SUPPLIER_INVOICE_MANAGE está registrado y activo en el front-end server.
Al abrir la transacción, se busca el servicio por su nombre técnico en el campo de filtro. El servicio debe aparecer en la lista con el nombre técnico MM_SUPPLIER_INVOICE_MANAGE, la versión 0001 y el estado activo. Si el servicio no aparece en la lista, es necesario registrarlo desde el sistema backend utilizando el botón Add Service y seleccionando el System Alias correspondiente.
Una vez localizado el servicio, se recomienda ejecutar dos verificaciones adicionales. La primera es acceder al metadata del servicio desde el navegador para confirmar que responde correctamente:
http://<host>:<port>/sap/opu/odata/sap/MM_SUPPLIER_INVOICE_MANAGE/$metadata
La segunda verificación consiste en ejecutar el SAP Gateway Client (botón disponible dentro de la propia transacción /IWFND/MAINT_SERVICE) y hacer un GET al metadata para confirmar que no existen errores de autorización ni de configuración.
Estas verificaciones previas constituyen la base sobre la que se construye el Extension Project. Si la aplicación estándar no carga correctamente o el servicio OData no responde, cualquier extensión desarrollada sobre ella heredará esos problemas.
3. Desarrollo
3.1. Custom Fields: campos adicionales sin código ABAP
3.1.1. Concepto y propósito
Los Custom Fields son campos adicionales que se agregan a objetos de negocio estándar de SAP sin modificar las tablas originales ni el código fuente del sistema. Desde una perspectiva técnica, cuando se crea un Custom Field, SAP genera automáticamente una columna en la tabla de extensión correspondiente (no en la tabla estándar directa), actualiza el metadata del servicio OData para exponer el nuevo campo y lo hace disponible para su uso en aplicaciones Fiori, reports y APIs.
De forma similar a como un formulario en papel puede tener un espacio reservado para campos adicionales al final de la página, los objetos de negocio de S/4HANA tienen una estructura preparada para recibir campos personalizados sin que esto altere la estructura principal del documento.
Para este escenario, se necesitan dos campos en la cabecera de la factura de proveedor: uno para almacenar el código del método de pago seleccionado y otro para registrar los días de plazo asociados.
3.1.2. Creación de los campos desde la aplicación Fiori
La creación se realiza desde la aplicación Custom Fields and Logic (App ID: F1481) del Fiori Launchpad. Esta aplicación es parte del pilar de Key User Extensibility y no requiere conocimientos de ABAP ni acceso al entorno de desarrollo.
El primer campo se configura con las siguientes propiedades:
| Propiedad | Valor |
| Label | Método de Pago Ext. |
| Identifier | PaymentMethodExt |
| Field Type | Text |
| Length | 3 |
| Business Context | Supplier Invoice (nivel cabecera) |
El segundo campo almacena los días de plazo:
| Propiedad | Valor |
| Label | Días Plazo Ext. |
| Identifier | PaymentDaysExt |
| Field Type | Number (Integer) |
| Length | 3 |
| Business Context | Supplier Invoice (nivel cabecera) |
Es importante destacar que el Business Context determina en qué nivel del documento se agrega el campo. En este caso, ambos campos se agregan al nivel de cabecera de la factura de proveedor, lo que significa que cada factura tendrá un único valor para cada campo, no uno por línea.
3.1.3. Publicación y activación en servicios OData
Una vez creados los campos, se seleccionan ambos y se ejecuta la acción Publish. El sistema realiza ajustes de base de datos en segundo plano para hacer efectivos los nuevos campos.
El paso siguiente es crítico y frecuentemente omitido: activar los campos en la pestaña UIs and Reports de cada campo. Para este escenario, se activan dos data sources:
- MM_SUPPLIER_INVOICE_MANAGE: la aplicación Fiori donde se utilizarán los campos.
- I_SUPPLIERINVOICEAPI01: la API que expone los campos en el modelo OData que la aplicación consume.
Sin esta activación, los campos existirían en la base de datos pero no serían visibles ni accesibles desde la aplicación ni desde el servicio OData.
Para confirmar que los campos se generaron correctamente, se consulta el metadata del servicio OData de la aplicación:
http://<host>:<port>/sap/opu/odata/sap/MM_SUPPLIER_INVOICE_MANAGE/$metadata
En el EntityType Header deben aparecer las dos propiedades nuevas:
<Property Name=”ZZ1_PaymentMethodExt_MIH” Type=”Edm.String” MaxLength=”3″
sap:is-extension-field=”true”/>
<Property Name=”ZZ1_PaymentDaysExt_MIH” Type=”Edm.Decimal” Precision=”3″ Scale=”0″
sap:is-extension-field=”true”/>
Cabe observar que el sistema agrega automáticamente el prefijo ZZ1_ y el sufijo _MIH (Manage Invoice Header). En entornos de S/4HANA Cloud, el prefijo cambia a YY1_. Este detalle es relevante al momento de construir los bindings en el fragment XML del Extension Project.
3.2. Custom Business Object: datos maestros configurables
Antes de crear el objeto de negocio, conviene evaluar las alternativas disponibles para almacenar la lista de métodos de pago y sus días por defecto.
Un CodeList (lista de códigos) solo admite dos columnas: código y descripción. Para este escenario se necesita una tercera columna (DefaultDays) que almacene los días de plazo asociados a cada método, lo que descarta esta opción.
Un servicio RAP Z (ABAP RESTful Application Programming Model) permitiría construir una tabla con su servicio OData, pero requeriría escribir código ABAP: definir el CDS view, la behavior definition, la service definition y la service binding. Para un requerimiento de esta complejidad, resulta desproporcionado.
El Custom Business Object (CBO) ofrece el equilibrio adecuado: genera automáticamente la tabla de base de datos, el servicio OData y una interfaz de mantenimiento, todo sin escribir una sola línea de código. El usuario de negocio puede incluso mantener los datos (agregar nuevos métodos de pago, modificar plazos) desde la aplicación Fiori correspondiente.
3.2.2. Creación y configuración del objeto
La creación se realiza desde la aplicación Custom Business Objects del Fiori Launchpad. Se configura el nombre como “Payment Method Extension” con el identificador PAYMETHODEXT.
En la sección Features de la pestaña General Information, se activa la opción Service Generation, que es la que instruye al sistema para generar el servicio OData automáticamente.
Los campos del objeto se definen de la siguiente manera:
| Field Label | Identifier | Type | Key | Length |
| Payment Method ID | PaymentMethodID | Text | Sí | 3 |
| Description | Description | Text | No | 40 |
| Default Days | DefaultDays | Number | No | 3 |
El campo PaymentMethodID se marca como clave primaria, lo que garantiza la unicidad de cada método de pago. Tras hacer clic en Publish, el sistema genera la tabla, el servicio OData y la interfaz de mantenimiento.
El servicio generado queda disponible en la ruta:
http://<host>:<port>/sap/opu/odata/sap/ZZ1_PAYMETHODEXT_CDS/$metadata
3.2.3. Carga de datos mediante SAP Gateway Client
Las tablas de Custom Business Objects no permiten operaciones SQL directas ni acceso mediante la transacción SE16N. La forma más directa de insertar datos iniciales es a través del SAP Gateway Client (transacción /IWFND/GW_CLIENT), que permite ejecutar requests OData directamente desde el SAP GUI.
Se configura una request POST hacia la URI del servicio:
/sap/opu/odata/sap/ZZ1_PAYMETHODEXT_CDS/ZZ1_PAYMETHODEXT
Con el header Content-Type: application/json, se insertan los registros uno a uno. Un detalle importante es que el campo DefaultDays, aunque es numérico en la tabla, se representa como Edm.Decimal en OData V2, por lo que su valor se envía como string (entre comillas) en el JSON:
{“PaymentMethodID”:”TRF”,”Description”:”Transferencia Bancaria”,”DefaultDays”:”30″}
{“PaymentMethodID”:”CHQ”,”Description”:”Cheque”,”DefaultDays”:”15″}
{“PaymentMethodID”:”LCR”,”Description”:”Letra de Cambio”,”DefaultDays”:”60″}
{“PaymentMethodID”:”PAG”,”Description”:”Pagaré”,”DefaultDays”:”90″}
{“PaymentMethodID”:”EFC”,”Description”:”Efectivo”,”DefaultDays”:”0″}
Cada request debe devolver el código HTTP 201 Created. Para verificar que los cinco registros se insertaron correctamente, se ejecuta un GET a la colección completa con el parámetro $format=json.
3.3. Extension Project: la extensión de la interfaz de usuario
3.3.1. Qué es un Extension Project y cómo se diferencia de otras técnicas
Un Extension Project es un mecanismo de Developer Extensibility que permite extender una aplicación Fiori estándar sin modificar su código fuente. El concepto es comparable al patrón de herencia en la programación orientada a objetos: se crea una “clase hija” que hereda todo el comportamiento de la “clase padre” y agrega o sobreescribe solo lo necesario.
En términos concretos, el Extension Project hereda el componente original de la aplicación estándar y permite agregar controllers adicionales, views, fragments, modelos y archivos de internacionalización (i18n). La aplicación estándar permanece intacta en el repositorio BSP del sistema, y el Extension Project se despliega como una variante independiente que el Launchpad puede cargar en lugar del original.
Esta técnica se diferencia del Adaptation Project (SAPUI5 Flexibility) en que el Extension Project trabaja a nivel de código JavaScript y XML, mientras que el Adaptation Project trabaja a nivel de cambios declarativos (mover, ocultar, renombrar controles) sin escribir código. Para escenarios que requieren lógica de negocio en el frontend, como el auto-llenado de campos basado en la selección de un ComboBox, el Extension Project es la opción adecuada.
3.3.2. Creación del proyecto desde SAP Business Application Studio
El proyecto se crea desde SAP Business Application Studio (BAS) utilizando el generador de plantillas:
File > New Project from Template > SAP Fiori Application > SAPUI5 Application Extension
Se selecciona la aplicación Manage Supplier Invoices (MM_SUPPIV_MANS1) y la conexión al sistema On-Premise. El generador crea automáticamente la estructura base del proyecto.
3.3.3. Estructura de archivos generada
El generador produce tres archivos fundamentales que configuran la relación entre el Extension Project y la aplicación estándar.
El archivo Component.js es el punto de entrada del componente. Su función es cargar el componente estándar desde el servidor y extenderlo con el namespace del Extension Project:
jQuery.sap.declare(“customer.ui.s2p.mm.supplinvoice.manage.s1.test.Component”);
sap.ui.component.load({
name: “ui.s2p.mm.supplinvoice.manage.s1”,
url: “/sap/bc/ui5_ui5/sap/MM_SUPPIV_MANS1”
});
ui.s2p.mm.supplinvoice.manage.s1.Component.extend(
“customer.ui.s2p.mm.supplinvoice.manage.s1.test.Component”, {
metadata: {
manifest: “json”
}
});
El archivo manifest.json contiene la configuración del descriptor de la aplicación. Inicialmente, la sección extends.extensions está vacía, ya que aún no se han definido extensiones concretas. La propiedad extends.component indica que este proyecto extiende el componente ui.s2p.mm.supplinvoice.manage.s1.
El archivo ui5.yaml configura el servidor de desarrollo local con los proxies necesarios para conectar con el backend SAP y con el CDN de UI5 para las librerías del framework.
3.3.4. Migración a UI5 CLI v4
Un problema frecuente en proyectos generados con versiones anteriores del generador es la incompatibilidad entre la versión del @ui5/cli declarada en package.json y el specVersion del ui5.yaml. El generador puede crear el proyecto con @ui5/cli versión ^3.0.0 y specVersion: “2.6”, pero al ejecutar npm install, npm puede resolver la dependencia a la versión 4 del CLI, que no es compatible con specVersion: “2.6”.
El síntoma de este problema es un error al ejecutar npm start:
The UI5 CLI version of the project is outdated. Please upgrade your project to UI5 CLI v4.
La solución consiste en actualizar ambos archivos. En package.json, se cambia la dependencia a “@ui5/cli”: “^4.0.0”. En ui5.yaml, se cambia la primera línea a specVersion: “4.0”. Luego se eliminan node_modules y package-lock.json y se ejecuta npm install nuevamente.
3.3.5. Resolución del error de batch (504)
Otro problema común al iniciar un Extension Project es el error 504 Gateway Timeout en la request $batch. Este error ocurre porque el batch agrupa múltiples llamadas OData en una sola request HTTP, y el backend no logra procesar todas las llamadas dentro del tiempo límite configurado.
La solución es desactivar el batch en el modelo principal de la aplicación. En el manifest.json, dentro de sap.ui5.models, se agrega la configuración useBatch: false al modelo default (modelo sin nombre):
“models”: {
“”: {
“settings”: {
“useBatch”: false
}
}
}
Con esta configuración, las requests OData se ejecutan individualmente en lugar de agrupadas, lo que elimina el timeout.
3.4. Incorporación del servicio OData del CBO
3.4.1. Registro del DataSource en el manifest
Para que el Extension Project pueda consumir los datos del Custom Business Object, es necesario registrar su servicio OData como un DataSource en el manifest.json. Este registro se realiza dentro de la sección sap.app:
“dataSources”: {
“ZPaymentMethodService”: {
“uri”: “/sap/opu/odata/sap/ZZ1_PAYMETHODEXT_CDS/”,
“type”: “OData”,
“settings”: {
“odataVersion”: “2.0”
}
}
}
El DataSource actúa como la declaración de la fuente de datos. Sin embargo, por sí solo no crea ningún modelo OData; simplemente le indica al framework dónde encontrar el servicio.
3.4.2. Configuración del modelo OData
El modelo OData se registra en la sección sap.ui5.models con el nombre zPayMethod:
“zPayMethod”: {
“dataSource”: “ZPaymentMethodService”,
“type”: “sap.ui.model.odata.v2.ODataModel”,
“settings”: {
“defaultOperationMode”: “Server”,
“defaultBindingMode”: “OneWay”,
“useBatch”: false
}
}
Al registrar el modelo en el manifest, el framework lo instancia automáticamente al cargar el componente. Esto es un punto clave: no se necesita escribir código en el controller para inicializar el modelo ni para cargar los datos. Cualquier control que haga binding al modelo zPayMethod recibirá los datos directamente del servicio OData.
Este comportamiento es comparable a cómo funciona la inyección de dependencias en un framework como Spring: el desarrollador declara que necesita un recurso (el modelo OData) y el framework se encarga de crearlo, configurarlo y ponerlo a disposición de los componentes que lo soliciten.
3.5. Construcción del Fragment XML
3.5.1. Diseño del fragment con ComboBox y campo de días
Un fragment en SAPUI5 es un archivo XML reutilizable que contiene controles de interfaz de usuario. A diferencia de un view, un fragment no tiene controller propio; utiliza el controller del view donde se inserta. Esta característica lo convierte en la pieza ideal para construir secciones de UI que se inyectarán dinámicamente en la aplicación estándar.
El fragment se crea en la ruta webapp/fragment/PaymentMethodFragment.fragment.xml y contiene dos controles principales: un ComboBox para la selección del método de pago y un Input de solo lectura para mostrar los días de plazo.
<core:FragmentDefinition
xmlns=”sap.m”
xmlns:core=”sap.ui.core”
xmlns:l=”sap.ui.layout”>
<VBox id=”paymentMethodExtVBox”
class=”sapUiSmallMarginTop sapUiSmallMarginStart”
width=”100%”>
<l:Grid defaultSpan=”XL4 L4 M6 S12″>
<VBox>
<Label text=”{i18nExt>paymentMethodExtLabel}” />
<ComboBox
placeholder=”{i18nExt>paymentMethodExtPlaceholder}”
selectedKey=”{ZZ1_PaymentMethodExt_MIH}”
items=”{zPayMethod>/ZZ1_PAYMETHODEXT}”
selectionChange=”.onPaymentMethodChange”
width=”100%”>
<core:Item
key=”{zPayMethod>PaymentMethodID}”
text=”{zPayMethod>PaymentMethodID} – {zPayMethod>Description}” />
</ComboBox>
</VBox>
<VBox>
<Label text=”{i18nExt>paymentDaysExtLabel}” />
<Input
value=”{ZZ1_PaymentDaysExt_MIH}”
editable=”false”
width=”100px”
type=”Number” />
</VBox>
</l:Grid>
</VBox>
</core:FragmentDefinition>
El sap.ui.layout.Grid con defaultSpan=”XL4 L4 M6 S12″ garantiza que los controles se distribuyan de forma responsiva: cuatro columnas en pantallas grandes, seis en medianas y doce (ancho completo) en dispositivos móviles.
3.5.2. Binding directo al modelo del CBO
El fragment utiliza tres bindings que conviene analizar en detalle:
El binding items=”{zPayMethod>/ZZ1_PAYMETHODEXT}” conecta la agregación items del ComboBox directamente con la colección del servicio OData del CBO. Como el modelo zPayMethod fue registrado en el manifest, el framework ya lo tiene instanciado y el ComboBox solicita los datos automáticamente al renderizarse.
El binding selectedKey=”{ZZ1_PaymentMethodExt_MIH}” conecta la clave seleccionada del ComboBox con el Custom Field de la factura. Nótese que este binding no lleva prefijo de modelo, lo que significa que utiliza el modelo principal (default model) de la aplicación, que es el modelo OData de la factura de proveedor. Cuando el usuario selecciona un método de pago, el framework escribe automáticamente el PaymentMethodID en el campo ZZ1_PaymentMethodExt_MIH del documento.
El binding value=”{ZZ1_PaymentDaysExt_MIH}” en el Input conecta el campo de días de plazo con el Custom Field correspondiente de la factura. El atributo editable=”false” impide que el usuario modifique el valor manualmente; los días se calculan automáticamente al seleccionar el método de pago.
3.6. Controller Extension: lógica de inyección y eventos
3.6.1. Registro del controller en el manifest
Para que el framework reconozca el controller extension, es necesario registrarlo en la sección extends.extensions del manifest.json. La estructura requiere dos datos: el nombre completo del controller original que se desea extender y el nombre completo del controller extension.
El nombre del controller original se obtiene inspeccionando la aplicación en el navegador. Al abrir la consola de F12 y ejecutar:
sap.ui.getCore().byId(“application-title-display-component—MMIV_HEADER_ID_S1”)
.getController().getMetadata().getName()
El resultado es ui.s2p.mm.supplinvoice.manage.s1.controller.S1, que es el nombre exacto que se utiliza como clave en el manifest:
“sap.ui.controllerExtensions”: {
“ui.s2p.mm.supplinvoice.manage.s1.controller.S1”: {
“controllerName”: “customer.ui.s2p.mm.supplinvoice.manage.s1.test.controller.S1Custom”
}
}
3.6.2. Patrón de controller extension en UI5 1.78
En S/4HANA 2020, la versión de UI5 utilizada es la 1.78. En esta versión, los controller extensions para Extension Projects deben seguir un patrón específico que combina sap.ui.define con sap.ui.controller. El sap.ui.define se encarga de gestionar las dependencias (imports), y el sap.ui.controller registra el controller con el nombre que el framework espera encontrar:
sap.ui.define([
// dependencias
], function (/* parámetros */) {
“use strict”;
sap.ui.controller(“customer…controller.S1Custom”, {
// lifecycle hooks y métodos
});
});
Es importante aclarar por qué no se utiliza un objeto plano con return {} dentro del sap.ui.define. En versiones más recientes de UI5, este patrón es válido para controller extensions, pero en UI5 1.78, el framework no reconoce un objeto plano como controller extension. Se necesita la llamada explícita a sap.ui.controller() para que el registro funcione correctamente.
3.6.3. Inyección dinámica del fragment como ObjectPageSection
La aplicación estándar utiliza un sap.uxap.ObjectPageLayout como contenedor principal. Las pestañas visibles (General Information, Payment, Tax, entre otras) son instancias de ObjectPageSection. El objetivo es inyectar una nueva sección que contenga el fragment del método de pago.
La inyección se realiza en el lifecycle hook onAfterRendering, que se ejecuta después de que el view se ha renderizado completamente. Se utiliza una variable de módulo (_bFragmentLoaded) para garantizar que el fragment se inyecte solo una vez, ya que onAfterRendering puede ejecutarse múltiples veces durante el ciclo de vida de la aplicación.
El proceso de inyección sigue tres pasos:
Primero, se localiza el ObjectPageLayout de forma dinámica utilizando findAggregatedObjects. Este método recorre todo el árbol de controles del view y permite filtrar por tipo:
var oObjectPageLayout = null;
oView.findAggregatedObjects(true).forEach(function (oControl) {
if (oControl.isA(“sap.uxap.ObjectPageLayout”)) {
oObjectPageLayout = oControl;
}
});
Segundo, se carga el fragment utilizando sap.ui.xmlfragment, pasando el ID del view como prefijo para evitar colisiones de IDs:
var oFragment = sap.ui.xmlfragment(
oView.getId(),
“customer.ui.s2p.mm.supplinvoice.manage.s1.test.fragment.PaymentMethodFragment”,
that
);
Tercero, se crea una nueva ObjectPageSection con una ObjectPageSubSection que contiene el fragment como bloque, y se inserta en la posición deseada:
var oNewSection = new ObjectPageSection({
title: that._getI18nText(“paymentMethodExtSection”),
subSections: [
new ObjectPageSubSection({
blocks: [oFragment]
})
]
});
oObjectPageLayout.insertSection(oNewSection, iHeaderIndex + 1);
La posición se calcula recorriendo las secciones existentes y buscando la que tiene el título “General Information”. La nueva sección se inserta inmediatamente después.
3.6.4. Lógica del evento onPaymentMethodChange
El evento selectionChange del ComboBox dispara el método onPaymentMethodChange cada vez que el usuario selecciona un método de pago diferente. La lógica del método sigue un flujo claro:
onPaymentMethodChange: function (oEvent) {
var oSelectedItem = oEvent.getParameter(“selectedItem”);
if (!oSelectedItem) {
return;
}
var oCtx = oSelectedItem.getBindingContext(“zPayMethod”);
var sMethod = oCtx.getProperty(“PaymentMethodID”);
var iDays = parseInt(oCtx.getProperty(“DefaultDays”), 10);
var oHeaderCtx = this.getView().getBindingContext();
if (oHeaderCtx) {
var oMainModel = oHeaderCtx.getModel();
var sPath = oHeaderCtx.getPath();
oMainModel.setProperty(sPath + “/ZZ1_PaymentMethodExt_MIH”, sMethod);
oMainModel.setProperty(sPath + “/ZZ1_PaymentDaysExt_MIH”, iDays.toString());
MessageToast.show(
this._getI18nText(“paymentMethodExtSuccess”, [sMethod, iDays])
);
}
}
Del item seleccionado se obtiene el bindingContext del modelo zPayMethod, que proporciona acceso al PaymentMethodID y al DefaultDays del registro del CBO. Luego, del bindingContext del view (sin nombre, es decir, el modelo principal), se obtiene el path del documento de factura actual.
Con ambos datos, se actualizan los Custom Fields del modelo principal utilizando setProperty. Cuando el usuario guarde la factura, los valores de los Custom Fields se persistirán automáticamente en la tabla de cabecera (RBKP), ya que forman parte del modelo OData estándar de la aplicación.
3.7. Internacionalización con i18n
3.7.1. Por qué usar i18nExt en lugar de i18n
Un error común al implementar internacionalización en un Extension Project es nombrar el modelo de recursos como i18n. El problema es que la aplicación estándar ya utiliza un modelo con ese nombre para sus propios textos. Si el Extension Project registra otro modelo con el mismo nombre, sobreescribe el original, lo que provoca que todos los textos de la aplicación estándar se muestren como claves sin resolver (por ejemplo, SECTIONS_HEADER en lugar de “General Information”).
La solución es utilizar un nombre diferente para el modelo de recursos del Extension Project. En este caso se utiliza i18nExt, que identifica claramente que se trata del resource model de la extensión.
3.7.2. Configuración del ResourceModel
El archivo de propiedades se crea en webapp/i18n/i18n.properties:
# Payment Method Extension
appTitle=Create Supplier Invoice
paymentMethodExtSection=Método de Pago Extensión
paymentMethodExtLabel=Método de Pago Ext.
paymentDaysExtLabel=Días Plazo Ext.
paymentMethodExtPlaceholder=Seleccione método de pago…
paymentMethodExtSuccess=Método: {0} -> Plazo: {1} días
paymentMethodExtError=Error cargando métodos de pago
El modelo se registra en sap.ui5.models del manifest:
“i18nExt”: {
“type”: “sap.ui.model.resource.ResourceModel”,
“settings”: {
“bundleName”: “customer.ui.s2p.mm.supplinvoice.manage.s1.test.i18n.i18n”
}
}
En el controller, se implementa un método auxiliar _getI18nText que accede al resource bundle del modelo i18nExt y resuelve las claves con sus parámetros:
_getI18nText: function (sKey, aArgs) {
var oBundle = this.getOwnerComponent().getModel(“i18nExt”).getResourceBundle();
return oBundle.getText(sKey, aArgs);
}
Este método se utiliza tanto para el título de la sección inyectada como para los mensajes toast que informan al usuario sobre la selección realizada.
4. Conclusión
Al finalizar el recorrido descrito en este artículo, el resultado es una extensión completa de la aplicación Create Supplier Invoice que agrega una nueva pestaña con un ComboBox alimentado por un Custom Business Object y un campo de días de plazo que se auto-llena según la selección del usuario. La estructura del proyecto refleja la limpieza del enfoque: un controller extension (S1Custom.controller.js), un fragment XML (PaymentMethodFragment.fragment.xml), un archivo de internacionalización (i18n.properties), el componente heredado (Component.js) y el descriptor de la aplicación (manifest.json). Cinco archivos que extienden una aplicación estándar compleja sin modificar una sola línea de su código fuente.
La necesidad de esta extensión surgió de una brecha concreta entre lo que la aplicación estándar ofrece y lo que el negocio requiere. El flujo estándar de Create Supplier Invoice gestiona las condiciones de pago desde la perspectiva contable del sistema, pero no registra el método de pago real acordado comercialmente con el proveedor. Esta distinción, que puede parecer menor desde una perspectiva técnica, tiene implicaciones directas en la operación diaria: sin un campo que registre si el pago se hará por transferencia, cheque o letra de cambio, el equipo de cuentas por pagar debe gestionar esa información fuera del sistema, con las ineficiencias y los riesgos de error que eso conlleva.
La estrategia Clean Core demostró ser el camino adecuado para cerrar esa brecha. En la capa de datos maestros, el Custom Business Object proporcionó una tabla configurable con su servicio OData generado automáticamente, sin escribir código ABAP. En la capa de campos, los Custom Fields extendieron la cabecera de la factura con dos campos adicionales que el servicio OData expone de forma nativa. En la capa de interfaz de usuario, el Extension Project inyectó la nueva sección con su lógica de auto-llenado, manteniendo la aplicación estándar intacta.
| Capa | Técnica | Herramienta | Código ABAP |
| Datos maestros | Custom Business Object | App Fiori (Key User) | Ninguno |
| Campos en factura | Custom Fields | App Fiori (Key User) | Ninguno |
| Extensión de UI | Extension Project | BAS / VS Code (Developer) | Ninguno |
El valor de este enfoque no reside únicamente en la solución técnica, sino en lo que garantiza a largo plazo. Cada Support Package y Feature Pack que SAP libere para S/4HANA se aplicará sin conflicto, porque la extensión opera en una capa que el sistema reconoce y respeta. El key user puede mantener los métodos de pago y sus plazos desde la interfaz Fiori sin depender del equipo de desarrollo. El desarrollador puede evolucionar la extensión (agregar validaciones, nuevos campos, lógica adicional) sin temor a romper el flujo estándar. Y el equipo de base puede transportar la solución completa entre sistemas (desarrollo, calidad, producción) con las herramientas habituales de SAP, porque tanto los Custom Fields como el CBO y el Extension Project son objetos transportables.
En definitiva, este escenario demuestra que la extensión de aplicaciones Fiori estándar en S/4HANA no requiere comprometer la integridad del sistema. La combinación de Key User Extensibility y Developer Extensibility permite cubrir requerimientos de negocio reales con soluciones profesionales, mantenibles y preparadas para el futuro, sin escribir una sola línea de código ABAP y sin tocar el núcleo del sistema.

