In doua articole anterioare am prezentat metode de autentificare prin OAuth si obtinere a tokenului de acces JWT, precum si metode pentru comunicarea cu platforma SPV ANAF in vederea incarcarii facturilor electronice in format xml.
Mai jos urmeaza metode pentru descarcarea unei facturi din SPV, factura care a fost incarcata de furnizor. Fluxul operatiunilor pentru descarcarea unei facturi este:
- consultarea listei de raspunsuri
- descarcarea facturii in format xml
- conversia facturii xml in format pdf
- interpretarea continutului facturii xml (optional)
1. Consultarea listei de mesaje
Este important de notat faptul ca lista cu raspunsuri returneaza maxim 500 de elemente (facturi, erori sau mesaje). De aceea, aceasta interogare se poate face in doua moduri: simplu sau cu paginatie. Eu am ales sa merg pe varianta cu paginatie, pentru ca acopera inclusiv situatia in care raspunsul contine mai putin de 500 de elemente (deci o singura pagina). Apelul acestui serviciu necesita autentificare.
La apelul acestui serviciu sunt necesari urmatorii parametri:
startTime
– data de start pentru care se face interogarea, in format numeric (unixtimestamp cu milisecunde)endTime
– data de final pentru care se face interogarea, in format numeric (unixtimestamp cu milisecunde)cif
– codul fiscal (numeric) pentru care se face interogarea raspunsurilorpagina
– numarul paginii interogatefiltru
(optional) – poate avea valorile urmatoare
E = ERORI FACTURA
T = FACTURA TRIMISA
P = FACTURA PRIMITA
R = MESAJ CUMPARATOR PRIMIT / MESAJ CUMPARATOR TRANSMIS
Se pot interoga raspunsurile pentru cel mult 60 de zile in urma. Pentru a nu ne lovi de problemele care apar chiar la limita intervalului de timp (datorita sincronizarii imperfecte a timpului intre serverul nostru si cel al ANAF) am ales ca parametrul endTime
sa fie intotdeauna cu 10 minute in urma fata de momentul prezent. De asemenea, in webinariile organizate de ANAF am fost asigurati ca nu vor apare raspunsuri noi la o data din trecut, ci, toate raspunsurile sunt furnizate cronologic, in ordinea emiterii lor. Asta inseamna ca daca interogam lista de raspunsuri o data pe zi, nu are rost sa interogam un interval mai mare de 1 zi (sa zicem totusi 2 zile pentru siguranta).
Un aspect care merita notat este faptul ca in lista de raspunsuri mai intai sunt furnizate cele cu id impar, iar apoi cele cu id par. Chiar si asa, nu exista o ordonare utila pentru utilizator. Asta inseamna ca informatiile nu vor fi afisate utilizatorilor pentru fiecare pagina de raspunsuri interogata la ANAF, ci vor fi descarcate toate paginile, se va face ordonarea necesara si abia apoi se va face afisarea.
Fiecare pagina de raspunsuri contine si parametrul numar_total_pagini
, asa incat chiar de la prima pagina returnata se va cunoaste numarul total de pagini care trebuie interogate.
Apelul de mai jos este facut pentru facturile primite, deci contine parametrul filtru=P
.
public function getListaRaspunsuri($nr_zile = 60){
global $db;
$out = [];
//verificare existenta token valid pentru ANAF
if (!$this->get_last_valid_token()){
return ['result' => 1, 'error' => 'Nu exista token valabil.'];
}
//initializari
$startTime = strtotime("-{$zile} days") * 1000;
$endTime = strtotime("-10 min") * 1000;
$pagina = 1;
$nr_pagini = 1;
//ciclu de interogari
while($pagina <= $nr_pagini){
//interogare
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.anaf.ro/prod/FCTEL/rest/listaMesajePaginatieFactura?startTime=$startTime&endTime=$endTime&cif=COD_FISCAL&pagina=$pagina&filtru=P");
curl_setopt($ch, CURLOPT_POST, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_HTTPHEADER,
array(
"Authorization: Bearer {$anaf->token_info['access_token']}",
"Content-Type: text/plain",
"Accept-Encoding: gzip, deflate, br",
"Connection: keep-alive"
)
);
//verbose output
curl_setopt($ch, CURLOPT_VERBOSE, true);
$streamVerboseHandle = fopen('php://temp', 'w+');
curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);
$resp = curl_exec($ch);
rewind($streamVerboseHandle);
$verboseLog = stream_get_contents($streamVerboseHandle);
//mesaje
$result = json_decode($resp, true);
if ($result['eroare']){
$errors[] = $result['eroare'];
}
else{
//tratare alte erori
if (!is_array($result['mesaje']) || !isset($result['numar_total_pagini'])){
$errors[] = 'Eroare la interogarea ANAF';
$errors[] = $verboseLog;
return ['result' => 1, 'errors' => $errors];
}
foreach($result['mesaje'] AS $mesaj){
$out[] = $mesaj;
}
$nr_pagini = $result['numar_total_pagini'];
$pagina++;
}
}
if (count($errors)){
return ['result' => 1, 'errors' => $errors];
}
else{
return ['result' => 0, 'mesaje' => $out];
}
}
Fiecare pozitie din lista de mesaje returnate are structura urmatoare:
Array
(
[data_creare] => 202403170835
[cif] => COD_FISCAL
[id_solicitare] => 1234567890
[detalii] => Factura cu id_incarcare=1234567890 emisa de cif_emitent=COD_FISCAL_FURNIZOR pentru cif_beneficiar=COD_FISCAL
[tip] => FACTURA PRIMITA
[id] => 3124567890
)
data_creare
– reprezinta data la care furnizorul a incarcat factura in SPVcif
– cod fiscal pentru care se face interogarea (clientul)id_solicitare
– reprezintaindex_incarcare
pe care l-a primit furnizorul la incarcarea facturiidetalii
– un string cu cateva detalii din care putem extrage codul fiscal al furnizoruluitip
– tipul mesajului (mai sus la pasul 1 apare lista cu valorile posibile ale acestui parametru)id
– id_descarcare
2. Descarcarea unei facturi de la furnizor
Pentru descarcarea unei facturi de la furnizor din platforma SPV se poate utiliza metoda de descarcare a unui mesaj (prezentata in articolul anterior), singura diferenta fiind aceea ca metoda nu poate fi apelata utilizand numarul facturii, ci parametrul id_descarcare
. Acest parametru il obtinem din lista de mesaje de la pasul anterior. Apelul acestui serviciu necesita autentificare.
public function getFacturaFurnizor($id_descarcare){
global $db;
//verificare existenta token valid pentru ANAF
if (!$this->get_last_valid_token()){
return ['result' => 1, 'error' => 'Nu exista token valabil.'];
}
//interogare
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.anaf.ro/prod/FCTEL/rest/descarcare?id=' . $id_descarcare);
curl_setopt($ch, CURLOPT_POST, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_HTTPHEADER,
array(
"Authorization: Bearer {$this->token_info['access_token']}",
"Content-Type: text/plain",
"Accept-Encoding: gzip, deflate, br",
"Connection: keep-alive"
)
);
//verbose output
curl_setopt($ch, CURLOPT_VERBOSE, true);
$streamVerboseHandle = fopen('php://temp', 'w+');
curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);
$resp = curl_exec($ch);
//testez daca raspunsul este continut de fisier zip
if (substr($resp, 0, 4) == "\x50\x4b\x03\x04"){
//salvez fisierul zip
$filepath = LOCAL_PATH . "/documents/eFactura/$id_descarcare";
$filename = "{$id_descarcare}.zip";
//daca nu exista folderul destinatie il creez
if (!is_dir($filepath)) mkdir($filepath);
//scriere continut fisier zip
$f = fopen($filepath . '/' . $filename, 'w');
fwrite($f, $resp);
fclose($f);
chmod($filepath . '/' . $filename, 0755);
//returnare rezultat
return ['result' => 0, 'filename' => $filepath . '/' . $filename];
}
else{
//NU este continut fisier zip
return ['result' => 1, 'error' => $resp];
}
}
3. Conversie xml in pdf
Acesta este un serviciu web pus la dispozitie de ANAF prin care factura in format xml poate fi convertita in format pdf. Structura si formatul (macheta) documentului pdf nu este una foarte placuta, dar…e singura varianta pe care o avem la dispozitie.
Apelul acestui serviciu nu necesita autentificare.
Parametrii pentru request sunt:
tip
– tipul documentului, poate avea valorileFACT1
sauFCN
novld
– optional, se accepta doar valoareaDA
, caz in care fisierul xml transmis nu va fi validat
public function convertXML2PDF($tip, $xml_filepath, $invoice_no){
global $db;
$pathinfo = pathinfo($xml_filepath);
$filepath = $pathinfo['dirname'];
$xml_filename = basename($xml_filepath);
$pdf_filename = $invoice_no . '.pdf';
//preluare continut fisier xml
$xml_file_content = file_get_contents($filepath . '/' . $xml_filename);
//elimin headerul BOM (UTF-8) din continutul fisierului
$xml_file_content = remove_bom($xml_file_content);
//elimin secventa care poate genera erori
$xml_file_content = str_replace('xsi:schemaLocation="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2 ../../UBL-2.1(1)/xsd/maindoc/UBL-Invoice-2.1.xsd"', '', $xml_file_content);
//interogare
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://webservicesp.anaf.ro/prod/FCTEL/rest/transformare/$tip");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml_file_content);
curl_setopt($ch, CURLOPT_HTTPHEADER,
array(
"Content-Type: text/plain",
"Accept-Encoding: gzip, deflate, br",
"Connection: keep-alive"
)
);
//verbose output
curl_setopt($ch, CURLOPT_VERBOSE, true);
$streamVerboseHandle = fopen('php://temp', 'w+');
curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);
$resp = curl_exec($ch);
rewind($streamVerboseHandle);
$verboseLog = stream_get_contents($streamVerboseHandle);
//testez daca este continut de fisier pdf
if (substr($resp, 0, 4) == "\x25\x50\x44\x46"){
//salvez fisierul pdf
//daca nu exista folderul destinatie il creez
if (!is_dir($filepath)) mkdir($filepath);
//scriere continut fisier zip
$f = fopen($filepath . '/' . $pdf_filename, 'w');
fwrite($f, $resp);
fclose($f);
chmod($filepath . '/' . $pdf_filename, 0755);
//returnare rezultat
return ['result' => 0, 'filename' => $filepath . '/' . $pdf_filename];
}
else{
//NU este continut fisier zip
return ['result' => 1, 'error' => $resp];
}
}
4. Interpretarea facturii xml de la furnizor (optional)
Pentru interpretarea continutului unui document xml am utilizat clasa UBLParser
pe care am obtinut-o de aici: https://github.com/ahmeti/ubl-parser-php/blob/master/UBLParser.php si pe care am adaptat-o situatiei mele. Aceasta clasa permite traversarea elementelor DOM ale fisierului xml si transformarea acestuia intr-un array. Secventa de cod de mai jos citeste continutul fisierului XML, iar apoi il interpreteaza (parseaza) in functie de tipul documentului: Factura sau Credit Note. La final, in variabila $f vom avea continutul documentului in format Array
din care putem extrage mai usor informatiile necesare.
Fisierul xml trebuie extras din arhiva zip descarcata din SPV. Pentru dezarhivare am folosit clasa ZipArchive
din PHP.
//DEZARHIVARE
$zip = new ZipArchive();
if ($zip->open($zip_filename)){
//extrag fisierul factura
$zip->extractTo(LOCAL_PATH . "/documents/eFactura/{$id_descarcare}", "{$id_solicitare}.xml");
//extrag fisierul semnatura
$zip->extractTo(LOCAL_PATH . "/documents/eFactura/{$id_descarcare}", "semnatura_{$id_solicitare}.xml");
$zip->close();
}
//citire continut fisier
$xml = file_get_contents(LOCAL_PATH . "/documents/eFactura/{$id_descarcare}/{$id_solicitare}.xml");
//eliminarea din continutul fisierului a secventei care poate genera erori de interpretare a fisierului XML
$xml = str_replace('xmlns:schemaLocation="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2 http://docs.oasis-open.org/ubl/os-UBL-2.1/xsd/maindoc/UBL-Invoice-2.1.xsd"', '', $xml);
//INTERPRETARE CONTINUT XML
$ubl = new UBLParser;
$ubl->getDocDetails($xml);
if ($ubl->docType == 'CreditNote'){
$tipDocument = 'FCN';
$ubl->parseCreditNote($xml);
$f = $ubl->getCreditNote();
}
else{
$tipDocument = 'FACT1';
$ubl->parseInvoice($xml);
$f = $ubl->getInvoice();
}
//numarul documentului
$invoice_no = $f['ID'];