{"nofollow":true,"url":"https://tienda-uat.aenor.com/e/producto-tienda/30025/155429577"}

-50% de descuento* Si compras la misma norma UNE en distintos idiomas. * Dto. sobre el pvp inferior. Ver condiciones

DIN EN ISO 13680:2025-08

Oil and gas industries including lower carbon energy - Corrosion-resistant alloy seamless products for use as casing, tubing, coupling stock and accessory material - Technical delivery conditions (ISO 13680:2024); English version prEN ISO 13680:2025 / Note: Date of issue 2025-07-11

Se ha producido un error al procesar la plantilla.
The following has evaluated to null or missing:
==> languageEntries?filter(entry -> entry.key == keyValue)?first  [in template "34352066712900#33336#null" at line 162, column 30]

----
Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
----

----
FTL stack trace ("~" means nesting-related):
	- Failed at: #assign match = languageEntries?filte...  [in template "34352066712900#33336#null" in function "addAllMatchingLanguagesByField" at line 162, column 13]
----
1<#-- Variables --> 
2<#assign isDebug = false> 
3<#assign channelResponse = restClient.get("/headless-commerce-delivery-catalog/v1.0/channels?filter=name eq 'Aenor Tienda'")> 
4<#assign channel = channelResponse.items[0]> 
5<#assign channelId = channel.id> 
6<#assign product = getProduct(channelId, CPDefinition_cProductId.getData()) /> 
7<#assign siteGroup = themeDisplay.getSiteGroup() /> 
8<#assign currentLocale = themeDisplay.getLocale()> 
9<#assign currentLanguage = currentLocale?substring(0,2)> 
10 
11<#-- Product data --> 
12<#assign displayDateProduct = CPDefinition_displayDate.getData() /> 
13<#assign productId = product.productId /> 
14<#assign cpDefinitionId = product.id /> 
15 
16<#assign categoriesProduct = getProductCategories(channelId, productId) /> 
17<#assign hasProductCategoriaTipoEntidadLibro = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'libro') /> 
18<#assign hasProductCategoriaTipoEntidadNorma = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'norma') /> 
19<#assign hasProductCategoriaTipoEntidadColeccionTematica = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'coleccion tematica') /> 
20<#assign categoryProductInfoTematica = getCategoriesByVocabularyAsString(categoriesProduct, "temáticas", " / ", "title") /> 
21<#assign categoryProductInfoStatus = getCategoriesByVocabularyAsString(categoriesProduct, "status", " / ", "title") /> 
22 
23<#-- PickList de languages --> 
24<#assign ercOfListTypeEntryLanguages = 'IDIOMAS_NORMAS_PICKLIST' /> 
25<#assign languageEntries = getListTypeEntriesByERC(ercOfListTypeEntryLanguages) /> 
26 
27<#-- Specifications/Specifications language --> 
28<#assign specificationsLanguagesProduct = getSpecificationsProduct(channelId, productId, 'specificationKey', 'standard-languages') /> 
29<#assign filteredSpecificationsLanguagesProductTemp = filterOutItems(specificationsLanguagesProduct, 'value', ['BI', 'TR']) /> 
30<#assign filteredSpecificationsLanguagesProduct = addAllMatchingLanguagesByField(filteredSpecificationsLanguagesProductTemp, languageEntries, 'value', 'title') /> 
31 
32<#-- Specifications/Specifications others --> 
33<#assign specificationsCTNProduct = getSpecificationsProduct(channelId, productId, 'specificationKey', 'ctn') /> 
34<#assign specificationsICSProduct = getSpecificationsProduct(channelId, productId, 'specificationKey', 'ics') /> 
35 
36<#-- Standard/Norma data --> 
37<#assign standardRelationsProduct = getStandardRelationsProduct(cpDefinitionId) /> 
38<#assign standardInfoProduct = getStandardInfoProduct(cpDefinitionId) /> 
39 
40<#assign ercOfListTypeEntryTipoRelacionesNormasSections = 'TIPO_RELACIONES_NORMAS-SECTIONS' /> 
41<#assign tipoRelacionesNormasSectionsEntries = getListTypeEntriesByERC(ercOfListTypeEntryTipoRelacionesNormasSections) /> 
42<#assign ercOfListTypeEntryTipoRelacionesNormasTypes = 'TIPO_RELACIONES_NORMAS-TYPE' /> 
43<#assign tipoRelacionesNormasTypesEntries = getListTypeEntriesByERC(ercOfListTypeEntryTipoRelacionesNormasTypes) /> 
44<#assign standardRelationsTypesProduct = getStandardRelationsTypesProduct(tipoRelacionesNormasSectionsEntries, tipoRelacionesNormasTypesEntries) /> 
45 
46 
47<#-- Functions --> 
48<#function getProduct channelId productId> 
49    <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}")> 
50</#function> 
51 
52 
53<#function getProductByERC erc> 
54    <#-- TODO: estamos usando el endpoint del product admin, hay que usar la del Product de Liferay NO admin: headless-commerce-delivery-catalog --> 
55    <#-- <#return restClient.get("/headless-commerce-admin-catalog/v1.0/products/by-externalReferenceCode/${erc}")> --> 
56    <#assign response = restClient.get("/headless-commerce-admin-catalog/v1.0/products/by-externalReferenceCode/${erc}")> 
57    <#-- Si el producto no existe o tiene status NOT_FOUND, devolvemos string vacío --> 
58    <#if response?? && response.status?? && response.status == "NOT_FOUND"> 
59        <#return {}> 
60    <#elseif !response??> 
61        <#return {}> 
62    <#else> 
63        <#return response> 
64    </#if> 
65</#function> 
66 
67 
68<#function getURLsOfProduct product baseURL="" siteFriendlyURL="" languageFieldName="language" urlFieldName="url"> 
69    <#-- Inicializamos un array vacío --> 
70    <#assign urlsArray = []> 
71 
72    <#-- Validamos que product y product.urls existan --> 
73    <#if product?? && product.urls??> 
74        <#-- Iteramos sobre los idiomas --> 
75        <#list product.urls?keys as lang> 
76            <#assign urlValue = product.urls[lang] /> 
77            <#if urlValue?? && urlValue?has_content> 
78                <#-- Aseguramos que la URL empiece con "/p/" --> 
79                <#assign cleanUrl = urlValue?starts_with("/")?then(urlValue, "/p/" + urlValue) /> 
80 
81                <#-- Generamos la URL completa --> 
82                <#-- Si baseURL tiene contenido, construimos URL completa --> 
83                <#if baseURL?? && baseURL?has_content> 
84 
85                    <#-- Aseguramos que el slug empiece con /p/ --> 
86                    <#assign cleanUrl = urlValue?starts_with("/")?then(urlValue, "/p/" + urlValue) /> 
87 
88                    <#-- Tomamos las dos primeras letras del idioma para el prefijo --> 
89                    <#assign langPrefix = lang?substring(0,2)> 
90 
91                    <#-- Construimos la URL completa --> 
92                    <#if siteFriendlyURL?? && siteFriendlyURL?has_content> 
93                        <#assign fullUrl = baseURL + "/" + langPrefix + siteFriendlyURL + cleanUrl> 
94                    <#else> 
95                        <#assign fullUrl = baseURL + "/" + langPrefix + cleanUrl> 
96                    </#if> 
97 
98                <#else> 
99                    <#assign fullUrl = cleanUrl> 
100                </#if> 
101 
102                <#-- Creamos el objeto language+url --> 
103                <#assign newItem = {(languageFieldName): lang, (urlFieldName): fullUrl} /> 
104 
105                <#-- Lo agregamos al array --> 
106                <#assign urlsArray = urlsArray + [newItem] /> 
107            </#if> 
108        </#list> 
109    </#if> 
110 
111    <#return urlsArray> 
112</#function> 
113 
114 
115<#function getProductCategories channelId productId> 
116    <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}/categories?sort=vocabulary").items> 
117</#function> 
118 
119 
120<#function getSpecificationsProduct channelId productId field="" value=""> 
121    <#assign response = restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}/product-specifications?pageSize=100&sort")> 
122    <#assign items = response.items> 
123 
124    <#-- Si se pasa campo y valor, filtrar --> 
125    <#if field?has_content && value?has_content> 
126        <#assign items = items?filter(item -> 
127            (item[field]??) && (item[field]?string?lower_case == value?lower_case) 
128        )> 
129    </#if> 
130 
131    <#return items> 
132</#function> 
133 
134 
135<#function filterOutItems items field valuesToExclude> 
136    <#-- Si el array o los valores no existen, devolver items sin cambios --> 
137    <#if !items?? || !valuesToExclude??> 
138        <#return items> 
139    </#if> 
140 
141    <#-- Filtramos: solo mantener los items cuyo campo no esté en la lista --> 
142    <#assign filteredItems = items?filter(item -> 
143        !(item[field]?? && (valuesToExclude?seq_contains(item[field]?string))) 
144    )> 
145 
146    <#return filteredItems> 
147</#function> 
148 
149 
150<#function addAllMatchingLanguagesByField specificationsLanguagesProduct languageEntries specValueField newSpecField> 
151 
152    <#if specificationsLanguagesProduct?? && languageEntries?? && specValueField?? && newSpecField??> 
153 
154        <#-- Creamos una copia para no modificar el original directamente --> 
155        <#assign updatedSpecs = []> 
156 
157        <#list specificationsLanguagesProduct as spec> 
158            <#-- Buscar coincidencia --> 
159            <#assign keyValue = spec[specValueField]> 
160            <#assign match = languageEntries?filter(entry -> entry.key == keyValue)?first> 
161 
162            <#-- Crear una copia del objeto actual --> 
163            <#assign newSpec = spec> 
164 
165            <#-- Si hay match con nombre válido, añadir el nuevo campo --> 
166            <#if match?? && match.name?? && match.name?has_content> 
167                <#assign newSpec = newSpec + { (newSpecField): match.name }> 
168            </#if> 
169 
170            <#-- Añadir al array final --> 
171            <#assign updatedSpecs = updatedSpecs + [newSpec]> 
172        </#list> 
173 
174        <#return updatedSpecs> 
175    <#else> 
176        <#return specificationsLanguagesProduct> 
177    </#if> 
178</#function> 
179 
180 
181<#function getListTypeEntriesByERC erc fields="key,name,externalReferenceCode,name_i18n" sort="key" pageSize="1000"> 
182    <#attempt> 
183        <#return restClient.get( 
184            "/headless-admin-list-type/v1.0/list-type-definitions/by-external-reference-code/${erc}/list-type-entries?fields=${fields}&sort=${sort}&pageSize=${pageSize}" 
185        ).items> 
186    <#recover> 
187        <#return []> 
188    </#attempt> 
189</#function> 
190 
191 
192<#function getStandardRelationsProduct cpDefinitionId 
193    ercProductFieldName="ercProduct" defaultLang = "es_ES" urlProductCurrentLanguageFieldName="urlProductCurrentLanguage" urlsProductByLangFieldName="urlsProductByLang" 
194    languageFieldName="language" urlFieldName="url" sort="type"> 
195 
196    <#assign result = [] /> 
197 
198    <#-- Obtener relaciones desde la API --> 
199    <#assign standardRelations = restClient.get("/c/standardrelationses/?filter=r_standardRelations_CPDefinitionId eq '${cpDefinitionId}'&sort=${sort}").items /> 
200 
201    <#-- Iterar sobre cada relación --> 
202    <#list standardRelations as standardRelationProduct> 
203 
204        <#-- Crear ERC del producto relacionado --> 
205        <#assign ercProduct = (standardRelationProduct.relatedOrganismStandardName!"relatedOrganismStandardName") + "-" + (standardRelationProduct.relatedStandardId!"relatedStandardId") /> 
206 
207        <#-- Obtener el producto por ERC --> 
208        <#assign productByERC = getProductByERC(ercProduct) /> 
209        <#assign findProductByERC = (productByERC?? && productByERC?has_content)> 
210 
211        <#-- Obtener URLs del producto --> 
212        <#assign baseURL = themeDisplay.getPortalURL()> 
213        <#assign siteFriendlyURL = "/web" + siteGroup.getFriendlyURL()> 
214        <#assign urlsProductByLang = getURLsOfProduct(productByERC, baseURL, siteFriendlyURL, languageFieldName, urlFieldName) /> 
215 
216        <#-- Obtenemos URLs del producto default y current language --> 
217        <#assign selectedURLCurrent = ""> 
218        <#assign selectedURLDefault = ""> 
219        <#list urlsProductByLang as item> 
220            <#-- URL del idioma actual --> 
221            <#if item.language == currentLocale && selectedURLCurrent == ""> 
222                <#assign selectedURLCurrent = item.url> 
223            </#if> 
224 
225            <#-- URL del idioma por defecto --> 
226            <#if item.language == defaultLang && selectedURLDefault == ""> 
227                <#assign selectedURLDefault = item.url> 
228            </#if> 
229 
230            <#-- Salimos si ya tenemos ambas --> 
231            <#if selectedURLCurrent != "" && selectedURLDefault != ""> 
232                <#break> 
233            </#if> 
234        </#list> 
235 
236        <#-- Usamos el idioma actual si existe, sino el por defecto --> 
237        <#if selectedURLCurrent != ""> 
238            <#assign urlCurrentLanguage = selectedURLCurrent> 
239        <#else> 
240            <#assign urlCurrentLanguage = selectedURLDefault> 
241        </#if> 
242 
243        <#-- Combinar todos los datos del objeto original + nuevos campos --> 
244        <#assign resultItem = standardRelationProduct + { 
245            (ercProductFieldName): ercProduct, 
246            (urlProductCurrentLanguageFieldName): urlCurrentLanguage, 
247            (urlsProductByLangFieldName): urlsProductByLang, 
248            ("findProductByERC"): findProductByERC 
249        } /> 
250 
251        <#-- Añadir al array final --> 
252        <#assign result = result + [resultItem] /> 
253    </#list> 
254 
255    <#return result> 
256</#function> 
257 
258 
259<#function getStandardInfoProduct cpDefinitionId> 
260    <#assign response = restClient.get("/c/standardinfos/?filter=r_standardInfo_CPDefinitionId eq '${cpDefinitionId}'")!{} /> 
261    <#assign items = response.items![] /> 
262    <#return (items?size > 0)?then(items[0], {}) /> 
263</#function> 
264 
265 
266<#function getStandardRelationsTypesProduct tipoRelacionesNormasSectionsEntries tipoRelacionesNormasTypesEntries> 
267    <#attempt> 
268 
269        <#-- Obtener los items del endpoint --> 
270        <#assign itemsStandardRelationsTypeProduct = restClient.get("/c/standardrelationtypeses/?pageSize=100").items> 
271 
272        <#-- Crear array vacío para ir acumulando los items enriquecidos --> 
273        <#assign enrichedItemsStandardRelationsTypeProduct = []> 
274 
275        <#assign enrichedItemsStandardRelationsTypeProduct = applyTipoRelacionesNormasEntries( 
276            itemsStandardRelationsTypeProduct, tipoRelacionesNormasSectionsEntries, 
277            "relatedSection", "section" 
278        )> 
279 
280        <#assign enrichedItemsStandardRelationsTypeProduct = applyTipoRelacionesNormasEntries( 
281            enrichedItemsStandardRelationsTypeProduct, tipoRelacionesNormasTypesEntries, 
282            "relatedType", "type" 
283        )> 
284 
285        <#assign enrichedItemsStandardRelationsTypeProduct = addKeysFieldNested( 
286            enrichedItemsStandardRelationsTypeProduct, "section", "name", "sectionName", "all") 
287
288 
289        <#assign enrichedItemsStandardRelationsTypeProduct = addKeysFieldNested( 
290            enrichedItemsStandardRelationsTypeProduct, "section", "key", "sectionKey", "first") 
291
292 
293        <#assign enrichedItemsStandardRelationsTypeProduct = addKeysFieldNested( 
294            enrichedItemsStandardRelationsTypeProduct, "section", "externalReferenceCode", "sectionKeyERC", "first", "relatedSection") 
295
296 
297         <#assign enrichedItemsStandardRelationsTypeProduct = addKeysFieldNested( 
298            enrichedItemsStandardRelationsTypeProduct, "type", "name", "typeName", "all") 
299
300 
301        <#assign enrichedItemsStandardRelationsTypeProduct = addKeysFieldNested( 
302            enrichedItemsStandardRelationsTypeProduct, "type", "key", "typeKey", "all") 
303
304 
305        <#assign enrichedItemsStandardRelationsTypeProduct = addKeysFieldNested( 
306            enrichedItemsStandardRelationsTypeProduct, "type", "externalReferenceCode", "typeKeyERC", "all", "relatedType") 
307
308 
309        <#assign enrichedItemsStandardRelationsTypeProductSorted = sortByField(enrichedItemsStandardRelationsTypeProduct, "section?first.key")> 
310        <#return enrichedItemsStandardRelationsTypeProductSorted> 
311 
312    <#recover> 
313        <#return []> 
314    </#attempt> 
315</#function> 
316 
317 
318<#function applyTipoRelacionesNormasEntries 
319    itemsStandardRelationsTypeProduct tipoRelacionesNormasEntries 
320    relatedEntriesFieldName="relatedEntries" putRelatiedEntriesFieldName="entries" 
321
322 
323    <#assign enrichedItems = []> 
324 
325    <#list itemsStandardRelationsTypeProduct as item> 
326 
327        <#assign relatedEntries = []> 
328 
329        <#list item[putRelatiedEntriesFieldName] as sec> 
330            <#assign matchedEntry = {}> 
331 
332            <#list tipoRelacionesNormasEntries as te> 
333                <#if te.key?string?trim == sec.key?string?trim> 
334                    <#assign matchedEntry = te> 
335                    <#break> 
336                </#if> 
337            </#list> 
338 
339            <#assign relatedEntry = []> 
340            <#if matchedEntry?has_content> 
341                <#assign relatedEntry = [ matchedEntry ]> 
342            </#if> 
343 
344            <#assign relatedItem = sec + { (relatedEntriesFieldName): relatedEntry }> 
345            <#assign relatedEntries = relatedEntries + [ relatedItem ]> 
346 
347        </#list> 
348 
349        <#assign enrichedItem = item + { 
350            (putRelatiedEntriesFieldName): relatedEntries 
351        }> 
352 
353        <#assign enrichedItems = enrichedItems + [ enrichedItem ]> 
354 
355    </#list> 
356 
357    <#return enrichedItems> 
358</#function> 
359 
360 
361<#function sortByField items fieldPath sortFieldName="sortKey" order="asc"> 
362 
363    <#-- Array temporal con el campo auxiliar --> 
364    <#assign prepared = []> 
365 
366    <#list items as e> 
367        <#-- Evaluar el valor del campo dinámico --> 
368        <#assign sortValue = "" /> 
369        <#if fieldPath == "section?first.key"> 
370            <#assign sortValue = (e.section?first.key)!""?lower_case> 
371        <#elseif fieldPath == "section?first.name"> 
372            <#assign sortValue = (e.section?first.name)!""?lower_case> 
373        <#elseif fieldPath == "type?first.key"> 
374            <#assign sortValue = (e.type?first.key)!""?lower_case> 
375        <#else> 
376            <#-- Si el campoPath no está mapeado, usar vacío --> 
377            <#assign sortValue = "" /> 
378        </#if> 
379 
380        <#-- Agregar objeto enriquecido con campo auxiliar --> 
381        <#assign prepared = prepared + [ e + { (sortFieldName): sortValue } ]> 
382    </#list> 
383 
384    <#-- Ordenar --> 
385    <#assign sorted = prepared?sort_by(sortFieldName)> 
386 
387    <#-- Si order = desc, invertir --> 
388    <#if order?lower_case == "desc"> 
389        <#assign sorted = sorted?reverse> 
390    </#if> 
391 
392    <#return sorted> 
393</#function> 
394 
395 
396<#function addKeysFieldNested items arrayField nestedField newFieldName mode="first" subArrayField=""> 
397 
398    <#assign enrichedItems = []> 
399 
400    <#list items as e> 
401        <#assign resultValue = ""> 
402 
403        <#-- Detectamos el array del objeto principal --> 
404        <#assign targetArray = e[arrayField]![]> 
405 
406        <#if targetArray?has_content> 
407            <#if mode == "all"> 
408                <#assign keysList = []> 
409                <#list targetArray as t> 
410                    <#if subArrayField?has_content> 
411                        <#assign subArray = t[subArrayField]![]> 
412                        <#if subArray?has_content> 
413                            <#list subArray as sub> 
414                                <#assign keysList = keysList + [ sub[nestedField]!"" ]> 
415                            </#list> 
416                        </#if> 
417                    <#else> 
418                        <#assign keysList = keysList + [ t[nestedField]!"" ]> 
419                    </#if> 
420                </#list> 
421                <#assign resultValue = keysList?join(", ")> 
422            <#elseif mode == "first"> 
423                <#assign firstItem = targetArray?first> 
424                <#if subArrayField?has_content> 
425                    <#assign subArray = firstItem[subArrayField]![]> 
426                    <#if subArray?has_content> 
427                        <#assign resultValue = subArray?first[nestedField]!"" > 
428                    <#else> 
429                        <#assign resultValue = "" > 
430                    </#if> 
431                <#else> 
432                    <#assign resultValue = firstItem[nestedField]!"" > 
433                </#if> 
434            </#if> 
435        <#else> 
436            <#assign resultValue = ""> 
437        </#if> 
438 
439        <#-- Añadimos el nuevo campo al objeto --> 
440        <#assign enrichedItems = enrichedItems + [ 
441            e + { (newFieldName): resultValue } 
442        ]> 
443    </#list> 
444 
445    <#return enrichedItems> 
446</#function> 
447 
448 
449<#function isVocabularyNameIntoCategories categories vocabulary name> 
450    <#assign found = false /> 
451 
452    <#if categories?has_content && vocabulary?has_content && name?has_content> 
453 
454        <#assign vocabNorm = normalize(vocabulary) /> 
455        <#assign nameNorm  = normalize(name) /> 
456 
457        <#list categories as category> 
458            <#if !found> 
459                <#assign catVocabNorm = normalize(category.vocabulary) /> 
460                <#assign catNameNorm  = normalize(category.name) /> 
461 
462                <#if catVocabNorm == vocabNorm && catNameNorm == nameNorm> 
463                    <#assign found = true /> 
464                </#if> 
465            </#if> 
466        </#list> 
467 
468    </#if> 
469 
470    <#return found> 
471</#function> 
472 
473 
474<#function normalize text onlyAccents = false> 
475    <#-- proteger null --> 
476    <#if !text?has_content> 
477        <#return ""> 
478    </#if> 
479 
480    <#assign t = text /> 
481 
482    <#-- quitar acentos --> 
483    <#assign t = t 
484        ?replace("á","a")?replace("é","e")?replace("í","i") 
485        ?replace("ó","o")?replace("ú","u")?replace("ü","u") 
486        ?replace("ñ","n") 
487        ?replace("Á","A")?replace("É","E")?replace("Í","I") 
488        ?replace("Ó","O")?replace("Ú","U")?replace("Ü","U") 
489        ?replace("Ñ","N") 
490    /> 
491 
492    <#-- si NO es solo acentos, normalización completa --> 
493    <#if !onlyAccents> 
494        <#assign t = t?lower_case /> 
495        <#assign t = t?trim /> 
496        <#assign t = t?replace("\\s+", " ", "r") /> 
497    </#if> 
498 
499    <#return t> 
500</#function> 
501 
502 
503<#function getStandardRelationsProductsByField 
504    itemStandardRelationsTypeProduct standardRelationsProducts itemStandardRelationsTypeProductField="typeKeyERC" standardRelationsProductField="type"> 
505 
506    <#assign matched = []> 
507 
508    <#-- Verificar que el item tenga el campo indicado --> 
509    <#if itemStandardRelationsTypeProduct[itemStandardRelationsTypeProductField]?? && itemStandardRelationsTypeProduct[itemStandardRelationsTypeProductField]?has_content> 
510 
511        <#-- Normalizamos (minusculas, sin espacios a al principio/final y separamos en lista si hay varios valores) --> 
512        <#assign itemStandardRelationsTypeProductValues = itemStandardRelationsTypeProduct[itemStandardRelationsTypeProductField]?split(",")?map(v -> v?trim?lower_case)> 
513 
514        <#-- Recorrer los productos --> 
515        <#list standardRelationsProducts as standardRelationsProduct> 
516            <#-- Asegurar que el campo del producto existe --> 
517            <#if standardRelationsProduct[standardRelationsProductField]?? && standardRelationsProduct[standardRelationsProductField]?has_content> 
518 
519                <#-- 
520                    - Los elementos de [standardRelationsProducts.type] tienen datos como: REVISED_BY 
521                    - En los PickList, en este caso la picklist [erc: TIPO_RELACIONES_NORMAS], no permite guardar 
522                      keys con "_" por lo que existirá una key: REVISEDBY pero entonces comparamos por su 
523                      campo ERC que si permite "_", entonces tendremos como ERC: REVISED_BY y como key: REVISEDBY. 
524                    - Entonces buscaremos, usando de la picklist, la key (el ERC) del tipo de relación [itemStandardRelationsTypeProduct.typeKeyERC] 
525                      en [standardRelationsProducts.type]. 
526                    - Convertimos el texto a minúsculas y quitamos los espacio del principio/fin. 
527                    - Dentro de [itemStandardRelationsTypeProduct.typeKeyERC] podemos tener 
528                      varios valores, por ejemplo [typeKey: REPLACED_BY, REPLACEDBY]. 
529                --> 
530                <#assign normalizedProdValue = standardRelationsProduct[standardRelationsProductField]?trim?lower_case> 
531 
532                <#-- 
533                    Verificar si coincide alguno de los valores del item StandardRelationsTypeProduct con 
534                    el valor normalizado de standardRelationsProduct 
535                --> 
536                <#if itemStandardRelationsTypeProductValues?seq_contains(normalizedProdValue)> 
537                    <#assign matched = matched + [standardRelationsProduct]> 
538                </#if> 
539            </#if> 
540        </#list> 
541    </#if> 
542 
543    <#return matched> 
544</#function> 
545 
546 
547<#-- Función que retorna los valores de categorías como string, normalizando vocabulario --> 
548<#function getCategoriesByVocabularyAsString categories vocabulary separator=" / " field="name"> 
549    <#assign matchedValues = []> 
550    <#if categories?has_content && vocabulary?has_content> 
551        <#list categories as category> 
552            <#-- Normalizamos tanto el vocabulary de la categoría como el buscado --> 
553            <#if normalize(category.vocabulary, true)?lower_case == normalize(vocabulary, true)?lower_case> 
554                <#if field == "title"> 
555                    <#assign matchedValues += [category.title]> 
556                <#else> 
557                    <#assign matchedValues += [category.name]> 
558                </#if> 
559            </#if> 
560        </#list> 
561    </#if> 
562    <#return matchedValues?join(separator)> 
563</#function> 
564 
565 
566<#macro printObject obj> 
567    <#-- Permite hacer un output de un array de objetos o un objeto que se pasa como parámetro --> 
568    <#if obj?is_hash> 
569
570        <#list obj?keys as k> 
571            "${k}": 
572            <#assign value = obj[k]> 
573            <#if value?is_hash || value?is_sequence> 
574                <@printObject obj=value/> 
575            <#elseif value?is_boolean> 
576                ${value?c} 
577            <#elseif value?is_number> 
578                ${value} 
579            <#elseif value?has_content> 
580                "${value?string}" 
581            <#else> 
582                null 
583            </#if> 
584            <#if k_has_next>, </#if> 
585        </#list> 
586
587    <#elseif obj?is_sequence> 
588
589        <#list obj as item> 
590            <@printObject obj=item/> 
591            <#if item_has_next>, </#if> 
592        </#list> 
593
594    <#elseif obj?is_boolean> 
595        ${obj?c} 
596    <#elseif obj?is_number> 
597        ${obj} 
598    <#elseif obj?has_content> 
599        "${obj?string}" 
600    <#else> 
601        null 
602    </#if> 
603</#macro> 
604 
605 
606<#macro renderStandardRelationsRows standardRelationsTypesProduct standardRelationsProduct isDebug=false> 
607 
608    <#assign lastSectionKey = ""> 
609    <#assign openRow = false> 
610 
611    <#list standardRelationsTypesProduct as standardRelationsTypeProduct> 
612 
613        <#assign currentSectionKey = (standardRelationsTypeProduct.sectionKey)!"" /> 
614        <#assign standardRelationsProductsByTypeKey = getStandardRelationsProductsByField(standardRelationsTypeProduct, standardRelationsProduct)> 
615        <#assign hasElementsStandardRelationsProducts = (standardRelationsProductsByTypeKey?size > 0)> 
616 
617        <#if isDebug || hasElementsStandardRelationsProducts> 
618 
619            <#-- Si la sección cambia, cerramos la fila anterior --> 
620            <#if openRow && currentSectionKey != lastSectionKey> 
621                </td> 
622                </tr> 
623                <#assign openRow = false> 
624            </#if> 
625 
626            <#-- Si es una nueva sección, abrimos una nueva fila --> 
627            <#if !openRow> 
628                <tr 
629                    data-section-key="${currentSectionKey}" 
630                    data-type-key="${standardRelationsTypeProduct.typeKey}" 
631                    data-section-name="${standardRelationsTypeProduct.sectionName}" 
632                    data-sort-key="${standardRelationsTypeProduct.sortKey}"> 
633                    <th> 
634                        <p>${standardRelationsTypeProduct.sectionName}</p> 
635                    </th> 
636                    <td> 
637                <#assign openRow = true> 
638            </#if> 
639 
640            <#if isDebug> 
641                <div class=""> 
642                    <p><strong>typeKey:</strong> ${standardRelationsTypeProduct.typeKey}</p> 
643                    <p><strong>sectionKey:</strong> ${standardRelationsTypeProduct.sectionKey}</p> 
644                    <p><strong>sectionName:</strong> ${standardRelationsTypeProduct.sectionName}</p> 
645                    <p><strong>sortKey:</strong> ${standardRelationsTypeProduct.sortKey}</p> 
646 
647                    <p class="mb-0"><strong>standardRelationsTypeProduct:</strong></p> 
648                    <div class="print-object-json-content mb-3" 
649                         style="max-height:200px; overflow:auto; border:1px solid #ccc; padding:5px; cursor:pointer;" 
650                         onclick="const r=document.createRange(); r.selectNodeContents(this); const s=window.getSelection(); s.removeAllRanges(); s.addRange(r);"> 
651                        <@printObject standardRelationsTypeProduct /> 
652                    </div> 
653 
654                    <p class="mb-0"><strong>standardRelationsProductsByTypeKey:</strong></p> 
655                    <div class="print-object-json-content mb-3" 
656                         style="max-height:200px; overflow:auto; border:1px solid #ccc; padding:5px; cursor:pointer;" 
657                         onclick="const r=document.createRange(); r.selectNodeContents(this); const s=window.getSelection(); s.removeAllRanges(); s.addRange(r);"> 
658                        <@printObject standardRelationsProductsByTypeKey /> 
659                    </div> 
660                </div> 
661            </#if> 
662 
663            <#if standardRelationsProductsByTypeKey?has_content> 
664                <#list standardRelationsProductsByTypeKey as standardRelationProductsByTypeKey> 
665                    <#assign urlProductCurrentLanguage = standardRelationProductsByTypeKey.urlProductCurrentLanguage> 
666 
667                    <p> 
668                        ${standardRelationsTypeProduct.description} 
669 
670                        <#-- 
671                            <#if urlProductCurrentLanguage?? && urlProductCurrentLanguage?has_content> 
672                                <a href="${urlProductCurrentLanguage}" target="_blank"> 
673                                    ${standardRelationProductsByTypeKey.relatedStandardName!""} 
674                                </a> 
675                            <#else> 
676                                <a class="add-link-product-by-erc" data-product-erc="${standardRelationProductsByTypeKey.ercProduct}" href="" target="_blank"> 
677                                    ${standardRelationProductsByTypeKey.relatedStandardName!""} 
678                                </a> 
679                            </#if> 
680                        --> 
681 
682                        <a class="add-link-product-by-erc text-decoration-none text-body" data-product-erc="${standardRelationProductsByTypeKey.ercProduct}" href="" target="_blank"> 
683                            ${standardRelationProductsByTypeKey.relatedStandardName!""} 
684                        </a> 
685 
686                    </p> 
687 
688                </#list> 
689 
690                <#if isDebug> 
691                    <p> 
692                        <strong>Info:</strong>SI hay <strong>Standard/Norma Relation</strong> con el type [<strong>typeKey:</strong> ${standardRelationsTypeProduct.typeKey}] 
693                        en la sección [<strong>sectionKey:</strong> ${standardRelationsTypeProduct.sectionKey}] 
694                    </p> 
695                </#if> 
696            <#else> 
697                <#if isDebug> 
698                    <p> 
699                        <strong>Info:</strong>NO hay <strong>Standard/Norma Relation</strong> con el type [<strong>typeKey:</strong> ${standardRelationsTypeProduct.typeKey}] 
700                        en la sección [<strong>sectionKey:</strong> ${standardRelationsTypeProduct.sectionKey}] 
701                    </p> 
702                <#else> 
703                    <p>N/A</p> 
704                </#if> 
705            </#if> 
706 
707            <#-- Separador visual en debug entre elementos standardRelations dentro del mismo section --> 
708            <#if isDebug> 
709                <#-- Separador visual mejorado --> 
710                <div style="margin:10px 0; padding:5px; border-top:2px dashed #007BFF; background-color:#f0f8ff;"></div> 
711            </#if> 
712 
713            <#assign lastSectionKey = currentSectionKey> 
714        </#if> 
715    </#list> 
716 
717    <#-- Cerrar la última fila abierta --> 
718    <#if openRow> 
719        </td> 
720        </tr> 
721    </#if> 
722 
723</#macro> 
724 
725 
726 
727 
728<#-- HTML --> 
729<#if hasProductCategoriaTipoEntidadNorma> 
730    <div id="ecom-norma-detail" class="ecom-norma"> 
731        <div class="tabs-section"> 
732            <div class="tab-content"> 
733 
734                <table> 
735 
736                    <#-- Custom object [StandardInfo] > mostrarAlerta, tituloAlerta, descAlerta (tb existe como _i18n) --> 
737                    <#if standardInfoProduct?? && standardInfoProduct.mostrarAlerta?? && standardInfoProduct.mostrarAlerta> 
738                        <tr class="alert-row"> 
739                            <th class="alert-label"> 
740                                <#if standardInfoProduct.tituloAlerta??> 
741                                    <strong>${standardInfoProduct.tituloAlerta}</strong>: 
742                                </#if> 
743                            </th> 
744                            <td class="alert-content"> 
745                                <#if standardInfoProduct.descAlerta??> 
746                                    ${standardInfoProduct.descAlerta} 
747                                </#if> 
748                            </td> 
749                        </tr> 
750                    </#if> 
751 
752                    <#-- Categoria producto > Tematicas --> 
753                    <tr> 
754                        <th>${languageUtil.get(locale, "ecom-tematicas")}:</th> 
755                        <td>${categoryProductInfoTematica}</td> 
756                    </tr> 
757 
758                    <#-- Fecha edicion del producto > displayDate del Producto --> 
759                    <tr> 
760                        <th>${languageUtil.get(locale, "ecom-fecha_edicion")}:</th> 
761                        <td> 
762 
763                            <#if displayDateProduct?? && displayDateProduct?has_content> 
764                              ${displayDateProduct?datetime("d/MM/yy H:mm")?string("yyyy-MM-dd")} 
765                            <#else> 
766
767                            </#if> 
768 
769                            <#if categoryProductInfoStatus?? && categoryProductInfoStatus?has_content> 
770                            	<#assign statusTagClass = ''> 
771 
772                            	<#if categoryProductInfoStatus?trim?upper_case == 'EN VIGOR'> 
773                            		<#assign statusTagClass = 'tag-success'> 
774                            	<#elseif categoryProductInfoStatus?trim?upper_case == 'ANULADA'> 
775                            		<#assign statusTagClass = 'tag-danger'> 
776                            	<#elseif categoryProductInfoStatus?trim?upper_case == 'PROYECTO'> 
777                            		<#assign statusTagClass = 'tag-blue'> 
778                            	</#if> 
779 
780                            	<#if statusTagClass?? && statusTagClass?has_content> 
781                            		<#assign statusTagClass = "status-standard " + statusTagClass> 
782                            	</#if> 
783 
784                            	<div class="badge ${statusTagClass}">${categoryProductInfoStatus}</div> 
785                            </#if> 
786 
787                        </td> 
788                    </tr> 
789 
790                    <#-- Custom object [StandardInfo] > confirmationDate --> 
791                    <tr> 
792                        <th>${languageUtil.get(locale, "ecom-confirmationDate")}:</th> 
793                        <td> 
794                           <#if standardInfoProduct?? && standardInfoProduct.confirmationDate?has_content> 
795                               ${standardInfoProduct.confirmationDate?datetime("yyyy-MM-dd'T'HH:mm:ss.SSSX")?string("yyyy-MM-dd")} 
796                           <#else> 
797                               <span>-</span> 
798                           </#if> 
799                        </td> 
800                    </tr> 
801 
802                    <#-- Custom object [StandardInfo] > correctionDate --> 
803                    <tr> 
804                        <th>${languageUtil.get(locale, "ecom-correctionDate")}:</th> 
805                        <td> 
806                           <#if standardInfoProduct?? && standardInfoProduct.correctionDate?has_content> 
807                               ${standardInfoProduct.correctionDate?datetime("yyyy-MM-dd'T'HH:mm:ss.SSSX")?string("yyyy-MM-dd")} 
808                           <#else> 
809                               <span>-</span> 
810                           </#if> 
811                        </td> 
812                    </tr> 
813 
814                    <#-- Custom object [StandardInfo] > ratificationDate --> 
815                    <tr> 
816                        <th>${languageUtil.get(locale, "ecom-ratificationDate")}:</th> 
817                        <td> 
818                           <#if standardInfoProduct?? && standardInfoProduct.ratificationDate?has_content> 
819                               ${standardInfoProduct.ratificationDate?datetime("yyyy-MM-dd'T'HH:mm:ss.SSSX")?string("yyyy-MM-dd")} 
820                           <#else> 
821                               <span>-</span> 
822                           </#if> 
823                        </td> 
824                    </tr> 
825 
826                    <#-- Especificacion del producto > standard-languages (se aplica filtro para idiomas combinados) --> 
827                    <#if filteredSpecificationsLanguagesProduct?? && filteredSpecificationsLanguagesProduct?size gt 0> 
828                        <tr> 
829                            <th>${languageUtil.get(locale, "ecom-idiomas_disponibles")}:</th> 
830                            <td>${filteredSpecificationsLanguagesProduct?map(spec -> spec.title)?join(", ")}</td> 
831                        </tr> 
832                    </#if> 
833 
834                    <#-- Custom object [StandardInfo] > resumen --> 
835                    <tr> 
836                        <th class="th-content">${languageUtil.get(locale, "ecom-resumen")}:</th> 
837                        <td class="td-content"> 
838                           <#if standardInfoProduct?? && standardInfoProduct.resumen?has_content> 
839                               ${htmlUtil.unescape(standardInfoProduct.resumen)} 
840                           <#else> 
841                               <span>-</span> 
842                           </#if> 
843                        </td> 
844                    </tr> 
845 
846                    <#-- Product > expando.keywords --> 
847                    <tr> 
848                        <th class="th-content">${languageUtil.get(locale, "ecom-keywords")}:</th> 
849                        <td class="td-content"> 
850                            <#if product?? && product.expando?? && product.expando.keywords?has_content> 
851                                ${product.expando.keywords} 
852                            <#else> 
853                                <span>-</span> 
854                            </#if> 
855                        </td> 
856                    </tr> 
857 
858                    <#-- Custom object [StandardInfo] > scope --> 
859                    <tr> 
860                        <th class="th-content">${languageUtil.get(locale, "ecom-scope")}:</th> 
861                        <td class="td-content"> 
862                           <#if standardInfoProduct?? && standardInfoProduct.scope?has_content> 
863                               ${standardInfoProduct.scope} 
864                           <#else> 
865                               <span>-</span> 
866                           </#if> 
867                        </td> 
868                    </tr> 
869 
870                    <#-- Especificacion del producto > ics --> 
871                    <#if specificationsICSProduct?? && specificationsICSProduct?size gt 0> 
872                        <tr> 
873                            <th>${languageUtil.get(locale, "ecom-ics")}:</th> 
874                            <td> 
875                                ${specificationsICSProduct?filter(spec -> spec.value?? && spec.value?has_content)?map(spec -> spec.value)?join(", ")} 
876                            </td> 
877                        </tr> 
878                    </#if> 
879 
880                    <#-- Especificacion del producto > ctn --> 
881                    <#if specificationsCTNProduct?? && specificationsCTNProduct?size gt 0> 
882                        <tr> 
883                            <th>${languageUtil.get(locale, "ecom-ctn")}:</th> 
884                            <td> 
885                                ${specificationsCTNProduct?filter(spec -> spec.value?? && spec.value?has_content)?map(spec -> spec.value)?join(", ")} 
886                            </td> 
887                        </tr> 
888                    </#if> 
889 
890                    <#-- Para imprimir el contenido de los Objects --> 
891                    <#if isDebug> 
892                        <div class=""> 
893                            <p><strong>standardRelationsProduct:</strong></p> 
894                            <div class="print-object-json-content mb-3" style="max-height:200px; overflow:auto; border:1px solid #ccc; padding:5px; cursor:pointer;" 
895                                 onclick="const r=document.createRange(); r.selectNodeContents(this); const s=window.getSelection(); s.removeAllRanges(); s.addRange(r);"> 
896                                <@printObject standardRelationsProduct /> 
897                            </div> 
898                        </div> 
899                     </#if> 
900 
901                    <@renderStandardRelationsRows 
902                        standardRelationsTypesProduct=standardRelationsTypesProduct 
903                        standardRelationsProduct=standardRelationsProduct 
904                        isDebug=isDebug 
905                   /> 
906 
907                </table> 
908            </div> 
909        </div> 
910    </div> 
911</#if> 
Se ha producido un error al procesar la plantilla.
The following has evaluated to null or missing:
==> languageEntries?filter(entry -> entry.key == keyValue)?first  [in template "34352066712900#33336#null" at line 151, column 30]

----
Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
----

----
FTL stack trace ("~" means nesting-related):
	- Failed at: #assign match = languageEntries?filte...  [in template "34352066712900#33336#null" in function "addAllMatchingLanguagesByField" at line 151, column 13]
----
1<#-- Variables --> 
2<#assign isDebug = false> 
3<#assign channelResponse = restClient.get("/headless-commerce-delivery-catalog/v1.0/channels?filter=name eq 'Aenor Tienda'")> 
4<#assign channel = channelResponse.items[0]> 
5<#assign channelId = channel.id> 
6<#assign product = getProduct(channelId, CPDefinition_cProductId.getData()) /> 
7<#assign siteGroup = themeDisplay.getSiteGroup() /> 
8<#assign currentLocale = themeDisplay.getLocale()> 
9<#assign currentLanguage = currentLocale?substring(0,2)> 
10 
11<#-- Product data --> 
12<#assign displayDateProduct = CPDefinition_displayDate.getData() /> 
13<#assign productId = product.productId /> 
14<#assign cpDefinitionId = product.id /> 
15 
16<#assign categoriesProduct = getProductCategories(channelId, productId) /> 
17<#assign hasProductCategoriaTipoEntidadLibro = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'libro') /> 
18<#assign hasProductCategoriaTipoEntidadNorma = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'norma') /> 
19<#assign hasProductCategoriaTipoEntidadColeccionTematica = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'coleccion tematica') /> 
20<#assign categoryProductInfoTematica = getCategoriesByVocabularyAsString(categoriesProduct, "temáticas", " / ", "title") /> 
21<#assign categoryProductInfoStatus = getCategoriesByVocabularyAsString(categoriesProduct, "status", " / ", "title") /> 
22 
23<#-- PickList de languages --> 
24<#assign ercOfListTypeEntryLanguages = 'IDIOMAS_NORMAS_PICKLIST' /> 
25<#assign languageEntries = getListTypeEntriesByERC(ercOfListTypeEntryLanguages) /> 
26 
27<#-- Specifications/Specifications language --> 
28<#assign specificationsLanguagesProduct = getSpecificationsProduct(channelId, productId, 'specificationKey', 'standard-languages') /> 
29<#assign filteredSpecificationsLanguagesProductTemp = filterOutItems(specificationsLanguagesProduct, 'value', ['BI', 'TR']) /> 
30<#assign filteredSpecificationsLanguagesProduct = addAllMatchingLanguagesByField(filteredSpecificationsLanguagesProductTemp, languageEntries, 'value', 'title') /> 
31 
32<#-- Standard/Norma data //TODO: usar api endpoint coleccion_tematica custom object 
33    <#assign standardRelationsProduct = getStandardRelationsProduct(cpDefinitionId) /> 
34    <#assign standardInfoProduct = getStandardInfoProduct(cpDefinitionId) /> 
35--> 
36 
37 
38<#-- Functions --> 
39<#function getProduct channelId productId> 
40    <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}")> 
41</#function> 
42 
43 
44<#function getProductByERC erc> 
45    <#-- TODO: estamos usando el endpoint del product admin, hay que usar la del Product de Liferay NO admin: headless-commerce-delivery-catalog --> 
46    <#-- <#return restClient.get("/headless-commerce-admin-catalog/v1.0/products/by-externalReferenceCode/${erc}")> --> 
47    <#assign response = restClient.get("/headless-commerce-admin-catalog/v1.0/products/by-externalReferenceCode/${erc}")> 
48    <#-- Si el producto no existe o tiene status NOT_FOUND, devolvemos string vacío --> 
49    <#if response?? && response.status?? && response.status == "NOT_FOUND"> 
50        <#return {}> 
51    <#elseif !response??> 
52        <#return {}> 
53    <#else> 
54        <#return response> 
55    </#if> 
56</#function> 
57 
58 
59<#function getURLsOfProduct product baseURL="" siteFriendlyURL="" languageFieldName="language" urlFieldName="url"> 
60    <#-- Inicializamos un array vacío --> 
61    <#assign urlsArray = []> 
62 
63    <#-- Validamos que product y product.urls existan --> 
64    <#if product?? && product.urls??> 
65        <#-- Iteramos sobre los idiomas --> 
66        <#list product.urls?keys as lang> 
67            <#assign urlValue = product.urls[lang] /> 
68            <#if urlValue?? && urlValue?has_content> 
69                <#-- Aseguramos que la URL empiece con "/p/" --> 
70                <#assign cleanUrl = urlValue?starts_with("/")?then(urlValue, "/p/" + urlValue) /> 
71 
72                <#-- Generamos la URL completa --> 
73                <#-- Si baseURL tiene contenido, construimos URL completa --> 
74                <#if baseURL?? && baseURL?has_content> 
75 
76                    <#-- Aseguramos que el slug empiece con /p/ --> 
77                    <#assign cleanUrl = urlValue?starts_with("/")?then(urlValue, "/p/" + urlValue) /> 
78 
79                    <#-- Tomamos las dos primeras letras del idioma para el prefijo --> 
80                    <#assign langPrefix = lang?substring(0,2)> 
81 
82                    <#-- Construimos la URL completa --> 
83                    <#if siteFriendlyURL?? && siteFriendlyURL?has_content> 
84                        <#assign fullUrl = baseURL + "/" + langPrefix + siteFriendlyURL + cleanUrl> 
85                    <#else> 
86                        <#assign fullUrl = baseURL + "/" + langPrefix + cleanUrl> 
87                    </#if> 
88 
89                <#else> 
90                    <#assign fullUrl = cleanUrl> 
91                </#if> 
92 
93                <#-- Creamos el objeto language+url --> 
94                <#assign newItem = {(languageFieldName): lang, (urlFieldName): fullUrl} /> 
95 
96                <#-- Lo agregamos al array --> 
97                <#assign urlsArray = urlsArray + [newItem] /> 
98            </#if> 
99        </#list> 
100    </#if> 
101 
102    <#return urlsArray> 
103</#function> 
104 
105 
106<#function getProductCategories channelId productId> 
107    <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}/categories?sort=vocabulary").items> 
108</#function> 
109 
110 
111<#function getSpecificationsProduct channelId productId field="" value=""> 
112    <#assign response = restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}/product-specifications?pageSize=100&sort")> 
113    <#assign items = response.items> 
114 
115    <#-- Si se pasa campo y valor, filtrar --> 
116    <#if field?has_content && value?has_content> 
117        <#assign items = items?filter(item -> 
118            (item[field]??) && (item[field]?string?lower_case == value?lower_case) 
119        )> 
120    </#if> 
121 
122    <#return items> 
123</#function> 
124 
125 
126<#function filterOutItems items field valuesToExclude> 
127    <#-- Si el array o los valores no existen, devolver items sin cambios --> 
128    <#if !items?? || !valuesToExclude??> 
129        <#return items> 
130    </#if> 
131 
132    <#-- Filtramos: solo mantener los items cuyo campo no esté en la lista --> 
133    <#assign filteredItems = items?filter(item -> 
134        !(item[field]?? && (valuesToExclude?seq_contains(item[field]?string))) 
135    )> 
136 
137    <#return filteredItems> 
138</#function> 
139 
140 
141<#function addAllMatchingLanguagesByField specificationsLanguagesProduct languageEntries specValueField newSpecField> 
142 
143    <#if specificationsLanguagesProduct?? && languageEntries?? && specValueField?? && newSpecField??> 
144 
145        <#-- Creamos una copia para no modificar el original directamente --> 
146        <#assign updatedSpecs = []> 
147 
148        <#list specificationsLanguagesProduct as spec> 
149            <#-- Buscar coincidencia --> 
150            <#assign keyValue = spec[specValueField]> 
151            <#assign match = languageEntries?filter(entry -> entry.key == keyValue)?first> 
152 
153            <#-- Crear una copia del objeto actual --> 
154            <#assign newSpec = spec> 
155 
156            <#-- Si hay match con nombre válido, añadir el nuevo campo --> 
157            <#if match?? && match.name?? && match.name?has_content> 
158                <#assign newSpec = newSpec + { (newSpecField): match.name }> 
159            </#if> 
160 
161            <#-- Añadir al array final --> 
162            <#assign updatedSpecs = updatedSpecs + [newSpec]> 
163        </#list> 
164 
165        <#return updatedSpecs> 
166    <#else> 
167        <#return specificationsLanguagesProduct> 
168    </#if> 
169</#function> 
170 
171 
172<#function getListTypeEntriesByERC erc fields="key,name,externalReferenceCode,name_i18n" sort="key" pageSize="1000"> 
173    <#attempt> 
174        <#return restClient.get( 
175            "/headless-admin-list-type/v1.0/list-type-definitions/by-external-reference-code/${erc}/list-type-entries?fields=${fields}&sort=${sort}&pageSize=${pageSize}" 
176        ).items> 
177    <#recover> 
178        <#return []> 
179    </#attempt> 
180</#function> 
181 
182 
183<#-- Standard/Norma data //TODO: usar api endpoint coleccion_tematica custom object 
184 
185    <#function getStandardInfoProduct cpDefinitionId> 
186        <#assign response = restClient.get("/c/standardinfos/?filter=r_standardInfo_CPDefinitionId eq '${cpDefinitionId}'")!{} /> 
187        <#assign items = response.items![] /> 
188        <#return (items?size > 0)?then(items[0], {}) /> 
189    </#function> 
190 
191--> 
192 
193 
194<#function sortByField items fieldPath sortFieldName="sortKey" order="asc"> 
195 
196    <#-- Array temporal con el campo auxiliar --> 
197    <#assign prepared = []> 
198 
199    <#list items as e> 
200        <#-- Evaluar el valor del campo dinámico --> 
201        <#assign sortValue = "" /> 
202        <#if fieldPath == "section?first.key"> 
203            <#assign sortValue = (e.section?first.key)!""?lower_case> 
204        <#elseif fieldPath == "section?first.name"> 
205            <#assign sortValue = (e.section?first.name)!""?lower_case> 
206        <#elseif fieldPath == "type?first.key"> 
207            <#assign sortValue = (e.type?first.key)!""?lower_case> 
208        <#else> 
209            <#-- Si el campoPath no está mapeado, usar vacío --> 
210            <#assign sortValue = "" /> 
211        </#if> 
212 
213        <#-- Agregar objeto enriquecido con campo auxiliar --> 
214        <#assign prepared = prepared + [ e + { (sortFieldName): sortValue } ]> 
215    </#list> 
216 
217    <#-- Ordenar --> 
218    <#assign sorted = prepared?sort_by(sortFieldName)> 
219 
220    <#-- Si order = desc, invertir --> 
221    <#if order?lower_case == "desc"> 
222        <#assign sorted = sorted?reverse> 
223    </#if> 
224 
225    <#return sorted> 
226</#function> 
227 
228 
229<#function addKeysFieldNested items arrayField nestedField newFieldName mode="first" subArrayField=""> 
230 
231    <#assign enrichedItems = []> 
232 
233    <#list items as e> 
234        <#assign resultValue = ""> 
235 
236        <#-- Detectamos el array del objeto principal --> 
237        <#assign targetArray = e[arrayField]![]> 
238 
239        <#if targetArray?has_content> 
240            <#if mode == "all"> 
241                <#assign keysList = []> 
242                <#list targetArray as t> 
243                    <#if subArrayField?has_content> 
244                        <#assign subArray = t[subArrayField]![]> 
245                        <#if subArray?has_content> 
246                            <#list subArray as sub> 
247                                <#assign keysList = keysList + [ sub[nestedField]!"" ]> 
248                            </#list> 
249                        </#if> 
250                    <#else> 
251                        <#assign keysList = keysList + [ t[nestedField]!"" ]> 
252                    </#if> 
253                </#list> 
254                <#assign resultValue = keysList?join(", ")> 
255            <#elseif mode == "first"> 
256                <#assign firstItem = targetArray?first> 
257                <#if subArrayField?has_content> 
258                    <#assign subArray = firstItem[subArrayField]![]> 
259                    <#if subArray?has_content> 
260                        <#assign resultValue = subArray?first[nestedField]!"" > 
261                    <#else> 
262                        <#assign resultValue = "" > 
263                    </#if> 
264                <#else> 
265                    <#assign resultValue = firstItem[nestedField]!"" > 
266                </#if> 
267            </#if> 
268        <#else> 
269            <#assign resultValue = ""> 
270        </#if> 
271 
272        <#-- Añadimos el nuevo campo al objeto --> 
273        <#assign enrichedItems = enrichedItems + [ 
274            e + { (newFieldName): resultValue } 
275        ]> 
276    </#list> 
277 
278    <#return enrichedItems> 
279</#function> 
280 
281 
282<#function isVocabularyNameIntoCategories categories vocabulary name> 
283    <#assign found = false /> 
284 
285    <#if categories?has_content && vocabulary?has_content && name?has_content> 
286 
287        <#assign vocabNorm = normalize(vocabulary) /> 
288        <#assign nameNorm  = normalize(name) /> 
289 
290        <#list categories as category> 
291            <#if !found> 
292                <#assign catVocabNorm = normalize(category.vocabulary) /> 
293                <#assign catNameNorm  = normalize(category.name) /> 
294 
295                <#if catVocabNorm == vocabNorm && catNameNorm == nameNorm> 
296                    <#assign found = true /> 
297                </#if> 
298            </#if> 
299        </#list> 
300 
301    </#if> 
302 
303    <#return found> 
304</#function> 
305 
306 
307<#function normalize text onlyAccents = false> 
308    <#-- proteger null --> 
309    <#if !text?has_content> 
310        <#return ""> 
311    </#if> 
312 
313    <#assign t = text /> 
314 
315    <#-- quitar acentos --> 
316    <#assign t = t 
317        ?replace("á","a")?replace("é","e")?replace("í","i") 
318        ?replace("ó","o")?replace("ú","u")?replace("ü","u") 
319        ?replace("ñ","n") 
320        ?replace("Á","A")?replace("É","E")?replace("Í","I") 
321        ?replace("Ó","O")?replace("Ú","U")?replace("Ü","U") 
322        ?replace("Ñ","N") 
323    /> 
324 
325    <#-- si NO es solo acentos, normalización completa --> 
326    <#if !onlyAccents> 
327        <#assign t = t?lower_case /> 
328        <#assign t = t?trim /> 
329        <#assign t = t?replace("\\s+", " ", "r") /> 
330    </#if> 
331 
332    <#return t> 
333</#function> 
334 
335 
336<#-- Función que retorna los valores de categorías como string, normalizando vocabulario --> 
337<#function getCategoriesByVocabularyAsString categories vocabulary separator=" / " field="name"> 
338    <#assign matchedValues = []> 
339    <#if categories?has_content && vocabulary?has_content> 
340        <#list categories as category> 
341            <#-- Normalizamos tanto el vocabulary de la categoría como el buscado --> 
342            <#if normalize(category.vocabulary, true)?lower_case == normalize(vocabulary, true)?lower_case> 
343                <#if field == "title"> 
344                    <#assign matchedValues += [category.title]> 
345                <#else> 
346                    <#assign matchedValues += [category.name]> 
347                </#if> 
348            </#if> 
349        </#list> 
350    </#if> 
351    <#return matchedValues?join(separator)> 
352</#function> 
353 
354 
355<#macro printObject obj> 
356    <#-- Permite hacer un output de un array de objetos o un objeto que se pasa como parámetro --> 
357    <#if obj?is_hash> 
358
359        <#list obj?keys as k> 
360            "${k}": 
361            <#assign value = obj[k]> 
362            <#if value?is_hash || value?is_sequence> 
363                <@printObject obj=value/> 
364            <#elseif value?is_boolean> 
365                ${value?c} 
366            <#elseif value?is_number> 
367                ${value} 
368            <#elseif value?has_content> 
369                "${value?string}" 
370            <#else> 
371                null 
372            </#if> 
373            <#if k_has_next>, </#if> 
374        </#list> 
375
376    <#elseif obj?is_sequence> 
377
378        <#list obj as item> 
379            <@printObject obj=item/> 
380            <#if item_has_next>, </#if> 
381        </#list> 
382
383    <#elseif obj?is_boolean> 
384        ${obj?c} 
385    <#elseif obj?is_number> 
386        ${obj} 
387    <#elseif obj?has_content> 
388        "${obj?string}" 
389    <#else> 
390        null 
391    </#if> 
392</#macro> 
393 
394 
395 
396 
397<#-- HTML --> 
398<#if hasProductCategoriaTipoEntidadColeccionTematica> 
399    <div id="ecom-coleccion_tematica-detail" class="ecom-coleccion_tematica"> 
400 
401        <div class="tabs-section"> 
402            <div class="tab-content"> 
403 
404                <#-- Fecha edicion del producto > description del Producto --> 
405                <div class="ecom-coleccion_tematica-detail-body"> 
406                    ${product.description} 
407                </div> 
408 
409                <#-- Para imprimir el contenido de los Objects --> 
410                <#-- Standard/Norma data //TODO: usar api endpoint coleccion_tematica custom object 
411                <#if isDebug> 
412                    <div class=""> 
413                        <p><strong>standardRelationsProduct:</strong></p> 
414                        <div class="print-object-json-content mb-3" style="max-height:200px; overflow:auto; border:1px solid #ccc; padding:5px; cursor:pointer;" 
415                             onclick="const r=document.createRange(); r.selectNodeContents(this); const s=window.getSelection(); s.removeAllRanges(); s.addRange(r);"> 
416                            <@printObject standardRelationsProduct /> 
417                        </div> 
418                    </div> 
419                 </#if> 
420                 --> 
421                  
422            </div> 
423        </div> 
424    </div> 
425</#if> 

El libro en palabras del autor

Ultricies magna feugiat malesuada sociosqu varius vivamus cubilia parturient, himenaeos vitae vehicula nam placerat netus urna platea, nostra rutrum felis mattis penatibus velit quisque. LIBRO.

CTA

El libro en palabras del autor

Ultricies magna feugiat malesuada sociosqu varius vivamus cubilia parturient, himenaeos vitae vehicula nam placerat netus urna platea, nostra rutrum felis mattis penatibus velit quisque. COLECCION TEMATICA

CTA
Preguntas frecuentes ¿Tienes alguna duda sobre nuestros productos?

Desde la web

Libros y normas