Se ha producido un error inesperado.
Se ha producido un error inesperado.
Se ha producido un error al procesar la plantilla.
The following has evaluated to null or missing:
==> product.productId  [in template "34352066712900#33336#null" at line 12, column 22]

----
Tip: It's the step after the last dot that caused this error, not those before it.
----
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 productId = product.productId  [in template "34352066712900#33336#null" at line 12, column 1]
----
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 
8<#-- Product data --> 
9<#assign displayDateProduct = CPDefinition_displayDate.getData() /> 
10<#assign productId = product.productId /> 
11<#assign cpDefinitionId = product.id /> 
12<#assign productERC = product.externalReferenceCode /> 
13<#assign normaIdOnly = productERC?replace("^[^-]*-", "", "r") /> 
14 
15<#assign categoriesProduct = getProductCategories(channelId, productId) /> 
16<#assign hasProductCategoriaTipoEntidadLibro = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'libro') /> 
17<#assign hasProductCategoriaTipoEntidadNorma = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'norma') /> 
18<#assign hasProductCategoriaTipoEntidadColeccionTematica = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'coleccion tematica') /> 
19<#assign organism = getOrganism(categoriesProduct, 'organismos') /> 
20 
21 
22 
23<#-- Functions --> 
24<#function getProductCategories channelId productId> 
25    <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}/categories?sort=vocabulary").items> 
26</#function> 
27 
28 
29<#function isVocabularyNameIntoCategories categories vocabulary name> 
30    <#assign found = false /> 
31 
32    <#if categories?has_content && vocabulary?has_content && name?has_content> 
33 
34        <#assign vocabNorm = normalize(vocabulary) /> 
35        <#assign nameNorm  = normalize(name) /> 
36 
37        <#list categories as category> 
38            <#if !found> 
39                <#assign catVocabNorm = normalize(category.vocabulary) /> 
40                <#assign catNameNorm  = normalize(category.name) /> 
41 
42                <#if catVocabNorm == vocabNorm && catNameNorm == nameNorm> 
43                    <#assign found = true /> 
44                </#if> 
45            </#if> 
46        </#list> 
47 
48    </#if> 
49 
50    <#return found> 
51</#function> 
52 
53 
54<#function getProduct channelId productId> 
55    <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}")> 
56</#function> 
57 
58 
59<#function getOrganism categories vocabulary> 
60    <#assign organism = ""> 
61    <#assign vocabNorm = normalize(vocabulary)> 
62 
63    <#list categories as category> 
64            <#assign catVocabNorm = normalize(category.vocabulary) /> 
65            <#if catVocabNorm == vocabNorm> 
66                <#assign organism = category.name> 
67                <#if organism == "EN" || organism == "CEN" || organism == "CENELEC"> 
68                    <#assign organism = "UNE"> 
69                </#if> 
70            </#if> 
71    </#list> 
72    <#return organism> 
73</#function> 
74 
75 
76<#function normalize text onlyAccents = false> 
77    <#-- proteger null --> 
78    <#if !text?has_content> 
79        <#return ""> 
80    </#if> 
81 
82    <#assign t = text /> 
83 
84    <#-- quitar acentos --> 
85    <#assign t = t 
86        ?replace("á","a")?replace("é","e")?replace("í","i") 
87        ?replace("ó","o")?replace("ú","u")?replace("ü","u") 
88        ?replace("ñ","n") 
89        ?replace("Á","A")?replace("É","E")?replace("Í","I") 
90        ?replace("Ó","O")?replace("Ú","U")?replace("Ü","U") 
91        ?replace("Ñ","N") 
92    /> 
93 
94    <#-- si NO es solo acentos, normalización completa --> 
95    <#if !onlyAccents> 
96        <#assign t = t?lower_case /> 
97        <#assign t = t?trim /> 
98        <#assign t = t?replace("\\s+", " ", "r") /> 
99    </#if> 
100 
101    <#return t> 
102</#function> 
103 
104 
105<#-- Div with data --> 
106<#-- Se incluye desde dxp-ecom-portal/misc/adt/tienda/Detalle producto/ECOM-Generic-Scripts.ftl --> 
107<#-- 
108    <#if hasProductCategoriaTipoEntidadNorma> 
109        <div id="ecom-scripts"> 
110            <div class="data-product-wrapper" 
111                data-product-id="${productId}" 
112                data-product-erc="${productERC}" 
113                data-product-cpdefinition-id="${cpDefinitionId}" 
114                data-channel-id="${channelId}" 
115                data-product-islibro="${hasProductCategoriaTipoEntidadLibro?c}" 
116                data-product-isnorma="${hasProductCategoriaTipoEntidadNorma?c}" 
117                data-product-iscolecciontematica="${hasProductCategoriaTipoEntidadColeccionTematica?c}" 
118            /> 
119        </div> 
120    </#if> 
121--> 
122 
123 
124<#-- Script JS --> 
125<#if hasProductCategoriaTipoEntidadNorma> 
126 
127    <script id="ecom-norma-scripts"> 
128 
129        window.ecomNormaScripts = window.ecomNormaScripts || (function () { 
130 
131            /* ===================================== 
132               PROPIEDADES PRIVADAS 
133            ===================================== */ 
134            const privateProps = window.ecomNormaScripts?.privateProps || { 
135                //test1: {}, 
136                //test2: 0 
137            }; 
138 
139             /* ===================================== 
140               INICIALIZACIÓN DE PROPERTIES PRIVADAS 
141            ===================================== */ 
142            (function initPrivateProps() { 
143               /* privateProps.test1 = { 
144                    test11: "", 
145                    test111: { 
146                        test1112: "00,00", 
147                        test1113: "€", 
148                        test1114: "00", 
149                        test1115: "000-00-000", 
150                        test1116: "0000", 
151                        test1117: "000" 
152                    }, 
153                    test2: "" 
154                };*/ 
155            })(); 
156 
157            /* ===================================== 
158               PROPIEDADES PÚBLICAS 
159            ===================================== */ 
160 
161            const publicProps = { 
162                funcsEcomGlobalScripts: null, 
163                propsEcomGlobalScripts: null, 
164                isDebug: false 
165            }; 
166 
167            /* ===================================== 
168               INICIALIZACIÓN DE PROPERTIES PÚBLICAS 
169            ===================================== */ 
170            (function initPublicProps() { 
171 
172                //publicProps.TEST1 = ""; 
173                //publicProps.TEST2 = 0; 
174 
175            })(); 
176 
177            /* ===================================== 
178               FUNCIONES PÚBLICAS 
179            ===================================== */ 
180 
181            //En functions declaramos la functions publicas 
182            const functions = {}; 
183 
184            functions.init = async function () { 
185                await _initPublicProps(); 
186                await _DOMContentLoaded(); 
187            }; 
188 
189            /* ===================================== 
190               FUNCIONES PRIVADAS 
191            ===================================== */ 
192 
193            const _initPublicProps = async function() { 
194 
195                // Obtener referencias de ecomGlobalScripts 
196                publicProps.funcsEcomGlobalScripts = window.ecomGlobalScripts?.functions || {}; 
197 
198                publicProps.propsEcomGlobalScripts = window.ecomGlobalScripts?.properties || {}; 
199 
200                // Configurar debug 
201                publicProps.isDebug = (${isDebug?c} || publicProps.propsEcomGlobalScripts?.isDebug) ?? false; 
202 
203                // Asegurar que querySelectors existe (crearlo si no existe) 
204                publicProps.propsEcomGlobalScripts.querySelectors ??= {}; 
205 
206                // Agregar las propiedades querySelectors 
207                publicProps.propsEcomGlobalScripts.querySelectors.rootSelector = "#ecom-norma-detail-normas-referenced .standards-section"; 
208            }; 
209 
210            const _DOMContentLoaded = async function () { 
211                //Se llama desde [dxp-ecom-portal/misc/adt/tienda/Detalle producto/ECOM-Global-Scripts.ftl] desde: document.addEventListener("DOMContentLoaded") 
212 
213                if (publicProps.isDebug) console.log("DOMContentLoaded ecom Norma scripts"); 
214 
215                window.ecomGlobalScripts?.functions?.pushViewItemEvent?.({ 
216                    name: "${product.name?js_string}", 
217                    id: "${normaIdOnly?js_string}", 
218                    category: "Normas ${organism?js_string}" 
219                }); 
220            }; 
221 
222            // ---- Execute Listener DOMContentLoaded ---- 
223            document.addEventListener("DOMContentLoaded", async function() { 
224                //Se llama desde [dxp-ecom-portal/misc/adt/tienda/Detalle producto/ECOM-Global-Scripts.ftl] desde: document.addEventListener("DOMContentLoaded") 
225                //await _DOMContentLoaded(); 
226            }); 
227 
228            /* ===================================== 
229               API PÚBLICA 
230            ===================================== */ 
231            return { 
232                properties: publicProps, // properties public 
233                functions: functions     // functions public 
234            }; 
235 
236        })(); 
237 
238    </script> 
239 
240 
241    <script id="ecom-norma-referenced"> 
242        //TODO: repasar y quitar lo relacionado con selectores y precio. 
243 
244        /*TODO: 
245            -Se llama a la [async function init()] dentro de aqui [ecomNormaReferencedScript] 
246                desde fragment [dxp-ecom-portal/misc/fragments/Collection Tienda/ECOM-Global_functions/index.js] de la siguiente manera: 
247 
248                    if (typeof window.ecomNormaReferencedScript?.init === "function") { 
249                        await window.ecomNormaReferencedScript.init(); 
250
251        */ 
252 
253        window.ecomNormaReferencedScript = (function ($) { 
254 
255            /* === CONFIG === */ 
256            let DEBUG = false; 
257            let ROOT_SELECTOR = null; 
258 
259            function log(...args) { 
260                if (DEBUG) console.log("[ecomNorma]", ...args); 
261
262 
263            /* === FUNCIONES PRIVADAS === */ 
264 
265            function _euros(v) { 
266                const n = Number(v) || 0; 
267                return n.toLocaleString("es-ES", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " €"; 
268
269 
270            function _uniq(a) { 
271                return Array.from(new Set(a)); 
272
273 
274            function _fill($sel, values, textFn) { 
275                const keep = $sel.val(); 
276                $sel.empty(); 
277                values.forEach(v => { 
278                    $sel.append($("<option>", { value: v, text: textFn ? textFn(v) : v })); 
279                }); 
280                if (keep && values.includes(keep)) $sel.val(keep); 
281                else if (values.length) $sel.val(values[0]); 
282
283 
284            function _relabel($sel, textFn) { 
285                $sel.find("option").each(function () { 
286                    const v = $(this).attr("value"); 
287                    $(this).text(textFn ? textFn(v) : v); 
288                }); 
289
290 
291            /* === initCarousel — PÚBLICA === */ 
292            function initCarousel(force = false) { 
293                const $owl = $(ROOT_SELECTOR + " .standards-container"); 
294 
295                // no inicializar si no existe, está oculto o no tiene items 
296                if (!$owl.length || !$owl.hasClass("owl-carousel") || !$owl.is(':visible') || $owl.children().length === 0) { 
297                    log("initCarousel abortado: no visible o sin items", $owl); 
298                    return; 
299
300 
301                if (force && $owl.hasClass("owl-loaded") && $owl.data("owl.carousel")) { 
302                    log("initCarousel Destruyendo Owl Carousel existent:", $owl); 
303 
304                    $owl.trigger('destroy.owl.carousel'); 
305 
306                    // limpiar también owl-hidden para permitir reinicialización 
307                    $owl.removeClass('owl-loaded owl-hidden'); 
308 
309                    // limpiar markup de Owl 
310                    $owl.find('.owl-stage-outer').children().unwrap(); 
311
312 
313                if ($owl.length && !$owl.hasClass("owl-loaded") && !$owl.data("owl.carousel")) { 
314 
315                    $owl.owlCarousel({ 
316                        nav: true, 
317                        navText: [ 
318                            '<i class="fa-solid fa-chevron-left"></i>', 
319                            '<i class="fa-solid fa-chevron-right"></i>' 
320                        ], 
321                        loop: false, 
322                        dots: false, 
323                        pagination: false, 
324                        margin: 25, 
325                        autoHeight: false, 
326                        responsive: { 
327                            0: { items: 1 }, 500: { items: 1 }, 767: { items: 2 }, 1000: { items: 3 }, 1200: { items: 4 } 
328
329                    }); 
330 
331                    $owl.off("initialized.owl.carousel refreshed.owl.carousel resized.owl.carousel translated.owl.carousel") 
332                    .on("initialized.owl.carousel refreshed.owl.carousel resized.owl.carousel translated.owl.carousel", adjustHeights); 
333                    log("initCarousel executed"); 
334
335
336 
337            /* === initCard — PÚBLICA === */ 
338            function initCard($card) { 
339                const dataNode = $card.find("script.normas-data")[0]; 
340                let data = []; 
341                if (dataNode) { 
342                    try { data = JSON.parse(dataNode.textContent); } catch {} 
343
344                const hasData = Array.isArray(data) && data.length > 0; 
345 
346                let langMap = {}; 
347                const mapNode = $card.find("script.lang-map")[0]; 
348                try { if (mapNode) langMap = JSON.parse(mapNode.textContent); } catch {} 
349 
350                const $lang = $card.find(".select-language"); 
351                const $fmt = $card.find(".select-format"); 
352                const $price = $card.find(".price-ae"); 
353 
354                const toUC = s => (s || "").toString().toUpperCase(); 
355                const labelOf = lang => langMap[toUC(lang)] || lang; 
356 
357                const formatsFor = lang => _uniq(data.filter(d => d.language === lang).map(d => d.format)); 
358                const languagesFor = fmt => _uniq(data.filter(d => d.format === fmt).map(d => d.language)); 
359                const priceOf = (lang, fmt) => { 
360                    const m = data.find(d => d.language === lang && d.format === fmt); 
361                    return m ? Number(m.price) || 0 : 0; 
362                }; 
363                const parsePriceText = (text) => { 
364                    if (!text) return 0; 
365                    const clean = String(text).replace(/[^\d,.-]/g, "").trim(); 
366                    if (!clean) return 0; 
367                    if (clean.includes(",") && clean.includes(".")) { 
368                        const normalized = clean.replace(/\./g, "").replace(",", "."); 
369                        return Number(normalized) || 0; 
370
371                    const normalized = clean.replace(",", "."); 
372                    return Number(normalized) || 0; 
373                }; 
374 
375                if (hasData) { 
376                    function onLanguageChange() { 
377                        const lang = $lang.val(); 
378                        const fmts = formatsFor(lang); 
379                        _fill($fmt, fmts); 
380                        $price.text(_euros(priceOf(lang, $fmt.val()))); 
381
382 
383                    function onFormatChange() { 
384                        const fmt = $fmt.val(); 
385                        const langs = languagesFor(fmt); 
386                        _fill($lang, langs, labelOf); 
387                        $price.text(_euros(priceOf($lang.val(), fmt))); 
388
389 
390                    const l0 = $lang.val(); 
391                    const f0 = $fmt.val(); 
392                    if (!data.find(d => d.language === l0 && d.format === f0)) onLanguageChange(); 
393                    else $price.text(_euros(priceOf(l0, f0))); 
394 
395                    _relabel($lang, labelOf); 
396 
397                    $lang.off("change.ae").on("change.ae", onLanguageChange); 
398                    $fmt.off("change.ae").on("change.ae", onFormatChange); 
399
400 
401                async function onAdd(e) { 
402                    e.preventDefault(); 
403                    var entryType = ($card.data('entrytype') || 'Norma'); 
404                    var code = ($card.data('code') || '').toString(); 
405                    var codIdioma = ($card.find('.select-language').val() || '').toString(); 
406                    var codFormato = ($card.find('.select-format').val() || '').toString(); 
407                    if (!code) code = ($card.find('.title-standard').text() || '').trim(); 
408 
409                    await window.ecomGlobalScripts.functions.addToCart({ 
410                        entryType: entryType, 
411                        code: code, 
412                        codIdioma: codIdioma, 
413                        codFormato: codFormato, 
414                        price: Number(priceOf(codIdioma, codFormato)) || 0, 
415                        name: ($card.data('title') || '').toString(), 
416                        amount: 1 
417                    }); 
418
419 
420                $card.find(".standard-button").off("click.ae").on("click.ae", function(e){ 
421                    const btn = this; 
422                    const run = () => onAdd(e); 
423                    const helper = window.ecomGlobalScripts?.functions?.withButtonSpinner; 
424                    if (typeof helper === "function") { 
425                        return helper(btn, run, { spinnerClass: "spinner-border spinner-border-sm text-light", mode: "replace" }); 
426
427                    return run(); 
428                }); 
429                // Evita que Owl Carousel interprete el click como drag horizontal 
430                $card.find(".standard-button") 
431                    .off("pointerdown.owl mousedown.owl touchstart.owl") 
432                    .on("pointerdown.owl mousedown.owl touchstart.owl", function(e){ 
433                        e.stopPropagation(); 
434                    }); 
435
436 
437            /* === adjustHeights — PÚBLICA === */ 
438            function adjustHeights() { 
439                if ($(window).width() > 766.98) { 
440                    let hT = 0, hD = 0; 
441                    $(ROOT_SELECTOR + " .title-standard").css("height","auto").each(function(){ hT=Math.max(hT,$(this).outerHeight()); }); 
442                    $(ROOT_SELECTOR + " .description-text").css("height","auto").each(function(){ hD=Math.max(hD,$(this).outerHeight()); }); 
443                    $(ROOT_SELECTOR + " .title-standard").height(hT); 
444                    $(ROOT_SELECTOR + " .description-text").height(hD); 
445                } else { 
446                    $(ROOT_SELECTOR + " .title-standard, .description-text").css("height","auto"); 
447
448                log("adjustHeights executed"); 
449
450 
451            /* === init — PÚBLICA === */ 
452            async function init() { 
453 
454                // debug externo 
455                DEBUG = window.ecomNormaScripts?.properties?.isDebug === true; 
456 
457                // prefijo para todos los selectores 
458                ROOT_SELECTOR = 
459                    window.ecomGlobalScripts?.properties?.querySelectors?.rootSelector || 
460                    window.ecomNormaScripts?.properties?.querySelectors?.rootSelector || 
461                    "#ecom-norma-detail-normas-referenced .standards-section"; 
462 
463                log("window.ecomNormaReferencedScript init"); 
464 
465                $(ROOT_SELECTOR + " .item-standard").each(function(){ initCard($(this)); }); 
466                initCarousel(true); 
467                adjustHeights(); 
468                $(window).off("resize.standards").on("resize.standards", adjustHeights); 
469                log("init executed"); 
470
471 
472            /* === API pública === */ 
473            return { init, initCard, adjustHeights, initCarousel }; 
474 
475        })(jQuery); 
476 
477    </script> 
478 
479</#if> 
Se ha producido un error inesperado.

Nombre producto:

NF EN 14742

URL producto tienda:

{"nofollow":true,"url":"https://tienda-uat.aenor.com/e/normas-relacionadas/30025/183027448"}

CPDefinitionId producto:

183027448

CProductId producto:

183027449