import { InvoiceFormatter } from './formatter.js'; import { Big, parseStrict, parseStrictOr, formatRaw, format2, setRaw, getRaw, markDirty, lineTotal as numericLineTotal, wireDatasetRaw, withinTolerance } from './numeric.js'; import { getJSON, setJSON, cacheGet, cacheSet } from './storage.js'; import JSZip from './vendor/jszip.mjs'; import { validateCIF } from './validation/cif.js'; import { validateIBAN } from './validation/iban.js'; import { runBRRules } from './validation/br-ro.js'; import { probeReceiver, anafValidate, anafCifLookup } from './anaf.js'; import { catalogAdd, catalogSearch, catalogDelete } from './catalog.js'; // Constants const XML_NAMESPACES = { ubl: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2", cbc: "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2", cac: "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" }; const VAT_TYPES = { "S": "Cotă Standard", "AE": "Taxare Inversă", "O": "Neplătitor TVA", "Z": "Cotă 0% TVA", "E": "Neimpozabil" }; // PR-TIPURI (A4): UN/CEFACT 1001 InvoiceTypeCode — subset folosit în RO. // 380 = Factură comercială (default) // 381 = Notă de credit (storno) // 384 = Factură corectată (rectificare) // 389 = Autofactură (self-billed) const INVOICE_TYPES = { '380': 'Factură comercială', '381': 'Notă de credit', '384': 'Factură corectată', '389': 'Autofactură' }; // PR-TODO (A5): cod-uri UN/CEFACT 4461 PaymentMeansCode folosite frecvent în RO. const PAYMENT_MEANS_CODES = { '30': 'Transfer bancar', '10': 'Numerar', '48': 'Card', '58': 'Transfer SEPA', '42': 'Plată în cont furnizor', '49': 'Debit direct', '97': 'Compensare', '1': 'Instrument neidentificat' }; const VAT_EXEMPTION_CODES = { 'AE': { code: 'VATEX-EU-AE', reason: 'Taxare inversa' }, 'K': { code: 'VATEX-EU-IC', reason: 'Livrare intracomunitara' }, 'O': { code: 'VATEX-EU-O', reason: 'Neplatitor TVA' }, 'E': [ { code: '', reason: 'Scutit' }, { code: 'VATEX-EU-F', reason: 'Bunuri second hand' }, { code: 'VATEX-EU-D', reason: 'Regim special agentii de turism' } ] }; const UNIT_CODES = new Map([ ['EA', 'Bucată (EA)'], ['XPP', 'Bucată (XPP)'], ['KGM', 'Kilogram (KGM)'], ['MTR', 'Metri (MTR)'], ['LTR', 'Litru (LTR)'], ['H87', 'Bucată (H87)'], ['MTQ', 'Metri cubi (MTQ)'] ]); const ISO_3166_1_CODES = new Set([ 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'XI', 'YE', 'YT', 'ZA', 'ZM', 'ZW' ]); const ROMANIAN_COUNTY_CODES = new Set([ 'RO-AB', 'RO-AG', 'RO-AR', 'RO-B', 'RO-BC', 'RO-BH', 'RO-BN', 'RO-BR', 'RO-BT', 'RO-BV', 'RO-BZ', 'RO-CJ', 'RO-CL', 'RO-CS', 'RO-CT', 'RO-CV', 'RO-DB', 'RO-DJ', 'RO-GJ', 'RO-GL', 'RO-GR', 'RO-HD', 'RO-HR', 'RO-IF', 'RO-IL', 'RO-IS', 'RO-MH', 'RO-MM', 'RO-MS', 'RO-NT', 'RO-OT', 'RO-PH', 'RO-SB', 'RO-SJ', 'RO-SM', 'RO-SV', 'RO-TL', 'RO-TM', 'RO-TR', 'RO-VL', 'RO-VN', 'RO-VS' ]); const CHARGE_REASON_CODES = { 'TV': 'Cheltuieli de transport', 'FC': 'Taxe transport', 'ZZZ': 'Definite reciproc' }; const ALLOWANCE_REASON_CODES = { '95': 'Reducere', '41': 'Bonus lucrări în avans', '42': 'Alt bonus', '60': 'Reducere volum', '62': 'Alte reduceri', '63': 'Reducere producător', '64': 'Din cauza războiului', '65': 'Reducere outlet nou', '66': 'Reducere mostre', '67': 'Reducere end-of-range', '68': 'Cost ambalaj returnabil', '70': 'Reducere Incoterm', '71': 'Prag vânzări', '88': 'Suprataxă/deducere materiale', '100': 'Reducere specială', '102': 'Termen lung fix', '103': 'Temporar', '104': 'Standard', '105': 'Cifră de afaceri anuală' }; // Structure for item identifications const IDENTIFICATION_TYPES = { SELLERS: { type: 'sellers', label: 'Cod Furnizor', xmlTag: 'SellersItemIdentification' }, BUYERS: { type: 'buyers', label: 'Cod Client', xmlTag: 'BuyersItemIdentification' }, STANDARD: { type: 'standard', label: 'Cod Bare', xmlTag: 'StandardItemIdentification', schemeID: '0160' }, COMMODITY: { type: 'commodity', label: 'Cod Clasificare', xmlTag: 'CommodityClassification', schemes: [ { id: 'CV', name: 'Cod Vamal' }, { id: 'TSP', name: 'Cod CPV' }, { id: 'STI', name: 'Cod NC8' } ] } }; const formatter = new InvoiceFormatter() const resolver = { lookupNamespaceURI: prefix => { const ns = { 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', 'ubl': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2' }; return ns[prefix] || null; } }; // Global variables let currentInvoice = null; let originalTotals = null; let vatRates = new Map(); let manuallyEditedVatRows = new Set(); // PR-A14: Multi-XML bulk state let _multiXmlFiles = []; // [{name, content, dirty}] let _activeFileIdx = -1; // index în _multiXmlFiles (-1 = single-file mode) let _loadingFile = false; // suprimă dirty events cât timp parseXML populează formularul // Initialize event listeners document.addEventListener('DOMContentLoaded', async function() { document.getElementById('fileInput').addEventListener('change', handleFileSelect); setupDragAndDrop(); initializeUI(); if (!currentInvoice) { currentInvoice = createEmptyInvoice(); } const totalElements = [ 'subtotal', 'totalAllowances', 'totalCharges', 'netAmount', 'vat', 'total' ]; totalElements.forEach(elementId => { const element = document.getElementById(elementId); setupInlineEditing(element); }); // Add currency code validation const currencyInputs = document.querySelectorAll('[name="documentCurrencyCode"], [name="taxCurrencyCode"]'); currencyInputs.forEach(input => { input.addEventListener('input', function(e) { // Convert to uppercase this.value = this.value.toUpperCase(); // Remove any non-letter characters this.value = this.value.replace(/[^A-Z]/g, ''); // Limit to 3 characters if (this.value.length > 3) { this.value = this.value.slice(0, 3); } }); }); // Make document currency code required const documentCurrencyInput = document.querySelector('[name="documentCurrencyCode"]'); if (documentCurrencyInput) { documentCurrencyInput.required = true; } const supplierVATInput = document.querySelector('[name="supplierVAT"]'); if (supplierVATInput) { supplierVATInput.addEventListener('change', function() { updateAllVATTypes(); }); } addExchangeRateField(); initializeLocationSelectors(); // Verifică dacă avem parametru XML în URL const urlParams = new URLSearchParams(window.location.search); const xmlFileName = urlParams.get('xml'); if (xmlFileName) { try { // Încarcă XML-ul din fișierul temporar const response = await fetch('temp/' + xmlFileName); if (response.ok) { const xmlContent = await response.text(); parseXML(xmlContent); // Curăță fișierul temporar fetch('receiver.php?cleanup=' + xmlFileName) .catch(error => console.error('Eroare la ștergerea fișierului temporar:', error)); } } catch (error) { console.error('Eroare la încărcarea XML:', error); } } // Pre-populare număr factură din secvență dacă câmpul e gol const numEl = document.querySelector('[name="invoiceNumber"]'); if (numEl && !numEl.value) { const seq = _seqRead(); numEl.value = _seqFormat(seq); } }); // Initialize event listeners for existing line items document.querySelectorAll('.line-item').forEach((item, index) => { handleLineItemChange(index); }); // Inline editing setup function function setupInlineEditing(element) { let originalValue; element.addEventListener('click', function() { this.setAttribute('contenteditable', 'true'); // Store exact displayed value originalValue = this.textContent; this.focus(); }); element.addEventListener('blur', function() { this.setAttribute('contenteditable', 'false'); if (this.textContent !== originalValue) { updateTotals(); } }); element.addEventListener('keydown', function(event) { if (event.key === 'Enter') { event.preventDefault(); this.blur(); } }); } function updateTotalDisplay(elementId, value) { const element = document.getElementById(elementId); if (element) { // Convertim la număr folosind parseFloat pentru a evita probleme de truncare const numValue = parseFloat(value); element.textContent = formatter.formatCurrency(numValue); } } function displayTotals(totals) { // Actualizează toate totalurile cu formatare updateTotalDisplay('subtotal', totals.subtotal); updateTotalDisplay('totalAllowances', totals.allowances); updateTotalDisplay('totalCharges', totals.charges); updateTotalDisplay('netAmount', totals.netAmount); updateTotalDisplay('vat', totals.totalVat); updateTotalDisplay('total', totals.total); // Actualizează defalcarea TVA dacă există const container = document.getElementById('vatBreakdownRows'); if (container) { container.innerHTML = ''; if (totals.vatBreakdown) { totals.vatBreakdown.forEach((data, key) => { const [rate, type] = key.split('-'); addVATBreakdownRow( parseFloat(rate), data.baseAmount, data.vatAmount, type ); }); } } } function updateVATDisplay(row, amount, type = 'amount') { const input = row.querySelector(`.vat-${type}`); if (input) { input.value = formatter.formatCurrency(amount); } } function validateVATExemption() { const vatRows = document.querySelectorAll('.vat-row'); let isValid = true; vatRows.forEach(row => { const vatType = row.querySelector('.vat-type').value; if (['E', 'K', 'AE', 'O'].includes(vatType)) { const exemptionCodeInput = row.querySelector('.vat-exemption-code'); const exemptionReasonInput = row.querySelector('.vat-exemption-reason'); const exemptionCode = exemptionCodeInput?.value; const exemptionReason = exemptionReasonInput?.value; // Pentru neplătitori de TVA (tip O), verificăm să aibă valorile corecte if (vatType === 'O') { if (exemptionCode !== 'VATEX-EU-O') { isValid = false; exemptionCodeInput?.classList.add('invalid'); } } // Pentru celelalte tipuri de scutire, trebuie să aibă cel puțin unul dintre câmpuri completat else if (!exemptionCode && !exemptionReason) { isValid = false; exemptionCodeInput?.classList.add('invalid'); exemptionReasonInput?.classList.add('invalid'); } else { exemptionCodeInput?.classList.remove('invalid'); exemptionReasonInput?.classList.remove('invalid'); } } }); return isValid; } function addDynamicVatExemptionCode(vatType, exemptionCode, exemptionReason) { if (!vatType || !exemptionCode) return; if (vatType === 'E') { // Verifică dacă codul există deja const exists = VAT_EXEMPTION_CODES.E.some(e => e.code === exemptionCode); if (!exists) { VAT_EXEMPTION_CODES.E.push({ code: exemptionCode, reason: exemptionReason || exemptionCode }); } } else if (!VAT_EXEMPTION_CODES[vatType]) { // Adaugă un nou tip de TVA dacă nu există VAT_EXEMPTION_CODES[vatType] = { code: exemptionCode, reason: exemptionReason || exemptionCode }; } } function getDisplayValue(elementId) { const element = document.getElementById(elementId); return element ? formatter.parseCurrency(element.textContent) : 0; } // Event delegation for dynamic elements document.addEventListener('click', function(event) { const target = event.target; if (target.matches('.delete-line-item')) { const lineItem = target.closest('.line-item'); if (lineItem) { removeLineItem(parseInt(lineItem.dataset.index)); } } if (target.matches('.delete-allowance-charge')) { const charge = target.closest('.allowance-charge'); if (charge) { removeAllowanceCharge(parseInt(charge.dataset.index)); } } }); // File handling functions // ZIP magic bytes: "PK\x03\x04" — local file header signature. async function isZipFile(file) { if (!file || file.size < 4) return false; const head = await file.slice(0, 4).arrayBuffer(); const b = new Uint8Array(head); return b[0] === 0x50 && b[1] === 0x4B && b[2] === 0x03 && b[3] === 0x04; } // Încarcă un fișier (XML simplu sau ZIP cu un XML eFactura înăuntru). // Detectează ZIP pe magic bytes (PK\x03\x04). Fallback la text() pentru XML. async function loadInvoiceFile(file) { if (!file) return; try { if (await isZipFile(file)) { const buf = await file.arrayBuffer(); const zip = await JSZip.loadAsync(buf); const xmlEntries = Object.values(zip.files).filter(f => !f.dir && f.name.toLowerCase().endsWith('.xml')); if (xmlEntries.length === 0) { alert('Arhiva ZIP nu conține niciun fișier .xml.'); return; } // Primul .xml din arhivă (sortat după nume pentru determinism). xmlEntries.sort((a, b) => a.name.localeCompare(b.name)); const xmlContent = await xmlEntries[0].async('string'); parseXML(xmlContent); } else { const xmlContent = await file.text(); parseXML(xmlContent); } } catch (err) { console.error('Eroare la încărcarea fișierului:', err); alert('Nu s-a putut încărca fișierul: ' + (err && err.message ? err.message : err)); } } function handleFileSelect(event) { const files = event.target.files; if (!files || files.length === 0) return; if (files.length === 1) { loadInvoiceFile(files[0]); } else { loadMultipleFiles(Array.from(files)); } // Reset input so same file(s) can be re-selected event.target.value = ''; } // ───────────────────────────────────────────────────────── // PR-A14: Multi-XML bulk mode // ───────────────────────────────────────────────────────── /** * Citește conținutul text al unui fișier XML sau al primului XML dintr-un ZIP. * @param {File} file * @returns {Promise} conținut XML sau null la eroare */ async function _readFileContent(file) { try { if (await isZipFile(file)) { const buf = await file.arrayBuffer(); const zip = await JSZip.loadAsync(buf); const xmlEntries = Object.values(zip.files) .filter(f => !f.dir && f.name.toLowerCase().endsWith('.xml')); if (xmlEntries.length === 0) return null; xmlEntries.sort((a, b) => a.name.localeCompare(b.name)); return await xmlEntries[0].async('string'); } else { return await file.text(); } } catch (err) { console.error('_readFileContent:', file.name, err); return null; } } /** * Încarcă mai multe fișiere XML/ZIP în modul bulk. * - Citește fișierele async cu yield între ele (responsivitate UI). * - Aplică limitarea la 50 de fișiere, cu toast de avertizare. * - Afișează sidebar-ul și încarcă primul fișier în formular. * @param {File[]} files */ async function loadMultipleFiles(files) { const MAX = 50; const existing = _multiXmlFiles.length; const available = Math.max(0, MAX - existing); let dropped = 0; if (files.length > available) { dropped = files.length - available; files = files.slice(0, available); } if (files.length === 0) { if (dropped > 0) { showToast(`Limita de ${MAX} fișiere atinsă — ${dropped} fișier(e) ignorat(e).`, 'warning'); } return; } // Citire async cu yield între iterații const newEntries = []; for (const file of files) { const content = await _readFileContent(file); if (content !== null) { newEntries.push({ name: file.name, content, dirty: false }); } // yield pentru ca UI să rămână responsiv await new Promise(r => setTimeout(r, 0)); } if (newEntries.length === 0) { showToast('Niciun fișier XML valid găsit.', 'error'); return; } const wasEmpty = _multiXmlFiles.length === 0; _multiXmlFiles.push(...newEntries); if (dropped > 0) { showToast(`Limita de ${MAX} fișiere: ${dropped} fișier(e) ignorat(e).`, 'warning'); } // Prima activare: dacă nu era niciun fișier activ sau era single-file mode if (wasEmpty) { _activeFileIdx = 0; } // Altfel păstrăm indexul activ curent _renderXmlSidebar(); _activateFile(_activeFileIdx, false /* nu salva — tocmai am intrat în bulk mode */); } /** * Salvează starea curentă a formularului înapoi în _multiXmlFiles[idx].content. * Apelat înaintea switch-ului de fișier. */ function _saveCurrentFileState() { if (_activeFileIdx < 0 || _activeFileIdx >= _multiXmlFiles.length) return; const xml = _currentXMLString(); if (xml) { _multiXmlFiles[_activeFileIdx].content = xml; } } /** * Activează fișierul la indexul dat: parsează XML-ul în formular. * @param {number} idx * @param {boolean} [saveFirst=true] dacă să salveze starea fișierului activ înainte de switch */ function _activateFile(idx, saveFirst = true) { if (idx < 0 || idx >= _multiXmlFiles.length) return; if (saveFirst && _activeFileIdx >= 0 && _activeFileIdx !== idx) { _saveCurrentFileState(); } _activeFileIdx = idx; const entry = _multiXmlFiles[idx]; _loadingFile = true; parseXML(entry.content); // Suprimă dirty events generate de popularea formularului queueMicrotask(() => { _loadingFile = false; }); _renderXmlSidebar(); } /** * Marchează fișierul activ ca dirty (modificat). * Apelat din event delegation pe formularul de editare. */ function _markActiveDirty() { if (_loadingFile) return; if (_activeFileIdx < 0 || _activeFileIdx >= _multiXmlFiles.length) return; if (_multiXmlFiles[_activeFileIdx].dirty) return; // deja marcat _multiXmlFiles[_activeFileIdx].dirty = true; _updateSidebarItem(_activeFileIdx); } /** Redă întregul sidebar cu lista de fișiere. */ function _renderXmlSidebar() { const hasBulk = _multiXmlFiles.length >= 2; document.body.classList.toggle('has-xml-sidebar', hasBulk); if (!hasBulk) { const old = document.getElementById('xml-sidebar'); if (old) old.remove(); return; } let sidebar = document.getElementById('xml-sidebar'); if (!sidebar) { sidebar = document.createElement('div'); sidebar.id = 'xml-sidebar'; sidebar.setAttribute('role', 'navigation'); sidebar.setAttribute('aria-label', 'Fișiere XML'); document.body.insertBefore(sidebar, document.body.firstChild); } sidebar.innerHTML = ''; // Header const hdr = document.createElement('div'); hdr.className = 'xml-sidebar-header'; hdr.textContent = `Fișiere (${_multiXmlFiles.length})`; sidebar.appendChild(hdr); // Items const list = document.createElement('ul'); list.className = 'xml-sidebar-list'; _multiXmlFiles.forEach((entry, idx) => { const li = _buildSidebarItem(entry, idx); list.appendChild(li); }); sidebar.appendChild(list); } /** Construiește un
  • pentru un fișier din sidebar. */ function _buildSidebarItem(entry, idx) { const li = document.createElement('li'); li.className = 'xml-sidebar-item' + (idx === _activeFileIdx ? ' is-active' : ''); li.dataset.idx = idx; li.title = entry.name; const nameSpan = document.createElement('span'); nameSpan.className = 'xml-sidebar-item-name'; nameSpan.textContent = entry.name; const dirtyDot = document.createElement('span'); dirtyDot.className = 'xml-sidebar-dirty' + (entry.dirty ? ' is-dirty' : ''); dirtyDot.title = entry.dirty ? 'Modificat (nesalvat)' : ''; dirtyDot.setAttribute('aria-hidden', 'true'); li.appendChild(nameSpan); li.appendChild(dirtyDot); li.addEventListener('click', () => { if (idx === _activeFileIdx) return; _activateFile(idx, true); }); return li; } /** Actualizează doar un item din sidebar (fără re-render complet). */ function _updateSidebarItem(idx) { const sidebar = document.getElementById('xml-sidebar'); if (!sidebar) return; const list = sidebar.querySelector('.xml-sidebar-list'); if (!list) return; const items = list.querySelectorAll('.xml-sidebar-item'); if (!items[idx]) return; const entry = _multiXmlFiles[idx]; const old = items[idx]; const replacement = _buildSidebarItem(entry, idx); list.replaceChild(replacement, old); } // Expune pe window pentru butonul "Salvează XML" în bulk mode window.saveCurrentFileAndMark = function() { if (_activeFileIdx < 0) return; _saveCurrentFileState(); _multiXmlFiles[_activeFileIdx].dirty = false; _updateSidebarItem(_activeFileIdx); }; // ───────────────────────────────────────────────────────── // Drag-and-drop: acceptă XML sau ZIP la drop pe oriunde în pagină. function setupDragAndDrop() { const target = document.body; const prevent = (e) => { e.preventDefault(); e.stopPropagation(); }; ['dragenter', 'dragover'].forEach(ev => { target.addEventListener(ev, (e) => { if (e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files')) { prevent(e); e.dataTransfer.dropEffect = 'copy'; target.classList.add('drag-over'); } }); }); ['dragleave', 'dragend'].forEach(ev => { target.addEventListener(ev, (e) => { if (e.target === target) target.classList.remove('drag-over'); }); }); target.addEventListener('drop', (e) => { if (!e.dataTransfer || !e.dataTransfer.files || e.dataTransfer.files.length === 0) return; prevent(e); target.classList.remove('drag-over'); const files = Array.from(e.dataTransfer.files); if (files.length === 1) { loadInvoiceFile(files[0]); } else { loadMultipleFiles(files); } }); } function parseXML(xmlContent) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlContent, "text/xml"); const parserError = xmlDoc.querySelector('parsererror'); if (parserError) { throw new Error('Eroare la parsarea XML: ' + parserError.textContent); } // Extrage și adaugă codurile de scutire din TaxTotal const taxSubtotals = xmlDoc.querySelectorAll('cac\\:TaxSubtotal, TaxSubtotal'); taxSubtotals.forEach(subtotal => { const taxCategory = subtotal.querySelector('cac\\:TaxCategory, TaxCategory'); if (taxCategory) { const vatType = getXMLValue(taxCategory, 'cbc\\:ID, ID'); const exemptionCode = getXMLValue(taxCategory, 'cbc\\:TaxExemptionReasonCode, TaxExemptionReasonCode'); const exemptionReason = getXMLValue(taxCategory, 'cbc\\:TaxExemptionReason, TaxExemptionReason'); if (vatType && exemptionCode) { addDynamicVatExemptionCode(vatType, exemptionCode, exemptionReason); } } }); currentInvoice = xmlDoc; manuallyEditedVatRows.clear(); populateBasicDetails(xmlDoc); populatePartyDetails(xmlDoc); populateBillingReference(xmlDoc); populatePaymentMeans(xmlDoc); populateAllowanceCharges(xmlDoc); populateLineItems(xmlDoc); storeOriginalTotals(xmlDoc); restoreOriginalTotals(); displayVATBreakdown(xmlDoc); // PR-A11: după ce toate elementele sunt populate, rulează math // validation pentru a popula badge-urile (per-line + per-VAT-row + footer). if (typeof validateMath === 'function') validateMath(); // PR-BR: inițializează panelul de validare după load XML. _updateBRPanel(); } catch (error) { handleError(error, 'Eroare la parsarea fișierului XML'); } } // Create allowance charge HTML function createAllowanceChargeHTML(index, charge) { return `
    `; } function createReasonCodeOptions(isCharge, selectedCode = '') { const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES; return Object.entries(codes) .map(([code, description]) => `` ).join(''); } window.updateReasonCodeOptions = function(index) { const chargeTypeSelect = document.querySelector(`[name="chargeType${index}"]`); const reasonCodeSelect = document.querySelector(`[name="chargeReasonCode${index}"]`); const reasonInput = document.querySelector(`[name="chargeReason${index}"]`); const isCharge = chargeTypeSelect.value === 'true'; reasonCodeSelect.innerHTML = createReasonCodeOptions(isCharge); // Update reason text based on selected code const selectedCode = reasonCodeSelect.value; const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES; reasonInput.value = codes[selectedCode] || ''; } // Create line item HTML function createLineItemHTML(index, description = '', quantity = '1', price = '0', vatRate = '19', unitCode = 'EA', vatTypeId = 'S', itemDescription = '', lineDiscount = '0', discountReasonCode = '') { return `
    Total linie: 0,00
    `; } function createLineDiscountReasonOptions(selectedCode = '') { return Object.entries(ALLOWANCE_REASON_CODES) .map(([code, description]) => `` ).join(''); } // Add VAT breakdown row let _vatRowCounter = 0; function addVATBreakdownRow(rate, baseAmount, vatAmount, vatType = 'S', existingRowId = null, exemptionCode = '', exemptionReason = '') { const container = document.getElementById('vatBreakdownRows'); // ID unic chiar dacă mai multe rânduri sunt create în același ms (Date.now() // singur cauza coliziuni → querySelector(#id) pică pe primul match și // dataset.raw al rândurilor noi era fie suprascris, fie nesetat → BR-CO-15 // false positive după Recalculează Totaluri. const rowId = existingRowId || `vat-row-${Date.now()}-${++_vatRowCounter}`; const rowHtml = `
    %
    `; container.insertAdjacentHTML('beforeend', rowHtml); // PR-E E1: seed dataset.raw pentru rate/base/amount + wire blur listeners. const rateEl = document.querySelector(`#${rowId} .vat-rate`); const baseEl = document.querySelector(`#${rowId} .vat-base`); const amountEl = document.querySelector(`#${rowId} .vat-amount`); [ { el: rateEl, val: rate, decimals: 2 }, { el: baseEl, val: baseAmount, decimals: 2 }, { el: amountEl, val: vatAmount, decimals: 2 } ].forEach(f => { if (!f.el) return; const big = parseStrict(f.val); if (big !== null) f.el.dataset.raw = big.toFixed(f.decimals); wireDatasetRaw(f.el, f.decimals); }); // Add event listener for VAT type changes const vatTypeSelect = document.querySelector(`#${rowId} .vat-type`); vatTypeSelect.addEventListener('change', () => { const exemptionContainer = document.querySelector(`#${rowId} .vat-exemption`); const newVatType = vatTypeSelect.value; if (['E', 'K', 'O', 'AE'].includes(newVatType)) { exemptionContainer.classList.remove('hidden'); // Update exemption code options const codeSelect = document.querySelector(`#${rowId} .vat-exemption-code`); codeSelect.innerHTML = generateExemptionCodeOptions(newVatType); // Set default values const defaultExemption = getDefaultExemption(newVatType); if (defaultExemption) { codeSelect.value = defaultExemption.code; document.querySelector(`#${rowId} .vat-exemption-reason`).value = defaultExemption.reason; } } else { exemptionContainer.classList.add('hidden'); } }); } function generateExemptionCodeOptions(vatType, selectedCode = '') { if (vatType === 'E') { return VAT_EXEMPTION_CODES.E.map(exemption => `` ).join(''); } else if (vatType === 'K' || vatType === 'AE' || vatType === 'O') { const exemption = VAT_EXEMPTION_CODES[vatType]; return ``; } return ''; } function getDefaultExemption(vatType) { if (vatType === 'E') { return VAT_EXEMPTION_CODES.E[0]; } return VAT_EXEMPTION_CODES[vatType]; } // Add this to window object window.updateExemptionReason = function(rowId) { const row = document.getElementById(rowId); if (!row) return; const vatType = row.querySelector('.vat-type').value; const codeSelect = row.querySelector('.vat-exemption-code'); const reasonInput = row.querySelector('.vat-exemption-reason'); if (vatType === 'E') { const selectedExemption = VAT_EXEMPTION_CODES.E.find(e => e.code === codeSelect.value); if (selectedExemption) { reasonInput.value = selectedExemption.reason; } } else if (vatType === 'K' || vatType === 'AE') { reasonInput.value = VAT_EXEMPTION_CODES[vatType].reason; } }; // Toggle optional details window.toggleOptionalDetails = function(index) { const optionalDetails = document.getElementById(`optionalDetails${index}`); const button = optionalDetails.previousElementSibling.querySelector('button'); if (optionalDetails.style.display === 'none') { optionalDetails.style.display = 'block'; button.innerHTML = '▲ Detalii Suplimentare'; } else { optionalDetails.style.display = 'none'; button.innerHTML = '▼ Detalii Suplimentare'; } } // Form validation function validateForm(silent = false) { let isValid = true; let firstInvalidField = null; // Validare câmpuri existente... const requiredFields = [ 'invoiceNumber', 'issueDate', 'dueDate', 'supplierName', 'supplierVAT', 'customerName', ]; requiredFields.forEach(fieldName => { const field = document.querySelector(`[name="${fieldName}"]`); if (!field || !field.value.trim()) { field.classList.add('invalid'); isValid = false; if (!firstInvalidField) firstInvalidField = field; } else { field.classList.remove('invalid'); } }); // Adaugă validarea TVA const vatExemptionValid = validateVATExemption(); if (!vatExemptionValid) { isValid = false; if (!silent) { alert('Vă rugăm să completați codul și/sau motivul scutirii de TVA pentru toate categoriile care necesită această informație.'); } } // Restul validărilor existente... const lineItems = document.querySelectorAll('.line-item'); if (lineItems.length === 0) { isValid = false; if (!silent) { alert('Este necesară cel puțin o linie în factură'); } return false; } lineItems.forEach((item, index) => { const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value); const price = parseFloat(document.querySelector(`[name="price${index}"]`).value); const description = document.querySelector(`[name="description${index}"]`).value; if (!description.trim()) { document.querySelector(`[name="description${index}"]`).classList.add('invalid'); isValid = false; } if (isNaN(quantity)) { document.querySelector(`[name="quantity${index}"]`).classList.add('invalid'); isValid = false; } if (isNaN(price)) { document.querySelector(`[name="price${index}"]`).classList.add('invalid'); isValid = false; } }); const dateInputs = document.querySelectorAll('.date-input'); dateInputs.forEach(input => { if (!validateDateInput(input)) { isValid = false; if (!firstInvalidField) firstInvalidField = input; } }); // Validare curs valutar dacă este cazul const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.trim(); const documentCurrencyCode = document.querySelector('[name="documentCurrencyCode"]').value.trim(); if (taxCurrencyCode && taxCurrencyCode !== documentCurrencyCode) { const exchangeRate = document.querySelector('[name="exchangeRate"]'); if (!exchangeRate || !exchangeRate.value || parseFloat(exchangeRate.value) <= 0) { exchangeRate.classList.add('invalid'); isValid = false; if (!firstInvalidField) { firstInvalidField = exchangeRate; } } else { exchangeRate.classList.remove('invalid'); } } if (!isValid && !silent) { if (firstInvalidField) { firstInvalidField.focus(); } alert('Vă rugăm să completați toate câmpurile obligatorii'); } return isValid; } function handleError(error, message) { console.error(message, error); alert(`${message}\nVă rugăm să verificați consola pentru detalii.`); } function formatDateToRomanian(date) { const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); return `${day}.${month}.${year}`; } function parseRomanianDate(dateStr) { const [day, month, year] = dateStr.split('.'); return `${year}-${month}-${day}`; } function createDatePicker(input, button) { const picker = new Pikaday({ field: input, trigger: button, format: 'DD.MM.YYYY', i18n: { previousMonth: 'Luna anterioară', nextMonth: 'Luna următoare', months: ['Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie'], weekdays: ['Duminică', 'Luni', 'Marți', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'], weekdaysShort: ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm'] }, firstDay: 1, onSelect: function(date) { input.value = formatDateToRomanian(date); validateDateInput(input); } }); return picker; } function validateDateInput(input) { const value = input.value; // Câmp gol = valid (regula "required" e tratată separat în validateForm). if (value === '') { input.classList.remove('invalid'); return true; } const regex = /^(\d{2})\.(\d{2})\.(\d{4})$/; const match = value.match(regex); if (match) { const day = parseInt(match[1]); const month = parseInt(match[2]); const year = parseInt(match[3]); const date = new Date(year, month - 1, day); if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) { input.classList.remove('invalid'); return true; } } input.classList.add('invalid'); return false; } function restrictDateInput(input) { input.addEventListener('input', function(e) { let value = e.target.value; // Remove any non-digit characters except dots value = value.replace(/[^\d.]/g, ''); // Auto-add dots after day and month if (value.length >= 2 && value.charAt(2) !== '.') { value = value.slice(0, 2) + '.' + value.slice(2); } if (value.length >= 5 && value.charAt(5) !== '.') { value = value.slice(0, 5) + '.' + value.slice(5); } // Restrict to exactly 10 characters (dd.mm.yyyy) value = value.slice(0, 10); e.target.value = value; }); input.addEventListener('blur', function() { validateDateInput(input); }); } function createCountryOptions() { return Array.from(ISO_3166_1_CODES).map(code => `` ).join(''); } function createCountyOptions() { return Array.from(ROMANIAN_COUNTY_CODES).map(code => { const label = code.replace('RO-', ''); const counties = { 'AB': 'Alba', 'AR': 'Arad', 'AG': 'Argeș', 'BC': 'Bacău', 'BH': 'Bihor', 'BN': 'Bistrița-Năsăud', 'BT': 'Botoșani', 'BV': 'Brașov', 'BR': 'Brăila', 'B': 'București', 'BZ': 'Buzău', 'CS': 'Caraș-Severin', 'CL': 'Călărași', 'CJ': 'Cluj', 'CT': 'Constanța', 'CV': 'Covasna', 'DB': 'Dâmbovița', 'DJ': 'Dolj', 'GL': 'Galați', 'GR': 'Giurgiu', 'GJ': 'Gorj', 'HR': 'Harghita', 'HD': 'Hunedoara', 'IL': 'Ialomița', 'IS': 'Iași', 'IF': 'Ilfov', 'MM': 'Maramureș', 'MH': 'Mehedinți', 'MS': 'Mureș', 'NT': 'Neamț', 'OT': 'Olt', 'PH': 'Prahova', 'SM': 'Satu Mare', 'SJ': 'Sălaj', 'SB': 'Sibiu', 'SV': 'Suceava', 'TR': 'Teleorman', 'TM': 'Timiș', 'TL': 'Tulcea', 'VS': 'Vaslui', 'VL': 'Vâlcea', 'VN': 'Vrancea' }; return ``; }).join(''); } function initializeLocationSelectors() { // Replace country inputs with selects for both supplier and customer ['supplier', 'customer'].forEach(party => { const countryInput = document.querySelector(`[name="${party}Country"]`); if (countryInput) { const select = document.createElement('select'); select.className = 'form-input'; select.name = countryInput.name; select.innerHTML = createCountryOptions(); select.value = countryInput.dataset.xmlValue || countryInput.value || 'RO'; countryInput.parentNode?.replaceChild(select, countryInput); } const countyInput = document.querySelector(`[name="${party}CountrySubentity"]`); if (countyInput) { const select = document.createElement('select'); select.className = 'form-input'; select.name = countyInput.name; select.innerHTML = createCountyOptions(); select.value = countyInput.dataset.xmlValue || countyInput.value || ''; countyInput.parentNode?.replaceChild(select, countyInput); } }); // Initialize location handlers for both parties setupPartyLocationHandlers('supplier'); setupPartyLocationHandlers('customer'); } function updateCountyVisibility(countrySelect, countySelect) { if (!countrySelect || !countySelect) return; const showCounty = countrySelect.value === 'RO'; countySelect.style.display = showCounty ? 'block' : 'none'; countySelect.required = showCounty; } function setupPartyLocationHandlers(party) { const countrySelect = document.querySelector(`[name="${party}Country"]`); const countySelect = document.querySelector(`[name="${party}CountrySubentity"]`); const cityContainer = document.querySelector(`[name="${party}City"]`)?.parentNode; if (!countrySelect || !countySelect || !cityContainer) return; const handleLocationChange = () => { const isBucharest = countrySelect.value === 'RO' && countySelect.value === 'RO-B'; const currentElement = cityContainer.querySelector('input, select'); const isCurrentlySector = currentElement.tagName.toLowerCase() === 'select'; if (isBucharest && !isCurrentlySector) { const sectorSelect = document.createElement('select'); sectorSelect.className = 'form-input'; sectorSelect.name = `${party}City`; sectorSelect.innerHTML = ` `; // Try to preserve any existing sector value const currentValue = currentElement.value || ''; if (currentValue.toUpperCase().includes('SECTOR')) { sectorSelect.value = currentValue.toUpperCase().replace(/\s+/g, ''); } cityContainer.replaceChild(sectorSelect, currentElement); } else if (!isBucharest && isCurrentlySector) { const cityInput = document.createElement('input'); cityInput.type = 'text'; cityInput.className = 'form-input'; cityInput.name = `${party}City`; cityInput.value = ''; cityContainer.replaceChild(cityInput, currentElement); } }; // Set up event listeners countrySelect.addEventListener('change', () => { updateCountyVisibility(countrySelect, countySelect); handleLocationChange(); }); countySelect.addEventListener('change', handleLocationChange); // Initial setup updateCountyVisibility(countrySelect, countySelect); handleLocationChange(); } // ============================================================================ // PR-VALID-IDS (A9 + A10): Helpers validare CIF și IBAN pe blur. // Afișează eroare inline sub câmp: border --danger + bg --danger-soft + // mesaj 11px. Curăță eroarea la re-focus sau la câmp valid. // ============================================================================ /** * Afișează sau actualizează mesajul de eroare sub un câmp de input. * @param {HTMLInputElement} input * @param {string} errorId — ID-ul elementului helper text (ex: "supplierVAT-error") * @param {string} message — mesajul de afișat */ function _setFieldError(input, errorId, message) { input.style.borderColor = 'var(--danger)'; input.style.backgroundColor = 'var(--danger-soft)'; input.setAttribute('aria-invalid', 'true'); input.setAttribute('aria-describedby', errorId); let helper = document.getElementById(errorId); if (!helper) { helper = document.createElement('span'); helper.id = errorId; helper.style.cssText = 'display:block;font-size:11px;color:var(--danger);margin-top:3px;'; input.parentNode.appendChild(helper); } helper.textContent = message; } /** * Curăță eroarea de validare de pe un câmp. * @param {HTMLInputElement} input * @param {string} errorId */ function _clearFieldError(input, errorId) { input.style.borderColor = ''; input.style.backgroundColor = ''; input.removeAttribute('aria-invalid'); input.removeAttribute('aria-describedby'); const helper = document.getElementById(errorId); if (helper) helper.remove(); } /** * Wire blur + validare CIF pe un câmp dat. * @param {HTMLInputElement} input * @param {string} errorId */ function _wireCIFValidation(input, errorId) { if (!input) return; input.addEventListener('blur', function() { const result = validateCIF(this.value); if (!result.valid) { _setFieldError(this, errorId, result.message); } else { _clearFieldError(this, errorId); } }); // Curăță eroarea la tastare (nu re-validează — doar curăță indicatorul vizual) input.addEventListener('input', function() { _clearFieldError(this, errorId); }); } /** * Wire blur + validare IBAN pe un câmp dat. * @param {HTMLInputElement} input * @param {string} errorId */ function _wireIBANValidation(input, errorId) { if (!input) return; input.addEventListener('blur', function() { const result = validateIBAN(this.value); if (!result.valid) { _setFieldError(this, errorId, result.message); } else { _clearFieldError(this, errorId); } }); input.addEventListener('input', function() { _clearFieldError(this, errorId); }); } function initializeUI() { document.querySelectorAll('.form-input').forEach(input => { input.addEventListener('input', function() { this.classList.remove('invalid'); updateTotals(); }); }); document.addEventListener('keydown', function(event) { if (event.ctrlKey || event.metaKey) { switch (event.key.toLowerCase()) { case 's': event.preventDefault(); saveXML(); break; case 'o': event.preventDefault(); document.getElementById('fileInput').click(); break; case 'n': event.preventDefault(); addLineItem(); break; } } }); // Initialize date pickers const dateInputs = document.querySelectorAll('.date-input'); dateInputs.forEach(input => { const button = input.parentElement.querySelector('.calendar-button'); createDatePicker(input, button); restrictDateInput(input); }); if (!currentInvoice) { const today = new Date(); const dueDate = new Date(); dueDate.setDate(today.getDate() + 30); document.querySelector('[name="issueDate"]').value = formatDateToRomanian(today); document.querySelector('[name="dueDate"]').value = formatDateToRomanian(dueDate); } // Add note counter event listener const noteInput = document.querySelector('[name="invoiceNote"]'); if (noteInput) { noteInput.addEventListener('input', updateNoteCounter); updateNoteCounter(); // Initial count } // Initialize location selectors initializeLocationSelectors(); window.addLineItem = addLineItem; window.removeLineItem = removeLineItem; window.addAllowanceCharge = addAllowanceCharge; window.removeAllowanceCharge = removeAllowanceCharge; window.addPaymentMeansRow = addPaymentMeansRow; window.removePaymentMeansRow = removePaymentMeansRow; window.handleStorno = handleStorno; window.updateTotals = updateTotals; window.saveXML = saveXML; window.refreshTotals = refreshTotals; window.displayVATBreakdown = displayVATBreakdown; // A12: expune butoane profil furnizor și inițializează starea. window.saveSupplierProfile = saveSupplierProfile; window.useSupplierProfile = useSupplierProfile; window.deleteSupplierProfile = deleteSupplierProfile; _updateProfileButtons(); // PR-VALID-IDS (A9 + A10): wire CIF și IBAN pe blur. _wireCIFValidation(document.querySelector('[name="supplierVAT"]'), 'supplierVAT-error'); _wireCIFValidation(document.querySelector('[name="customerVAT"]'), 'customerVAT-error'); // IBAN: delegare pe containerul de Payment Means (câmpuri dinamice). const pmContainer = document.getElementById('paymentMeansRows'); if (pmContainer) { pmContainer.addEventListener('blur', function(e) { const input = e.target; if (!input || !input.name || !input.name.startsWith('paymentMeansIBAN')) return; const errorId = input.name + '-error'; const result = validateIBAN(input.value); if (!result.valid) { _setFieldError(input, errorId, result.message); } else { _clearFieldError(input, errorId); } }, true); // capture=true pentru blur care nu bubblează pmContainer.addEventListener('input', function(e) { const input = e.target; if (!input || !input.name || !input.name.startsWith('paymentMeansIBAN')) return; _clearFieldError(input, input.name + '-error'); }); } // PR-A14: dirty tracking — marcăm fișierul activ la orice editare a formularului const invoiceForm = document.getElementById('invoiceForm'); if (invoiceForm) { invoiceForm.addEventListener('input', () => _markActiveDirty()); invoiceForm.addEventListener('change', () => _markActiveDirty()); } } // ============================================================================ // A12: Profil furnizor (PR-PROFIL) // Salvează / aplică / șterge datele furnizorului în localStorage. // Cheia: efactura.profil.v1 (prefix enforced de storage.js). // ============================================================================ const SUPPLIER_PROFILE_KEY = 'efactura.profil.v1'; const SUPPLIER_FIELDS = [ 'supplierName', 'supplierVAT', 'supplierCompanyId', 'supplierAddress', 'supplierCity', 'supplierCountrySubentity', 'supplierCountry', 'supplierPhone', 'supplierContactName', 'supplierEmail' ]; /** Actualizează vizibilitatea butoanelor "Folosește / Șterge profil". */ function _updateProfileButtons() { const profile = getJSON(SUPPLIER_PROFILE_KEY); const hasProfile = profile !== null; const btnUse = document.getElementById('btnUseProfile'); const btnDelete = document.getElementById('btnDeleteProfile'); if (btnUse) btnUse.style.display = hasProfile ? '' : 'none'; if (btnDelete) btnDelete.style.display = hasProfile ? '' : 'none'; } /** Salvează câmpurile furnizorului în localStorage. */ function saveSupplierProfile() { const profile = {}; SUPPLIER_FIELDS.forEach(f => { const el = document.querySelector(`[name="${f}"]`); if (el) profile[f] = el.value; }); setJSON(SUPPLIER_PROFILE_KEY, profile); _updateProfileButtons(); showToast('Profil furnizor salvat.', 'success'); } /** Populează câmpurile furnizorului din profilul salvat. */ function useSupplierProfile() { const profile = getJSON(SUPPLIER_PROFILE_KEY); if (!profile) { showToast('Nu există profil salvat.', 'info'); return; } SUPPLIER_FIELDS.forEach(f => { const el = document.querySelector(`[name="${f}"]`); if (el && profile[f] !== undefined) el.value = profile[f]; }); showToast('Profil furnizor aplicat.', 'success'); } /** Șterge profilul furnizorului din localStorage. */ function deleteSupplierProfile() { localStorage.removeItem(SUPPLIER_PROFILE_KEY); _updateProfileButtons(); showToast('Profil furnizor șters.', 'info'); } // Handling VAT type changes window.handleVatTypeChange = function(index) { const vatTypeSelect = document.querySelector(`[name="vatType${index}"]`); const vatRateInput = document.querySelector(`[name="vatRate${index}"]`); // Verifică dacă furnizorul este neplătitor TVA if (!isVATRegistered()) { vatTypeSelect.value = 'O'; vatTypeSelect.disabled = true; // Dezactivează selectul pentru neplătitori vatRateInput.value = '0'; vatRateInput.disabled = true; // Setează codul și motivul scutirii pentru neplătitori const row = document.querySelector(`.vat-row`); if (row) { const exemptionCode = row.querySelector('.vat-exemption-code'); const exemptionReason = row.querySelector('.vat-exemption-reason'); if (exemptionCode) exemptionCode.value = 'VATEX-EU-O'; if (exemptionReason) exemptionReason.value = 'Operațiune efectuată de neplătitor de TVA'; } } else { vatTypeSelect.disabled = false; // Activează selectul pentru plătitori switch(vatTypeSelect.value) { case 'O': // Pentru neplătitori vatRateInput.value = '0'; vatRateInput.disabled = true; break; case 'AE': // Taxare inversă case 'Z': // Cotă 0% case 'E': // Scutit vatRateInput.value = '0'; vatRateInput.disabled = true; break; case 'S': // Standard vatRateInput.value = '19'; vatRateInput.disabled = false; break; } } updateTotals(); } function isVATRegistered() { const supplierVAT = document.querySelector('[name="supplierVAT"]').value.trim().toUpperCase(); return supplierVAT.startsWith('RO'); } function handleChargeVatTypeChange(index) { const vatTypeSelect = document.querySelector(`[name="chargeVatType${index}"]`); const vatRateInput = document.querySelector(`[name="chargeVatRate${index}"]`); if (!vatTypeSelect || !vatRateInput) return; // Verifică dacă furnizorul este neplătitor TVA if (!isVATRegistered()) { vatTypeSelect.value = 'O'; vatTypeSelect.disabled = true; // Dezactivează selectul pentru neplătitori vatRateInput.value = '0'; vatRateInput.disabled = true; // Setează codul și motivul scutirii pentru neplătitori const row = document.querySelector(`.vat-row`); if (row) { const exemptionCode = row.querySelector('.vat-exemption-code'); const exemptionReason = row.querySelector('.vat-exemption-reason'); if (exemptionCode) exemptionCode.value = 'VATEX-EU-O'; if (exemptionReason) exemptionReason.value = 'Operațiune efectuată de neplătitor de TVA'; } } else { vatTypeSelect.disabled = false; // Activează selectul pentru plătitori switch(vatTypeSelect.value) { case 'O': // Pentru neplătitori vatRateInput.value = '0'; vatRateInput.disabled = true; break; case 'AE': // Taxare inversă case 'Z': // Cotă 0% case 'E': // Scutit vatRateInput.value = '0'; vatRateInput.disabled = true; break; case 'S': // Standard vatRateInput.value = '19'; vatRateInput.disabled = false; break; } } // Clear manual edits and refresh manuallyEditedVatRows.clear(); refreshTotals(); } // Funcție pentru actualizarea tuturor categoriilor TVA când se modifică codul fiscal function updateAllVATTypes() { const isNotVATRegistered = !isVATRegistered(); // Actualizează toate liniile de articole document.querySelectorAll('.line-item').forEach((item, index) => { const vatTypeSelect = item.querySelector(`[name="vatType${index}"]`); const vatRateInput = item.querySelector(`[name="vatRate${index}"]`); if (isNotVATRegistered) { vatTypeSelect.value = 'O'; vatTypeSelect.disabled = true; vatRateInput.value = '0'; vatRateInput.disabled = true; } else { vatTypeSelect.disabled = false; // Restabilește valorile implicite pentru plătitori if (vatTypeSelect.value === 'O') { vatTypeSelect.value = 'S'; vatRateInput.value = '19'; vatRateInput.disabled = false; } } }); // Actualizează breakdown-ul TVA updateVATBreakdown(); refreshTotals(); } // XML modifications function addUnitCode(code) { if (!UNIT_CODES.has(code)) { UNIT_CODES.set(code, `${code} (${code})`); } } function createUnitCodeOptionsHTML(selectedCode = 'EA') { return Array.from(UNIT_CODES.entries()) .map(([code, description]) => `` ) .join(''); } function storeOriginalTotals(xmlDoc) { const taxTotal = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); const monetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); originalTotals = { subtotal: getXMLValue(monetaryTotal, 'cbc\\:LineExtensionAmount, LineExtensionAmount'), allowances: getXMLValue(monetaryTotal, 'cbc\\:AllowanceTotalAmount, AllowanceTotalAmount', '0'), charges: getXMLValue(monetaryTotal, 'cbc\\:ChargeTotalAmount, ChargeTotalAmount', '0'), netAmount: getXMLValue(monetaryTotal, 'cbc\\:TaxExclusiveAmount, TaxExclusiveAmount'), totalVat: getXMLValue(taxTotal, 'cbc\\:TaxAmount, TaxAmount'), total: getXMLValue(monetaryTotal, 'cbc\\:TaxInclusiveAmount, TaxInclusiveAmount') }; // console.log('Original totals from XML:', originalTotals); const vatBreakdown = []; const taxSubtotals = xmlDoc.querySelectorAll('cac\\:TaxSubtotal, TaxSubtotal'); taxSubtotals.forEach(subtotal => { const taxCategory = subtotal.querySelector('cac\\:TaxCategory, TaxCategory'); vatBreakdown.push({ taxableAmount: getXMLValue(subtotal, 'cbc\\:TaxableAmount, TaxableAmount'), taxAmount: getXMLValue(subtotal, 'cbc\\:TaxAmount, TaxAmount'), percent: getXMLValue(taxCategory, 'cbc\\:Percent, Percent'), type: getXMLValue(taxCategory, 'cbc\\:ID, ID', 'S'), exemptionCode: getXMLValue(taxCategory, 'cbc\\:TaxExemptionReasonCode, TaxExemptionReasonCode'), exemptionReason: getXMLValue(taxCategory, 'cbc\\:TaxExemptionReason, TaxExemptionReason') }); }); originalTotals.vatBreakdown = vatBreakdown; } function restoreOriginalTotals() { if (!originalTotals) return; // Display exact values from XML with formatting document.getElementById('subtotal').textContent = formatter.formatCurrency(originalTotals.subtotal); document.getElementById('totalAllowances').textContent = formatter.formatCurrency(originalTotals.allowances); document.getElementById('totalCharges').textContent = formatter.formatCurrency(originalTotals.charges); document.getElementById('netAmount').textContent = formatter.formatCurrency(originalTotals.netAmount); document.getElementById('vat').textContent = formatter.formatCurrency(originalTotals.totalVat); // Bypass formatting for total document.getElementById('total').textContent = originalTotals.total; const container = document.getElementById('vatBreakdownRows'); if (container) { container.innerHTML = ''; if (originalTotals.vatBreakdown && originalTotals.vatBreakdown.length > 0) { originalTotals.vatBreakdown.forEach(vat => { addVATBreakdownRow( vat.percent, vat.taxableAmount, vat.taxAmount, vat.type, null, vat.exemptionCode, vat.exemptionReason ); }); } } // PR-A11: run math validation după ce displayed-urile sunt setate la // valorile XML loaded (înainte ca user să editeze ceva). if (typeof validateMath === 'function') validateMath(); } function updateNoteCounter() { const noteInput = document.querySelector('[name="invoiceNote"]'); const counter = document.querySelector('.note-counter'); if (noteInput && counter) { const length = noteInput.value.length; counter.textContent = `${length}/900 caractere`; } } function splitNoteIntoChunks(text, maxLength) { if (!text) return []; const chunks = []; let remainingText = text; while (remainingText.length > 0) { if (remainingText.length <= maxLength) { chunks.push(remainingText); break; } let splitPoint = remainingText.substr(0, maxLength).lastIndexOf('\n'); if (splitPoint === -1) { splitPoint = remainingText.substr(0, maxLength).lastIndexOf(' '); } if (splitPoint === -1) splitPoint = maxLength; chunks.push(remainingText.substr(0, splitPoint)); remainingText = remainingText.substr(splitPoint + 1); } return chunks.filter(chunk => chunk.trim().length > 0); } function populateBasicDetails(xmlDoc) { document.querySelector('[name="invoiceNumber"]').value = getXMLValue(xmlDoc, 'cbc\\:ID, ID'); // PR-TIPURI (A4): citește cbc:InvoiceTypeCode (default 380 dacă absent). const invoiceTypeCode = getXMLValue(xmlDoc, 'cbc\\:InvoiceTypeCode, InvoiceTypeCode', '380'); const typeSelect = document.querySelector('[name="invoiceTypeCode"]'); if (typeSelect) { // Dacă XML conține un cod necunoscut, păstrează default-ul 380. typeSelect.value = INVOICE_TYPES[invoiceTypeCode] ? invoiceTypeCode : '380'; } // Get and combine all Note elements const notes = xmlDoc.querySelectorAll('cbc\\:Note, Note'); const combinedNotes = Array.from(notes).map(note => note.textContent).join('\n'); document.querySelector('[name="invoiceNote"]').value = combinedNotes; updateNoteCounter(); const issueDate = getXMLValue(xmlDoc, 'cbc\\:IssueDate, IssueDate'); const dueDate = getXMLValue(xmlDoc, 'cbc\\:DueDate, DueDate'); if (issueDate) { const [year, month, day] = issueDate.split('-'); document.querySelector('[name="issueDate"]').value = `${day}.${month}.${year}`; } if (dueDate) { const [year, month, day] = dueDate.split('-'); document.querySelector('[name="dueDate"]').value = `${day}.${month}.${year}`; } // Add currency code handling const documentCurrencyCode = getXMLValue(xmlDoc, 'cbc\\:DocumentCurrencyCode, DocumentCurrencyCode', 'RON'); const taxCurrencyCode = getXMLValue(xmlDoc, 'cbc\\:TaxCurrencyCode, TaxCurrencyCode', ''); document.querySelector('[name="documentCurrencyCode"]').value = documentCurrencyCode; document.querySelector('[name="taxCurrencyCode"]').value = taxCurrencyCode; // Store original totals and display them storeOriginalTotals(xmlDoc); restoreOriginalTotals(); } function populatePartyDetails(xmlDoc) { function extractPartyDetails(party, prefix) { // Extract contact information const contact = party.querySelector('cac\\:Contact, Contact'); const phone = contact?.querySelector('cbc\\:Telephone, Telephone')?.textContent || ''; const contactName = contact?.querySelector('cbc\\:Name, Name')?.textContent || ''; const email = contact?.querySelector('cbc\\:ElectronicMail, ElectronicMail')?.textContent || ''; // Country Code Extraction const countryCodeElement = party.querySelector('cac\\:Country cbc\\:IdentificationCode, Country IdentificationCode'); const countryCode = countryCodeElement ? countryCodeElement.textContent.trim() : 'RO'; // Postal Address Details const postalAddress = party.querySelector('cac\\:PostalAddress, PostalAddress'); const streetName = postalAddress ? getXMLValue(postalAddress, 'cbc\\:StreetName, StreetName') : ''; const cityName = postalAddress ? getXMLValue(postalAddress, 'cbc\\:CityName, CityName') : ''; const countyCode = postalAddress ? getXMLValue(postalAddress, 'cbc\\:CountrySubentity, CountrySubentity') : ''; // Set inputs document.querySelector(`[name="${prefix}Name"]`).value = getXMLValue(party, 'cac\\:PartyLegalEntity cbc\\:RegistrationName, PartyLegalEntity RegistrationName'); // Cod TVA / Nr. înregistrare — derivă în funcție de prezența PartyTaxScheme. // Plătitor TVA: PartyTaxScheme/CompanyID e codul TVA, PartyLegalEntity/CompanyID e nr. registru. // Neplătitor TVA: PartyTaxScheme lipsește; CIF-ul stă în PartyLegalEntity/CompanyID, // iar nr. registru în PartyIdentification/ID. Trebuie inversate la încărcare ca să apară // în câmpurile corecte din formular (simetric cu createPartyElement la salvare). const taxSchemeCompanyId = getXMLValue(party, 'cac\\:PartyTaxScheme cbc\\:CompanyID, PartyTaxScheme CompanyID'); const legalEntityCompanyId = getXMLValue(party, 'cac\\:PartyLegalEntity cbc\\:CompanyID, PartyLegalEntity CompanyID'); const partyIdentificationId = getXMLValue(party, 'cac\\:PartyIdentification cbc\\:ID, PartyIdentification ID'); let vatValue, companyIdValue; if (taxSchemeCompanyId) { vatValue = taxSchemeCompanyId; companyIdValue = legalEntityCompanyId || partyIdentificationId; } else { vatValue = legalEntityCompanyId; companyIdValue = partyIdentificationId; } document.querySelector(`[name="${prefix}VAT"]`).value = vatValue; document.querySelector(`[name="${prefix}CompanyId"]`).value = companyIdValue; document.querySelector(`[name="${prefix}Address"]`).value = streetName; document.querySelector(`[name="${prefix}City"]`).value = cityName; document.querySelector(`[name="${prefix}Phone"]`).value = phone; document.querySelector(`[name="${prefix}ContactName"]`).value = contactName; document.querySelector(`[name="${prefix}Email"]`).value = email; // Country Select const countrySelect = document.querySelector(`[name="${prefix}Country"]`); if (countrySelect) { countrySelect.value = countryCode; countrySelect.dataset.xmlValue = countryCode; } // County Select const countySelect = document.querySelector(`[name="${prefix}CountrySubentity"]`); if (countySelect) { countySelect.value = countyCode; countySelect.dataset.xmlValue = countyCode; } } const supplierParty = xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty'); if (supplierParty) { const supplierPartyDetails = supplierParty.querySelector('cac\\:Party, Party'); if (supplierPartyDetails) { extractPartyDetails(supplierPartyDetails, 'supplier'); } } const customerParty = xmlDoc.querySelector('cac\\:AccountingCustomerParty, AccountingCustomerParty'); if (customerParty) { const customerPartyDetails = customerParty.querySelector('cac\\:Party, Party'); if (customerPartyDetails) { extractPartyDetails(customerPartyDetails, 'customer'); } } initializeLocationSelectors(); } function populateAllowanceCharges(xmlDoc) { const charges = parseAllowanceCharges(xmlDoc); displayAllowanceCharges(charges); charges.forEach((_, index) => { setupAllowanceChargeListeners(index); addChargeVatTypeChangeListener(index); }); } // ============================================================================ // A6: BillingReference (cac:BillingReference / cac:InvoiceDocumentReference) // Populate + update helpers. UI: câmpuri billingRefId + billingRefDate în // Detalii Factură. La Stornare, handleStorno() auto-populează cu ID + dată // facturii originale. // ============================================================================ function populateBillingReference(xmlDoc) { const refEl = xmlDoc.querySelector('cac\\:BillingReference cac\\:InvoiceDocumentReference, BillingReference InvoiceDocumentReference'); const idInput = document.querySelector('[name="billingRefId"]'); const dateInput = document.querySelector('[name="billingRefDate"]'); if (!idInput || !dateInput) return; if (refEl) { idInput.value = getXMLValue(refEl, 'cbc\\:ID, ID') || ''; const rawDate = getXMLValue(refEl, 'cbc\\:IssueDate, IssueDate') || ''; if (rawDate) { const [year, month, day] = rawDate.split('-'); dateInput.value = `${day}.${month}.${year}`; } else { dateInput.value = ''; } } else { idInput.value = ''; dateInput.value = ''; } } function updateBillingReference(xmlDoc) { // Remove existing BillingReference elements. xmlDoc.querySelectorAll('cac\\:BillingReference, BillingReference').forEach(el => el.remove()); const id = (document.querySelector('[name="billingRefId"]')?.value || '').trim(); if (!id) return; // Nu scriem BillingReference dacă câmpul e gol. const dateRaw = (document.querySelector('[name="billingRefDate"]')?.value || '').trim(); let isoDate = ''; if (dateRaw && /^\d{2}\.\d{2}\.\d{4}$/.test(dateRaw)) { const [dd, mm, yyyy] = dateRaw.split('.'); isoDate = `${yyyy}-${mm}-${dd}`; } const billingRef = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:BillingReference'); const docRef = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:InvoiceDocumentReference'); docRef.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ID', id)); if (isoDate) { docRef.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:IssueDate', isoDate)); } billingRef.appendChild(docRef); // Insert before cac:AccountingSupplierParty (per UBL 2.1 ordering). const supplierParty = xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty'); if (supplierParty) { xmlDoc.documentElement.insertBefore(billingRef, supplierParty); } else { xmlDoc.documentElement.appendChild(billingRef); } } // ============================================================================ // A5: PaymentMeans (cac:PaymentMeans multiple) // UI: secțiune "Modalități de Plată" cu rânduri dinamice (cod + IBAN). // PAYMENT_MEANS_CODES definit la top lângă VAT_TYPES (nu duplicat). // ============================================================================ let _paymentMeansCount = 0; function createPaymentMeansRowHTML(index, code = '30', iban = '') { const options = Object.entries(PAYMENT_MEANS_CODES) .map(([val, label]) => ``) .join(''); return `
    `; } function addPaymentMeansRow(code = '30', iban = '') { const container = document.getElementById('paymentMeansRows'); if (!container) return; const index = _paymentMeansCount++; container.insertAdjacentHTML('beforeend', createPaymentMeansRowHTML(index, code, iban)); } function removePaymentMeansRow(index) { const row = document.querySelector(`.payment-means-row[data-pm-index="${index}"]`); if (row) row.remove(); } function populatePaymentMeans(xmlDoc) { const container = document.getElementById('paymentMeansRows'); if (!container) return; container.innerHTML = ''; _paymentMeansCount = 0; const pmElements = xmlDoc.querySelectorAll('cac\\:PaymentMeans, PaymentMeans'); pmElements.forEach(pmEl => { const code = getXMLValue(pmEl, 'cbc\\:PaymentMeansCode, PaymentMeansCode') || '30'; const iban = getXMLValue(pmEl, 'cac\\:PayeeFinancialAccount cbc\\:ID, PayeeFinancialAccount ID') || ''; addPaymentMeansRow(code, iban); }); } function updatePaymentMeans(xmlDoc) { // Remove existing PaymentMeans elements. xmlDoc.querySelectorAll('cac\\:PaymentMeans, PaymentMeans').forEach(el => el.remove()); const rows = document.querySelectorAll('.payment-means-row'); if (rows.length === 0) return; // Insert before cac:AllowanceCharge (sau TaxTotal dacă nu există AllowanceCharge). const refEl = xmlDoc.querySelector('cac\\:AllowanceCharge, AllowanceCharge') || xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); rows.forEach(row => { const idx = row.dataset.pmIndex; const code = document.querySelector(`[name="paymentMeansCode${idx}"]`)?.value || '30'; const iban = (document.querySelector(`[name="paymentMeansIBAN${idx}"]`)?.value || '').trim(); const pmEl = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:PaymentMeans'); pmEl.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:PaymentMeansCode', code)); if (iban) { const account = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:PayeeFinancialAccount'); account.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ID', iban)); pmEl.appendChild(account); } if (refEl) { xmlDoc.documentElement.insertBefore(pmEl, refEl); } else { xmlDoc.documentElement.appendChild(pmEl); } }); } function populateLineItems(xmlDoc) { const lineItems = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine'); const lineItemsContainer = document.getElementById('lineItems'); lineItemsContainer.innerHTML = '

    Articole Factură

    '; lineItems.forEach((item, index) => { const quantity = getXMLValue(item, 'cbc\\:InvoicedQuantity, InvoicedQuantity', '0'); const unitCode = item.querySelector('cbc\\:InvoicedQuantity, InvoicedQuantity')?.getAttribute('unitCode') || 'EA'; const price = getXMLValue(item, 'cac\\:Price cbc\\:PriceAmount, Price PriceAmount', '0'); // PR-A11: capture original LineExtensionAmount from XML for math validation badge. const xmlLineAmount = getXMLValue(item, 'cbc\\:LineExtensionAmount, LineExtensionAmount', ''); const itemElement = item.querySelector('cac\\:Item, Item'); const description = getXMLValue(itemElement, 'cbc\\:Name, Name', ''); const itemDescription = getXMLValue(itemElement, 'cbc\\:Description, Description', ''); const taxCategory = itemElement.querySelector('cac\\:ClassifiedTaxCategory, ClassifiedTaxCategory'); const vatType = getXMLValue(taxCategory, 'cbc\\:ID, ID') || 'S'; const vatRate = getXMLValue(taxCategory, 'cbc\\:Percent, Percent') || '19'; // Extragem discountul și codul de motiv de pe linie dacă există let lineDiscount = 0; let discountReasonCode = ''; const allowanceCharge = item.querySelector('cac\\:AllowanceCharge, AllowanceCharge'); if (allowanceCharge) { const chargeIndicator = getXMLValue(allowanceCharge, 'cbc\\:ChargeIndicator, ChargeIndicator'); if (chargeIndicator === 'false') { lineDiscount = parseFloat(getXMLValue(allowanceCharge, 'cbc\\:Amount, Amount', '0')); discountReasonCode = getXMLValue(allowanceCharge, 'cbc\\:AllowanceChargeReasonCode, AllowanceChargeReasonCode', ''); } } addUnitCode(unitCode); const lineItemHtml = createLineItemHTML( index, description, quantity, price, vatRate, unitCode, vatType, itemDescription, lineDiscount, discountReasonCode ); lineItemsContainer.insertAdjacentHTML('beforeend', lineItemHtml); // PR-A11: stash original XML LineExtensionAmount on the line-item element // for math validation badge (compare computed vs XML loaded with ±0.01 RON // tolerance când row-ul e clean — toate input-urile dataset.dirty='0'). const lineItemEl = lineItemsContainer.querySelector(`.line-item[data-index="${index}"]`); if (lineItemEl && xmlLineAmount !== '') { const parsedXmlAmount = parseStrict(xmlLineAmount); if (parsedXmlAmount !== null) { lineItemEl.dataset.xmlLineAmount = parsedXmlAmount.toFixed(2); } } // PR-E E1: dataset.raw seeded din XML (canonical source of truth). // Wire blur listeners care normalizează la commit (parseStrict + setRaw). seedNumericRawForLineItem(index, { quantity, price, lineDiscount, vatRate }); // Parse identifications after adding the line item HTML if (itemElement) { parseIdentifications(itemElement, index); } if (!isVATRegistered()) { const vatTypeSelect = document.querySelector(`[name="vatType${index}"]`); const vatRateInput = document.querySelector(`[name="vatRate${index}"]`); if (vatTypeSelect && vatRateInput) { vatTypeSelect.value = 'O'; vatTypeSelect.disabled = true; vatRateInput.value = '0'; vatRateInput.disabled = true; } } // Enable/disable discount reason code field based on discount value const discountInput = document.querySelector(`[name="lineDiscount${index}"]`); const reasonCodeSelect = document.querySelector(`[name="discountReasonCode${index}"]`); if (discountInput && reasonCodeSelect) { reasonCodeSelect.disabled = lineDiscount <= 0; } handleLineItemChange(index); }); } // PR-E E1: helper care setează dataset.raw + wire blur normalize pe toate // input-urile numerice ale unei linii de factură. Se apelează după // insertAdjacentHTML, când DOM-ul există. function seedNumericRawForLineItem(index, raw) { const fields = [ { name: 'quantity', decimals: 3, value: raw.quantity }, { name: 'price', decimals: 4, value: raw.price }, { name: 'lineDiscount', decimals: 2, value: raw.lineDiscount }, { name: 'vatRate', decimals: 2, value: raw.vatRate }, ]; fields.forEach(f => { const input = document.querySelector(`[name="${f.name}${index}"]`); if (!input) return; const big = parseStrict(f.value); if (big !== null) { // Stocăm raw canonical decimal-dot. NU schimbăm input.value // (lăsăm display ca în markup-ul existent — type=number e // display-locale-agnostic în input field și are nevoie de '.'). input.dataset.raw = big.toFixed(f.decimals); } wireDatasetRaw(input, f.decimals); }); } // PR-E E1: idem pentru allowance/charge. function seedNumericRawForCharge(index, raw) { const fields = [ { name: 'chargeAmount', decimals: 2, value: raw.amount }, { name: 'chargeBaseAmount', decimals: 2, value: raw.baseAmount }, { name: 'chargeVatRate', decimals: 2, value: raw.vatRate }, ]; fields.forEach(f => { const input = document.querySelector(`[name="${f.name}${index}"]`); if (!input) return; const big = parseStrict(f.value); if (big !== null) { input.dataset.raw = big.toFixed(f.decimals); } wireDatasetRaw(input, f.decimals); }); } function setupAllowanceChargeListeners(index) { const chargeAmountInput = document.querySelector(`[name="chargeAmount${index}"]`); const chargeTypeInput = document.querySelector(`[name="chargeType${index}"]`); const chargeVatTypeInput = document.querySelector(`[name="chargeVatType${index}"]`); const chargeVatRateInput = document.querySelector(`[name="chargeVatRate${index}"]`); const reasonCodeSelect = document.querySelector(`[name="chargeReasonCode${index}"]`); const reasonInput = document.querySelector(`[name="chargeReason${index}"]`); // Add change listeners to all inputs [chargeAmountInput, chargeTypeInput, chargeVatTypeInput, chargeVatRateInput].forEach(input => { if (input) { input.addEventListener('change', () => { manuallyEditedVatRows.clear(); refreshTotals(); }); } }); // Add reason code change listener if (reasonCodeSelect) { reasonCodeSelect.addEventListener('change', () => { const isCharge = chargeTypeInput.value === 'true'; const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES; reasonInput.value = codes[reasonCodeSelect.value] || ''; }); } // Special handling for VAT type changes if (chargeVatTypeInput) { chargeVatTypeInput.addEventListener('change', () => handleChargeVatTypeChange(index)); } } function addChargeVatTypeChangeListener(index) { const vatTypeSelect = document.querySelector(`[name="chargeVatType${index}"]`); if (vatTypeSelect) { vatTypeSelect.addEventListener('change', function() { handleChargeVatTypeChange(index); // Force refresh of VAT breakdown displayVATBreakdown(); updateTotals(); }); } } function parseAllowanceCharges(xmlDoc) { const charges = []; // Folosim Set pentru a preveni dublarea elementelor din cauza namespace-urilor const processedIds = new Set(); // Selectăm toate elementele AllowanceCharge care sunt copii direcți ai Invoice const allowanceCharges = xmlDoc.querySelectorAll('cac\\:AllowanceCharge, AllowanceCharge'); allowanceCharges.forEach(ac => { // Verificăm dacă elementul este copil direct al Invoice și nu a fost deja procesat if (ac.parentElement === xmlDoc.documentElement) { const amount = getXMLValue(ac, 'cbc\\:Amount, Amount'); const reasonCode = getXMLValue(ac, 'cbc\\:AllowanceChargeReasonCode, AllowanceChargeReasonCode'); // Creăm un ID unic bazat pe cod și valoare pentru a evita duplicatele const uniqueId = `${reasonCode}-${amount}`; if (!processedIds.has(uniqueId)) { processedIds.add(uniqueId); const charge = { isCharge: getXMLValue(ac, 'cbc\\:ChargeIndicator, ChargeIndicator') === 'true', reasonCode: reasonCode, reason: getXMLValue(ac, 'cbc\\:AllowanceChargeReason, AllowanceChargeReason'), amount: parseFloat(amount) || 0, baseAmount: parseFloat(getXMLValue(ac, 'cbc\\:BaseAmount, BaseAmount')) || 0, vatRate: parseFloat(getXMLValue(ac, 'cac\\:TaxCategory cbc\\:Percent, TaxCategory Percent')) || 0, vatTypeId: getXMLValue(ac, 'cac\\:TaxCategory cbc\\:ID, TaxCategory ID', 'S'), multiplierFactorNumeric: parseFloat(getXMLValue(ac, 'cbc\\:MultiplierFactorNumeric, MultiplierFactorNumeric')) || 0 }; charges.push(charge); } } }); return charges; } function displayAllowanceCharges(charges) { const container = document.getElementById('allowanceCharges'); container.innerHTML = '

    Reduceri și Taxe Suplimentare

    '; charges.forEach((charge, index) => { const html = createAllowanceChargeHTML(index, charge); container.insertAdjacentHTML('beforeend', html); // PR-E E1: seed dataset.raw + wire blur listeners. seedNumericRawForCharge(index, { amount: charge.amount, baseAmount: charge.baseAmount, vatRate: charge.vatRate }); }); } function addAllowanceCharge() { const container = document.getElementById('allowanceCharges'); const index = document.querySelectorAll('.allowance-charge').length; // Create new charge with default values const newCharge = { isCharge: true, reasonCode: 'TV', reason: 'Cheltuieli transport', amount: 0, vatRate: 19.0, vatTypeId: 'S' }; // Add the HTML const html = createAllowanceChargeHTML(index, newCharge); container.insertAdjacentHTML('beforeend', html); // PR-E E1: seed dataset.raw + wire blur listeners. seedNumericRawForCharge(index, { amount: newCharge.amount, baseAmount: newCharge.baseAmount, vatRate: newCharge.vatRate }); // Setup event listeners setupAllowanceChargeListeners(index); // Force refresh of totals and VAT refreshTotals(); } function removeAllowanceCharge(index) { const charge = document.querySelector(`.allowance-charge[data-index="${index}"]`); if (charge) { // Remove the element charge.remove(); // Renumber remaining charges renumberAllowanceCharges(); // Clear manual edits and refresh totals manuallyEditedVatRows.clear(); refreshTotals(); } } function renumberAllowanceCharges() { document.querySelectorAll('.allowance-charge').forEach((charge, newIndex) => { // Update data-index charge.dataset.index = newIndex; // Update all input names charge.querySelectorAll('input, select').forEach(input => { const name = input.getAttribute('name'); if (name) { const baseName = name.replace(/\d+$/, ''); input.setAttribute('name', baseName + newIndex); } }); }); } function addLineItem() { const container = document.getElementById('lineItems'); const index = document.querySelectorAll('.line-item').length; // Determină tipul implicit de TVA bazat pe statusul furnizorului const defaultVatType = isVATRegistered() ? 'S' : 'O'; const defaultVatRate = isVATRegistered() ? '19' : '0'; const lineItemHtml = createLineItemHTML(index, '', '1', '0', defaultVatRate, 'EA', defaultVatType); container.insertAdjacentHTML('beforeend', lineItemHtml); const newItem = container.lastElementChild; const vatTypeSelect = newItem.querySelector(`[name="vatType${index}"]`); // PR-E E1: seed dataset.raw + wire blur listeners. seedNumericRawForLineItem(index, { quantity: '1', price: '0', lineDiscount: '0', vatRate: defaultVatRate }); if (!isVATRegistered()) { vatTypeSelect.disabled = true; newItem.querySelector(`[name="vatRate${index}"]`).disabled = true; } // Adăugăm event handlers pentru noua linie handleLineItemChange(index); manuallyEditedVatRows.clear(); refreshTotals(); } function removeLineItem(index) { const lineItem = document.querySelector(`.line-item[data-index="${index}"]`); if (lineItem) { lineItem.remove(); renumberLineItems(); manuallyEditedVatRows.clear(); // Clear manual edits refreshTotals(); // Recalculate all totals } } function handleLineItemChange(index) { const quantityInput = document.querySelector(`[name="quantity${index}"]`); const priceInput = document.querySelector(`[name="price${index}"]`); const discountInput = document.querySelector(`[name="lineDiscount${index}"]`); const reasonCodeSelect = document.querySelector(`[name="discountReasonCode${index}"]`); // Handler pentru schimbarea discount-ului discountInput?.addEventListener('input', function() { const discountValue = parseFloat(this.value) || 0; if (reasonCodeSelect) { reasonCodeSelect.disabled = discountValue == 0; // activat pentru orice valoare nenulă // Dacă discount != 0 și nu e selectat niciun cod, selectăm codul implicit if (discountValue != 0 && !reasonCodeSelect.value) { reasonCodeSelect.value = '95'; // Cod implicit pentru reducere } } updateTotals(); }); // Handlers pentru cantitate și preț quantityInput?.addEventListener('change', updateTotals); priceInput?.addEventListener('change', updateTotals); // Force refresh updateTotals(); } function handleStorno() { // A6: populează câmpurile BillingReference cu datele facturii curente // înainte de negare (referința facturii originale pentru nota de credit). const currentInvoiceNumber = document.querySelector('[name="invoiceNumber"]')?.value || ''; const currentIssueDate = document.querySelector('[name="issueDate"]')?.value || ''; const billingRefIdInput = document.querySelector('[name="billingRefId"]'); const billingRefDateInput = document.querySelector('[name="billingRefDate"]'); if (billingRefIdInput && !billingRefIdInput.value && currentInvoiceNumber) { billingRefIdInput.value = currentInvoiceNumber; } if (billingRefDateInput && !billingRefDateInput.value && currentIssueDate) { billingRefDateInput.value = currentIssueDate; } // PR-TIPURI (A4): la storno setăm cbc:InvoiceTypeCode = 381 (Notă de // credit). User poate suprascrie din dropdown dacă vrea alt cod. const typeSelect = document.querySelector('[name="invoiceTypeCode"]'); if (typeSelect) typeSelect.value = '381'; // PR-A11: folosim getRaw/setRaw/markDirty în loc de parseFloat // pentru a păstra dataset.raw consistent cu input.value după negare. document.querySelectorAll('.line-item').forEach((item, index) => { const quantityInput = document.querySelector(`[name="quantity${index}"]`); if (!quantityInput) return; setRaw(quantityInput, getRaw(quantityInput).times(-1), 3); markDirty(quantityInput); }); document.querySelectorAll('.allowance-charge').forEach((item, index) => { const amountInput = document.querySelector(`[name="chargeAmount${index}"]`); if (!amountInput) return; setRaw(amountInput, getRaw(amountInput).times(-1), 2); markDirty(amountInput); }); document.querySelectorAll('.vat-row').forEach(row => { const baseInput = row.querySelector('.vat-base'); const amountInput = row.querySelector('.vat-amount'); if (baseInput) { setRaw(baseInput, getRaw(baseInput).times(-1), 2); markDirty(baseInput); } if (amountInput) { setRaw(amountInput, getRaw(amountInput).times(-1), 2); markDirty(amountInput); } }); manuallyEditedVatRows.clear(); refreshTotals(); if (currentInvoice) { updateTaxTotals(currentInvoice); } } function updateTotals() { // Calculăm totalurile liniilor (deja nete, după discount) const lineItemTotals = calculateLineItemTotals(); const chargeTotals = calculateChargeTotals(); const subtotal = lineItemTotals.subtotal; const allowances = chargeTotals.allowances; const charges = chargeTotals.charges; const netAmount = subtotal - allowances + charges; // Calculate VAT breakdown const { vatBreakdown } = calculateVATBreakdown(); let totalVat = 0; vatBreakdown.forEach((entry) => { if (entry.type === 'S') { totalVat += entry.vatAmount; } }); // Display totals with 2 decimal places displayTotals({ subtotal: roundNumber(subtotal, 2), allowances: roundNumber(allowances, 2), charges: roundNumber(charges, 2), netAmount: roundNumber(netAmount, 2), totalVat: roundNumber(totalVat, 2), total: roundNumber(netAmount + totalVat, 2), vatBreakdown }); // PR-A11: refresh badge-uri math validation după displayTotals. if (typeof validateMath === 'function') validateMath(); // PR-BR: actualizează panelul de validare BR live pe orice editare. _updateBRPanel(); } function refreshTotals() { // Calculate line items first const lineItemTotals = calculateLineItemTotals(); const chargeTotals = calculateChargeTotals(); const subtotal = lineItemTotals.subtotal; const allowances = chargeTotals.allowances; const charges = chargeTotals.charges; const netAmount = subtotal - allowances + charges; // Calculate VAT breakdown const { vatBreakdown } = calculateVATBreakdown(); let totalVat = 0; if (vatBreakdown) { vatBreakdown.forEach(entry => { totalVat += entry.vatAmount; }); } // Display totals displayTotals({ subtotal, allowances, charges, netAmount, totalVat, total: netAmount + totalVat, vatBreakdown }); // PR-A11: refresh badge-uri math validation după displayTotals. if (typeof validateMath === 'function') validateMath(); // PR-BR: actualizează panelul de validare BR live. _updateBRPanel(); } function calculateLineItemTotals() { // PR-E E4: pipeline Big.js. Citește din dataset.raw (canonical), fallback // la input.value via getRaw. Niciun parseFloat. let subtotal = new Big('0'); document.querySelectorAll('.line-item').forEach((item, index) => { const quantity = getRaw(document.querySelector(`[name="quantity${index}"]`)); const price = getRaw(document.querySelector(`[name="price${index}"]`)); const lineDiscount = getRaw(document.querySelector(`[name="lineDiscount${index}"]`)); // LineExtensionAmount = cantitate * preț - discount const lineAmount = quantity.times(price).minus(lineDiscount); subtotal = subtotal.plus(lineAmount); }); return { subtotal: Number(subtotal.round(2, 1).toFixed(2)) }; } function calculateChargeTotals() { // PR-E E4: pipeline Big.js. let allowances = new Big('0'); let charges = new Big('0'); document.querySelectorAll('.allowance-charge').forEach((item, index) => { const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; const amount = getRaw(document.querySelector(`[name="chargeAmount${index}"]`)); if (isCharge) { charges = charges.plus(amount); } else { allowances = allowances.plus(amount); } }); return { allowances: Number(allowances.round(2, 1).toFixed(2)), charges: Number(charges.round(2, 1).toFixed(2)) }; } function calculateTotals() { // PR-E E4: pipeline Big.js end-to-end. Toate citirile via getRaw → Big. let subtotalBig = new Big('0'); let vatBreakdown = new Map(); // Calculăm totalurile liniilor document.querySelectorAll('.line-item').forEach((item, index) => { const quantity = getRaw(document.querySelector(`[name="quantity${index}"]`)); const price = getRaw(document.querySelector(`[name="price${index}"]`)); const lineDiscount = getRaw(document.querySelector(`[name="lineDiscount${index}"]`)); const vatType = document.querySelector(`[name="vatType${index}"]`).value; const vatRate = getRaw(document.querySelector(`[name="vatRate${index}"]`)); // LineExtensionAmount = (cantitate * preț) - discount linie const lineNet = quantity.times(price).minus(lineDiscount); subtotalBig = subtotalBig.plus(lineNet); const rateKey = vatRate.toFixed(2); const key = `${rateKey}-${vatType}`; if (!vatBreakdown.has(key)) { vatBreakdown.set(key, { baseAmount: 0, vatAmount: 0, rate: Number(vatRate.toString()), type: vatType, _baseBig: new Big('0') }); } const entry = vatBreakdown.get(key); entry._baseBig = entry._baseBig.plus(lineNet); }); // Calculăm discounturile și taxele globale (returnate ca Number — convertim la Big intern) const { allowances, charges } = calculateChargeTotals(); const allowancesBig = new Big(String(allowances)); const chargesBig = new Big(String(charges)); // Net = subtotal - allowances + charges const netAmountBig = subtotalBig.minus(allowancesBig).plus(chargesBig); // Ajustăm baza de TVA cu discount/taxe globale document.querySelectorAll('.allowance-charge').forEach((item, index) => { const amount = getRaw(document.querySelector(`[name="chargeAmount${index}"]`)); const vatType = document.querySelector(`[name="chargeVatType${index}"]`).value; const vatRate = getRaw(document.querySelector(`[name="chargeVatRate${index}"]`)); const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; const rateKey = vatRate.toFixed(2); const key = `${rateKey}-${vatType}`; if (!vatBreakdown.has(key)) { vatBreakdown.set(key, { baseAmount: 0, vatAmount: 0, rate: Number(vatRate.toString()), type: vatType, _baseBig: new Big('0') }); } const entry = vatBreakdown.get(key); entry._baseBig = isCharge ? entry._baseBig.plus(amount) : entry._baseBig.minus(amount); }); // VAT pe rată — round HALF_UP la 2 zecimale let totalVatBig = new Big('0'); vatBreakdown.forEach((entry) => { const baseRounded = entry._baseBig.round(2, 1); entry.baseAmount = Number(baseRounded.toFixed(2)); if (entry.type === 'S') { const vatBig = entry._baseBig.times(entry.rate).div(100).round(2, 1); entry.vatAmount = Number(vatBig.toFixed(2)); totalVatBig = totalVatBig.plus(vatBig); } else { entry.vatAmount = 0; } delete entry._baseBig; }); const subtotal = Number(subtotalBig.round(2, 1).toFixed(2)); const netAmount = Number(netAmountBig.round(2, 1).toFixed(2)); const totalVat = Number(totalVatBig.round(2, 1).toFixed(2)); return { subtotal, allowances, charges, netAmount, totalVat, total: Number(netAmountBig.plus(totalVatBig).round(2, 1).toFixed(2)), vatBreakdown }; } function calculateVATBases(lines, globalAllowances, globalCharges) { // Initialize VAT bases const vatBases = {}; // First pass: calculate base amounts per VAT rate from lines lines.forEach(line => { const vatRate = line.vatRate; if (!vatBases[vatRate]) { vatBases[vatRate] = { base: 0, allowances: 0, charges: 0 }; } const lineTotal = line.quantity * line.price; const lineAfterDiscount = lineTotal - line.lineDiscount; vatBases[vatRate].base += lineAfterDiscount; }); // Second pass: apply global allowances and charges per VAT rate globalAllowances.forEach(allowance => { const vatRate = allowance.vatRate; if (vatBases[vatRate]) { vatBases[vatRate].allowances += allowance.amount; } }); globalCharges.forEach(charge => { const vatRate = charge.vatRate; if (vatBases[vatRate]) { vatBases[vatRate].charges += charge.amount; } }); // Calculate final VAT amounts Object.keys(vatBases).forEach(rate => { const rateData = vatBases[rate]; const netBase = rateData.base - rateData.allowances + rateData.charges; rateData.vatAmount = netBase * (parseFloat(rate) / 100); }); return vatBases; } function calculateTotalVAT() { let totalVat = Array.from(document.querySelectorAll('.vat-amount')) .reduce((sum, input) => sum + formatter.parseCurrency(input.value), 0); return roundNumber(totalVat, 2); } function calculateVATBreakdown() { // PR-E E4: pipeline Big.js. Aceeași semantică ca calculateTotals dar // doar partea de breakdown (folosit de displayVATBreakdown). let vatBreakdown = new Map(); let totalVatBig = new Big('0'); // Process line items document.querySelectorAll('.line-item').forEach((item, index) => { const quantity = getRaw(document.querySelector(`[name="quantity${index}"]`)); const price = getRaw(document.querySelector(`[name="price${index}"]`)); const lineDiscount = getRaw(document.querySelector(`[name="lineDiscount${index}"]`)); const vatType = document.querySelector(`[name="vatType${index}"]`).value; const vatRate = getRaw(document.querySelector(`[name="vatRate${index}"]`)); const lineAmount = quantity.times(price).minus(lineDiscount); const rateKey = vatRate.toFixed(2); const key = `${rateKey}-${vatType}`; if (!vatBreakdown.has(key)) { vatBreakdown.set(key, { baseAmount: 0, vatAmount: 0, rate: Number(vatRate.toString()), type: vatType, _baseBig: new Big('0') }); } const entry = vatBreakdown.get(key); entry._baseBig = entry._baseBig.plus(lineAmount); }); // Process allowances and charges document.querySelectorAll('.allowance-charge').forEach((charge, index) => { const amount = getRaw(document.querySelector(`[name="chargeAmount${index}"]`)); const vatType = document.querySelector(`[name="chargeVatType${index}"]`).value; const vatRate = getRaw(document.querySelector(`[name="chargeVatRate${index}"]`)); const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; if (amount.eq(0)) return; const rateKey = vatRate.toFixed(2); const key = `${rateKey}-${vatType}`; if (!vatBreakdown.has(key)) { vatBreakdown.set(key, { baseAmount: 0, vatAmount: 0, rate: Number(vatRate.toString()), type: vatType, _baseBig: new Big('0') }); } const entry = vatBreakdown.get(key); entry._baseBig = isCharge ? entry._baseBig.plus(amount) : entry._baseBig.minus(amount); }); // Calculate VAT for each rate vatBreakdown.forEach((entry) => { entry.baseAmount = Number(entry._baseBig.round(2, 1).toFixed(2)); if (entry.type === 'S') { const vatBig = entry._baseBig.times(entry.rate).div(100).round(2, 1); entry.vatAmount = Number(vatBig.toFixed(2)); totalVatBig = totalVatBig.plus(vatBig); } else { entry.vatAmount = 0; } delete entry._baseBig; }); return { vatBreakdown, totalVat: Number(totalVatBig.round(2, 1).toFixed(2)) }; } window.updateVATRow = function(rowId, source) { const row = document.getElementById(rowId); if (!row) return; const typeSelect = row.querySelector('.vat-type'); const rateInput = row.querySelector('.vat-rate'); const baseInput = row.querySelector('.vat-base'); const amountInput = row.querySelector('.vat-amount'); if (source === 'manual') { manuallyEditedVatRows.add(rowId); // Keep existing values, just update totals updateTotalVAT(); refreshTotals(); return; } // Only calculate VAT amount for non-manual updates if (!manuallyEditedVatRows.has(rowId)) { // PR-E E4: Big.js pentru calcul VAT. const type = typeSelect.value; const rate = getRaw(rateInput); const base = getRaw(baseInput); let calculatedAmountBig; if (type === 'S') { calculatedAmountBig = base.times(rate).div(100).round(2, 1); } else { calculatedAmountBig = new Big('0'); } const calculatedAmount = Number(calculatedAmountBig.toFixed(2)); amountInput.value = formatter.formatCurrency(calculatedAmount); amountInput.dataset.raw = calculatedAmountBig.toFixed(2); updateTotalVAT(); refreshTotals(); } }; window.updateVATRowFromAmount = function(rowId) { const row = document.getElementById(rowId); if (!row) return; // Just mark as manually edited and update the totals // Do not recalculate base amount manuallyEditedVatRows.add(rowId); const amountInput = row.querySelector('.vat-amount'); if (amountInput) { // PR-E E1+E4: parse strict, sync dataset.raw, format display. const valueBig = parseStrictOr(amountInput.value, '0'); amountInput.value = formatter.formatCurrency(Number(valueBig.toString())); amountInput.dataset.raw = valueBig.toFixed(2); } let totalVatBig = new Big('0'); document.querySelectorAll('.vat-row').forEach(vatRow => { totalVatBig = totalVatBig.plus(getRaw(vatRow.querySelector('.vat-amount'))); }); // Update just total VAT and final total const netAmountBig = parseStrictOr(document.getElementById('netAmount').textContent, '0'); document.getElementById('vat').textContent = formatter.formatCurrency(Number(totalVatBig.toFixed(2))); document.getElementById('total').textContent = formatter.formatCurrency(Number(netAmountBig.plus(totalVatBig).toFixed(2))); // PR-A11: refresh badge-uri math validation după edit manual de vat-amount. if (typeof validateMath === 'function') validateMath(); }; window.removeVATRow = function(rowId) { const row = document.getElementById(rowId); if (row) { manuallyEditedVatRows.delete(rowId); row.remove(); updateTotalVAT(); refreshTotals(); } }; window.addVATRate = function() { const container = document.getElementById('vatBreakdownRows'); addVATBreakdownRow(19, 0, 0); refreshTotals(); }; function updateTotalVAT() { // PR-E E4: Big.js. Citește din dataset.raw → fallback parseStrict pe value. const totalVatBig = Array.from(document.querySelectorAll('.vat-amount')) .reduce((sum, input) => sum.plus(getRaw(input)), new Big('0')); document.getElementById('vat').textContent = totalVatBig.toFixed(2); const netAmountBig = parseStrictOr(document.getElementById('netAmount').textContent, '0'); const total = netAmountBig.plus(totalVatBig); document.getElementById('total').textContent = total.toFixed(2); } function updateVATBreakdown() { // Șterge și reconstruiește rândurile TVA const container = document.getElementById('vatBreakdownRows'); if (!container) return; container.innerHTML = ''; const { vatBreakdown } = calculateVATBreakdown(); vatBreakdown.forEach((data, key) => { const [rate, type] = key.split('-'); addVATBreakdownRow( parseFloat(rate), data.baseAmount, data.vatAmount, type ); }); } function displayVATBreakdown(xmlDoc = null) { const container = document.getElementById('vatBreakdownRows'); if (!container) return; // Clear container container.innerHTML = ''; // If XML is provided, use its VAT breakdown if (xmlDoc && originalTotals && originalTotals.vatBreakdown) { originalTotals.vatBreakdown.forEach((vat, index) => { addVATBreakdownRow( vat.percent, vat.taxableAmount, vat.taxAmount, vat.type, `vat-row-${index}`, vat.exemptionCode, vat.exemptionReason ); }); } else { // Calculate current VAT breakdown const { vatBreakdown } = calculateVATBreakdown(); vatBreakdown.forEach((data, key) => { const [rate, type] = key.split('-'); addVATBreakdownRow( parseFloat(rate), data.baseAmount, data.vatAmount, type ); }); } } function createEmptyInvoice() { const parser = new DOMParser(); const xmlString = ` urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1 380 RON RO RO VAT RO VAT `; return parser.parseFromString(xmlString, "text/xml"); } function saveXML() { if (!validateForm()) return; // PR-A11: math validation pre-save. Ruleaza validateMath și dacă footer // diff > tolerance, afișează toast warning ORANGE — DAR NU bloca save-ul. try { if (typeof validateMath === 'function') { const { footerDiff, footerOver } = validateMath(); if (footerOver) { showToast( `Atenție: totalul afișat diferă cu ${footerDiff.toFixed(2)} RON față de calculul intern.`, 'warning', 'Verifică liniile și defalcarea TVA. Salvarea continuă.' ); } } } catch (_) { /* validation must never block save */ } try { if (!currentInvoice) { currentInvoice = createEmptyInvoice(); } const xmlDoc = currentInvoice; // Update all the data updateBasicDetails(xmlDoc); updatePartyDetails(xmlDoc); updateBillingReference(xmlDoc); updatePaymentMeans(xmlDoc); updateAllowanceCharges(xmlDoc); // Remove existing TaxTotal and LegalMonetaryTotal elements const existingTaxTotals = xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal'); existingTaxTotals.forEach(el => el.remove()); const existingMonetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); if (existingMonetaryTotal) { existingMonetaryTotal.remove(); } // Remove existing InvoiceLine elements const existingLines = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine'); existingLines.forEach(el => el.remove()); // Add elements in the correct order updateTaxTotals(xmlDoc); updateMonetaryTotals(xmlDoc); updateLineItems(xmlDoc); downloadXML(xmlDoc); } catch (error) { handleError(error, 'Eroare la salvarea fișierului XML'); } } function updateBasicDetails(xmlDoc) { setXMLValue(xmlDoc, 'cbc\\:ID, ID', document.querySelector('[name="invoiceNumber"]').value); // PR-TIPURI (A4): scrie cbc:InvoiceTypeCode din dropdown. Dacă elementul // lipsește (factură creată cu createEmptyInvoice care îl include deja, sau // XML legacy fără el), îl creăm și inserăm imediat după cbc:DueDate. const typeSelect = document.querySelector('[name="invoiceTypeCode"]'); const typeCode = (typeSelect && INVOICE_TYPES[typeSelect.value]) ? typeSelect.value : '380'; let typeEl = xmlDoc.querySelector('cbc\\:InvoiceTypeCode, InvoiceTypeCode'); if (typeEl) { typeEl.textContent = typeCode; } else { typeEl = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:InvoiceTypeCode', typeCode); const after = xmlDoc.querySelector('cbc\\:DueDate, DueDate') || xmlDoc.querySelector('cbc\\:IssueDate, IssueDate'); if (after && after.parentNode) { after.parentNode.insertBefore(typeEl, after.nextSibling); } else { xmlDoc.documentElement.appendChild(typeEl); } } // Remove existing Note elements const existingNotes = xmlDoc.querySelectorAll('cbc\\:Note, Note'); existingNotes.forEach(note => note.remove()); // Split note text and create new Note elements const noteText = document.querySelector('[name="invoiceNote"]').value; if (noteText) { const insertAfter = xmlDoc.querySelector('cbc\\:InvoiceTypeCode, InvoiceTypeCode'); const chunks = splitNoteIntoChunks(noteText, 300); chunks.forEach(chunk => { const noteElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Note", chunk); if (insertAfter && insertAfter.parentNode) { insertAfter.parentNode.insertBefore(noteElement, insertAfter.nextSibling); } }); } const issueDateValue = document.querySelector('[name="issueDate"]').value; const dueDateValue = document.querySelector('[name="dueDate"]').value; setXMLValue(xmlDoc, 'cbc\\:IssueDate, IssueDate', parseRomanianDate(issueDateValue)); setXMLValue(xmlDoc, 'cbc\\:DueDate, DueDate', parseRomanianDate(dueDateValue)); // Update currency codes const documentCurrencyCode = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; setXMLValue(xmlDoc, 'cbc\\:DocumentCurrencyCode, DocumentCurrencyCode', documentCurrencyCode); const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(); if (taxCurrencyCode) { let taxCurrencyElement = xmlDoc.querySelector('cbc\\:TaxCurrencyCode, TaxCurrencyCode'); if (!taxCurrencyElement) { taxCurrencyElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxCurrencyCode"); const insertAfter = xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode'); if (insertAfter && insertAfter.parentNode) { insertAfter.parentNode.insertBefore(taxCurrencyElement, insertAfter.nextSibling); } } taxCurrencyElement.textContent = taxCurrencyCode; } else { // Remove TaxCurrencyCode if it exists and is empty const taxCurrencyElement = xmlDoc.querySelector('cbc\\:TaxCurrencyCode, TaxCurrencyCode'); if (taxCurrencyElement) { taxCurrencyElement.parentNode.removeChild(taxCurrencyElement); } } } function createPartyElement(xmlDoc, isSupplier, partyData) { const party = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Party"); const hasVatPrefix = /^[A-Z]{2}/.test(partyData.vat?.trim() || ''); const IsNeplatitor = vatHasO(); // Add PartyIdentification if (partyData.companyId) { const partyIdentification = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyIdentification"); partyIdentification.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", partyData.companyId)); party.appendChild(partyIdentification); } function validateCountryCode(countryCode) { const code = countryCode?.trim().toUpperCase() || 'RO'; return ISO_3166_1_CODES.has(code) ? code : 'RO'; } function validateCountyCode(countryCode, countyCode) { if (countryCode === 'RO') { return ROMANIAN_COUNTY_CODES.has(countyCode) ? countyCode : 'RO-B'; } return countyCode; } const postalAddress = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PostalAddress"); postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:StreetName", partyData.address)); postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CityName", partyData.city)); postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CountrySubentity", partyData.county)); const country = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Country"); // Ensure country code is valid ISO 3166-1 format (2 uppercase letters) const countryCode = partyData.country?.trim().toUpperCase() || 'RO'; country.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:IdentificationCode", countryCode)); postalAddress.appendChild(country); party.appendChild(postalAddress); const vatTypesInUse = new Set(); document.querySelectorAll('.line-item').forEach((item, index) => { const vatType = document.querySelector(`[name="vatType${index}"]`).value; vatTypesInUse.add(vatType); }); // Add PartyTaxScheme for all suppliers VAT types except 'O' or all customers with VAT prefix if (hasVatPrefix ) { const partyTaxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyTaxScheme"); partyTaxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.vat.toUpperCase())); const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); if (!IsNeplatitor) { taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); } else { // console.log('not adding VAT ID'); } partyTaxScheme.appendChild(taxScheme); party.appendChild(partyTaxScheme); } // Funcție pentru detectarea dacă există un VAT cu tipul 'O' Neplatitor de TVA // În cazul în care există, nu se va adăuga un element TaxScheme cu ID-ul 'VAT' function vatHasO() { const vatTypesInUse = new Set(); document.querySelectorAll('.line-item').forEach((item, index) => { const vatType = document.querySelector(`[name="vatType${index}"]`).value; vatTypesInUse.add(vatType); }); return vatTypesInUse.size > 0 && vatTypesInUse.has('O'); } // Add PartyLegalEntity const partyLegalEntity = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyLegalEntity"); partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:RegistrationName", partyData.name)); // Add CompanyID if (!hasVatPrefix) { // For non-VAT registered supplier, use VAT number in CompanyID partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.vat)); } else { // For others, use companyId partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.companyId)); } party.appendChild(partyLegalEntity); // Add Contact if phone, email or contactName exists if (partyData.phone || partyData.email || partyData.contactName) { const contact = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Contact"); if (partyData.contactName) { contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Name", partyData.contactName)); } if (partyData.phone) { contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Telephone", partyData.phone)); } if (partyData.email) { contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ElectronicMail", partyData.email)); } party.appendChild(contact); } return party; } function updatePartyDetails(xmlDoc) { // Update supplier details const supplierData = { name: document.querySelector('[name="supplierName"]').value, vat: document.querySelector('[name="supplierVAT"]').value, companyId: document.querySelector('[name="supplierCompanyId"]').value, address: document.querySelector('[name="supplierAddress"]').value, city: document.querySelector('[name="supplierCity"]').value, county: document.querySelector('[name="supplierCountrySubentity"]').value, country: document.querySelector('[name="supplierCountry"]').value, phone: document.querySelector('[name="supplierPhone"]').value, contactName: document.querySelector('[name="supplierContactName"]').value, email: document.querySelector('[name="supplierEmail"]').value }; updatePartyXML(xmlDoc, true, supplierData); // Update customer details const customerData = { name: document.querySelector('[name="customerName"]').value, vat: document.querySelector('[name="customerVAT"]').value, companyId: document.querySelector('[name="customerCompanyId"]').value, address: document.querySelector('[name="customerAddress"]').value, city: document.querySelector('[name="customerCity"]').value, county: document.querySelector('[name="customerCountrySubentity"]').value, country: document.querySelector('[name="customerCountry"]').value, phone: document.querySelector('[name="customerPhone"]').value, contactName: document.querySelector('[name="customerContactName"]').value, email: document.querySelector('[name="customerEmail"]').value }; updatePartyXML(xmlDoc, false, customerData); } function updatePartyXML(xmlDoc, isSupplier, partyData) { const partyElement = createPartyElement(xmlDoc, isSupplier, partyData); const parentTag = isSupplier ? 'AccountingSupplierParty' : 'AccountingCustomerParty'; let parentElement = xmlDoc.querySelector(`cac\\:${parentTag}, ${parentTag}`); if (!parentElement) { parentElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, `cac:${parentTag}`); const insertPoint = isSupplier ? xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode') : xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty'); if (insertPoint && insertPoint.parentNode) { insertPoint.parentNode.insertBefore(parentElement, insertPoint.nextSibling); } else { xmlDoc.documentElement.appendChild(parentElement); } } // Remove existing Party element if it exists const existingParty = parentElement.querySelector('cac\\:Party, Party'); if (existingParty) { existingParty.remove(); } parentElement.appendChild(partyElement); } function getAllowanceCharges() { const charges = []; document.querySelectorAll('.allowance-charge').forEach((item, index) => { charges.push({ isCharge: document.querySelector(`[name="chargeType${index}"]`).value === 'true', reasonCode: document.querySelector(`[name="chargeReasonCode${index}"]`).value, reason: document.querySelector(`[name="chargeReason${index}"]`).value, amount: parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0, vatRate: parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 19.0, vatTypeId: document.querySelector(`[name="chargeVatType${index}"]`).value || 'S' }); }); return charges; } function updateAllowanceCharges(xmlDoc) { const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; // Remove existing global allowances/charges const existingCharges = xmlDoc.querySelectorAll('Invoice > cac\\:AllowanceCharge, Invoice > AllowanceCharge'); existingCharges.forEach(charge => charge.remove()); // Add global allowances document.querySelectorAll('.allowance-charge').forEach((item, index) => { const allowanceElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:AllowanceCharge"); const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0; const baseAmount = parseFloat(document.querySelector(`[name="chargeBaseAmount${index}"]`).value) || 0; const reasonCode = document.querySelector(`[name="chargeReasonCode${index}"]`).value; const reason = document.querySelector(`[name="chargeReason${index}"]`).value; const vatTypeId = document.querySelector(`[name="chargeVatType${index}"]`).value; const vatRate = parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 0; allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ChargeIndicator", isCharge.toString())); if (reasonCode) { allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:AllowanceChargeReasonCode", reasonCode)); } if (reason) { allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:AllowanceChargeReason", reason)); } // Add multiplier if exists if (baseAmount > 0) { const multiplier = (amount / baseAmount) * 100; allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:MultiplierFactorNumeric", multiplier.toFixed(2))); } allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Amount", amount.toFixed(2), { currencyID })); if (baseAmount > 0) { allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:BaseAmount", baseAmount.toFixed(2), { currencyID })); } // Add tax category const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxCategory"); taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatTypeId)); taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", vatRate.toString())); const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); taxCategory.appendChild(taxScheme); allowanceElement.appendChild(taxCategory); // Insert before TaxTotal const taxTotal = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); if (taxTotal) { xmlDoc.documentElement.insertBefore(allowanceElement, taxTotal); } else { xmlDoc.documentElement.appendChild(allowanceElement); } }); } function updateLineItems(xmlDoc) { const currencyID = xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode').textContent; // Șterge liniile existente const existingLines = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine'); existingLines.forEach(line => line.remove()); document.querySelectorAll('.line-item').forEach((item, index) => { const invoiceLine = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:InvoiceLine"); // Obține valorile liniei const quantity = document.querySelector(`[name="quantity${index}"]`)?.value || '0'; const unitCode = document.querySelector(`[name="unit${index}"]`)?.value || 'EA'; const price = document.querySelector(`[name="price${index}"]`)?.value || '0'; const description = document.querySelector(`[name="description${index}"]`)?.value || ''; const itemDescription = document.querySelector(`[name="itemDescription${index}"]`)?.value || ''; const vatType = document.querySelector(`[name="vatType${index}"]`)?.value || 'S'; const vatRate = vatType === 'AE' ? '0.00' : (document.querySelector(`[name="vatRate${index}"]`)?.value || '0'); const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`)?.value) || 0; // Calculează valorile liniei const baseLineAmount = quantity * price; const lineAmount = roundNumber(baseLineAmount - lineDiscount); // Adaugă elementele liniei invoiceLine.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", (index + 1).toString())); const quantityElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:InvoicedQuantity", quantity); quantityElement.setAttribute('unitCode', unitCode); invoiceLine.appendChild(quantityElement); // Adaugă LineExtensionAmount (valoarea după discount) invoiceLine.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:LineExtensionAmount", lineAmount.toFixed(2), { currencyID })); // Adaugă discount pe linie dacă există if (lineDiscount != 0) { const allowanceCharge = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:AllowanceCharge"); allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ChargeIndicator", lineDiscount < 0 ? "true" : "false")); const reasonCode = document.querySelector(`[name="discountReasonCode${index}"]`)?.value || '95'; allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:AllowanceChargeReasonCode", reasonCode)); allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:AllowanceChargeReason", ALLOWANCE_REASON_CODES[reasonCode] || 'Reducere')); // BaseAmount trebuie să fie valoarea netă înainte de discount const baseAmount = Math.abs(lineAmount); const discountAmount = Math.abs(lineDiscount); const multiplierFactor = (discountAmount / baseAmount * 100).toFixed(2); allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:MultiplierFactorNumeric", multiplierFactor)); allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Amount", discountAmount.toFixed(2), { currencyID })); allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:BaseAmount", baseAmount.toFixed(2), { currencyID })); invoiceLine.appendChild(allowanceCharge); } // Adaugă detaliile articolului const itemElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Item"); if (itemDescription) { itemElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Description", itemDescription)); } itemElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Name", description)); // Adaugă identificările const identificationsContainer = document.querySelector(`#identifications${index}`); if (identificationsContainer) { saveIdentificationsToXML(xmlDoc, itemElement, index); } // Adaugă categoria de taxă const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:ClassifiedTaxCategory"); taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatType)); if (vatType !== 'O') { taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", vatRate)); } const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); taxCategory.appendChild(taxScheme); itemElement.appendChild(taxCategory); invoiceLine.appendChild(itemElement); // Adaugă prețul const priceElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Price"); priceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:PriceAmount", price, { currencyID })); invoiceLine.appendChild(priceElement); xmlDoc.documentElement.appendChild(invoiceLine); }); } function updateTaxTotals(xmlDoc) { const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(); // Remove existing TaxTotal elements const existingTaxTotals = xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal'); existingTaxTotals.forEach(element => element.parentNode.removeChild(element)); // Create main TaxTotal for document currency const taxTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxTotal"); // Use precise currency parsing for total VAT const uiTotalVat = formatter.parseCurrency(document.getElementById('vat').textContent); const taxAmountElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount", uiTotalVat.toFixed(2), { currencyID }); taxTotal.appendChild(taxAmountElement); // Add TaxSubtotal elements to the main TaxTotal const vatRows = document.querySelectorAll('.vat-row'); vatRows.forEach(row => { const taxSubtotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxSubtotal"); const baseAmount = formatter.parseCurrency(row.querySelector('.vat-base').value) || 0; const vatAmount = formatter.parseCurrency(row.querySelector('.vat-amount').value) || 0; const vatType = row.querySelector('.vat-type').value; const vatRate = parseFloat(row.querySelector('.vat-rate').value) || 0; taxSubtotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxableAmount", baseAmount.toFixed(2), { currencyID })); taxSubtotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount", vatAmount.toFixed(2), { currencyID })); const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxCategory"); taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatType)); const percent = vatType === 'AE' ? '0.00' : vatRate.toFixed(2); taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", percent)); // Add exemption code and reason for special VAT types if (['E', 'K', 'O', 'AE'].includes(vatType)) { const exemptionCode = row.querySelector('.vat-exemption-code')?.value; const exemptionReason = row.querySelector('.vat-exemption-reason')?.value; if (exemptionCode) { taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxExemptionReasonCode", exemptionCode)); } if (exemptionReason) { taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxExemptionReason", exemptionReason)); } } const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); taxCategory.appendChild(taxScheme); taxSubtotal.appendChild(taxCategory); taxTotal.appendChild(taxSubtotal); }); // Insert TaxTotal in the correct position let insertionPoint = xmlDoc.querySelector('cac\\:AllowanceCharge, AllowanceCharge'); if (insertionPoint) { while (insertionPoint.nextElementSibling && (insertionPoint.nextElementSibling.localName === 'AllowanceCharge' || insertionPoint.nextElementSibling.localName === 'TaxTotal')) { insertionPoint = insertionPoint.nextElementSibling; } insertionPoint.parentNode.insertBefore(taxTotal, insertionPoint.nextSibling); } else { const monetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); if (monetaryTotal) { monetaryTotal.parentNode.insertBefore(taxTotal, monetaryTotal); } else { xmlDoc.documentElement.appendChild(taxTotal); } } // If tax currency is specified, add another TaxTotal element if (taxCurrencyCode && taxCurrencyCode !== currencyID) { const taxCurrencyTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxTotal"); const exchangeRateInput = document.querySelector('[name="exchangeRate"]'); const exchangeRate = exchangeRateInput ? parseFloat(exchangeRateInput.value) || 1 : 1; const taxCurrencyVAT = uiTotalVat * exchangeRate; const taxCurrencyAmountElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount", taxCurrencyVAT.toFixed(2), { currencyID: taxCurrencyCode }); taxCurrencyTotal.appendChild(taxCurrencyAmountElement); // Insert after the main TaxTotal taxTotal.parentNode.insertBefore(taxCurrencyTotal, taxTotal.nextSibling); } } function updateMonetaryTotals(xmlDoc) { const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; // Use direct parsing to preserve full precision and manual edits const subtotal = formatter.parseCurrency(document.getElementById('subtotal').textContent); const allowances = formatter.parseCurrency(document.getElementById('totalAllowances').textContent); const charges = formatter.parseCurrency(document.getElementById('totalCharges').textContent); const netAmount = formatter.parseCurrency(document.getElementById('netAmount').textContent); const totalVat = formatter.parseCurrency(document.getElementById('vat').textContent); const total = formatter.parseCurrency(document.getElementById('total').textContent); // Rest of the function remains the same... const existingMonetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); if (existingMonetaryTotal) { existingMonetaryTotal.parentNode.removeChild(existingMonetaryTotal); } const monetaryTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:LegalMonetaryTotal"); const amounts = { "LineExtensionAmount": subtotal, "TaxExclusiveAmount": netAmount, "TaxInclusiveAmount": total, "AllowanceTotalAmount": allowances, "ChargeTotalAmount": charges, "PayableAmount": total }; Object.entries(amounts).forEach(([elementName, value]) => { monetaryTotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, `cbc:${elementName}`, value.toFixed(2), { currencyID })); }); // Insertion logic remains the same let insertionPoint = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); if (insertionPoint) { while (insertionPoint.nextElementSibling && insertionPoint.nextElementSibling.localName === 'TaxTotal') { insertionPoint = insertionPoint.nextElementSibling; } insertionPoint.parentNode.insertBefore(monetaryTotal, insertionPoint.nextSibling); } else { const firstInvoiceLine = xmlDoc.querySelector('cac\\:InvoiceLine, InvoiceLine'); if (firstInvoiceLine) { firstInvoiceLine.parentNode.insertBefore(monetaryTotal, firstInvoiceLine); } else { xmlDoc.documentElement.appendChild(monetaryTotal); } } } function getExchangeRate(fromCurrency, toCurrency) { // For now, return 1 as default exchange rate // TODO: Implement proper exchange rate handling return 1; } // Update the invoice form to include exchange rate when tax currency is different function addExchangeRateField() { const taxCurrencyInput = document.querySelector('[name="taxCurrencyCode"]'); const documentCurrencyInput = document.querySelector('[name="documentCurrencyCode"]'); function updateExchangeRateVisibility() { const taxCurrency = taxCurrencyInput.value.toUpperCase(); const documentCurrency = documentCurrencyInput.value.toUpperCase(); let exchangeRateContainer = document.getElementById('exchangeRateContainer'); if (taxCurrency && taxCurrency !== documentCurrency) { if (!exchangeRateContainer) { const container = document.createElement('div'); container.id = 'exchangeRateContainer'; container.className = 'form-group'; container.innerHTML = ` `; taxCurrencyInput.parentNode.after(container); } else { // Update label if currencies changed const label = exchangeRateContainer.querySelector('label'); label.textContent = `Curs Valutar ${documentCurrency}/${taxCurrency}`; } } else if (exchangeRateContainer) { exchangeRateContainer.remove(); } } taxCurrencyInput.addEventListener('input', updateExchangeRateVisibility); documentCurrencyInput.addEventListener('input', updateExchangeRateVisibility); // Initial check updateExchangeRateVisibility(); } // Utility functions function getXMLValue(xmlDoc, selector, defaultValue = '') { if (!xmlDoc) return defaultValue; try { const element = xmlDoc.querySelector(selector); const value = element ? element.textContent : defaultValue; // console.log(`getXMLValue: Selector: ${selector}, Value: ${value}`); return value; } catch (error) { console.warn(`Eroare la obținerea valorii pentru selectorul ${selector}:`, error); return defaultValue; } } function setXMLValue(xmlDoc, selector, value) { try { const element = xmlDoc.querySelector(selector); if (element) { element.textContent = value; return true; } return false; } catch (error) { console.warn(`Eroare la setarea valorii pentru selectorul ${selector}:`, error); return false; } } function createXMLElement(xmlDoc, namespace, elementName, value = '', attributes = {}) { const element = xmlDoc.createElementNS(namespace, elementName); if (value) { element.textContent = value; } Object.entries(attributes).forEach(([key, value]) => { element.setAttribute(key, value); }); return element; } function formatXML(xmlString) { let formatted = ''; let indent = ''; const tab = ' '; xmlString.split(/>\s* { if (node.match(/^\/\w/)) { indent = indent.substring(tab.length); } formatted += indent + '<' + node + '>\r\n'; if (node.match(/^]*[^\/]$/) && !node.startsWith("?")) { indent += tab; } }); return formatted.substring(1, formatted.length - 3); } function downloadXML(xmlDoc) { const serializer = new XMLSerializer(); let xmlString = serializer.serializeToString(xmlDoc); if (!xmlString.startsWith('\n' + xmlString; } xmlString = formatXML(xmlString); const blob = new Blob([xmlString], { type: 'application/xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'factura_' + document.querySelector('[name="invoiceNumber"]').value + '.xml'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function roundNumber(number, decimals = 2) { return Math.round(number * Math.pow(10, decimals)) / Math.pow(10, decimals); } function renumberLineItems() { document.querySelectorAll('.line-item').forEach((item, newIndex) => { item.dataset.index = newIndex; item.querySelectorAll('input, select').forEach(input => { const name = input.getAttribute('name'); if (name) { const baseName = name.replace(/\d+$/, ''); input.setAttribute('name', baseName + newIndex); } }); }); } function createIdentificationHTML(index, type, value = '', schemeId = '') { const typeInfo = IDENTIFICATION_TYPES[type]; const id = `${type.toLowerCase()}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; if (type === 'COMMODITY') { return `
    `; } return `
    ${type === 'STANDARD' ? `` : ''}
    `; } window.addIdentification = function(lineItemIndex, type) { const container = document.querySelector(`#identifications${lineItemIndex}`); if (container) { container.insertAdjacentHTML('beforeend', createIdentificationHTML(lineItemIndex, type)); validateIdentifications(lineItemIndex); } } window.removeIdentification = function(id) { const element = document.querySelector(`[data-id="${id}"]`); if (element) { const lineItem = element.closest('.line-item'); element.remove(); if (lineItem) { validateIdentifications(parseInt(lineItem.dataset.index)); } } }; // Update XML parsing function parseIdentifications(itemElement, lineItemIndex) { // console.log("Parsing identifications for line", lineItemIndex); const container = document.querySelector(`#identifications${lineItemIndex}`); if (!container) return; const listContainer = container.querySelector('.identifications-list'); listContainer.innerHTML = ''; // Parse BuyersItemIdentification const buyersId = itemElement.querySelector('cac\\:BuyersItemIdentification cbc\\:ID, BuyersItemIdentification ID'); if (buyersId) { listContainer.insertAdjacentHTML('beforeend', createIdentificationHTML(lineItemIndex, 'BUYERS', buyersId.textContent) ); } // Parse SellersItemIdentification const sellersId = itemElement.querySelector('cac\\:SellersItemIdentification cbc\\:ID, SellersItemIdentification ID'); if (sellersId) { listContainer.insertAdjacentHTML('beforeend', createIdentificationHTML(lineItemIndex, 'SELLERS', sellersId.textContent) ); } // Parse StandardItemIdentification const standardId = itemElement.querySelector('cac\\:StandardItemIdentification cbc\\:ID, StandardItemIdentification ID'); if (standardId) { listContainer.insertAdjacentHTML('beforeend', createIdentificationHTML(lineItemIndex, 'STANDARD', standardId.textContent) ); } // Parse CommodityClassifications const commodityClassifications = itemElement.querySelectorAll('cac\\:CommodityClassification cbc\\:ItemClassificationCode, CommodityClassification ItemClassificationCode'); commodityClassifications.forEach(classification => { const listId = classification.getAttribute('listID') || 'CV'; const code = classification.textContent; if (code && listId) { listContainer.insertAdjacentHTML('beforeend', createIdentificationHTML(lineItemIndex, 'COMMODITY', code, listId) ); } }); } // Update XML saving function saveIdentificationsToXML(xmlDoc, itemElement, lineItemIndex) { const container = document.querySelector(`#identifications${lineItemIndex}`); if (!container) return; container.querySelectorAll('.identification-row').forEach(row => { const type = row.dataset.type; const id = row.dataset.id; const schemeInput = document.querySelector(`[name="scheme_${id}"]`); const valueInput = document.querySelector(`[name="value_${id}"]`); if (!valueInput?.value) return; if (type === 'COMMODITY') { const schemeSelect = document.querySelector(`[name="scheme_${id}"]`); if (!schemeSelect) return; const identificationElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:CommodityClassification'); const idElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ItemClassificationCode', valueInput.value); idElement.setAttribute('listID', schemeSelect.value); identificationElement.appendChild(idElement); itemElement.appendChild(identificationElement); } else { const typeInfo = IDENTIFICATION_TYPES[type]; const identificationElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, `cac:${typeInfo.xmlTag}`); const idElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ID', valueInput.value); if (type === 'STANDARD') { idElement.setAttribute('schemeID', '0160'); } identificationElement.appendChild(idElement); itemElement.appendChild(identificationElement); } }); } // Add validation function validateIdentifications(lineItemIndex) { const container = document.querySelector(`#identifications${lineItemIndex}`); if (!container) return true; let isValid = true; container.querySelectorAll('.identification-row').forEach(row => { const type = row.dataset.type; const id = row.dataset.id; const input = row.querySelector(`[name="value_${id}"]`); if (type === 'COMMODITY') { const scheme = row.querySelector('.scheme-select').value; if (scheme === 'TSP' && !/^\d{8}-\d$/.test(input.value)) { input.classList.add('invalid'); isValid = false; } else if (scheme === 'STI' && !/^\d{8}$/.test(input.value)) { input.classList.add('invalid'); isValid = false; } else { input.classList.remove('invalid'); } } }); return isValid; } // ============================================================================ // PR-A11: math validation inline (badge per-line / per-VAT-row / footer total) // ============================================================================ const A11_TOLERANCE_LEGACY = new Big('0.01'); // ±0.01 RON pe rows complet clean const A11_TOLERANCE_DIRTY = new Big('0'); // zero pe rows cu dirty='1' function _a11RowDirty(rowEl) { if (!rowEl) return false; return Array.from(rowEl.querySelectorAll('input')).some( i => i.dataset && i.dataset.dirty === '1' ); } function _a11Tolerance(rowEl) { return _a11RowDirty(rowEl) ? A11_TOLERANCE_DIRTY : A11_TOLERANCE_LEGACY; } function _a11SetBadge(badgeEl, computedBig, displayedBig, epsBig, opts = {}) { if (!badgeEl) return; const { footer = false } = opts; const diff = computedBig.minus(displayedBig); const absDiff = diff.abs(); badgeEl.classList.remove('badge-ok', 'badge-warn', 'badge-error'); if (absDiff.lte(epsBig)) { badgeEl.classList.add('badge-ok'); badgeEl.textContent = '✓'; return false; } if (footer) { badgeEl.classList.add('badge-warn'); badgeEl.textContent = `diferență ${absDiff.toFixed(2)} RON`; } else { badgeEl.classList.add('badge-error'); const sign = diff.gte(0) ? '+' : '−'; badgeEl.textContent = `${sign}${absDiff.toFixed(2)} RON`; } return true; } /** * PR-A11: validează consistența matematică a formularului și actualizează * badge-urile (per-line, per-VAT-row, footer total). * * Lect: input.dataset.raw (canonical decimal-dot, source of truth E1). * Compară: * - line item: computed (qty*price-discount) vs XML loaded LineExtensionAmount. * - VAT row: computed (base*rate/100, doar pentru type='S') vs vat-amount input. * - footer total: computed (subtotal-allow+charges+totalVat) vs #total displayed. * * Tolerance switching pe row: dacă orice input din row are dataset.dirty='1' → * zero (newly computed must match exact). Altfel ±0.01 RON (legacy float * reconciliation pe XML încărcat). * * @returns {{footerDiff: Big, footerOver: boolean}} info pentru save toast. */ function validateMath() { // Per-line: computed line net vs XML loaded LineExtensionAmount. document.querySelectorAll('.line-item').forEach(item => { const idx = item.dataset.index; const qtyInput = document.querySelector(`[name="quantity${idx}"]`); const priceInput = document.querySelector(`[name="price${idx}"]`); const discountInput = document.querySelector(`[name="lineDiscount${idx}"]`); if (!qtyInput || !priceInput) return; const qty = getRaw(qtyInput); const price = getRaw(priceInput); const discount = discountInput ? getRaw(discountInput) : new Big('0'); const computed = qty.times(price).minus(discount); // Update vizibilul "Total linie" la valoarea recalculată. const totalEl = item.querySelector(`[data-line-total-index="${idx}"]`); if (totalEl) totalEl.textContent = format2(computed.round(2, 1)); const badgeEl = item.querySelector(`[data-line-badge-index="${idx}"]`); if (!badgeEl) return; // Linii nou-adăugate (fără XML referință) → badge ✓ trivial. const xmlAmt = item.dataset.xmlLineAmount; if (xmlAmt === undefined || xmlAmt === '') { badgeEl.textContent = ''; badgeEl.classList.remove('badge-ok', 'badge-warn', 'badge-error'); return; } const displayed = parseStrictOr(xmlAmt, '0'); const eps = _a11Tolerance(item); _a11SetBadge(badgeEl, computed.round(2, 1), displayed, eps); }); // Per-VAT-row: computed (base*rate/100) vs vat-amount input. document.querySelectorAll('.vat-row').forEach(row => { const typeSelect = row.querySelector('.vat-type'); const baseInput = row.querySelector('.vat-base'); const rateInput = row.querySelector('.vat-rate'); const amountInput = row.querySelector('.vat-amount'); const badgeEl = row.querySelector('.vat-amount-badge'); if (!badgeEl || !typeSelect || !baseInput || !rateInput || !amountInput) return; const type = typeSelect.value; const base = getRaw(baseInput); const rate = getRaw(rateInput); const displayed = getRaw(amountInput); const computed = (type === 'S') ? base.times(rate).div(100).round(2, 1) : new Big('0'); const eps = _a11Tolerance(row); _a11SetBadge(badgeEl, computed, displayed, eps); }); // Footer total: computed (subtotal-allow+charges+totalVat) vs #total displayed. const totalBadge = document.getElementById('total-badge'); let footerDiff = new Big('0'); let footerOver = false; if (totalBadge) { const subtotalBig = parseStrictOr(document.getElementById('subtotal').textContent, '0'); const allowancesBig = parseStrictOr(document.getElementById('totalAllowances').textContent, '0'); const chargesBig = parseStrictOr(document.getElementById('totalCharges').textContent, '0'); const vatBig = parseStrictOr(document.getElementById('vat').textContent, '0'); const displayed = parseStrictOr(document.getElementById('total').textContent, '0'); const computed = subtotalBig.minus(allowancesBig).plus(chargesBig).plus(vatBig).round(2, 1); // Footer tolerance: orice input dirty în întreaga formă → zero, altfel ±0.01. const anyDirty = Array.from( document.querySelectorAll('.line-item, .vat-row, .allowance-charge') ).some(_a11RowDirty); const eps = anyDirty ? A11_TOLERANCE_DIRTY : A11_TOLERANCE_LEGACY; footerDiff = computed.minus(displayed).abs(); footerOver = _a11SetBadge(totalBadge, computed, displayed, eps, { footer: true }) === true; } return { footerDiff, footerOver }; } /** * PR-A11: minimal toast helper. Compatibil cu DESIGN.md spec D14 (border-left * 3px semantic, slide-in 150ms, auto-dismiss success 4s / info 6s / warning 6s * / error persistent). NU bloca save — doar avertizează. */ function showToast(message, variant = 'info', subtext = '') { let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.className = 'toast-container'; container.setAttribute('aria-live', 'polite'); document.body.appendChild(container); } const toast = document.createElement('div'); toast.className = `toast toast-${variant}`; toast.setAttribute('role', (variant === 'warning' || variant === 'error') ? 'alert' : 'status'); const msgEl = document.createElement('div'); msgEl.className = 'toast-message'; msgEl.textContent = message; if (subtext) { const subEl = document.createElement('span'); subEl.className = 'toast-sub'; subEl.textContent = subtext; msgEl.appendChild(subEl); } toast.appendChild(msgEl); const dismiss = document.createElement('button'); dismiss.className = 'toast-dismiss'; dismiss.type = 'button'; dismiss.setAttribute('aria-label', 'Închide'); dismiss.textContent = '×'; dismiss.onclick = () => toast.remove(); toast.appendChild(dismiss); container.appendChild(toast); const timeouts = { success: 4000, info: 6000, warning: 6000, error: 0 }; const ms = timeouts[variant] ?? 6000; if (ms > 0) { setTimeout(() => { if (toast.isConnected) toast.remove(); }, ms); } } // Expose pentru consumeri externi / debugging. window.validateMath = validateMath; window.showToast = showToast; // ============================================================================ // PR-BR (A2): Floating BR Validation Panel (D5) // Panel sticky bottom-right — colecteaza snapshot form, rulează cele 30 // reguli BR/CIUS-RO și afișează violările cu link la câmp + highlight 2s. // ============================================================================ /** * Colectează snapshot-ul datelor din formular pentru validarea BR. * Citește valorile afișate (sau dataset.raw pentru numerice). */ function collectInvoiceDataForBR() { const lineItemEls = Array.from(document.querySelectorAll('.line-item')); const lineItems = lineItemEls.map(el => { const idx = el.dataset.index; const qEl = document.querySelector(`[name="quantity${idx}"]`); const pEl = document.querySelector(`[name="price${idx}"]`); const dEl = document.querySelector(`[name="lineDiscount${idx}"]`); const tEl = document.querySelector(`[data-line-total-index="${idx}"]`); return { index: parseInt(idx, 10), description: (document.querySelector(`[name="description${idx}"]`)?.value || '').trim(), quantity: qEl ? (qEl.dataset.raw || qEl.value) : '', unitPrice: pEl ? (pEl.dataset.raw || pEl.value) : '', discount: dEl ? (dEl.dataset.raw || dEl.value || '0') : '0', vatType: (document.querySelector(`[name="vatType${idx}"]`)?.value || ''), vatRate: (document.querySelector(`[name="vatRate${idx}"]`)?.value || '0'), lineTotal: tEl ? tEl.textContent.replace(/[^\d,.-]/g, '') : '0', }; }); const vatRows = Array.from(document.querySelectorAll('.vat-row')).map(row => { const rEl = row.querySelector('.vat-rate'); const bEl = row.querySelector('.vat-base'); const aEl = row.querySelector('.vat-amount'); return { type: (row.querySelector('.vat-type')?.value || ''), rate: rEl ? (rEl.dataset.raw || rEl.value) : '0', base: bEl ? (bEl.dataset.raw || bEl.value) : '0', amount: aEl ? (aEl.dataset.raw || aEl.value) : '0', }; }); // IBAN-uri din rândurile Payment Means dinamice const ibans = Array.from(document.querySelectorAll('[name^="paymentMeansIBAN"]')) .map(el => el.value.trim()); const parseDisplay = (id) => { const el = document.getElementById(id); if (!el) return '0'; return el.textContent.replace(/[^\d,.-]/g, ''); }; return { invoiceNumber: (document.querySelector('[name="invoiceNumber"]')?.value || '').trim(), issueDate: (document.querySelector('[name="issueDate"]')?.value || '').trim(), dueDate: (document.querySelector('[name="dueDate"]')?.value || '').trim(), invoiceTypeCode: (document.querySelector('[name="invoiceTypeCode"]')?.value || '').trim(), currencyCode: (document.querySelector('[name="documentCurrencyCode"]')?.value || '').trim(), supplierName: (document.querySelector('[name="supplierName"]')?.value || '').trim(), supplierVAT: (document.querySelector('[name="supplierVAT"]')?.value || '').trim(), supplierCity: (document.querySelector('[name="supplierCity"]')?.value || '').trim(), supplierCountry: (document.querySelector('[name="supplierCountry"]')?.value || '').trim(), customerName: (document.querySelector('[name="customerName"]')?.value || '').trim(), customerVAT: (document.querySelector('[name="customerVAT"]')?.value || '').trim(), customerCity: (document.querySelector('[name="customerCity"]')?.value || '').trim(), customerCountry: (document.querySelector('[name="customerCountry"]')?.value || '').trim(), lineItems, vatRows, ibans, subtotal: parseDisplay('subtotal'), allowances: parseDisplay('totalAllowances'), charges: parseDisplay('totalCharges'), totalVat: parseDisplay('vat'), grandTotal: parseDisplay('total'), }; } /** Injectează panelul BR în DOM dacă nu există deja. */ function _ensureBRPanel() { if (document.getElementById('br-panel')) return; const panel = document.createElement('div'); panel.id = 'br-panel'; panel.className = 'br-panel br-panel--hidden'; panel.setAttribute('role', 'region'); panel.setAttribute('aria-label', 'Probleme de validare'); panel.setAttribute('aria-live', 'polite'); panel.innerHTML = `

    Toate verificările trecute.

    `; document.body.appendChild(panel); // Toggle expand/collapse const header = panel.querySelector('#br-panel-header'); header.addEventListener('click', () => { const expanded = panel.classList.toggle('is-expanded'); header.setAttribute('aria-expanded', expanded ? 'true' : 'false'); panel.querySelector('#br-panel-toggle').textContent = expanded ? '▼ restrânge' : '▲ extinde'; }); header.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); header.click(); } }); } /** * Actualizează panelul BR cu lista de violări. * Apelat după fiecare updateTotals() și după parseXML(). */ function _updateBRPanel() { _ensureBRPanel(); const panel = document.getElementById('br-panel'); const summary = document.getElementById('br-panel-summary'); const body = document.getElementById('br-panel-body'); if (!panel || !summary || !body) return; // Rulează regulile const data = collectInvoiceDataForBR(); const violations = runBRRules(data); const fatals = violations.filter(v => v.severity === 'fatal').length; const errors = violations.filter(v => v.severity === 'error').length; const warnings = violations.filter(v => v.severity === 'warning').length; const totalIssues = fatals + errors + warnings; if (totalIssues === 0) { panel.classList.add('br-panel--hidden'); panel.classList.remove('br-panel--errors'); summary.className = 'br-panel__summary br-panel__summary--ok'; summary.textContent = '✓ 0 erori BR, 0 warnings'; body.innerHTML = '

    Toate verificările trecute.

    '; return; } panel.classList.remove('br-panel--hidden'); panel.classList.toggle('br-panel--errors', (fatals + errors) > 0); // Summary text const parts = []; if (fatals > 0) parts.push(`${fatals} critice`); if (errors > 0) parts.push(`${errors} erori`); if (warnings > 0) parts.push(`${warnings} warning${warnings > 1 ? 's' : ''}`); summary.textContent = parts.join(' / '); summary.className = 'br-panel__summary ' + ( (fatals + errors) > 0 ? 'br-panel__summary--errors' : 'br-panel__summary--warnings' ); // Render items body.innerHTML = violations.map(v => { const sev = v.severity === 'fatal' ? 'fatal' : v.severity; return `
    ${v.code} ${_escapeHtml(v.message)}
    `; }).join(''); // Wire click → scroll + highlight body.querySelectorAll('.br-panel__item').forEach(item => { const handler = () => { const ref = item.dataset.fieldRef; if (!ref) return; const target = document.querySelector(ref); if (!target) return; target.scrollIntoView({ behavior: 'smooth', block: 'center' }); target.classList.remove('br-field-highlight'); void target.offsetWidth; // reflow to restart animation target.classList.add('br-field-highlight'); setTimeout(() => target.classList.remove('br-field-highlight'), 2100); target.focus?.(); }; item.addEventListener('click', handler); item.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } }); }); } function _escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ============================================================================ // PR-ANAF: Integrare API ANAF (validare + CIF lookup) // Butonul "Validare ANAF" și butoanele "Caută CIF" sunt ascunse la start; // probeReceiver() le afișează dacă receiver.php este disponibil. // ============================================================================ /** * Serializează starea curentă a formularului în XML string. * Atenție: mutează currentInvoice in-place (la fel ca saveXML). * @returns {string|null} XML string sau null dacă nu există factură. */ function _currentXMLString() { if (!currentInvoice) return null; const xmlDoc = currentInvoice; updateBasicDetails(xmlDoc); updatePartyDetails(xmlDoc); updateBillingReference(xmlDoc); updatePaymentMeans(xmlDoc); updateAllowanceCharges(xmlDoc); xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal').forEach(el => el.remove()); const mt = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); if (mt) mt.remove(); xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine').forEach(el => el.remove()); updateTaxTotals(xmlDoc); updateMonetaryTotals(xmlDoc); updateLineItems(xmlDoc); const serializer = new XMLSerializer(); let xmlString = serializer.serializeToString(xmlDoc); if (!xmlString.startsWith('\n' + xmlString; } return xmlString; } /** Setează starea loading pe un buton (spinner braille + disabled). */ function _btnLoading(btn, label) { btn.disabled = true; btn.dataset.origText = btn.textContent; btn.innerHTML = ` ${label}`; } /** Restaurează starea normală a unui buton. */ function _btnDone(btn) { btn.disabled = false; btn.textContent = btn.dataset.origText || btn.textContent; } /** * Validează factura curentă prin API-ul ANAF. * Necesită receiver.php cu "anaf_token" configurat în config.json. */ window.validateAnaf = async function() { if (!currentInvoice) { showToast('Nicio factură încărcată. Deschideți un XML eFactura mai întâi.', 'warning'); return; } const btn = document.getElementById('btnValidateAnaf'); if (btn) _btnLoading(btn, 'Se validează...'); try { const xmlString = _currentXMLString(); if (!xmlString) throw new Error('Nu s-a putut genera XML-ul facturii.'); const result = await anafValidate(xmlString); if (result.valid) { showToast('Factură validă ANAF — nicio eroare detectată.', 'success'); } else { const errors = result.messages.filter(m => m.severity === 'ERROR' || m.severity === 'FATAL'); const warns = result.messages.filter(m => m.severity === 'WARNING'); const parts = []; if (errors.length) parts.push(`${errors.length} erori`); if (warns.length) parts.push(`${warns.length} avertismente`); const sub = result.messages.slice(0, 3).map(m => m.message).join(' | '); showToast(`ANAF: ${parts.join(', ')}`, 'error', sub.slice(0, 200)); } } catch (err) { showToast('Eroare validare ANAF: ' + err.message, 'error', 'Verificați că receiver.php este configurat cu anaf_token valid.'); } finally { if (btn) _btnDone(btn); } }; /** * Caută datele firmei după CIF din câmpul supplierVAT / customerVAT. * @param {'supplier'|'customer'} party */ window.lookupCif = async function(party) { const isSupplier = party === 'supplier'; const cifInput = document.querySelector(`[name="${isSupplier ? 'supplier' : 'customer'}VAT"]`); if (!cifInput) return; const cif = (cifInput.value || '').trim(); if (!cif) { showToast('Introduceți un CIF/CUI în câmpul Cod TVA mai întâi.', 'warning'); return; } const btnId = isSupplier ? 'btnLookupSupplierCif' : 'btnLookupCustomerCif'; const btn = document.getElementById(btnId); if (btn) _btnLoading(btn, 'Caută...'); try { const result = await anafCifLookup(cif); if (!result.found) { const note = result.async ? 'API ANAF asincron — rezultatele nu sunt disponibile imediat. Reîncercați.' : 'CIF-ul nu a fost găsit în baza de date ANAF.'; showToast('CIF negăsit în ANAF.', 'info', note); return; } // Populează câmpurile firmei const prefix = isSupplier ? 'supplier' : 'customer'; const set = (name, val) => { const el = document.querySelector(`[name="${prefix}${name}"]`); if (el && val) el.value = val; }; set('Name', result.denumire); set('CompanyId', result.nrRegCom); // CIF cu prefix RO dacă plătitor TVA if (result.cui) { const cifFinal = result.tvaActiv ? `RO${result.cui}` : String(result.cui); const vatEl = document.querySelector(`[name="${prefix}VAT"]`); if (vatEl) vatEl.value = cifFinal; } // Adresă structurată cu fallback la string brut set('Address', result.strada || result.adresa); set('City', result.oras); // CountrySubentity este SELECT cu valori RO-XX if (result.judetCod) { const countyEl = document.querySelector(`[name="${prefix}CountrySubentity"]`); if (countyEl && [...countyEl.options].some(o => o.value === result.judetCod)) { countyEl.value = result.judetCod; } } // Country SELECT const countryEl = document.querySelector(`[name="${prefix}Country"]`); if (countryEl) countryEl.value = 'RO'; set('Phone', result.telefon); // Toast cu status TVA + eFactura const statuses = [ result.tvaActiv ? 'Plătitor TVA' : 'Neplătitor TVA', result.statusEFactura ? 'Înregistrat eFactura' : null ].filter(Boolean).join(' · '); showToast(`Date ANAF importate: ${result.denumire || cif}`, 'success', statuses); } catch (err) { showToast('Eroare lookup CIF ANAF: ' + err.message, 'error', 'Verificați că receiver.php este disponibil pe server.'); } finally { if (btn) _btnDone(btn); } }; // Probează receiver.php la startup — dacă e disponibil, afișează butoanele ANAF. (async () => { const available = await probeReceiver().catch(() => false); if (available) { document.getElementById('btnValidateAnaf')?.style?.setProperty('display', ''); document.querySelectorAll('.anaf-cif-btn').forEach(b => b.style.removeProperty('display')); } })(); // ============================================================================ // PR-PDF (A8): Descarcă PDF — client-side via html2pdf.js // Deschide template-ul de print și generează PDF fără dialog de imprimare. // ============================================================================ import getHtml2pdf from './vendor/html2pdf.mjs'; /** * Generează și descarcă PDF-ul facturii curente. * Deschide template-ul de print, îl populează via InvoicePrintHandler, * apoi html2pdf capturează .invoice-container ca PDF. */ window.downloadPDF = async function() { if (!currentInvoice) { showToast('Nicio factură încărcată. Deschideți un XML eFactura mai întâi.', 'warning'); return; } const btn = document.getElementById('btnDownloadPDF'); if (btn) _btnLoading(btn, 'Generare PDF...'); try { // Pasul 1: pre-încarcă html2pdf bundle (lazy, ~900KB) const html2pdf = await getHtml2pdf(); // Pasul 2: colectează datele facturii const invoiceData = printHandler.collectInvoiceData(); const invoiceNumber = invoiceData.invoiceNumber || 'factura'; // Pasul 3: randează HTML-ul facturii într-un container detașat (off-screen) const container = document.createElement('div'); container.style.cssText = 'position:absolute;left:-9999px;top:0;width:210mm;background:#fff;'; container.id = 'pdf-render-container'; document.body.appendChild(container); // Stiluri inline de bază pentru randarea off-screen container.innerHTML = `
    FACTURĂ
    Nr. ${_escapeHtml(invoiceData.invoiceNumber)} | Data: ${_escapeHtml(invoiceData.issueDate)} | Scadent: ${_escapeHtml(invoiceData.dueDate)}
    Monedă: ${_escapeHtml(invoiceData.documentCurrencyCode)}
    Furnizor

    ${_escapeHtml(invoiceData.supplier.name)}

    ${invoiceData.supplier.vat ? `

    CUI: ${_escapeHtml(invoiceData.supplier.vat)}

    ` : ''} ${invoiceData.supplier.companyId ? `

    Nr. reg.: ${_escapeHtml(invoiceData.supplier.companyId)}

    ` : ''} ${invoiceData.supplier.address ? `

    ${_escapeHtml(invoiceData.supplier.address)}, ${_escapeHtml(invoiceData.supplier.city)}

    ` : ''} ${invoiceData.supplier.country ? `

    ${_escapeHtml(invoiceData.supplier.country)}

    ` : ''}
    Client

    ${_escapeHtml(invoiceData.customer.name)}

    ${invoiceData.customer.vat ? `

    CUI: ${_escapeHtml(invoiceData.customer.vat)}

    ` : ''} ${invoiceData.customer.companyId ? `

    Nr. reg.: ${_escapeHtml(invoiceData.customer.companyId)}

    ` : ''} ${invoiceData.customer.address ? `

    ${_escapeHtml(invoiceData.customer.address)}, ${_escapeHtml(invoiceData.customer.city)}

    ` : ''} ${invoiceData.customer.country ? `

    ${_escapeHtml(invoiceData.customer.country)}

    ` : ''}
    ${invoiceData.items.map(it => ``).join('')}
    #DescriereUM Cant.Preț unit. TVA%Total net
    ${it.number} ${_escapeHtml(it.description)} ${_escapeHtml(it.unit)} ${_escapeHtml(it.quantity)} ${_escapeHtml(it.price)} ${_escapeHtml(String(it.vatRate))}% ${_escapeHtml(it.totalAmount)}
    ${invoiceData.note ? `

    Notă: ${_escapeHtml(invoiceData.note)}

    ` : ''}
    Subtotal:${_escapeHtml(invoiceData.totals.subtotal)}
    ${invoiceData.totals.allowances ? `
    Reduceri:-${_escapeHtml(invoiceData.totals.allowances)}
    ` : ''} ${invoiceData.totals.charges ? `
    Taxe:${_escapeHtml(invoiceData.totals.charges)}
    ` : ''}
    TVA total:${_escapeHtml(invoiceData.totals.vat)}
    TOTAL:${_escapeHtml(invoiceData.totals.total)}
    `; await html2pdf().set({ margin: [0, 0, 0, 0], filename: 'factura_' + invoiceNumber.replace(/[^a-zA-Z0-9_-]/g, '_') + '.pdf', image: { type: 'jpeg', quality: 0.97 }, html2canvas: { scale: 2, useCORS: true, logging: false }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } }).from(container.querySelector('#pdf-content')).save(); showToast('PDF generat și descărcat.', 'success'); } catch (err) { showToast('Eroare la generarea PDF: ' + err.message, 'error', 'Verificați că js/vendor/html2pdf.bundle.min.js este prezent.'); } finally { document.getElementById('pdf-render-container')?.remove(); if (btn) _btnDone(btn); } }; // ============================================================================ // PR-A19 (Track 2 D): Numerotare automată cu serie + contor + an // Cheia localStorage: efactura.sequence.v1 // Structura: { serie: string, an: number, contor: number } // Generează numere de forma: RFT2026-0001 // ============================================================================ const SEQ_KEY = 'efactura.sequence.v1'; const SEQ_DEFAULT = { serie: 'RFT', an: new Date().getFullYear(), contor: 1, includeAn: true, cifre: 4 }; /** Citește secvența curentă din localStorage. */ function _seqRead() { const stored = getJSON(SEQ_KEY, null); return { ...SEQ_DEFAULT, ...stored }; } /** Salvează secvența în localStorage. */ function _seqWrite(seq) { setJSON(SEQ_KEY, seq); } /** * Formatează un număr de factură din secvență. * Cu includeAn=true: "{serie} {an}{pad}" → ex. "RFT 20260042" * Cu includeAn=false: "{serie} {pad}" → ex. "RFT 0042" */ function _seqFormat(seq) { const cifre = Math.max(1, Math.min(8, parseInt(seq.cifre) || 4)); const pad = String(seq.contor).padStart(cifre, '0'); const serie = (seq.serie || 'RFT').trim(); const an = seq.an || new Date().getFullYear(); return seq.includeAn ? `${serie} ${an}${pad}` : `${serie} ${pad}`; } /** Actualizează previzualizarea din modal cu valorile curente. */ function _seqUpdatePreview() { const serie = (document.getElementById('seq-serie')?.value || '').trim().toUpperCase(); const year = parseInt(document.getElementById('seq-year')?.value) || new Date().getFullYear(); const contor = parseInt(document.getElementById('seq-counter')?.value) || 1; const includeAn = document.getElementById('seq-include-an')?.checked ?? true; const cifre = Math.max(1, Math.min(8, parseInt(document.getElementById('seq-cifre')?.value) || 4)); const preview = document.getElementById('seq-preview'); if (preview) { const pad = String(Math.max(1, contor)).padStart(cifre, '0'); preview.textContent = includeAn ? `${serie || 'RFT'} ${year}${pad}` : `${serie || 'RFT'} ${pad}`; } } /** Injectează modal-ul în DOM dacă nu există deja. */ function _ensureNewInvoiceModal() { if (document.getElementById('modal-new-invoice')) return; const modal = document.createElement('div'); modal.id = 'modal-new-invoice'; modal.className = 'modal-overlay'; modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-labelledby', 'modal-new-invoice-title'); modal.innerHTML = ` `; document.body.appendChild(modal); // Wire events document.getElementById('btnCloseNewInvoice').addEventListener('click', window.closeNewInvoiceModal); document.getElementById('btnGenerateInvoice').addEventListener('click', window.generateNewInvoice); document.getElementById('seq-serie').addEventListener('input', _seqUpdatePreview); document.getElementById('seq-counter').addEventListener('input', _seqUpdatePreview); document.getElementById('seq-include-an').addEventListener('change', _seqUpdatePreview); document.getElementById('seq-cifre').addEventListener('input', _seqUpdatePreview); // Close on backdrop click modal.addEventListener('click', function(e) { if (e.target === modal) window.closeNewInvoiceModal(); }); // Esc key document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && modal.classList.contains('is-open')) { window.closeNewInvoiceModal(); } }); } /** * Deschide modal-ul "Factură Nouă" cu valorile curente din secvență. */ window.openNewInvoiceModal = function() { _ensureNewInvoiceModal(); const seq = _seqRead(); const serieEl = document.getElementById('seq-serie'); const yearEl = document.getElementById('seq-year'); const counterEl = document.getElementById('seq-counter'); const includeAnEl = document.getElementById('seq-include-an'); const cifreEl = document.getElementById('seq-cifre'); if (serieEl) serieEl.value = seq.serie || 'RFT'; if (yearEl) yearEl.value = new Date().getFullYear(); if (counterEl) counterEl.value = seq.contor || 1; if (includeAnEl) includeAnEl.checked = seq.includeAn !== false; if (cifreEl) cifreEl.value = seq.cifre || 4; _seqUpdatePreview(); const modal = document.getElementById('modal-new-invoice'); modal.classList.add('is-open'); serieEl?.focus(); }; /** Închide modal-ul "Factură Nouă". */ window.closeNewInvoiceModal = function() { document.getElementById('modal-new-invoice')?.classList.remove('is-open'); }; /** * Generează o factură nouă cu numărul din secvență, incrementează contorul. */ window.generateNewInvoice = function() { const serieEl = document.getElementById('seq-serie'); const counterEl = document.getElementById('seq-counter'); const includeAnEl = document.getElementById('seq-include-an'); const cifreEl = document.getElementById('seq-cifre'); const serie = (serieEl?.value || 'RFT').trim().toUpperCase(); const contor = Math.max(1, parseInt(counterEl?.value) || 1); const year = new Date().getFullYear(); const includeAn = includeAnEl ? includeAnEl.checked : true; const cifre = Math.max(1, Math.min(8, parseInt(cifreEl?.value) || 4)); const seq = { serie, an: year, contor, includeAn, cifre }; const invoiceNumber = _seqFormat(seq); // Salvează secvența cu contorul incrementat _seqWrite({ ...seq, contor: contor + 1 }); // Creează o factură goală și populează formularul currentInvoice = createEmptyInvoice(); const serializer = new XMLSerializer(); const serialized = serializer.serializeToString(currentInvoice).replace(/^<\?xml[^?]*\?>\s*/, ''); const xmlString = '\n' + serialized; parseXML(xmlString); // Setează numărul de factură generat const numEl = document.querySelector('[name="invoiceNumber"]'); if (numEl) numEl.value = invoiceNumber; // Setează data emiterii la azi const today = new Date(); const pad = n => String(n).padStart(2, '0'); const dateStr = `${pad(today.getDate())}.${pad(today.getMonth() + 1)}.${today.getFullYear()}`; const dateEl = document.querySelector('[name="issueDate"]'); if (dateEl) dateEl.value = dateStr; // Populează furnizorul din profil dacă există const profil = getJSON('efactura.profil.v1', null); if (profil) { SUPPLIER_FIELDS.forEach(f => { const el = document.querySelector(`[name="${f}"]`); if (el && profil[f] !== undefined) el.value = profil[f]; }); } window.closeNewInvoiceModal(); _updateBRPanel(); showToast(`Factură nouă creată: ${invoiceNumber}`, 'success', 'Contorul a fost incrementat automat.'); }; /** * Banner an nou (D24) — resetează contorul la 1 pentru noul an. */ window.yearRolloverReset = function() { const seq = _seqRead(); const newYear = new Date().getFullYear(); _seqWrite({ ...seq, an: newYear, contor: 1 }); document.getElementById('year-rollover-banner')?.remove(); showToast(`Seria ${seq.serie}: contor resetat la 1 pentru ${newYear}.`, 'success'); }; /** * Banner an nou (D24) — continuă cu contorul existent pentru noul an. */ window.yearRolloverContinue = function() { const seq = _seqRead(); const newYear = new Date().getFullYear(); _seqWrite({ ...seq, an: newYear }); document.getElementById('year-rollover-banner')?.remove(); showToast(`Seria ${seq.serie}: continuă cu nr. ${seq.contor} pentru ${newYear}.`, 'info'); }; /** Verifică dacă secvența aparține unui an anterior → injectează banner D24. */ function _checkYearRollover() { const seq = _seqRead(); const year = new Date().getFullYear(); if (!seq.an || seq.an >= year) return; // nicio problemă // Evită duplicat if (document.getElementById('year-rollover-banner')) return; const banner = document.createElement('div'); banner.id = 'year-rollover-banner'; banner.className = 'year-rollover-banner'; banner.innerHTML = `

    An nou ${year} detectat. Seria ${_escapeHtml(seq.serie)} are contorul la ${seq.contor} (an ${seq.an}). Alegeți cum să continuați numerotarea în ${year}.

    `; // Injectează sub header (primul copil al .container) const container = document.querySelector('.container'); const header = container?.querySelector('.header'); if (header?.nextSibling) { container.insertBefore(banner, header.nextSibling); } else if (container) { container.appendChild(banner); } } // Verifică rollover la DOMContentLoaded (după ce restul UI este inițializat) document.addEventListener('DOMContentLoaded', _checkYearRollover); // ============================================================================ // PR-A13: Catalog produse IndexedDB — autocomplete + save (D15) // Câmpul "Denumire" din fiecare linie factură primește sugestii din catalog. // ============================================================================ /** ID-ul timeout-ului de debounce pentru autocomplete. */ let _catalogDebounceTimer = null; /** Dropdown curent deschis (referință pentru cleanup). */ let _activeCatalogDropdown = null; /** * Creează și afișează dropdown-ul de sugestii catalog sub input-ul dat. * @param {HTMLInputElement} input * @param {Array} items - Produse din catalog * @param {number} lineIndex */ function _showCatalogDropdown(input, items, lineIndex) { _hideCatalogDropdown(); const wrapper = input.closest('.description-wrapper'); if (!wrapper) return; const dd = document.createElement('div'); dd.className = 'catalog-dropdown'; dd.setAttribute('role', 'listbox'); dd.setAttribute('aria-label', 'Sugestii catalog produse'); if (!items.length) { dd.innerHTML = `
    Niciun produs în catalog pentru "${_escapeHtml(input.value)}"
    `; } else { dd.innerHTML = items.map((item, i) => `
    ${_escapeHtml(item.name)}
    ${_escapeHtml(item.unit)} · ${_escapeHtml(item.price)} RON · TVA ${_escapeHtml(item.vatType)} ${_escapeHtml(item.vatRate)}%
    `).join(''); // Click pe un item → aplică în linia de factură dd.addEventListener('mousedown', function(e) { const itemEl = e.target.closest('[data-catalog-id]'); if (!itemEl) return; e.preventDefault(); // evită blur pe input const idx = parseInt(itemEl.dataset.catalogIdx, 10); const chosen = items[idx]; if (chosen) _applyCatalogItem(chosen, lineIndex); _hideCatalogDropdown(); }); } wrapper.appendChild(dd); _activeCatalogDropdown = dd; } /** Ascunde dropdown-ul activ. */ function _hideCatalogDropdown() { if (_activeCatalogDropdown) { _activeCatalogDropdown.remove(); _activeCatalogDropdown = null; } } /** * Aplică un produs din catalog în câmpurile liniei de factură. * @param {Object} item - Produs din catalog * @param {number} lineIndex */ function _applyCatalogItem(item, lineIndex) { const set = (name, val) => { const el = document.querySelector(`[name="${name}${lineIndex}"]`); if (el && val !== undefined) el.value = val; }; set('description', item.name); set('unit', item.unit); const priceEl = document.querySelector(`[name="price${lineIndex}"]`); if (priceEl && item.price) { priceEl.value = item.price; if (priceEl.dataset) priceEl.dataset.raw = item.price; } const vatTypeEl = document.querySelector(`[name="vatType${lineIndex}"]`); if (vatTypeEl && item.vatType) vatTypeEl.value = item.vatType; const vatRateEl = document.querySelector(`[name="vatRate${lineIndex}"]`); if (vatRateEl && item.vatRate !== undefined) vatRateEl.value = item.vatRate; if (item.description) { const descEl = document.querySelector(`[name="itemDescription${lineIndex}"]`); if (descEl) descEl.value = item.description; } updateTotals(); } /** * Inițializează event delegation pe #lineItems pentru autocomplete catalog. * Apelat din initializeUI(). */ function _initCatalogAutocomplete() { const container = document.getElementById('lineItems'); if (!container) return; container.addEventListener('input', function(e) { const input = e.target; if (!input.dataset.catalogInput) return; const lineIndex = parseInt(input.dataset.catalogInput, 10); const prefix = input.value.trim(); clearTimeout(_catalogDebounceTimer); if (!prefix || prefix.length < 2) { _hideCatalogDropdown(); return; } _catalogDebounceTimer = setTimeout(async () => { try { const items = await catalogSearch(prefix, 8); _showCatalogDropdown(input, items, lineIndex); } catch (err) { // IndexedDB indisponibil (private browsing) — ignorat silențios } }, 200); }); container.addEventListener('blur', function(e) { if (!e.target.dataset?.catalogInput) return; // Delay pentru a permite click pe dropdown înainte de close setTimeout(_hideCatalogDropdown, 200); }, true); container.addEventListener('keydown', function(e) { if (e.key === 'Escape' && _activeCatalogDropdown) { _hideCatalogDropdown(); } }); } /** * Salvează linia de factură curentă în catalog local (IndexedDB). * @param {number} lineIndex */ window.saveLineToLocalCatalog = async function(lineIndex) { const nameInput = document.querySelector(`[name="description${lineIndex}"]`); const name = (nameInput?.value || '').trim(); if (!name) { showToast('Introduceți o denumire înainte de a salva în catalog.', 'warning'); return; } const priceEl = document.querySelector(`[name="price${lineIndex}"]`); const vatTypeEl = document.querySelector(`[name="vatType${lineIndex}"]`); const vatRateEl = document.querySelector(`[name="vatRate${lineIndex}"]`); const unitEl = document.querySelector(`[name="unit${lineIndex}"]`); const descEl = document.querySelector(`[name="itemDescription${lineIndex}"]`); const product = { name, unit: unitEl?.value || 'EA', price: priceEl?.dataset?.raw || priceEl?.value || '0', vatType: vatTypeEl?.value || 'S', vatRate: vatRateEl?.value || '19', description: descEl?.value || '', }; try { const id = await catalogAdd(product); showToast(`"${name}" salvat în catalog.`, 'success', `ID: ${id.slice(0, 8)}…`); } catch (err) { const isUnavailable = err?.message?.includes('indexeddb'); showToast( isUnavailable ? 'Catalogul nu este disponibil (private browsing sau browser blocat IndexedDB).' : 'Eroare la salvare în catalog: ' + err.message, 'error' ); } }; // Inițializare catalog la DOMContentLoaded document.addEventListener('DOMContentLoaded', _initCatalogAutocomplete); // Export for testing if needed if (typeof module !== 'undefined' && module.exports) { module.exports = { calculateTotals, validateForm, roundNumber, formatXML, createXMLElement, getXMLValue, setXMLValue, formatter, setupInlineEditing, updateTotalDisplay, displayTotals, updateVATDisplay, getDisplayValue }; } import { InvoicePrintHandler } from './print.js'; // Create print handler instance const printHandler = new InvoicePrintHandler(); // Initialize when the document is ready document.addEventListener('DOMContentLoaded', () => { // Add print controls to the UI const headerButtonGroup = document.querySelector('.button-group'); if (headerButtonGroup) { const printControls = document.createElement('div'); printControls.className = 'print-controls'; printControls.style.display = 'flex'; printControls.style.gap = '8px'; printControls.style.alignItems = 'center'; // Create template selector const templateSelect = document.createElement('select'); templateSelect.className = 'form-input'; templateSelect.style.width = 'auto'; templateSelect.innerHTML = ` `; templateSelect.addEventListener('change', (e) => { printHandler.setTemplate(e.target.value); }); // Create print button const printButton = document.createElement('button'); printButton.className = 'button'; printButton.onclick = () => printHandler.print(); printButton.innerHTML = 'Printează'; // Add elements to controls printControls.appendChild(templateSelect); printControls.appendChild(printButton); // Add controls to header headerButtonGroup.appendChild(printControls); } });