An error occurred while processing the template.
The following has evaluated to null or missing:
==> product.productId  [in template "34352066712900#33336#null" at line 10, 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 10, 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 
14<#assign categoriesProduct = getProductCategories(channelId, productId) /> 
15<#assign hasProductCategoriaTipoEntidadLibro = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'libro') /> 
16<#assign hasProductCategoriaTipoEntidadNorma = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'norma') /> 
17<#assign hasProductCategoriaTipoEntidadColeccionTematica = isVocabularyNameIntoCategories(categoriesProduct, 'entity type', 'coleccion tematica') /> 
18 
19 
20<#-- Functions --> 
21<#function getProductCategories channelId productId> 
22    <#return restClient.get("/headless-commerce-delivery-catalog/v1.0/channels/${channelId}/products/${productId}/categories?sort=vocabulary").items> 
23</#function> 
24 
25 
26<#function isVocabularyNameIntoCategories categories vocabulary name> 
27    <#assign found = false /> 
28 
29    <#if categories?has_content && vocabulary?has_content && name?has_content> 
30 
31        <#assign vocabNorm = normalize(vocabulary) /> 
32        <#assign nameNorm  = normalize(name) /> 
33 
34        <#list categories as category> 
35            <#if !found> 
36                <#assign catVocabNorm = normalize(category.vocabulary) /> 
37                <#assign catNameNorm  = normalize(category.name) /> 
38 
39                <#if catVocabNorm == vocabNorm && catNameNorm == nameNorm> 
40                    <#assign found = true /> 
41                </#if> 
42            </#if> 
43        </#list> 
44 
45    </#if> 
46 
47    <#return found> 
48</#function> 
49 
50 
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 normalize text onlyAccents = false> 
57    <#-- proteger null --> 
58    <#if !text?has_content> 
59        <#return ""> 
60    </#if> 
61 
62    <#assign t = text /> 
63 
64    <#-- quitar acentos --> 
65    <#assign t = t 
66        ?replace("á","a")?replace("é","e")?replace("í","i") 
67        ?replace("ó","o")?replace("ú","u")?replace("ü","u") 
68        ?replace("ñ","n") 
69        ?replace("Á","A")?replace("É","E")?replace("Í","I") 
70        ?replace("Ó","O")?replace("Ú","U")?replace("Ü","U") 
71        ?replace("Ñ","N") 
72    /> 
73 
74    <#-- si NO es solo acentos, normalización completa --> 
75    <#if !onlyAccents> 
76        <#assign t = t?lower_case /> 
77        <#assign t = t?trim /> 
78        <#assign t = t?replace("\\s+", " ", "r") /> 
79    </#if> 
80 
81    <#return t> 
82</#function> 
83 
84 
85 
86 
87<#-- Div with data --> 
88<div id="ecom-scripts"> 
89    <div class="data-product-wrapper" 
90        data-product-id="${productId}" 
91        data-product-erc="${productERC}" 
92        data-product-cpdefinition-id="${cpDefinitionId}" 
93        data-channel-id="${channelId}" 
94        data-product-islibro="${hasProductCategoriaTipoEntidadLibro?c}" 
95        data-product-isnorma="${hasProductCategoriaTipoEntidadNorma?c}" 
96        data-product-iscolecciontematica="${hasProductCategoriaTipoEntidadColeccionTematica?c}" 
97    /> 
98</div> 
99 
100 
101<#-- Script JS --> 
102<script id="ecom-generic-scripts"> 
103 
104    window.ecomGenericScripts = window.ecomGenericScripts || (function () { 
105 
106        /* ===================================== 
107           PROPIEDADES PRIVADAS 
108        ===================================== */ 
109        const privateProps = window.ecomGenericScripts?.privateProps || {}; 
110 
111        /* ===================================== 
112           PROPIEDADES PÚBLICAS 
113        ===================================== */ 
114 
115        const publicProps = { 
116 
117            isDebug: ${isDebug?c} 
118 
119        }; 
120 
121        /* ===================================== 
122           FUNCIONES PÚBLICAS 
123        ===================================== */ 
124 
125        //En functions declaramos la functions publicas 
126        const functions = {}; 
127 
128        functions.init = async function () { 
129            await _DOMContentLoaded(); 
130        }; 
131 
132        /* ===================================== 
133           FUNCIONES PRIVADAS 
134        ===================================== */ 
135 
136        const _loadInit = async function () { 
137            if (publicProps.isDebug) console.log("DOMContentLoaded ecom Generic scripts"); 
138        }; 
139 
140        const _parsePriceText = function (text) { 
141            if (!text) return 0; 
142            const cleaned = String(text) 
143                .replace(/[^\d,.-]/g, "") 
144                .replace(/\./g, "") 
145                .replace(",", "."); 
146            const n = Number(cleaned); 
147            return Number.isFinite(n) ? n : 0; 
148        }; 
149 
150        const _getDetailContext = function (button) { 
151            const wrapper = document.querySelector("#ecom-scripts .data-product-wrapper"); 
152            const purchaseBox = button?.closest(".purchase-box") || document; 
153            const langSelect = purchaseBox.querySelector(".select-language"); 
154            const formatSelect = purchaseBox.querySelector(".select-format"); 
155            const qtyInput = purchaseBox.querySelector(".qty input[type='number']"); 
156            const priceNode = purchaseBox.querySelector(".price-header .price"); 
157            const titleNode = document.querySelector(".ecom-libro .title, .ecom-norma .title, .ecom-coleccion_tematica .title, .title-book"); 
158 
159            return { 
160                wrapper, 
161                purchaseBox, 
162                langSelect, 
163                formatSelect, 
164                qtyInput, 
165                priceNode, 
166                titleNode 
167            }; 
168        }; 
169 
170        const _getDetailData = async function (productERC, productId, isLibro, isNorma, isColeccionTematica) { 
171            if (!window.ecomGlobalScripts?.functions) return null; 
172            try { 
173                if (isLibro) { 
174                    if (productERC) return await window.ecomGlobalScripts.functions.getProductBooksByERC(productERC); 
175                    if (productId) return await window.ecomGlobalScripts.functions.getProductBooks(productId); 
176
177                if (isColeccionTematica) { 
178                    if (productERC) return await window.ecomGlobalScripts.functions.getProductThematicCollectionsByERC(productERC); 
179                    if (productId) return await window.ecomGlobalScripts.functions.getProductThematicCollections(productId); 
180
181                if (isNorma) { 
182                    if (productERC) return await window.ecomGlobalScripts.functions.getProductNormasDetailsByERC(productERC); 
183                    if (productId) return await window.ecomGlobalScripts.functions.getProductNormasDetails(productId); 
184
185            } catch (e) {} 
186            return null; 
187        }; 
188 
189        const _syncDetailBuyButtonState = function (purchaseBox) { 
190            if (!purchaseBox) return; 
191 
192            const btn = purchaseBox.querySelector(".buy-btn"); 
193            if (!btn) return; 
194 
195            const langSelect = purchaseBox.querySelector(".select-language"); 
196            const formatSelect = purchaseBox.querySelector(".select-format"); 
197            const hasSelectors = !!(langSelect || formatSelect); 
198            const hasSelection = !!(langSelect?.value && formatSelect?.value); 
199 
200            if (!hasSelectors) { 
201                if (!btn.disabled) btn.disabled = true; 
202                if (!btn.classList.contains("disabled")) btn.classList.add("disabled"); 
203                return; 
204
205 
206            if (btn.disabled === hasSelection) { 
207                btn.disabled = !hasSelection; 
208
209            btn.classList.toggle("disabled", !hasSelection); 
210        }; 
211 
212        const _setupQtyPriceMultiplier = function (purchaseBox) { 
213            if (!purchaseBox || purchaseBox.dataset.aeQtyMultBound === "true") return; 
214            purchaseBox.dataset.aeQtyMultBound = "true"; 
215 
216            const priceNode = purchaseBox.querySelector(".price-header .price"); 
217            const qtyInput = purchaseBox.querySelector(".qty input[type='number']"); 
218            if (!priceNode || !qtyInput) return; 
219 
220            const formatter = window.ecomGlobalScripts?.properties?.language?.formatterPrice; 
221 
222            const splitPriceAndCurrency = function (text) { 
223                const m = String(text || "").match(/^(.*?)(\s*[^\d.,\s]+)\s*$/); 
224                if (m) return [m[1].trim(), m[2].trim()]; 
225                return [String(text || "").trim(), ""]; 
226            }; 
227 
228            const formatPrice = function (value, currency) { 
229                const numText = formatter 
230                    ? formatter.format(value) 
231                    : value.toLocaleString("es-ES", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 
232                return currency ? numText + " " + currency : numText; 
233            }; 
234 
235            const captureUnit = function () { 
236                const parts = splitPriceAndCurrency(priceNode.textContent); 
237                const parsed = _parsePriceText(parts[0]); 
238                if (parsed > 0) { 
239                    purchaseBox.dataset.aeUnitPrice = String(parsed); 
240                    purchaseBox.dataset.aeUnitCurrency = parts[1]; 
241
242            }; 
243 
244            let observer; 
245            const startObserve = function () { 
246                observer.observe(priceNode, { childList: true, characterData: true, subtree: true }); 
247            }; 
248 
249            const reapply = function () { 
250                const unit = Number(purchaseBox.dataset.aeUnitPrice) || 0; 
251                if (!unit) return; 
252                const qty = Math.max(1, parseInt(qtyInput.value, 10) || 1); 
253                const total = unit * qty; 
254                const currency = purchaseBox.dataset.aeUnitCurrency || ""; 
255                if (observer) observer.disconnect(); 
256                priceNode.textContent = formatPrice(total, currency); 
257                if (observer) startObserve(); 
258            }; 
259 
260            // Cuando _renderFragmentPrice (en ECOM-Global_functions/index.js) reescribe 
261            // .price con el precio unitario tras cambiar idioma/formato, recapturamos 
262            // el unitario y reaplicamos la cantidad actual. 
263            observer = new MutationObserver(function () { 
264                captureUnit(); 
265                reapply(); 
266            }); 
267            startObserve(); 
268 
269            qtyInput.addEventListener("input", reapply); 
270            qtyInput.addEventListener("change", reapply); 
271 
272            // Captura inicial por si el precio ya estaba renderizado antes de montar el observer. 
273            captureUnit(); 
274            reapply(); 
275        }; 
276 
277        const _bindDetailSelectorState = function (purchaseBox) { 
278            if (!purchaseBox) return; 
279 
280            const sync = function () { 
281                _syncDetailBuyButtonState(purchaseBox); 
282            }; 
283 
284            if (purchaseBox.dataset.aeSelectorsBound === "true") { 
285                sync(); 
286                return; 
287
288 
289            purchaseBox.dataset.aeSelectorsBound = "true"; 
290 
291            purchaseBox.addEventListener("change", function (event) { 
292                if (!event.target?.matches(".select-language, .select-format")) return; 
293                sync(); 
294            }); 
295 
296            const selectorsRoot = purchaseBox.querySelector(".selector-language_format") || purchaseBox; 
297            const observer = new MutationObserver(function () { 
298                sync(); 
299            }); 
300 
301            observer.observe(selectorsRoot, { 
302                childList: true, 
303                subtree: true 
304            }); 
305 
306            sync(); 
307        }; 
308 
309        const _attachDetailAddToCart = async function () { 
310            const buttons = document.querySelectorAll(".purchase-box .buy-btn"); 
311            if (!buttons.length) return; 
312 
313            buttons.forEach(btn => { 
314                _bindDetailSelectorState(btn.closest(".purchase-box")); 
315                _setupQtyPriceMultiplier(btn.closest(".purchase-box")); 
316 
317                if (btn.dataset.aeBound === "true") return; 
318                btn.dataset.aeBound = "true"; 
319 
320                btn.addEventListener("click", (e) => { 
321                    e.preventDefault(); 
322 
323                    const run = async () => { 
324                        const ctx = _getDetailContext(btn); 
325                        const wrapper = ctx.wrapper; 
326                        if (!wrapper) { 
327                            window.dispatchEvent(new CustomEvent("cart:error", { detail: { message: "Error al añadir el producto a la cesta" } })); 
328                            return; 
329
330 
331                        const productId = Number(wrapper.dataset.productId) || null; 
332                        const productERC = wrapper.dataset.productErc || ""; 
333                        const isLibro = wrapper.dataset.productIslibro === "true"; 
334                        const isNorma = wrapper.dataset.productIsnorma === "true"; 
335                        const isColeccionTematica = wrapper.dataset.productIscolecciontematica === "true"; 
336 
337                        const codIdioma = (ctx.langSelect?.value || "").toString(); 
338                        const codFormato = (ctx.formatSelect?.value || "").toString(); 
339 
340                        if ((ctx.langSelect || ctx.formatSelect) && (!codIdioma || !codFormato)) { 
341                            window.dispatchEvent(new CustomEvent("cart:error", { detail: { message: "Selecciona idioma y formato" } })); 
342                            return; 
343
344 
345                        const entryType = isLibro ? "Libro" : isColeccionTematica ? "Colección Temática" : "Norma"; 
346                        const code = productERC || (ctx.titleNode?.textContent || "").trim(); 
347                        const qty = Math.max(1, parseInt(ctx.qtyInput?.value, 10) || 1); 
348                        const name = (ctx.titleNode?.textContent || "").trim(); 
349 
350                        let priceToSend = 0; 
351                        const data = await _getDetailData(productERC, productId, isLibro, isNorma, isColeccionTematica); 
352                        if (Array.isArray(data) && data.length && codIdioma && codFormato) { 
353                            const entry = data.find(item => { 
354                                const langKey = item.codLanguage ?? item.language; 
355                                const fmtKey = item.codFormat ?? item.format; 
356                                return String(langKey) === codIdioma && String(fmtKey) === codFormato; 
357                            }); 
358                            if (entry) { 
359                                const basePrice = Number(entry.price) || 0; 
360                                const discount = Number(entry.webDiscount) || 0; 
361                                priceToSend = discount > 0 ? Math.floor((basePrice - (basePrice * discount / 100)) * 100) / 100 : basePrice; 
362
363
364                        if (!priceToSend && ctx.priceNode) { 
365                            priceToSend = _parsePriceText(ctx.priceNode.textContent); 
366
367 
368                        await window.ecomGlobalScripts.functions.addToCart({ 
369                            entryType: entryType, 
370                            code: code, 
371                            codIdioma: codIdioma, 
372                            codFormato: codFormato, 
373                            price: priceToSend, 
374                            name: name, 
375                            amount: qty 
376                        }); 
377                    }; 
378 
379                    const helper = window.ecomGlobalScripts?.functions?.withButtonSpinner; 
380                    if (typeof helper === "function") { 
381                        return helper(btn, run, { spinnerClass: "spinner-border spinner-border-sm text-light", mode: "replace" }); 
382
383                    return run(); 
384                }); 
385            }); 
386        }; 
387 
388        const _DOMContentLoaded = async function() { 
389            //Event DOMContentLoaded 
390            if (publicProps.isDebug) console.log("DOMContentLoaded ecom Generic scripts"); 
391 
392            if (publicProps.isDebug) console.log("DOMContentLoaded ecom Generic scripts - _loadInit execute"); 
393            await _loadInit(); 
394 
395            await _attachDetailAddToCart(); 
396 
397        }; 
398 
399        // ---- Execute Listener DOMContentLoaded ---- 
400        document.addEventListener("DOMContentLoaded", async function() { 
401            await _DOMContentLoaded(); 
402        }); 
403 
404        /* ===================================== 
405           API PÚBLICA 
406        ===================================== */ 
407        return { 
408            properties: publicProps, // properties public 
409            functions: functions     // functions public 
410        }; 
411 
412    })(); 
413 
414</script> 
-50% discount* If you buy the same UNE standard in different languages. * Discount on the lower pvp.

UNE-EN 16004:2012

Productos químicos utilizados para el tratamiento del agua destinada al consumo humano. Óxido de magnesio.

Edition date: 2012-06-13
In Force
Confirmation date: 2026-03-09
Available languages: Spanish, English
ICS: 71.100.80-Chemicals for purification of water
CTN: CTN 77/SC 1 - Agua

International Equivalence

Identic EN 16004:2011

The book in the author's words

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
Frequently Asked Questions Do you have any questions about our products?
  • Standards UNE, EN, ISO, IEC, BSI, DIN, ASTM, AFNOR, IEEE, SAE
  • In addition, you can request the rules of the rest of the organizations through the e-mail normas@aenor.com
  • Technical books on paper and in electronic format (PDF, epub).

The standards can be purchased in PDF, reading or paper. The reading standards are not download files, they can only be viewed in the client area. The standards ordered on paper and some of the books in the catalogue are printed on demand. 

Check deadlines in normas@aenor.com.

The license of use is for one user and one device, if you want to reproduce the content of the standard, you must request a license that will have an additional cost. Send us your inquiry here 

The AENOR standards and books that appear in the online store can only be purchased exclusively through the website. AENOR does not have a physical store.

Purchase procedure: by clicking on "Buy" the desired products will go to the shopping cart. If there are display problems, the recommended browser is Chrome.

To formalize the purchase you must access the customer area. If you are not registered as a customer, you must fill in a form with the data along with a password and username. This will create the account.

Once the "Customer data" form has been completed, "Order in progress" will be displayed with all the items loaded in the shopping cart, their prices, taxes established in current legislation and shipping costs if applicable.

The prices of the standards and books that appear in the various sections do not include taxes or shipping costs.

AENOR promotional codes consist of alphanumeric characters and can only be applied to online purchases, received through a specific offer and for a limited time. To apply your promotional code, you just have to enter it in step 2 of 4 of the purchase process on the website and click on "apply", after you have identified yourself and chosen the payment methods. Promo codes are not cumulative.

 

  • Credit or debit card (Visa, Mastercard) and PayPal.
  • Bank transfer. If you opt for this form of payment, you must first send AENOR a copy of the transfer by email to normas@aenor.com
  • The purchase invoice can be downloaded from the customer area, in my previous orders

In the case of clients of companies based abroad, the taxpayer identification number of the corresponding country (for example, in Argentina the CUIT), must be filled in the CIF/NIF - VAT field .

  • Direct download via the website in the Customer Area. In the customer area, which can only be accessed with a password and username, the products purchased will be available for a period of fifteen days from the date of purchase, as long as the payment has been accepted. Files in digital format are protected and in no case editable. Before purchasing them, it is important that the license of use is read and accepted as a prior step to purchase.
  • Shipping by courier. Products purchased on physical media are shipped by courier. The maximum delivery time in Spanish territory, from the acceptance of the order by AENOR, is:
  •  Approximately seven working days for all standards purchased through the store in paper format.
  • Approximately three days for books purchased through the store. Stocks of paper books are limited and their offer on the website does not imply availability within the indicated period. In the event that the requested book is not available, the customer is notified of the delay in receiving the order, which will be approximately seven working days. 

For the rest of the products that are not on the website, check availability and delivery time at normas@aenor.com.

1. For digital products (PDF, Epub), once delivery has been made by direct download via the website in the Customer Area, you will not have the right to exercise your right of withdrawal.

2. For personalised products on paper, once the purchase has been made, you will not have the right to exercise your right of withdrawal.

3.  For all other paper products, you have the right to withdraw from the sale within 14 calendar days from the date of purchase. Remember that for the return it is essential that the product is in perfect condition, sealed by the packaging and preserving its original packaging. The customer will be responsible for pickup and shipping costs.

The order invoice includes shipping costs, so there is no amount to pay to the courier. Shipping costs are calculated based on both the final destination of the order and the number of products ordered. They include transport and packaging costs. Shipping costs are subject to periodic revisions. Outlet books will have free shipping costs only if the shipment is made in the Peninsula.

Destination Up to three standards and/or publications From three standards and/or publications
Peninsula 7,31€ 8,60€
Balearic Islands 18,04€ 23,34€
Canary Islands, Ceuta and Melilla  18,04€ 23,34€
Europe 59,17€ 80,07€
United States and Canada 70,07€ 96,94€
Rest of the world 91,94€ 115,91€
  • Purchases made by residents of the Member States of the European Union will be subject to the payment of VAT (value added tax).
  • ​​
  • In the case of legal persons and natural persons who, acting as entrepreneurs, are domiciled in a Member State of the European Union (except residents in Spain) and have an intra-community NIF/VAT registered in the VIES census, they will be exempt from paying VAT, being an essential condition the sending of this document by email to normas@aenor.com.
  • Purchases made in a private capacity (natural person), regardless of where they have their residence, will be subject to the payment of VAT.
  • Purchases made by entities in non-EU countries will be exempt from paying VAT, as long as they send the corresponding tax residence document by email to normas@aenor.com.
  • The sale operations will be understood to have been carried out at AENOR's registered office: Génova 6, 28004, Madrid – Spain. 

The contract for the purchase of products through this Website shall be governed by Spanish law. Any dispute arising out of or in connection with the use of the Website or such contract shall be subject to the exclusive jurisdiction of the Courts and Tribunals of Madrid.

Notwithstanding the foregoing, if you are entering into this contract as a consumer under the terms of Royal Decree 1/2007, nothing in this clause shall affect the rights that may be granted to you as such under applicable law.