sync efactura-generator -> 0.9-beta-15
- Header reorganizat cu meniu Acțiuni (overflow dropdown) - Buton nou PDF ANAF (transformare oficială XML->PDF prin API ANAF) - Fix endpoint ANAF: validează default + ruta publică fără auth
This commit is contained in:
@@ -5,13 +5,14 @@
|
||||
* Pe hosting static (GitHub Pages, fără PHP), apelurile vor eșua cu eroare
|
||||
* "receiver indisponibil" — verificați cu probeReceiver() la inițializare.
|
||||
*
|
||||
* Configurare necesară în config.json (server-side):
|
||||
* "anaf_token": "<Bearer token OAuth ANAF>" — necesar pentru validate + pdf
|
||||
* Configurare opțională în config.json (server-side):
|
||||
* "anaf_token": "<Bearer token OAuth ANAF>" — folosește ruta OAuth (api.anaf.ro)
|
||||
* Fără token: receiver folosește ruta publică webservicesp.anaf.ro (fără auth).
|
||||
*
|
||||
* Endpoints ANAF (proxied):
|
||||
* Validate : POST https://api.anaf.ro/prod/FCTEL/rest/validare/FACT1
|
||||
* PDF/HTML : POST https://api.anaf.ro/prod/FCTEL/rest/transformare/FACT1/DA
|
||||
* CIF info : POST https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva
|
||||
* Validate : POST /FCTEL/rest/validare/FACT1
|
||||
* XmlToPdf : POST /FCTEL/rest/transformare/FACT1 (validează default; întoarce PDF)
|
||||
* CIF info : POST https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva
|
||||
*/
|
||||
|
||||
const RECEIVER = './receiver.php';
|
||||
@@ -65,11 +66,14 @@ export async function anafValidate(xmlContent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Obține vizualizarea ANAF a facturii (ZIP cu HTML).
|
||||
* Notă: ANAF /transformare returnează ZIP+HTML, nu PDF direct.
|
||||
* PDF-ul real este generat client-side prin PR-PDF / html2pdf.js.
|
||||
* Obține PDF-ul oficial ANAF al facturii (transformare XML → PDF).
|
||||
* ANAF /transformare/FACT1 validează XML-ul și întoarce direct PDF binary.
|
||||
* Dacă XML-ul nu trece validarea, ANAF întoarce JSON cu erori (status 400).
|
||||
*
|
||||
* @param {string} xmlContent - XML ca string UTF-8
|
||||
* @returns {Promise<Blob>} ZIP blob
|
||||
* @returns {Promise<{pdf: Blob}|{errors: Array<{message:string,severity:string}>}>}
|
||||
* - pdf: Blob `application/pdf` la succes
|
||||
* - errors: listă mesaje validare la eșec
|
||||
*/
|
||||
export async function anafPdf(xmlContent) {
|
||||
let res;
|
||||
@@ -82,10 +86,29 @@ export async function anafPdf(xmlContent) {
|
||||
} catch (e) {
|
||||
throw new Error('Receiver.php indisponibil — ' + e.message);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`ANAF vizualizare: HTTP ${res.status}`);
|
||||
|
||||
const ct = (res.headers.get('Content-Type') || '').toLowerCase();
|
||||
|
||||
if (res.ok && ct.includes('application/pdf')) {
|
||||
return { pdf: await res.blob() };
|
||||
}
|
||||
return res.blob();
|
||||
|
||||
// ANAF validation errors come back as JSON (HTTP 400 or 200 with JSON body)
|
||||
if (ct.includes('application/json')) {
|
||||
const data = await res.json().catch(() => null);
|
||||
const messages = (data?.Messages || data?.messages || []).map(m => ({
|
||||
message: m.message || m.Message || String(m),
|
||||
severity: (m.severity || m.Severity || 'ERROR').toUpperCase()
|
||||
}));
|
||||
if (messages.length) return { errors: messages };
|
||||
if (data?.error) throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => '');
|
||||
throw new Error(`ANAF transformare: HTTP ${res.status}` + (txt ? ' — ' + txt.slice(0, 200) : ''));
|
||||
}
|
||||
throw new Error(`ANAF transformare: răspuns neașteptat (${ct || 'fără content-type'})`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 { probeReceiver, anafValidate, anafCifLookup, anafPdf } from './anaf.js';
|
||||
import { catalogAdd, catalogSearch, catalogDelete } from './catalog.js';
|
||||
|
||||
// Constants
|
||||
@@ -4572,6 +4572,63 @@ window.validateAnaf = async function() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Descarcă PDF-ul oficial generat de ANAF din XML-ul curent.
|
||||
* Apelează endpoint-ul ANAF /transformare/FACT1 (validează implicit).
|
||||
* Dacă XML-ul nu trece validarea ANAF, afișează erorile în loc să descarce.
|
||||
* Necesită receiver.php disponibil pe server (proxy CORS).
|
||||
*/
|
||||
window.downloadPdfAnaf = async function() {
|
||||
if (!currentInvoice) {
|
||||
showToast('Nicio factură încărcată. Deschideți un XML eFactura mai întâi.', 'warning');
|
||||
return;
|
||||
}
|
||||
const btn = document.getElementById('btnPdfAnaf');
|
||||
if (btn) _btnLoading(btn, 'Generare PDF ANAF...');
|
||||
|
||||
try {
|
||||
const xmlString = _currentXMLString();
|
||||
if (!xmlString) throw new Error('Nu s-a putut genera XML-ul facturii.');
|
||||
|
||||
const result = await anafPdf(xmlString);
|
||||
|
||||
if (result.errors && result.errors.length) {
|
||||
// ANAF a respins XML-ul la validare; afișează primele erori în toast.
|
||||
const errs = result.errors.filter(m => m.severity === 'ERROR' || m.severity === 'FATAL');
|
||||
const sub = result.errors.slice(0, 3).map(m => m.message).join(' | ');
|
||||
showToast(
|
||||
`ANAF a respins XML-ul: ${errs.length || result.errors.length} mesaje`,
|
||||
'error',
|
||||
sub.slice(0, 220)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(result.pdf instanceof Blob)) {
|
||||
throw new Error('Răspuns ANAF fără PDF.');
|
||||
}
|
||||
|
||||
// Descarcă blob-ul ca fișier PDF.
|
||||
const inv = printHandler.collectInvoiceData();
|
||||
const safeNum = String(inv.invoiceNumber || 'factura').replace(/[^a-z0-9_-]+/gi, '_');
|
||||
const url = URL.createObjectURL(result.pdf);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${safeNum}_ANAF.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showToast('PDF ANAF descărcat.', 'success');
|
||||
} catch (err) {
|
||||
showToast('Eroare PDF ANAF: ' + err.message, 'error',
|
||||
'Verificați că receiver.php este disponibil pe server.');
|
||||
} finally {
|
||||
if (btn) _btnDone(btn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Caută datele firmei după CIF din câmpul supplierVAT / customerVAT.
|
||||
* @param {'supplier'|'customer'} party
|
||||
@@ -4647,11 +4704,12 @@ window.lookupCif = async function(party) {
|
||||
}
|
||||
};
|
||||
|
||||
// Probează receiver.php la startup — dacă e disponibil, afișează butoanele ANAF.
|
||||
// Probează receiver.php la startup — dacă e disponibil, afișează item-urile ANAF.
|
||||
(async () => {
|
||||
const available = await probeReceiver().catch(() => false);
|
||||
if (available) {
|
||||
document.getElementById('btnValidateAnaf')?.style?.setProperty('display', '');
|
||||
// Item-urile ANAF din meniul "Acțiuni" sunt grupate într-un wrapper hidden.
|
||||
document.querySelector('.actions-menu-anaf')?.removeAttribute('hidden');
|
||||
document.querySelectorAll('.anaf-cif-btn').forEach(b => b.style.removeProperty('display'));
|
||||
}
|
||||
})();
|
||||
@@ -5262,38 +5320,66 @@ 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 = `
|
||||
<option value="standard">Standard</option>
|
||||
<option value="compact">Compact</option>
|
||||
`;
|
||||
templateSelect.addEventListener('change', (e) => {
|
||||
printHandler.setTemplate(e.target.value);
|
||||
});
|
||||
setupActionsMenu();
|
||||
});
|
||||
|
||||
// Create print button
|
||||
const printButton = document.createElement('button');
|
||||
printButton.className = 'button';
|
||||
printButton.onclick = () => printHandler.print();
|
||||
printButton.innerHTML = 'Printează';
|
||||
/**
|
||||
* Cablează meniul "Acțiuni ▾" din header: toggle, click-outside, Esc, item handlers.
|
||||
* Înlocuiește vechea injecție DOM pentru print controls — toate output-urile
|
||||
* (Printează Standard/Compact, Descarcă PDF, PDF ANAF, Validare ANAF) sunt în meniu.
|
||||
*/
|
||||
function setupActionsMenu() {
|
||||
const trigger = document.getElementById('btnActionsMenu');
|
||||
const menu = document.getElementById('actionsMenu');
|
||||
if (!trigger || !menu) return;
|
||||
|
||||
// Add elements to controls
|
||||
printControls.appendChild(templateSelect);
|
||||
printControls.appendChild(printButton);
|
||||
|
||||
// Add controls to header
|
||||
headerButtonGroup.appendChild(printControls);
|
||||
}
|
||||
});
|
||||
const open = () => {
|
||||
menu.hidden = false;
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
};
|
||||
const close = () => {
|
||||
menu.hidden = true;
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
};
|
||||
const toggle = () => (menu.hidden ? open() : close());
|
||||
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggle();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!menu.hidden && !menu.contains(e.target) && e.target !== trigger) close();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !menu.hidden) {
|
||||
close();
|
||||
trigger.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Item dispatch — fiecare buton are data-action (+ data-template pentru print).
|
||||
menu.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('[data-action]');
|
||||
if (!item) return;
|
||||
const action = item.dataset.action;
|
||||
close();
|
||||
|
||||
switch (action) {
|
||||
case 'print':
|
||||
printHandler.setTemplate(item.dataset.template || 'standard');
|
||||
printHandler.print();
|
||||
break;
|
||||
case 'downloadPdf':
|
||||
window.downloadPDF?.();
|
||||
break;
|
||||
case 'pdfAnaf':
|
||||
window.downloadPdfAnaf?.();
|
||||
break;
|
||||
case 'validateAnaf':
|
||||
window.validateAnaf?.();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user