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

DIN EN ISO 16961:2015-12

Petroleum, petrochemicals and natural gas industries - Internal coating and lining of steel storage tanks (ISO 16961:2015); English version EN ISO 16961:2015

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

Las normas pueden adquirirse en PDF, lectura o papel. Las normas en lectura no son archivos de descarga, solo pueden visualizarse en el área de cliente. Las normas solicitadas en papel y algunos de los libros en catálogo se imprimen bajo demanda. 

Consultar plazos en normas@aenor.com​​.

La licencia de uso es para un usuario y un dispositivo, si deseas reproducir el contenido de la norma, debes solicitar una licencia que tendrá un coste adicional. Envíanos tu consulta aquí 

Las normas y libros de AENOR que aparecen en la tienda on-line solo se pueden comprar exclusivamente a través de la web. AENOR no dispone de tienda física.​

Procedimiento de compra: haciendo clic en “Comprar” los productos deseados irán a la cesta de la compra. Si hubiera problemas de visualización el navegador recomendado es Chrome.

Para formalizar la compra debe acceder al área de cliente. En caso de no estar registrado como cliente, se deberá rellenar un formulario con los datos junto con una clave y usuario. De este modo, quedará creada la cuenta.

Una vez cumplimentado el formulario “Datos del cliente” se podrá visualizar “Pedido en curso” con todos los artículos cargados en la cesta de la compra, sus precios, impuestos establecidos en la legislación vigente y gastos de envío si fueran de aplicación.

Los precios de las normas y libros que aparecen en las diversas secciones no incluyen impuestos, ni gastos de envío.​

Los códigos promocionales de AENOR constan de caracteres alfanuméricos y solo se podrán aplicar en la compra en web, recibir a través de una oferta específica y durante un tiempo limitado. Para aplicar tu código promocional, solo tienes que introducirlo en el paso 2 de 4 del proceso de compra en la web y clicar en “aplicar”, después de haberte identificado y elegido las formas de pago. Los códigos promocionales no son acumulables.

 

  • Tarjeta de crédito o débito (Visa, Mastercard) y PayPal.
  • Transferencia b​ancaria. En caso de optar por esta forma de pago es necesario enviar previamente a AENOR copia de la transferencia por correo electrónico a normas@aenor.com​​​
  • La factura de compra se puede descargar desde el área de cliente, en mis pedidos anteriores

​​En el caso de los ​clientes de empresas con sede ​en el extranjero, el número de identificación del contribuyente del país correspondiente (por ejemplo, en Argentina el CUIT), deberá rellenarse en el campo CIF/NIF - VAT.

  • Descarga directa a través de la web en el Área de clientes. En el área de clientes al que solo se puede acceder con clave y usuario, estarán disponibles los productos adquiridos durante un periodo de quince días desde la fecha de la compra, siempre y cuando el pago haya sido aceptado. Los archivos en formato digital están protegidos y en ningún caso son editables. Antes de adquirirlos, es importante que la licencia de uso sea leída y aceptada como paso previo a la compra.
  • Envío por mensajería. Los productos comprados en soporte físico se envían por mensajería. El plazo máximo de entrega en el territorio español, desde la aceptación del pedido por parte AENOR es de:
  •  Siete días laborables aproximados para todas las normas compradas a través de la tienda en soporte papel.
  • Tres días laborables aproximados para libros comprados a través de la tienda. Las existencias de los libros en papel son limitadas y su oferta en la web no implica disponibilidad en el plazo indicado. En el caso de no disponer del libro solicitado, se avisa al cliente de la demora en la recepción del pedido que será aproximadamente de siete días laborables. 

Para el resto de productos que no están en la web, consultar disponibilidad y plazo de entrega en normas@aenor.com.

1. Para los productos digitales (PDF, Epub), una vez realizada la entrega mediante descarga directa a través de la web en el Área de clientes, no tendrás derecho a ejercer tu derecho de desistimiento.

2. Para los productos personalizados en soporte papel, una vez realizada la compra, no tendrás derecho a ejercer tu derecho de desistimiento.

3.  Para el resto de productos en soporte papel, dispones del derecho a desistir de la compraventa en un plazo de 14 días naturales a contar desde la fecha de la compra. Recuerda que para la devolución es imprescindible que el producto se encuentre en perfectas condiciones, precintado por el envoltorio y conservando su embalaje original. El cliente será responsable de la recogida y los gastos de envío.

La factura del pedido incluye los gastos de envío​, por lo que no hay que abonar ningún importe al mensajero. ​Los gastos de envío se calculan en función tanto del destino final del pedido como del número de productos solicitados. Incluyen gastos de transporte y embalaje. Los gastos de envío están sujetos a revisiones periódicas. Los libros outlet tendrán gastos de envío gratuitos solo si el envío se realiza en Península.

​Destino ​Hasta tres normas y/o publicaciones ​A partir de tres normas y/o publicaciones
​Península ​ 7,31€ ​8,60€
​Baleares ​ 18,04€ ​23,34€
​Canarias, Ceuta y Melilla  ​​​18,04€ ​​23,34€
​Europa ​ 59,17€ ​80,07€
​Estados Unidos y Canadá ​ 70,07€ ​96,94€
​Resto del mundo ​ 91,94€ ​115,91€​​
  • ​​​​​Las compras realizadas por residentes en los Estados miembros de la Unión Europea estarán sujetas al pago de IVA (impuesto sobre el valor añadido).​
  • ​​
  • ​​En el caso de personas jurídicas y personas físicas que, actuando como empresarios, tengan su domicilio en algún Estado miembro de la Unión Europea (excepto residentes en España) y posean NIF/VAT intracomunitario inscrito en el censo VIES, estarán exentas del pago del IVA, siendo condición imprescindible el envío de dicho documento por correo electrónico a ​normas@aenor.com​​​.
  • Las compras realizadas a título particular (persona física), independientemente de donde tengan su residencia, estarán sujetas al pago del ​IVA.
  • L​as compras realizadas por entidades en países extracomunitarios estarán exentas del pago del IVA, siempre y cuando envíen el correspondiente documento de residencia fiscal por correo electrónico a normas@aenor.com​.
  • Las operaciones de venta se entenderán realizadas en el domicilio social de AENOR: Génova 6, 28004, Madrid – España.​

El contrato de compra de productos a través de este Sitio Web se regirá por la legislación española. Cualquier controversia que surja o guarde relación con el uso del Sitio Web o con dicho contrato será sometida a la jurisdicción exclusiva de los Juzgados y Tribunales de Madrid Capital.

No obstante lo anterior, si estás contratando como consumidor en los términos del Real Decreto 1/2007, nada en la presente cláusula afectará a los derechos que como tal te pudiera reconocer la legislación vigente.