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

DIN EN ISO/IEEE 11073-10103:2025-06

Health informatics - Device interoperability - Part 10103: Nomenclature - Implantable device, cardiac (ISO/IEEE FDIS 11073-10103:2025); English version prEN ISO/IEEE 11073-10103:2025 / Note: Date of issue 2025-05-02

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

Button
Preguntas frecuentes ¿Tienes alguna duda sobre nuestros productos?

Respuesta 2

Desde la web

Libros y normas