gestione gare pubbliche
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 

3187 righe
96 KiB

<?php
require_once __DIR__ . "/traits/npalogger.trait.php";
require_once __DIR__ . "/traits/suggestions.trait.php";
require_once __DIR__ . "/traits/render.trait.php";
require_once __DIR__ . "/traits/npa.api.trait.php";
require_once __DIR__ . "/traits/deleghe.trait.php";
global $beRoot;
require_once $beRoot . "/guue/class/eFormsManager.class.php";
class NuovaPiattaformaAppalti
{
use NPASuggestionsTrait;
use NuovaPiattaformaAppaltiRenderTrait;
use Deleghe;
use NPALogger;
use NPAAPI;
public const INTEGRATION_PATHS = [
"gare" => "/gare/guue/integration.php",
"pcp" => "/pcp/guue/integration.php",
"concorsi" => "/concorsi/guue/integration.php",
"albo_fornitori" => "/albo_fornitori/guue/integration.php",
];
/**
* Versione SDK di default
*
* @var string
*/
const LATEST_SDK_VERSION = "v1.0.2";
/**
* Ultima versione interna
*
* @var int
*/
const LATEST_INTERNAL_VERSION = 4;
/**
* Costante per indicare al costruttore di caricare
* automaticamente la versione SDK del fascicolo
*/
const AUTOMATICALLY_GET_SDK_VERSION = null;
/**
* Definisce lo stato minimo perchè una scheda sia considerata pubblicata dai vari check
*/
const TRASMITTED_STATO = 50;
/**
* Definisce lo stato minimo perchè una scheda sia considerata pubblicata dai vari check
*/
const PUBLISHED_STATO = 110;
/**
* Connessione al database
*
* @var myPDO
*/
private $pdo;
/**
* Riferimento a eFormsManager se presente
*
* @var eFormsManager
*/
private $ted;
/**
* Sdk version path
*
* @var string
*/
private $path;
/**
* Form Details
*
* @var array
*/
private $form;
/**
* lot
*
* @var mixed
*/
public $lot;
/**
* XML Document check
*
* @var array
*/
public $check;
/**
* Available cards
*
* @var array
*/
public $schede;
/**
* Available cards settings
*
* @var array
*/
public $metadataSchede;
/**
* Fascicolo NPA
*
* @var array
*/
public $fascicolo;
/**
* I contenuti della scheda attuale.
* Usa getCardData.
*/
private $card_data;
/**
* Info
*
* @var array
*/
public $info;
/**
* Current Card info
*
* @var array
*/
public $scheda;
/**
* version
*
* @var string
*/
public $version;
/**
* Object reference
*
* @var mixed
*/
public $oggetto;
/**
* Definisce se la scheda corrente necessita di aggiudicatari
*
* @var boolean
*/
public $has_aggiudicatari = false;
/**
* Definisce se la scheda corrente necessita di partecipanti
*
* @var boolean
*/
public $has_partecipanti = false;
/**
* Contiene la configurazione corrente delle API
*
* @var array
*/
private $api_config = [];
/**
* Se popolata sovrascrive la stazione appaltante usata per generare i JWT di autenticazione
*
* @var ?array
*/
public $overrideStazioneAppaltante;
/**
* Class initialization
*
* @param ?int $codice_fascicolo
* @param ?int $codice_scheda
* @param ?string $identificativo_scheda
* @param ?string $version
* @param ?eFormsManager $ted
* @return ?NuovaPiattaformaAppalti
*/
public static function init(?int $codice_fascicolo = null, ?int $codice_scheda = null, ?string $identificativo_scheda = null, ?string $modulo_riferimento = null, ?int $id_riferimento = null, ?string $version = self::AUTOMATICALLY_GET_SDK_VERSION, ?eFormsManager $ted = null): ?NuovaPiattaformaAppalti
{
if($version == self::AUTOMATICALLY_GET_SDK_VERSION) {
$version = self::fetchVersion($codice_fascicolo, $codice_scheda);
if($version === null) {
throw new Exception("Impossibile determinare la versione SDK NPA", 999);
}
}
$npa = new self($version);
$npa->fascicolo = get_campi("b_npa");
$npa->scheda = get_campi("b_npa_schede");
$npa->scheda["stato"] = 0;
$success = $npa->initCard($codice_scheda, $identificativo_scheda);
if ($success !== true && empty($codice_fascicolo)) {
// Scheda non caricabile
return null;
}
$identificativo_scheda = $npa->scheda["id_scheda"];
$metadata = $npa->metadataSchede[$identificativo_scheda] ?? [];
if(!empty($metadata)) {
$npa->has_aggiudicatari = $metadata["aggiudicatari"];
$npa->has_partecipanti = $metadata["partecipanti"];
}
if (empty($codice_fascicolo) && !empty($npa->scheda)) {
$codice_fascicolo = $npa->scheda["codice_npa"];
}
$result = $npa->loadNpaDossier(!empty($codice_fascicolo) ? $codice_fascicolo : null, $modulo_riferimento, $id_riferimento);
if(!$result) {
throw new Exception("Impossibile inizializzare il fascicolo. Potrebbe essere stato cancellato o resettato.", 999);
}
if($ted !== null) { $npa->ted = $ted; }
if($npa->scheda["codice"] > 0) {
$npa->checkIfCardNeedsXMLDocument();
if (!(defined("__CMDS_JOB_EXEC") && !empty(__CMDS_JOB_EXEC))) {
$npa->getSuggestions();
}
// Workaround per ottenere il noticeID inviato quando mancante
if(
$npa->check["eform"]["required"] && // Se la scheda ha eForms
!empty($npa->scheda["uuid"]) && // Se la scheda ha un idScheda (è già stata inviata)
empty($npa->scheda["guue_sent_notice_id"]) // Se la scheda non ha il record di un notice_id inviato
) {
$sent_notice_id = $npa->retrieveGUUESentNoticeID();
if(!empty($sent_notice_id)) {
// Verifichiamo che il notice_id sia corretto, altrimenti lo rigeneriamo
while(preg_match("/[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}/", $sent_notice_id) !== 1) {
$sent_notice_id = uuidgen();
}
// Aggiorniamo la scheda
if($npa->updateScheda(["guue_sent_notice_id" => $sent_notice_id])) {
$npa->checkIfCardNeedsXMLDocument();
}
}
}
// Primo collegamento automatico della scheda GUUE
if(
$npa->scheda["codice"] > 0 && // Se la scheda esiste
$npa->check["eform"]["required"]
) {
$compatibleCard = [];
if(empty($npa->scheda["codice_scheda_guue"])) {
$compatibleCard = $npa->fetchCompatibleGUUECards(false);
} else {
$compatibleCard["codice"] = $npa->scheda["codice_scheda_guue"];
}
if(!empty($compatibleCard)) {
// Aggiorniamo la scheda
if($npa->selezionaSchedaGUUE($compatibleCard["codice"])) {
$npa->checkIfCardNeedsXMLDocument();
}
}
}
}
return $npa;
}
/**
* Seleziona la nuova scheda GUUE
*
* @param integer $codiceScheda
* @return boolean
*/
public function selezionaSchedaGUUE(int $codiceScheda) : bool {
if($this->checkLock()) {
return false;
}
// Se è già settato ci assicuriamo solo che
if($codiceScheda !== $this->scheda["codice_scheda_guue"]) {
// Prima resettiamo il lotto della scheda a noi associata
if($this->scheda["codice_scheda_guue"] > 0) {
$success = $this->pdo->go(
"UPDATE b_guue SET lotto_riferimento = NULL WHERE codice = :codice",
[":codice" => $this->scheda["codice_scheda_guue"]]
);
if(!$success)
return false;
}
// Settiamo il nuovo codice scheda
$success = $this->updateScheda(["codice_scheda_guue" => $codiceScheda]);
if(!$success) return false;
}
// Dopodichè associamo la nuova al lotto, se presente e se necessario
$codiceLottoNpa = $this->scheda["codice_lotto"];
if(!empty($codiceLottoNpa)) {
$lot = $this->getLots($codiceLottoNpa) ?? null;
if(!empty($lot)) {
$lot = reset($lot);
$result = $this->pdo->go(
"UPDATE b_guue SET lotto_riferimento = :lotto WHERE codice = :codice",
[
":lotto" => $lot["codice"],
":codice" => $codiceScheda
]
);
return($result->errorInfo()[0] == "00000");
}
return false;
}
return true;
}
/**
* Class initialization for free requests
* @param ?string $version
* @return ?NuovaPiattaformaAppalti
*/
public static function initStatic( ?string $version = self::LATEST_SDK_VERSION): ?NuovaPiattaformaAppalti
{
$npa = new self($version);
$npa->fascicolo = get_campi("b_npa");
$npa->scheda = get_campi("b_npa_schede");
$npa->scheda["stato"] = 0;
$npa->fascicolo["codice"] = 0;
return $npa;
}
public static function initForImport(String $modulo_riferimento, int $id_riferimento, ?string $version = self::LATEST_SDK_VERSION) : ?NuovaPiattaformaAppalti
{
$npa = new self($version);
$npa->fascicolo = get_campi("b_npa");
$npa->fascicolo["modulo_riferimento"] = $modulo_riferimento;
$npa->fascicolo["id_riferimento"] = $id_riferimento;
$npa->scheda = get_campi("b_npa_schede");
$npa->scheda["stato"] = 0;
return $npa;
}
/**
* Check if the version exists.
*
* @param mixed $version
* @return void
*/
public static function check_version(String $version): void
{
$path = dirname(__FILE__, 2);
// If the folder for the specified version does not exist, we raise an exception.
if (!file_exists("{$path}/versions/{$version}")) {
throw new Exception("SDK '{$version}' not found!");
}
}
/**
* Retrieves a list of cards organized and classified based on their module types
*
* @param mixed $version
* @return array
*/
public static function getCategorizedCardsList(): ?array
{
if(valid_json(file_get_contents(__DIR__ . "/match-moduli.json"), $matchModuli, true)) {
return $matchModuli;
}
return null;
}
private static $cardMetadataCache = null;
/**
* Ottiene i metadata delle schede
*
* @return array|null
*/
public static function getCardsMetadata(): ?array
{
if(self::$cardMetadataCache === null) {
if(valid_json(file_get_contents(__DIR__ . "/schede.json"), $schede, true)) {
self::$cardMetadataCache = $schede;
}
}
return self::$cardMetadataCache;
}
public static function getInitialCards(?string $modulo = null, ?string $procedura = null, ?string $fase = null, ?string $settore = null, ?string $soglia = null, ?bool $concessione = null) {
if (!in_array($settore, ["ordinario", "speciale", "difesa", null])) {
throw new Exception("The '{$settore}' sector specified is invalid. Please choose from valid options: 'ordinario', 'speciale', 'difesa'.");
}
if (!in_array($soglia, ["soprasoglia", "sottosoglia", null])) {
throw new Exception("The specified '{$soglia}' threshold is invalid. Valid options: 'soprasoglia', 'sottosoglia'.");
}
$schede = self::getCardsMetadata();
$matches = self::getCategorizedCardsList();
$cards = [];
// If $modulo is specified, we retrieve all available cards for those modules.
if (!is_null($modulo) && !empty($matches[$modulo])) {
//TODO: Ensure array_get function returns desired results
$cards = DotPath::get($matches[$modulo], "procedure.*.*.*", null, false);
// If $procedura is specified, we retrieve all available cards for that procedure.
if (!is_null($procedura)) {
$cards = DotPath::get($matches[$modulo], "procedure.{$procedura}.*.*", null, false);
// If $fase is specified, we retrieve all available cards for that phase.
if (!is_null($fase)) {
$cards = $matches[$modulo]["procedure"][$procedura]["fasi"][$fase] ?? $matches[$modulo]["procedure"][$procedura]["fasi"]["*"];
}
}
}
$initialCards = [];
foreach($cards as $card) {
$metadata = $schede[$card] ?? [];
if(empty($metadata)) continue;
if($settore !== null) {
if(!in_array($settore, $metadata["settori"])) continue;
}
if($settore !== null) {
if(!$metadata[$soglia]) continue;
}
if(!is_null($concessione) && !is_null($metadata["concessione"])) {
if($metadata["concessione"] != $concessione) {
continue;
}
}
$initialCards[$card] = $metadata;
}
return $initialCards;
}
/**
* Retrieve the list of all saved cards from the database.
*
* @param ?array $filters
* @param ?array $limits
* @param ?array $order
* @return Generator
*/
public static function listCards(?array $filters = [], ?array $limits = [], ?array $order = []): ?Generator
{
global $pdo;
$count = "SELECT COUNT(codice)";
$select = "SELECT *";
$from = "FROM b_npa_schede";
$bind = [];
$conditions = ["SOFT_DELETE != 'S'"];
if ($_SESSION["utente"]->gerarchia > 10) {
$ids = Utente::getPermissionsInModulo($_SESSION["ente"]->codice, $_SESSION["utente"]->codice, "npa");
$ids = implode(",", $ids);
$conditions[] = "codice IN ({$ids})";
}
if (!$_SESSION["utente"]->fromHere()) {
$bind[":codice_ente"] = $_SESSION["utente"]->codice_ente;
$conditions[] = "codice_ente = :codice_ente";
}
$where = "";
if (!empty($conditions)) {
$where = "WHERE (" . implode(" ) AND (", $conditions) . ")";
}
$results = $pdo->go("{$count} {$from} {$where}", $bind)->fetch(PDO::FETCH_COLUMN, 0);
if (!empty($results)) {
if (!empty($filters)) {
$clauses = [];
if (!empty($filters["codice"])) {
$clauses[] = "codice = :codice";
$bind[":codice"] = $filters["codice"];
}
if (!empty($filters["codice_riferimento"])) {
$clauses[] = "id_riferimento = :id_riferimento";
$bind[":id_riferimento"] = $filters["codice_riferimento"];
}
if (!empty($filters["codice_scheda"])) {
$clauses[] = "id_scheda = :id_scheda";
$bind[":id_scheda"] = $filters["codice_scheda"];
}
if (!empty($filters["stato"])) {
$clauses[] = "stato = :stato";
$bind[":stato"] = $filters["stato"];
}
if (!empty($clauses)) {
$conditions[] = "(" . implode(") OR (", $clauses) . ")";
}
}
$where = "";
if (!empty($conditions)) {
$where = "WHERE (" . implode(" ) AND (", $conditions) . ")";
}
$sorting = "";
if (!empty($order)) {
$sorting = "ORDER BY {$order["column"]} {$order["dir"]}";
}
$restrictions = "";
if (!empty($limits) && $limits["lenght"] > 0) {
$restrictions = "LIMIT {$limits["start"]}, {$limits["lenght"]}";
}
$filtered = $pdo->go("{$count} {$from} {$where} {$sorting}", $bind)->fetch(PDO::FETCH_COLUMN, 0);
yield [
"recordsTotal" => $results,
"recordsFiltered" => $filtered
];
$results = $pdo->go("{$select} {$from} {$where} {$sorting} {$restrictions}", $bind);
while ($result = $results->fetch(PDO::FETCH_ASSOC)) {
yield $result;
}
}
return null;
}
/**
* Get the list of available statuses.
*
* @return array
*/
public static function listAvailableStatusList(): ?array
{
if(valid_json(file_get_contents(__DIR__ . "/stati.json"), $stati, true)) {
return $stati;
}
return null;
}
/**
* Retrieves a dictionary of errors within the npa
* @param mixed $version
*
* @return array
*/
public static function getErrorsDictionary(): ?array
{
$erroriData = file_get_contents(__DIR__ . "/errori.json");
$erroriOverrideData = file_get_contents(__DIR__ . "/errori.override.json");
if(valid_json($erroriData, $errori, true)) {
$errori = array_column($errori, null, "codice");
if(valid_json($erroriOverrideData, $erroriOverride, true)) {
$erroriOverride = array_column($erroriOverride, null, "codice");
$errori = array_merge_recursive_distinct($errori, $erroriOverride);
}
return $errori;
}
return null;
}
/**
* Get connected
*
* @return array
*/
public const MODULI_COLLEGATI = [
"gare", "concorsi", "albo_fornitori"
];
/**
* Evaluates and returns a boolean value indicating whether the user is currently logged into the application
*
* @return bool
*/
public function isUserLoggedIn(string $role = DelegheRoles::RP): Bool
{
if (!empty($this->fascicolo["data"])) {
$cf = $this->getUserFromRole($role, RoleSyncBehaviour::NEVER_SYNC);
$timestamp = $this->pdo->go("SELECT `b_check_sessions`.`timestamp` FROM b_check_sessions JOIN b_utenti ON b_utenti.codice = b_check_sessions.codice_utente WHERE b_utenti.cf = :cf", [":cf" => $cf])->fetch(PDO::FETCH_COLUMN, 0);
if (!empty($timestamp) && strtotime($timestamp) > strtotime("-3 hours")) {
return true;
}
}
return false;
}
/**
* Retrieves and loads the data associated with a specific card
*
* @param mixed $codice_scheda
* @param mixed $identificativo_scheda
* @return bool
*/
public function initCard(?int $codice_scheda = 0, ?string $identificativo_scheda = ""): Bool
{
global $pdo;
$bind = [];
$scheda = null;
if (!empty($codice_scheda)) {
$clauses[] = "codice = :codice_scheda";
$bind[":codice_scheda"] = $codice_scheda;
}
if (!empty($this->fascicolo["codice"])) {
$clauses[] = "codice_npa = :codice_fascicolo";
$bind[":codice_fascicolo"] = $this->fascicolo["codice"];
if (!empty($identificativo_scheda)) {
$clauses[] = "id_scheda = :identificativo_scheda";
$bind[":identificativo_scheda"] = $identificativo_scheda;
}
}
if (!empty($clauses)) {
$where = implode(" AND ", $clauses);
$sql = "SELECT * FROM b_npa_schede WHERE {$where} LIMIT 0, 1";
$scheda = $pdo->go($sql, $bind)->fetch(PDO::FETCH_ASSOC);
if (!empty($scheda)) {
// Verifica soft delete
if (($scheda["soft_delete"] ?? "N") == "S") {
return false;
}
$this->scheda = $scheda;
$this->form = jsonToArray("{$this->path}/schede/{$this->scheda["id_scheda"]}.json");
if(!$this->loadNpaDossier($scheda["codice_npa"]))
return false;
if(empty($this->scheda["versione_scheda"])) {
$this->scheda["versione_scheda"] = $this->version;
}
return true;
}
}
if (empty($scheda)) {
$metadata = $this->metadataSchede[$identificativo_scheda] ?? [];
if (!empty($metadata)) {
$this->scheda["codice"] = null;
$this->scheda["id_scheda"] = $identificativo_scheda;
$this->scheda["type"] = $metadata["tipologia"];
$this->scheda["stato"] = 0;
if(empty($this->scheda["versione_scheda"])) {
$this->scheda["versione_scheda"] = $this->version;
}
$this->form = jsonToArray("{$this->path}/schede/{$identificativo_scheda}.json");
return true;
}
}
return false;
}
/**
* Retrieves and loads information related to an NPA Dossier
*
* @param mixed $codice_fascicolo
* @return void
*/
public function loadNpaDossier(?int $codice_fascicolo = null, ?string $modulo_riferimento = null, ?int $id_riferimento = null): bool
{
global $pdo;
if (!empty($codice_fascicolo)) {
$bind = [":codice_fascicolo" => $codice_fascicolo];
$sql = "SELECT * FROM b_npa WHERE codice = :codice_fascicolo ORDER BY revisione_fascicolo DESC LIMIT 0,1";
$fascicolo = $pdo->go($sql, $bind)->fetch(PDO::FETCH_ASSOC);
} elseif (!is_null($modulo_riferimento) && !is_null($id_riferimento)) {
$bind = [];
$bind[":modulo_riferimento"] = $modulo_riferimento;
$bind[":id_riferimento"] = $id_riferimento;
$sql = "SELECT * FROM b_npa WHERE modulo_riferimento = :modulo_riferimento AND id_riferimento = :id_riferimento ORDER BY revisione_fascicolo DESC LIMIT 0,1";
$fascicolo = $pdo->go($sql, $bind)->fetch(PDO::FETCH_ASSOC);
}
if (! empty($fascicolo)) {
$this->fascicolo = $fascicolo;
if (empty($modulo_riferimento)) {
$modulo_riferimento = $this->fascicolo["modulo_riferimento"];
}
if (empty($id_riferimento)) {
$id_riferimento = $this->fascicolo["id_riferimento"];
}
}
$schedeAppalto = $this->getLastCompletedCards();
if(!empty($schedeAppalto)) {
foreach($schedeAppalto as $scheda) {
if(valid_json($scheda["dati"], $dati, true)) {
$stazioniAppaltanti = recursive_array_key_search_all("stazioniAppaltanti", $dati)[0] ?? [];
if(!empty($stazioniAppaltanti)) {
$stazioniAppaltanti = array_values($stazioniAppaltanti);
$this->updateFascicolo(["stazioni_appaltanti" => json_encode($stazioniAppaltanti)]);
break;
}
}
}
}
// Detect fascicolo resettato
if($codice_fascicolo < 0 || $id_riferimento < 0) {
return false;
}
$this->initOggetto($modulo_riferimento, $id_riferimento);
if (! empty($this->oggetto) && !is_a($this->oggetto, "AlboFornitori")) {
$this->syncLots();
}
$this->loadCards();
return true;
}
/**
* Questo metodo inizializza un oggetto falso per le importazioni
*
* @return void
*/
public function initFakeOggetto(string $modulo_riferimento) {
if(empty($_SESSION["ente"]) || empty($_SESSION["utente"]))
throw new Exception("Nessun ente/utente accessibile");
$codice_ente = $_SESSION["ente"]->codice;
$ente = $_SESSION["ente"]->getInfo();
if(empty($ente))
throw new Exception("Impossibile trovare l'ente {$codice_ente}");
$utente = $_SESSION["utente"];
$userInfo = method_exists($utente, "getInfo") ? $utente->getInfo() : [];
// Prendiamo il CF dell'utente
$cf = $userInfo["cf"] ?? null;
$this->updateFascicolo([
"data" => serialize([
"beneficiario" => ["codice_fiscale" => $ente["cf"], "codice_ausa" => $ente["codice_ausa"]],
"rup" => ["codice_fiscale" => $cf],
]),
"modulo_riferimento" => $modulo_riferimento
]);
}
public function initOggetto(string $modulo_riferimento, int $id_riferimento) {
switch ($modulo_riferimento) {
case 'albo_fornitori':
$this->oggetto = AlboFornitori::init($id_riferimento);
$this->oggetto->npa = $this;
break;
case 'concorsi':
$this->oggetto = concorso::init($id_riferimento);
$this->oggetto->npa = $this;
break;
case 'gare':
$this->oggetto = gara::init($id_riferimento);
// Per la scheda S1 è necessario riferirsi al primo round
if(($this->scheda["id_scheda"] ?? null) === "S1") {
$codice_primoRound = $this->oggetto->getRounds()[0]["codice"] ?? -1;
if($codice_primoRound > 0) {
$newOggetto = gara::init($this->oggetto->info["codice"], $codice_primoRound);
if(!empty($newOggetto)) {
$this->oggetto = $newOggetto;
}
}
}
$this->oggetto->npa = $this;
break;
default:
$this->oggetto = null;
break;
}
if(empty($this->fascicolo["modulo_riferimento"]))
$this->fascicolo["modulo_riferimento"] = $modulo_riferimento;
// Tiene i data aggiornati
if(empty($this->fascicolo["data"]) || (empty($this->fascicolo["id_appalto"]) && empty($this->fascicolo["id_pianificazione"]))) {
$current_data = serialize([
"benficiario" => $this->oggetto->getEnteBeneficiario(),
"rup" => $this->oggetto->getRup()
]);
$this->updateFascicolo(["data" => $current_data]);
}
if (! empty($this->oggetto) && empty($this->fascicolo["codice"])) {
$this->fascicolo["id_riferimento"] = $id_riferimento;
$this->fascicolo["data"] = serialize([
"benficiario" => $this->oggetto->getEnteBeneficiario(),
"rup" => $this->oggetto->getRup()
]);
}
}
/**
* Retrieves and loads information related to lots
*
* @return void
*/
public function syncLots(): void
{
$lots = $this->oggetto->getLotti();
$missing = array_column($lots, "codice");
$existing = $this->pdo->go("SELECT * FROM b_npa_lotti WHERE codice_npa = :codice_npa", [":codice_npa" => $this->fascicolo["codice"]])->fetchAll(PDO::FETCH_ASSOC);
if (!empty($existing)) {
$missing = array_diff(array_column($lots, "codice"), array_column($existing, "lotto_riferimento"));
}
if (!empty($missing)) {
$salva = new Salva();
$salva->debug = false;
$salva->nome_tabella = "b_npa_lotti";
$salva->operazione = "INSERT";
$salva->ignore = ["timestamp", "timestamp_trasmissione"];
$lots = array_column($lots, null, "codice");
foreach ($missing as $codice) {
if (!empty($lots[$codice])) {
$salva->oggetto = [
"codice_npa" => $this->fascicolo["codice"],
"lotto_riferimento" => $codice,
"timestamp_creazione" => date('d/m/Y H:i:s')
];
$salva->save();
}
}
}
}
/**
* Sets information for a specific lot
*
* @return void
*/
public function setLotto(Int $id): void
{
$lot = $this->getLots($id);
if (count($lot) == 1) {
$this->lot = reset($lot);
if(!empty($this->oggetto)) {
$this->oggetto->setLotto($this->lot["codice"]);
}
$this->loadCards();
}
}
/**
* Sets information for a specific lot
*
* @return void
*/
public function unsetLotto(): void
{
if (!empty($this->lot)) {
$this->lot = null;
$this->schede = null;
if(!empty($this->oggetto->lotto)) {
$this->oggetto->lotto = null;
}
}
}
/**
* Retrieves information about lots
*
* @return array
*/
public function getLots(Int $id = null): ?array
{
$data = $this->oggetto->getLotti();
if (!empty($this->fascicolo["codice"])) {
$sql = "SELECT lotto_riferimento, codice FROM b_npa_lotti WHERE codice_npa = :codice_npa";
$bind = [":codice_npa" => $this->fascicolo["codice"]];
if (!is_null($id)) {
$sql = "{$sql} AND codice = :codice";
$bind[":codice"] = $id;
}
$lots = $this->pdo->go($sql, $bind)->fetchAll(PDO::FETCH_ASSOC);
if (!empty($lots)) {
$lots = array_column($lots, null, "lotto_riferimento");
$ids = array_keys($lots);
foreach ($data as $index => $lot) {
if (!in_array($lot["codice"], $ids)) {
unset($data[$index]);
} else {
$data[$index]["codice_lotto_npa"] = $lots[$lot["codice"]]["codice"];
}
}
}
}
return $data;
}
/**
* Ottiene la versione da utilizzare
*
* @param integer|null $codice_fascicolo
* @param integer|null $codice_scheda
* @return string|null Versione da uasre
*/
public static function fetchVersion(?int $codice_fascicolo = null, ?int $codice_scheda = null) : string {
if($codice_scheda !== null) {
$scheda = self::fetchSchede(["versione_scheda", "stato"], [
"s.codice" => $codice_scheda
], false, false);
if(!empty($scheda)) {
if($scheda["stato"] < 20) {
return self::LATEST_SDK_VERSION;
}
return empty($scheda["versione_scheda"]) ? self::LATEST_SDK_VERSION : $scheda["versione_scheda"];
}
}
return self::LATEST_SDK_VERSION;
}
/**
* Fetch and load the set of card for the given dossier.
*
* @return array
*/
public static function fetchLotti(array $fields = ["*"], array $filters = [], bool $join_npa = false, bool $multiple = true): array
{
$fields = array_filter($fields, function($field) {
if($field === "*") return true;
if(!myPDO::validFieldName($field)) {
throw new Exception("Invalid field name {$field}");
}
return true;
}, ARRAY_FILTER_USE_BOTH);
global $pdo;
$fields_str = implode(",", $fields);
$bind = [];
$filter_str = [];
// Per ogni filtro, lo aggiungiamo nei bind e nella query
foreach ($filters as $field => $value) {
if(!myPDO::validFieldName($field)) {
throw new Exception("Invalid field name {$field}");
}
$operand = "=";
if (is_array($value)) {
$operand = $value[0];
$value = $value[1];
}
$bind_name = uniqid();
$bind[":{$bind_name}"] = $value;
$filter_str[] = "{$field} $operand :{$bind_name}";
}
// Ricostruiamo la query where
if (!empty($filter_str)) {
$filter_str = "WHERE " . implode(" AND ", $filter_str);
} else {
$filter_str = "";
}
$join_str = "";
if ($join_npa) {
if($fields_str == "*") {
$fields_str .= ", l.codice AS codice, n.codice AS codice_npa";
}
if (empty($filter_str)) {
$filter_str .= " WHERE n.id_riferimento > 0";
} else {
$filter_str .= " AND n.id_riferimento > 0";
}
$join_str = "LEFT JOIN b_npa n ON n.codice = l.codice_npa";
}
$limit_str = "";
if (!$multiple) {
$limit_str = "LIMIT 1";
}
$sql = "SELECT {$fields_str} FROM b_npa_lotti l {$join_str} {$filter_str} ORDER BY l.timestamp_creazione, l.codice ASC {$limit_str}";
$result = $pdo->go($sql, $bind);
$lotti = $multiple ? $result->fetchAll(PDO::FETCH_ASSOC) : $result->fetch(PDO::FETCH_ASSOC);
if (!is_array($lotti)) {
return [];
}
return $lotti;
}
/**
* Carica una lista di fascicoli con i filtri indicati
*
* @return array
*/
public static function fetchDossier(array $fields = ["*"], array $filters = [], bool $multiple = true): array
{
$fields = array_filter($fields, function($field) {
if($field === "*") return true;
if(!myPDO::validFieldName($field)) {
throw new Exception("Invalid field name {$field}");
}
return true;
}, ARRAY_FILTER_USE_BOTH);
global $pdo;
// Aggiungo soft delete
$filters["id_riferimento"] = [">", "0"];
$fields_str = implode(",", $fields);
$bind = [];
$filter_str = [];
if(empty($filters["id_riferimento"]))
$filters["id_riferimento"] = [">", "0"];
// Per ogni filtro, lo aggiungiamo nei bind e nella query
foreach ($filters as $field => $value) {
if(!myPDO::validFieldName($field)) {
throw new Exception("Invalid field name {$field}");
}
$operand = "=";
if (is_array($value)) {
$operand = $value[0];
$value = $value[1];
}
$bind[":{$field}"] = $value;
$filter_str[] = "`{$field}` $operand :{$field}";
}
// Ricostruiamo la query where
if (!empty($filter_str)) {
$filter_str = "WHERE " . implode(" AND ", $filter_str);
} else {
$filter_str = "";
}
$limit_str = "";
if (!$multiple) {
$limit_str = "LIMIT 1";
}
$sql = "SELECT {$fields_str} FROM b_npa {$filter_str} ORDER BY `codice`, `timestamp` ASC {$limit_str}";
$result = $pdo->go($sql, $bind);
$fascicoli = $multiple ? $result->fetchAll(PDO::FETCH_ASSOC) : $result->fetch(PDO::FETCH_ASSOC);
if (!is_array($fascicoli)) {
return [];
}
return $fascicoli;
}
private static function buildFilter(array $filters, array &$bind): array
{
$filter_str = [];
foreach ($filters as $field => $value) {
if (strpos($field, "or_") === 0) {
// Supporto ai gruppi OR
$or_filter_str = self::buildFilter($value, $bind);
if (!empty($or_filter_str)) {
$or_filter_str = implode(" OR ", $or_filter_str);
$filter_str[] = "({$or_filter_str})";
}
} else {
if (!myPDO::validFieldName($field)) {
throw new Exception("Invalid field name {$field}");
}
$operand = "=";
if (is_array($value)) {
$operand = $value[0];
$value = $value[1];
}
$bind_name = uniqid();
if ($operand === "IN" && is_array($value)) {
if (!empty($value)) {
$in_str = "{$field} IN (";
foreach ($value as $idx => $val) {
$in_str .= ":{$bind_name}_{$idx},";
$bind[":{$bind_name}_{$idx}"] = $val;
}
$in_str = rtrim($in_str, ",") . ")";
$filter_str[] = $in_str;
}
} elseif (in_array($operand, ["OR", "AND"], true) && is_array($value)) {
$in_str = "(";
foreach ($value as $idx => $val) {
$subOperand = "=";
if (is_array($val)) {
$subOperand = $val[0];
$val = $val[1];
} elseif ($val === null) {
$subOperand = "IS";
}
$in_str .= " {$field} {$subOperand} :{$bind_name}_{$idx} $operand";
$bind[":{$bind_name}_{$idx}"] = $val;
}
$in_str = rtrim($in_str, " $operand") . ")";
$filter_str[] = $in_str;
} else {
$bind[":{$bind_name}"] = $value;
$filter_str[] = "{$field} $operand :{$bind_name}";
}
}
}
return $filter_str;
}
/**
* Fetch and load the set of card for the given dossier.
*
* @return array
*/
public static function fetchSchede(array $fields = ["*"], array $filters = [], bool $join_npa = false, bool $multiple = true, $order = "ASC"): array
{
$fields = array_filter($fields, function($field) {
if($field === "*") return true;
if(!myPDO::validFieldName($field)) {
throw new Exception("Invalid field name {$field}");
}
return true;
}, ARRAY_FILTER_USE_BOTH);
global $pdo;
// Disambiguazione codice
$codiceKey = array_search("codice", $fields);
if($codiceKey !== false && $join_npa) {
$fields[$codiceKey] = "s.codice";
}
$fields_str = implode(",", $fields);
$bind = [];
$filter_str = [];
// Aggiungo soft delete
$filters["soft_delete"] = ["!=", "S"];
$join_str = "";
if ($join_npa) {
if($fields_str == "*") {
$fields_str .= ", s.codice AS codice, n.codice AS codice_npa";
}
$join_str = "LEFT JOIN b_npa n ON n.codice = s.codice_npa";
if(empty($filters["id_riferimento"]))
$filters["id_riferimento"] = [">", "0"];
}
// Per ogni filtro, lo aggiungiamo nei bind e nella query
$filter_str = self::buildFilter($filters, $bind);
// Ricostruiamo la query where
if (!empty($filter_str)) {
$filter_str = "WHERE " . implode(" AND ", $filter_str);
} else {
$filter_str = "";
}
$limit_str = "";
if (!$multiple) {
$limit_str = "LIMIT 1";
}
$orderBy = "s.codice";
$orderDir = $order;
if(is_array($order)) {
$orderBy = "s.{$order[0]}";
$orderDir = $order[1];
}
$orderDir = $orderDir == "ASC" ? "ASC" : "DESC";
$sql = "SELECT {$fields_str} FROM b_npa_schede s {$join_str} {$filter_str} ORDER BY {$orderBy} {$orderDir} {$limit_str}";
$result = $pdo->go($sql, $bind);
$schede = $multiple ? $result->fetchAll(PDO::FETCH_ASSOC) : $result->fetch(PDO::FETCH_ASSOC);
if (!is_array($schede)) {
return [];
}
return $schede;
}
/**
* Ottiene le schede categorizzate per id_scheda
* @param bool $multiple Se false viene ritornata solo l'ultima scheda per id_scheda
* @param bool $only_current_lot . Se true, vengono caricate le schede relative al lotto corrente (o alla gara se non c'è un lotto). Se false vengono incluse quelle del lotto corrente E della gara
*
* @return array
*/
public function getCardsByType($multiple = true, $only_current_lot = true): array
{
$schede = $this->getCards($only_current_lot);
$retval = [];
foreach ($schede as $scheda) {
$scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]);
if ($multiple) {
if (empty($retval[$scheda["id_scheda"]])) {
$retval[$scheda["id_scheda"]] = [];
}
$retval[$scheda["id_scheda"]][] = $scheda;
} else {
if (isset($retval[$scheda["id_scheda"]])) {
if ($retval[$scheda["id_scheda"]]["stato"] < $scheda["stato"]) {
$retval[$scheda["id_scheda"]] = $scheda;
}
} else {
$retval[$scheda["id_scheda"]] = $scheda;
}
}
}
return $retval;
}
/**
* Fetch and load the set of card for the given dossier.
*
* @return void
*/
public function loadCards(): void
{
if (!empty($this->fascicolo)) {
$this->schede = $this->getCards(true);
}
}
/**
* Ritorna, se presente, l'ultima scheda completata, al fine di stabilire il flusso
*
* @param boolean $only_current_lot Se true ottiene solo dalla scheda generale e dal lotto attuale
* @return array|null
*/
public function lastCompletedCardForFlusso(bool $only_current_lot = true): ?array
{
$cards = $this->getCards($only_current_lot, ["timestamp_trasmissione", "DESC"]);
foreach($cards as $scheda) {
$scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]);
// Evitiamo di far contare rettifiche o modifiche
if(!empty($scheda["codice_modifica"]) || !empty($scheda["codice_rettifica"])) {
continue;
}
// Contano invece come complete schede che sono state modificate o rettificate
if(self::checkCardComplete($scheda) || in_array($scheda["stato"], [-10, -20]) ) {
return $scheda;
}
}
return null;
}
/**
* Ritorna, se disponibili, tutte le schede confermate
*
* @param mixed $only_current_lot
* @return array
*/
public function getConfirmedCards(bool $only_current_lot = true) : ?array {
$cards = $this->getCards($only_current_lot, true);
if(! empty($cards)) {
foreach($cards as $index => &$scheda) {
$scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]);
if($scheda["stato"] < self::TRASMITTED_STATO) {
unset($cards[$index]);
}
}
return $cards;
}
return null;
}
/**
* Ritorna, se disponibili, tutte le schede completate
*
* @param mixed $only_current_lot
* @return array
*/
public function getCompletedCards(bool $only_current_lot = true) : ?array {
$cards = $this->getCards($only_current_lot, true);
if(! empty($cards)) {
foreach($cards as $index => &$scheda) {
$scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]);
if(! self::checkCardComplete($scheda) && ! in_array($scheda["stato"], [-10, -20]) ) {
unset($cards[$index]);
}
}
return $cards;
}
return null;
}
/**
* Ritorna le ultime schede completate
*
* @param boolean $only_current_lot Se true ottiene solo dalla scheda generale e dal lotto attuale
* @return array
*/
public function getLastCompletedCards(bool $only_current_lot = true): array
{
$retval = [];
$cards = $this->getCards($only_current_lot, ["timestamp_trasmissione", "DESC"]);
foreach($cards as $scheda) {
$scheda = array_merge($scheda, $this->metadataSchede[$scheda["id_scheda"]]);
// Contano invece come complete schede che sono state modificate o rettificate
if(self::checkCardComplete($scheda) || in_array($scheda["stato"], [-10, -20]) ) {
$retval[] = $scheda;
}
}
return $retval;
}
/**
* Ottiene le carte presenti per questo fascicolo
*
* @param boolean $only_current_lot Se true ottiene solo dalla scheda generale e dal lotto attuale
* @param string|array $desc Se stringa, ordina per codice $order - altrimenti ordina per $order[0] $order[¹]
* @return array
*/
public function getCards(bool $only_current_lot = true, $order = "ASC"): array
{
if (!empty($this->fascicolo)) {
$select = ["codice", "dati", "codice_rettifica", "codice_modifica", "codice_lotto", "id_scheda", "stato", "errore", "timestamp_trasmissione", "versione_scheda", "timestamp_creazione"];
$filters = ["codice_npa" => $this->fascicolo["codice"]];
if (!empty($this->scheda["codice_lotto"]) || !empty($this->lot["codice_lotto_npa"])) {
$codice_lotto = empty($this->scheda["codice_lotto"]) ? $this->lot["codice_lotto_npa"] : $this->scheda["codice_lotto"];
if ($only_current_lot) {
$filters["codice_lotto"] = $codice_lotto;
} else {
$filters["codice_lotto"] = ["OR", [$codice_lotto, null, '', 0]];
}
} else {
$filters["codice_lotto"] = ["OR", [null, '', 0]];
}
return self::fetchSchede($select, $filters, false, true, $order);
}
return [];
}
/**
* Checking if the card is in an unmodifiable state
*
* @return bool
*/
public function checkLock(): Bool
{
if (!empty($this->scheda) && $this->scheda["stato"] >= 40 && ($this->scheda["errore"] ?? "NO") === "NO") {
return true;
}
if (!empty($this->scheda) && $this->scheda["stato"] >= 90) {
return true;
}
if (!empty($this->scheda) && $this->scheda["stato"] < 0) {
return true;
}
return false;
}
/**
* Ottiene le schede successive
*
* @param String|null $settore Settore corrente, se null viene ignorato
* @param String|null $soglia Soglia corrente, se null viene ignorata
* @param boolean|null $concessione Si tratta di una concessione? Se null viene ignorato
* @param boolean|null $concorso Si tratta di un concorso? Se null viene ignorato
* @return array Ritorna una lista di schede
*/
public function getCardsAndItsDescendantsList(string $settore = null, ?string $soglia = null, ?bool $concessione = null, ?bool $concorso = null): array
{
$schede = $this->getCards(false);
if (!empty($schede)) {
$list = array_filter($schede, fn ($card) => !empty($card["timestamp_trasmissione"]));
if (empty($list)) {
$list = $schede;
}
$card = array_values($list)[0];
$cards = $this->getNextCards();
$cards = array_merge(array_column($list, "id_scheda"), $cards);
$cards = array_unique($cards);
$cards = array_filter($this->metadataSchede, function (&$card, $key) use ($cards, $settore, $soglia, $concessione, $concorso) {
if (in_array($key, $cards)) {
if (!is_null($settore)) {
if (!in_array($settore, $card["settori"])) {
return false;
}
}
if (!is_null($soglia)) {
if ($soglia === "soprasoglia" && !$card[$soglia]) {
return false;
}
}
if(!is_null($concessione) && !is_null($card["concessione"])) {
if($card["concessione"] != $concessione) {
return false;
}
}
if(!is_null($concorso) && !is_null($card["concorso"])) {
if($card["concorso"] != $concorso) {
return false;
}
}
return true;
}
return false;
}, ARRAY_FILTER_USE_BOTH);
foreach ($cards as $key => &$card) {
if (!empty($list[$key])) {
$card["codice"] = $list[$key]["codice"];
}
}
return $cards;
}
return [];
}
/**
* Organizes or sorts the cards based on their category
*
* @param mixed $cards
* @return array
*/
public function arrangeCardsByCategory(array $cards): array
{
$arranged = [];
foreach ($cards as $id => $card) {
$arranged[$card["categoria"]][$id] = $card;
}
$arranged = array_replace(["pianificazione" => [], "pubblicazione" => [], "affidamento" => [], "aggiudicazione" => [], "esecuzione" => []], $arranged);
$arranged = array_filter($arranged);
return $arranged;
}
/**
* Riordina una lista di schede in base alla gerarchia.
*
* @param array $cards Lista di schede in formato ["id" => scheda] (scheda non è usato, quindi può essere vuoto)
* @return array
*/
public static function sortCardsByHierarchy(array $cards): array
{
// Sort in base alla profondità
foreach ($cards as $_id => $_scheda) {
// Salviamo ID all'interno della scheda perchè sarà usato come riferimento
$cards[$_id]["id"] = $_id;
}
// Ordiniamo in base alla profondità (questo lo rende una lista ordinata)
usort($cards, function ($a, $b) {
return $a["order"] > $b["order"];
});
// Settiamo nuovamente id come chiave
return array_column($cards, null, "id");
}
/**
* Selectively retrieves cards based on their type
*
* @param mixed $cards
* @return array
*/
public function filterCardsByType(array $cards, Bool $lot_related): array
{
return array_filter($cards, function ($info) use ($lot_related) {
return $info["lotto"] === $lot_related;
});
}
/**
* Elimina la scheda corrente
*
* @return boolean
*/
public function deleteScheda(?string &$message=null): bool
{
if ($this->isCardDeletable()) {
// Cancellazione via API
if(!empty($this->scheda["uuid"])) {
return $this->scheda["stato"] > self::TRASMITTED_STATO ? $this->deleteNotice($message) : $this->delete($message);
}
$bind = array(":codice" => $this->scheda["codice"]);
$sql = "UPDATE b_npa_schede SET soft_delete = 'S' WHERE codice = :codice";
$ris = $this->pdo->go($sql, $bind);
if ($ris->rowCount() === 1) {
scriviLog("b_npa_schede", "DELETE", $this->pdo->getSQL(), $_POST["codice"]);
return true;
}
}
$message = __("Impossibile cancellare la scheda");
return false;
}
/**
* Metodo veloce per cambiare una voce nel fascicolo
*
* @param array $data Dati da cambiare, associativo
* @return boolean True in caso di successo
*/
public function updateFascicolo(array $data=[]): bool
{
$result = false;
if($this->fascicolo["codice"] > 0) {
$salva = new Salva();
$salva->nome_tabella = "b_npa";
$salva->operazione = "UPDATE";
$salva->debug = false;
$data["codice"] = intval($this->fascicolo["codice"]);
if(!empty($data["codice"])) {
$salva->expect = array_keys($data);
$salva->oggetto = $data;
$result = $salva->save();
}
}
foreach($data as $key => $value)
{
$this->fascicolo[$key] = $value;
}
return $result == true;
}
public function updateScheda(array $data=[]): bool
{
$result = false;
if($this->scheda["codice"] > 0) {
$salva = new Salva();
$salva->nome_tabella = "b_npa_schede";
$salva->operazione = "UPDATE";
$data["codice"] = intval($this->scheda["codice"]);
if(!empty($data["codice"])) {
$salva->expect = array_keys($data);
$salva->oggetto = $data;
$result = $salva->save();
}
foreach($data as $key => $value)
{
$this->scheda[$key] = $value;
}
}
return $result == true;
}
/**
* Store form data.
*
* @param array $data
* @return bool
*/
public function save(array $data = [], bool $scheda = true): ?array
{
$salva = new Salva();
$salva->debug = false;
if (empty($this->fascicolo["codice"])) {
$info = [];
if (!empty($this->oggetto)) {
$info["benficiario"] = $this->oggetto->getEnteBeneficiario();
$info["rup"] = $this->oggetto->getRup();
} else {
return null;
}
$this->fascicolo = [
"codice_ente" => $data["codice_ente"],
"modulo_riferimento" => in_array($data["modulo_riferimento"], ["gare", "concorsi", "albo_fornitori", "contratti"]) ? $data["modulo_riferimento"] : null,
"id_riferimento" => $data["id_riferimento"],
"data" => serialize($info),
"versione_interna" => self::LATEST_INTERNAL_VERSION
];
$salva->ignore = ["timestamp"];
$salva->nome_tabella = "b_npa";
$salva->operazione = "INSERT";
$salva->oggetto = $this->fascicolo;
$this->fascicolo["codice"] = $salva->save();
if (empty($this->fascicolo["codice"])) {
throw new Exception("Errore nel salvataggio del fascicolo.");
}
}
if (!$scheda) {
return null;
}
$record = $this->scheda;
$record["dati"] = $data["npa"];
$record["dati"] = json_encode($record["dati"], JSON_PRETTY_PRINT);
$record["codice_npa"] = $this->fascicolo["codice"];
$record["codice_lotto"] = $data["codice_lotto"] ?? null;
$record["round_riferimento"] = $data["round_riferimento"];
$record["stato"] = $data["bozza"] == 0 ? 10 : 0;
$record["soft_delete"] = "N";
$record["versione_scheda"] = $this->version;
$salva->nome_tabella = "b_npa_schede";
$operazioneScheda = $salva->operazione = "UPDATE";
$salva->oggetto = $record;
// Ignoriamo il codice rettifica dopo la prima iterazione
$salva->ignore = ["codice_npa", "timestamp", "timestamp_creazione", "timestamp_trasmissione", "pending_cancellazione", "codice_rettifica", "codice_modifica"];
if (empty($record["codice"])) {
$operazioneScheda = $salva->operazione = "INSERT";
$salva->ignore = ["timestamp", "timestamp_trasmissione", "pending_cancellazione"];
$salva->oggetto["errore"] = "NO";
$salva->oggetto["timestamp_creazione"] = date("d/m/Y H:i:s");
$salva->oggetto["errore"] = "NO";
$salva->oggetto["versione_scheda"] = self::LATEST_SDK_VERSION;
if(isset($data["codice_rettifica"]) && is_numeric($data["codice_rettifica"])) {
$salva->oggetto["codice_rettifica"] = $data["codice_rettifica"];
} else {
$salva->ignore[] = "codice_rettifica";
}
if(isset($data["codice_modifica"]) && is_numeric($data["codice_modifica"])) {
$salva->oggetto["codice_modifica"] = $data["codice_modifica"];
} else {
$salva->ignore[] = "codice_modifica";
}
}
if(isset($data["codice_scheda_guue"]) && is_numeric($data["codice_scheda_guue"])) {
$salva->oggetto["codice_scheda_guue"] = $data["codice_scheda_guue"];
} else {
$salva->ignore[] = "codice_scheda_guue";
}
$record["codice"] = $salva->save();
if (!empty($record["codice"])) {
$this->scheda = $record;
if ($_SESSION["utente"]->gerarchia > 15 && $salva->operazione == "INSERT") {
$permesso = [
"sezione" => "npa",
"codice_elemento" => $this->info["codice"],
"codice_gestore" => $_SESSION["ente"]->codice,
"funzione" => "0",
"codice_utente" => $_SESSION["utente"]->codice,
];
$salva_p = new salva();
$salva_p->debug = false;
$salva_p->nome_tabella = "r_permessi";
$salva_p->operazione = "INSERT";
$salva_p->oggetto = $permesso;
$salva_p->save();
}
if (isset($this->oggetto) && method_exists($this->oggetto,"addToLog")) {
$this->oggetto->addToLog($operazioneScheda,"Scheda ANAC: " . $this->scheda["id_scheda"]);
}
return $record;
}
return null;
}
/**
* Rretrieves the codes associated with cost centers
*
* @param string $identifier
* @return array
*/
public function getCodiciCentriDiCosto(String $identifier): ?array
{
$retval = [];
if(DEVELOP_ENV) {
$source_anac = $this->getCodiciCentriDiCostoFromAnac($identifier);
$source_db = self::getCodiciCentriDiCostoFromDb($identifier);
foreach($source_anac as $uuid => $centro) {
$retval[$uuid] = ">>> $centro ({$uuid}) [COLL]";
}
foreach($source_db as $uuid => $centro) {
if(isset($retval[$uuid])) {
$retval[$uuid] = "$centro ({$uuid}) [COLL+PROD]";
} else {
$retval[$uuid] = "$centro ({$uuid}) [PROD]";
}
}
} else {
$retval = $this->getCodiciCentriDiCostoFromAnac($identifier);
if(empty($retval))
$retval = $this->getCodiciCentriDiCostoFromDB($identifier);
foreach ($retval as $uuid => $centro) {
$retval[$uuid] = "$centro ({$uuid})";
}
}
return $retval;
}
/**
* Rretrieves the codes associated with cost centers
*
* @param string $identifier
*
* @return array
*/
public static function getCodiciCentriDiCostoFromDB(String $identifier): ?array
{
global $pdo;
// Nota: su necessità questo metodo può diventare statico
$identifier = trim(strtoupper($identifier));
$centri = $pdo->go(
"SELECT * FROM b_centri_costo WHERE cf = :cf",
["cf" => $identifier]
)->fetchAll(PDO::FETCH_ASSOC);
if (!is_array($centri) || empty($centri)) {
return [];
}
$retval = [];
foreach ($centri as $centro) {
$retval[$centro["uuid"]] = $centro["denominazione"];
if($centro["attivo"] !== "S") {
$retval[$centro["uuid"]] .= " (Non Attivo)";
}
}
return $retval;
}
/**
* Retrieves keys and UUIDs (Universally Unique Identifiers) associated with the current request
*
* @return array
*/
private function getKeysAndUUIDsForCurrentRequest(?string $type = null): ?array
{
$idAppalto = $this->getUUID("appalto");
$idPianificazione = $this->getUUID("pianificazione");
$idScheda = $this->getUUID("scheda");
if (empty($type)) { $type = $this->scheda["type"]; }
if ($type == "pianificazione") {
return [
"idPianificazione" => $idPianificazione,
"idScheda" => $idScheda
];
}
return [
"idAppalto" => $idAppalto,
"idScheda" => $idScheda
];
}
/**
* Chiude tutte le operazioni in corso per questa scheda,
* risolvendo eventuali problemi prima che nascano.
*
* @return void
*/
public function closeAllPendingOperations() {
$this->pdo->go(
"UPDATE b_npa_operazioni_schede SET esito = 'KO' WHERE (esito IS NULL OR esito = '') AND codice_scheda = :codice_scheda",
[":codice_scheda" => $this->scheda["codice"]]
);
}
public static function fetchLastOperation(?int $card_id = null, ?string $operation = null, $esito = false, bool $multiple = false, ?int $lot = null, ?string $codice_partecipante = null) : ?array {
global $pdo;
$bind = [":codice_scheda" => $card_id];
$sql = "SELECT * FROM b_npa_operazioni_schede WHERE (";
$sql = "{$sql} codice_scheda = :codice_scheda";
if(empty($bind[":codice_scheda"])) {
$sql = "{$sql} OR codice_scheda IS NULL";
}
$sql = "{$sql} )";
if (!empty($lot)) {
$sql = "{$sql} AND codice_lotto = :codice_lotto";
$bind[":codice_lotto"] = $lot;
}
if (!empty($operation)) {
$sql = "{$sql} AND operazione = :operazione";
$bind[":operazione"] = $operation;
}
if(!empty($codice_partecipante)) {
$sql = "{$sql} AND codice_partecipante = :codice_partecipante";
$bind[":codice_partecipante"] = $codice_partecipante;
}
if($esito === null) {
$sql .= " AND (esito IS NULL OR esito = '' OR esito = 0)";
} elseif($esito !== false) {
$sql .= " AND esito = :esito";
$bind[":esito"] = $esito;
}
$sql .= " ORDER BY timestamp DESC";
if(!$multiple)
$sql = "{$sql} LIMIT 0,1";
$result = $pdo->go($sql, $bind);
if ($result->rowCount() > 0) {
$operations = $result->fetchAll(PDO::FETCH_ASSOC);
return $multiple ? $operations : $operations[0];
}
return null;
}
/**
* Ottiene il link alla scheda corrente
*
* @return string|null
*/
public function getCardLink() : ?string
{
if(empty($this->scheda["codice"])) return null;
$query = [
"scheda" => $this->scheda["id_scheda"],
"codice" => $this->scheda["codice"],
"modulo_riferimento" => $this->fascicolo["modulo_riferimento"] ?? null,
"id_riferimento" => $this->fascicolo["id_riferimento"] ?? null,
];
if ($this->metadataSchede[$this->scheda["id_scheda"]]["lotto"]) {
$query["codice_lotto"] = $this->scheda["codice_lotto"];
}
return "/backend/npa/edit.php?" . http_build_query($query);
}
/** Retrieves the latest operation associated with a given card
*
* @param ?int $card_id
* @param ?string $operation
* @param ?bool $esito
* @param ?bool $multiple
* @param ?int $lot
* @return array
*/
public function getLastOperation(?int $card_id = null, ?string $operation = null, $esito = false, bool $multiple = false, ?int $lot = null): ?array
{
return self::fetchLastOperation($card_id ?? $this->scheda["codice"], $operation, $esito, $multiple, $lot);
}
/**
* Recupera il notice_id inviato ad ANAC nei precedenti invii
*
* @return string|null
*/
private function retrieveGUUESentNoticeID() : ?string
{
if(!$this->canDoRequest()) return null;
$readData = $this->read();
if(!empty($readData->_scheda)) {
$scheda = $readData->_scheda;
if (isset($scheda->body->eform)) {
$eform = base64_decode($scheda->body->eform);
if($eform) {
libxml_use_internal_errors(true);
$eform_xml = new DOMDocument();
$eform_xml->loadXML($eform);
if(!empty(libxml_get_errors()))
return null;
$xpath10 = new DOMXPath($eform_xml);
libxml_clear_errors();
return $xpath10->evaluate("string(/*/cbc:ID[@schemeName='notice-id'])");
}
}
}
return null;
}
/**
* Examines whether cards associated with the given tender require either GUUE or ESPD CML documents
*
* @return void
*/
public function checkIfCardNeedsXMLDocument(): Bool
{
if(empty($this->check)) {
$this->check = [
"eform" => ["required" => false, "ready" => false],
"espd" => ["required" => false, "ready" => false]
];
if (!empty($this->scheda["id_scheda"]) && !empty($this->form["content"])) {
if (!empty($this->oggetto)) {
if (!empty(array_column($this->form["content"], null, "id")["espd"])) {
$this->check["espd"]["required"] = true;
$this->check["espd"]["xml"] = $this->oggetto->getManagerDGUE()->getRequestXML();
if (!empty($this->check["espd"]["xml"])) {
$this->check["espd"]["ready"] = true;
} else {
$this->check["espd"]["ready"] = false;
$check = false;
}
}
}
if (!empty($this->metadataSchede[$this->scheda["id_scheda"]]["guue"])) {
$this->check["eform"]["required"] = true;
if(!empty($this->scheda["codice_scheda_guue"])) {
$idScheda = $this->scheda["codice_scheda_guue"];
if($this->ted === null) {
try {
$this->ted = new eFormsManager(eFormsManager::ANY_NOTICE_TYPE, $idScheda, $this);
} catch(\Throwable $t) {
$this->ted = null;
}
}
if($this->ted) {
// Verifica che la notice_id corrisponda
if(!empty($this->scheda["guue_sent_notice_id"])) {
$eform_notice_id = $this->ted->getNoticeID();
$sent_notice_id = $this->scheda["guue_sent_notice_id"];
// Se non corrisponde, forziamo
$this->ted->overrideNoticeID($sent_notice_id);
}
$this->check["eform"]["codice"] = $this->scheda["codice_scheda_guue"];
$this->check["eform"]["ready"] = true;
$this->check["eform"]["notice_id"] = $this->ted->getNoticeID();
$this->check["eform"]["ted"] = $this->ted;
$this->check["eform"]["ready_for_transmission"] = $this->ted->isReadyForAnacTransmission();
} else {
$this->check["eform"]["ready"] = false;
$this->updateScheda(["codice_scheda_guue" => 0]);
}
} else {
$this->check["eform"]["ready"] = false;
}
}
}
}
return !$this->check["eform"]["required"] || ($this->check["eform"]["required"] && $this->check["eform"]["ready"]) &&
!$this->check["espd"]["required"] || ($this->check["espd"]["required"] && $this->check["espd"]["ready"]);
}
/**
* Updates the CIG (Contract Identification Code) within the application
*
* @param String $identifier
* @param String $cig
* @return bool
*/
private function updateCig(String $identifier, String $cig): Bool
{
if (!empty($this->fascicolo)) {
$select = "SELECT b_npa_lotti.codice";
$select = "{$select}, b_npa_lotti.lotto_riferimento AS id_riferimento";
$from = "FROM b_npa_lotti";
$where = "WHERE codice_npa = :codice_npa";
$bind = [":codice_npa" => $this->fascicolo["codice"]];
switch ($this->fascicolo["modulo_riferimento"]) {
case 'concorsi':
$select = "{$select}, b_concorsi_lotti.numero";
$join = "JOIN b_concorsi_lotti ON b_concorsi_lotti.codice = b_npa_lotti.lotto_riferimento";
$where = "{$where} AND b_concorsi_lotti.numero = :numero";
$bind[":numero"] = intval(str_replace("LOT-", "", $identifier));
break;
case 'gare':
$select = "{$select}, b_gare_lotti.numero";
$join = "JOIN b_gare_lotti ON b_gare_lotti.codice = b_npa_lotti.lotto_riferimento";
$where = "{$where} AND b_gare_lotti.numero = :numero";
$bind[":numero"] = intval(str_replace("LOT-", "", $identifier));
break;
default:
break;
}
$lot = $this->pdo->go("{$select} {$from} {$join} {$where}", $bind)->fetch(PDO::FETCH_ASSOC);
if (!empty($lot["codice"])) {
$this->pdo->go("UPDATE b_npa_lotti SET cig = :cig WHERE codice = :codice", [":codice" => $lot["codice"], ":cig" => $cig]);
if (!empty($this->oggetto)) {
$this->oggetto->updateCig($lot["id_riferimento"], $cig);
return true;
}
}
}
return false;
}
/**
* Verifica che l'istanza di npa corrente abbia un dossier che si può cancellare
*
* @return boolean
*/
public function canClearDossier() {
// Otteniamo il codice
$codice = $this->fascicolo["codice"] ?? null;
if($codice === null) return false;
// Verifichiamo che non ci sia alcuna scheda trasmessa
$filterSchede = [
"stato" => [">=", 20], // Solo schede trasmesse in su
"codice_npa" => $codice, // Solo schede di questo fascicolo
];
$schede_trasmesse = self::fetchSchede(["id_scheda"], $filterSchede);
return empty($schede_trasmesse);
}
/**
* Elimina il dossier dell'istanza NPA attuale
*
* @return void
*/
public function clearDossier($full = false) {
// Otteniamo il codice corrente
$codice = $this->fascicolo["codice"] ?? null;
$data = $this->fascicolo["data"] ?? null;
// Controlliamo se possiamo
if($codice === null || $data === null || !$this->canClearDossier()) return;
// Verifichiamo i permessi
$canFullyClear = true;
// Se è un cron skippiamo la verifica utente
$isCron = defined("__CMDS_JOB_EXEC") && !empty(__CMDS_JOB_EXEC);
if (!$isCron) {
$utente = $_SESSION["utente"] ?? null;
if(!$utente) {
return false;
}
// Altrimenti verifichiamo chi può fare un clear completo
$canFullyClear = $utente->isSupportoOrRoot();
}
if(!$canFullyClear && $full) return;
if($full) {
// Supporto e root può pulire il fascicolo per intero
$this->pdo->go("UPDATE b_npa SET data = NULL WHERE codice = :codice", [":codice" => $codice]);
} else if(isset($this->oggetto) && method_exists($this->oggetto, "getRup")) {
// Altrimenti viene solo refreshato il RP
$data = unserialize($this->fascicolo["data"]);
$rup_cf = $this->oggetto->getRup()["codice_fiscale"] ?? null;
self::setUserRoleToDB($rup_cf, DelegheRoles::RP, $this->fascicolo["codice"]);
}
}
/**
* Verifica che la scheda sia nel suo ultimo stadio
*
* @var array $card La scheda da vrificare
*
* @return boolean
*/
public static function checkCardComplete(Array $card) : bool
{
return self::hasToBePublished($card) ? $card["stato"] >= self::PUBLISHED_STATO : $card["stato"] >= self::TRASMITTED_STATO;
}
/**
* Verifica che una scheda abbia modifiche
*
* @param array $card
* @return bool|int
*/
public static function cardHasModifyCard(Array $card)
{
$modifica = self::fetchSchede(
["*"],
[
"codice_modifica" => $card["codice"],
], false, false
);
if(empty($modifica)) return false;
return $modifica["codice"];
}
/**
* Verifica che una scheda abbia rettifiche
*
* @param array $card
* @return bool|int
*/
public static function cardHasRectifyCard(Array $card)
{
$rettifica = self::fetchSchede(
["*"],
[
"codice_rettifica" => $card["codice"],
], false, false
);
if(empty($rettifica)) return false;
return $rettifica["codice"];
}
/**
* Verifica che una scheda sia rettificabile
*
* @param array $card Dati della scheda
* @return boolean Se true la scheda è rettificabile
*/
public static function checkCardRectifiable(Array $card) : bool
{
if($card["stato"] < self::PUBLISHED_STATO)
return false;
// Andiamo a cercare eventuali schede di rettifica non pubblicate
$rettifiche = self::fetchSchede(
["*"],
[
"codice_rettifica" => $card["codice"],
"stato" => ["<", "110"],
]
);
if(!empty($rettifiche)) return false;
return true;
}
/**
* Verifica che una scheda sia modificabile
*
* @param array $card Dati della scheda
* @return boolean Se true la scheda è rettificabile
*/
public static function checkCardModifiable(Array $card) : bool
{
if($card["stato"] < 90 || $card["stato"] >= self::PUBLISHED_STATO)
return false;
if(!empty($card["codice_modifica"]))
return false;
if(!empty($card["codice_rettifica"]))
return false;
// Rilevazione pubblicazione EU completa
if(!empty($card["pubblicazione"])) {
$pubblicazione = unserialize($card["pubblicazione"]);
if(!empty($pubblicazione) && !empty($pubblicazione->datiPubblicazioneEU) && !empty($pubblicazione->datiPubblicazioneEU->dataPubblicazione)) {
return false;
}
}
// Andiamo a cercare eventuali schede di modifica non pubblicate
$modifiche = self::fetchSchede(
["*"],
[
"codice_modifica" => $card["codice"],
"stato" => ["<", "110"],
]
);
if(!empty($modifiche)) return false;
return true;
}
/**
* Fetches and provides a list of subordinate cards
*
* @return array
*/
public function getNextCards(): array
{
// Otteniamo le schede attuali categorizzate per ID, ci sarà ritornata per ognuna la più aggiornata
$schedeByType = $this->getCardsByType(true, false);
// Togliamo le pubblicazioni, loro non settano un flusso pare
$schedeByTypeFiltrate = [];
foreach($schedeByType as $id => $schede) {
foreach($schede as $scheda) {
// Evitiamo di far contare rettifiche o modifiche
if(!empty($scheda["codice_modifica"]) || !empty($scheda["codice_rettifica"])) {
continue;
}
// Contano invece come complete schede che sono state modificate o rettificate
if(!(self::checkCardComplete($scheda) || in_array($scheda["stato"], ["-10", "-20"])) ){
continue;
}
// Se la scheda non definisce un flusso, la ignoriamo
if(empty($scheda["flusso"]) || in_array("*", $scheda["flusso"])) {
continue;
}
$schedeByTypeFiltrate[$id] = $scheda;
break;
}
}
$retval = [];
// Per il momento usiamo il flusso della prima scheda
if(empty($schedeByTypeFiltrate)) {
$flussi = [];
} else {
$flussi = reset($schedeByTypeFiltrate)["flusso"];
}
// Per l'ultima scheda completata
$ultimaScheda = $this->lastCompletedCardForFlusso(false);
if(empty($ultimaScheda)) {
return [];
}
$retval = [];
foreach ($ultimaScheda["schede_successive"] as $next_id_scheda) {
// Se l'abbiamo già processata continuiamo
if (in_array($next_id_scheda, $retval)) continue;
// Otteniamo i metadata della successiva
$next_metadata = $this->metadataSchede[$next_id_scheda] ?? null;
if (!$next_metadata) continue;
// Verifichiamo che sia compatibile con i flussi attuali ( se ci sono )
$next_flussi = $next_metadata["flusso"];
if(!empty($flussi) && !empty($next_flussi) && $next_flussi[0] != ["*"]) {
$compatible = false;
if(in_array("*", $next_flussi)) $compatible = true;
foreach($flussi as $flusso) {
if(in_array("{$flusso}", $next_flussi)) $compatible = true;
if(in_array("!{$flusso}", $next_flussi)) $compatible = false; break;
}
if(!$compatible) continue;
}
// Aggiungiamo la scheda a quelle trovate
$retval[] = $next_id_scheda;
}
return $retval;
}
/**
* Settings default stuff for initialization
*
* @return void
*/
private function __construct(String $version = self::LATEST_SDK_VERSION)
{
self::check_version($version);
global $pdo, $config;
if ((DEMO_ENV || DEVELOP_ENV) && !empty($_SESSION["utente"]) && $_SESSION["ente"]->getInfo()["spid"] == "N") {
$_SESSION["utente"]->authenticationLevel = 2;
$_SESSION["utente"]->publicSSO = "SPID";
}
$this->pdo = $pdo;
$this->version = $version;
$this->path = dirname(__FILE__, 2) . "/versions/{$version}";
$this->metadataSchede = self::getCardsMetadata();
$this->info = get_campi("b_npa");
$this->info["codice"] = 0;
$this->info["stato"] = 0;
$this->info["codice_gestore"] = $_SESSION["ente"]->codice;
$this->info["codice_ente"] = $_SESSION["utente"]->codice_ente ?? $_SESSION["ente"]->codice;
$this->api_config = require __DIR__ . "/configurazione.php";
}
private static function __processCardDataForRendering($values) {
if(is_array($values)) {
/*if(array_is_list($values)) { // Conversione da formato ANAC
$newvalues = [];
foreach($values as $value) {
$newvalues[uniqid("rep_")] = $value;
}
$values = $newvalues;
}*/
// Conversione da vecchio formato
$keys = array_keys($values);
if (isset($keys[0]) && strpos($keys[0], "rep_") !== false) {
$values = array_values($values);
}
foreach($values as $index => $value) {
if(in_array($index, ["espd", "eform"], true)) {
$value = "TO-BE-FILLED";
}
$values[$index] = self::__processCardDataForRendering($value);
}
} elseif(is_bool($values)) {
return $values ? "true" : "false";
} elseif(is_numeric($values)) {
return "{$values}";
} elseif(is_string($values)) {
$datetime = DateTime::createFromFormat(DateTime::ATOM, $values);
if (!is_bool($datetime)) {
return $datetime->format("d/m/Y H:i");
}
}
return $values;
}
/**
* Ottiene i dati della scheda per il rendering.
* Questo metodo è compatibile con il formato interno del json
* e anche con quello usato da ANAC. Utile nei casi in cui la scheda venga
* importata
*
* @return array
*/
public function getCardData(): Array
{
if(!empty($this->card_data))
return $this->card_data;
if(empty($this->scheda["dati"]) || !valid_json($this->scheda["dati"], $data, TRUE)) {
return [];
}
$this->card_data = self::__processCardDataForRendering($data);
return $this->card_data;
}
/**
* Organize repeteble group from dictionary to array
*
* @param array $data
* @return void
*/
public function organizeRepeteableGroups(array &$data, ?array $types = [], bool $root = true): void
{
date_default_timezone_set('Europe/Rome');
$index = "";
if (empty($types)) {
$types = $this->getFormInputsTypes($this->form["content"]);
}
foreach ($data as $key => &$content) {
if (is_array($content)) {
$content = array_filter($content, function ($value) {
if (is_array($value) && count($value) == 0) {
return false;
}
return ($value !== NULL && $value !== FALSE && $value !== '');
});
if (!array_is_list($content) && count($content) == 1 && isset($content["idTipologica"])) {
$content = [];
} else {
$keys = array_keys($content);
if (isset($keys[0]) && strpos($keys[0], "rep_") !== false) {
$content = array_values($content);
}
}
$info = $types;
if (!is_numeric($key) && !empty($types[$key])) {
$info = $types[$key];
}
$this->organizeRepeteableGroups($content, $info, false);
$content = array_filter($content, function (&$value, $key) use($info) {
$required = $info[$key]["_required"] ?? false;
if($required) {
// Workaround per assicurarci che un oggetto vuoto sia effettivamente un oggetto
if(empty($value)) {
$dataType = $info[$key]["_dataType"] ?? null;
$repeatable = $info[$key]["_repeatable"] ?? false;
$contentType = $info[$key]["contentType"] ?? null;
if($this->version === "v1.0") {
if ($dataType === "object" && !$repeatable)
$value["forceJSONObject"] = true;
}
else {
if (($contentType === "group" || $dataType === "object") && !$repeatable)
$value["forceJSONObject"] = true;
}
// Probabilmente questo funzionerebbe anche, ma non ci giurerei
// $value = new stdClass();
}
return true;
}
//if($key === "codiceAusa") return TRUE;
if (is_array($value) && count($value) == 0) {
return false;
}
return ($value !== NULL || $value !== '');
}, ARRAY_FILTER_USE_BOTH);
} else {
if (!empty($types[$key]["type"])) {
switch ($types[$key]["type"]) {
case 'cpv':
$content = str_pad($content, 8, "0", STR_PAD_RIGHT);
break;
case 'codice_istat':
$option = $this->pdo->go("SELECT codice_istat FROM b_istat_anac WHERE codice_istat = :codice_istat", [":codice_istat" => $content])->fetch(PDO::FETCH_ASSOC);
if($option) {
if(self::LATEST_SDK_VERSION === "v1.0")
$content = ltrim($option["codice_istat"], "0");
else
$content = $option["codice_istat"];
}
break;
}
}
if (!empty($types[$key]["format"])) {
switch ($types[$key]["format"]) {
case 'double':
$content = filter_var($content, FILTER_VALIDATE_FLOAT);
break;
case 'datetime':
case 'date-time':
$datetime = DateTime::createFromFormat("Y-m-d H:i", datetime2mysql($content));
if (!is_bool($datetime)) {
$content = $datetime->format(DateTime::ATOM);
}
break;
case 'integer':
case 'number':
$content = filter_var($content, FILTER_VALIDATE_INT);
break;
case 'bool':
$content = filter_var($content, FILTER_VALIDATE_BOOLEAN);
break;
default:
break;
}
}
}
}
if($root) {
if(($this->scheda["id_scheda"] ?? null) === "P7_3") {
if(empty($data["anacForm"]["lotti"])) {
$data["anacForm"]["lotti"] = [];
}
}
}
}
/**
* Get the card uuid
*
* @return string
*/
private function getUUID(String $type = "appalto"): String
{
if ($type == "appalto" && !empty($this->fascicolo["id_appalto"])) {
return $this->fascicolo["id_appalto"];
}
if ($type == "pianificazione" && !empty($this->fascicolo["id_pianificazione"])) {
return $this->fascicolo["id_pianificazione"];
}
if ($type == "scheda" && !empty($this->scheda["uuid"])) {
return $this->scheda["uuid"];
}
return "00000000-0000-0000-0000-000000000000";
}
/**
* Verifica che una scheda possa essere risincronizzata con ANAC
*
* @return boolean
*/
public function canSyncScheda() : bool {
if($this->scheda["stato"] < 20)
return false;
$dataFascicolo = unserialize($this->fascicolo["data"]);
if(!($dataFascicolo["abilita_resync"] ?? false)) {
return false;
}
return true;
}
/**
* Verifica che una scheda sia modificabile
*
* @return boolean Se true la scheda è modificabile
*/
public function isCardModifiable() : bool {
$metadata = $this->metadataSchede[$this->scheda["id_scheda"]];
$scheda = array_merge($metadata, $this->scheda);
$dataFascicolo = unserialize($this->fascicolo["data"]);
if(!($dataFascicolo["abilita_modifica_appalto"] ?? false)) {
// Workaround per disattivare modifiche su schede d'appalto (provvisorio)
if($scheda["lotto"] === false && $scheda["categoria"] != "pianificazione") {
return false;
}
}
return self::checkCardModifiable($scheda);
}
/**
* Verifica che una scheda sia rettificabile
*
* @return boolean Se true la scheda è rettificabile
*/
public function isCardRectifiable() : bool {
$metadata = $this->metadataSchede[$this->scheda["id_scheda"]];
$scheda = array_merge($metadata, $this->scheda);
return self::checkCardRectifiable($scheda);
}
/**
* Determina se la scheda i nquestione ha bisogno di essere pubblicata
*
* @param array $scheda
* @return boolean
*/
public static function hasToBePublished(array $scheda) : bool {
// Retrocompatibilità
if(($scheda["id_scheda"] ?? "") == "AD3" && ($scheda["versione_scheda"] ?? self::LATEST_SDK_VERSION) == "v1.0") {
return false;
}
$metadata = self::getCardsMetadata()[$scheda["id_scheda"]];
return $metadata["pubblicazione"];
}
/**
* Determina che una scheda sia cancellabile
*
* @param string|null $text Se passato viene popolato con il testo giusto da mostrare all'utente
* @param boolean|null $server_side Se passato viene popolato con una booleana che determina che la richiesta sia server_side o meno
* @return boolean
*/
public function isCardDeletable(?string &$text = null, ?bool &$server_side = null) : bool {
$server_side = true;
// temporaneamente non consentiamo di cancellare rettifiche
if(!empty($card["codice_rettifica"])) {
return false;
}
// Non trasmessa
if(empty($this->scheda["uuid"]) && $this->scheda["stato"] < 40) {
$server_side = false;
$text = "Elimina";
return true;
}
// Trasmessa ma non confermata
if($this->scheda["stato"] < 40) {
$text = "Elimina scheda trasmessa";
return true;
}
// In attesa di conferma
if($this->scheda["stato"] == 40) {
$text = "Elimina scheda in conferma";
return true;
}
// In attesa di pubblicazione
if($this->scheda["stato"] >= 90 && $this->scheda["stato"] < 110) {
$text = "Richiedi cancellazione della pubblicazione";
return true;
}
return false;
}
public function createDossierRevision() : bool {
if(empty($this->fascicolo))
throw new Exception("Impossibile effettuare l'operazione senza un fascicolo");
// Crea una copia del dossier attuale
$salva = new salva();
$new_fascicolo = $this->fascicolo;
unset($new_fascicolo["id_pianificazione"]);
unset($new_fascicolo["id_appalto"]);
if(empty($new_fascicolo["revisione_fascicolo"]))
$new_fascicolo["revisione_fascicolo"] = 1;
$new_fascicolo["revisione_fascicolo"] += 1;
$new_fascicolo["versione_interna"] = self::LATEST_INTERNAL_VERSION;
$salva->nome_tabella = "b_npa";
$salva->oggetto = $new_fascicolo;
$salva->operazione = "INSERT";
$vecchio_codice = $this->fascicolo["codice"];
$nuovo_codice = $salva->save();
if($nuovo_codice) {
// Evitiamo di associare quello attuale
$this->fascicolo["id_riferimento"] *= -1;
$deleteOld = new salva();
$deleteOld->nome_tabella = "b_npa";
$deleteOld->oggetto = $this->fascicolo;
$deleteOld->operazione = "UPDATE";
if(!$deleteOld->save())
return false;
// Resettiamo i notice_id della GUUE
$this->pdo->go(<<<SQL
UPDATE b_guue SET notice_id = NULL, stato ='BOZZA' WHERE codice IN
(
select
`codice_scheda_guue`
from b_npa_schede where codice_npa = :vecchio_codice AND (codice_lotto = 0 OR codice_lotto IS NULL OR codice_lotto = '') AND soft_delete != 'S' AND codice_scheda_guue > 0
)
SQL,[
":vecchio_codice" => $vecchio_codice,
//":guue_sent_notice_id" => uuidgen_guue(),
]);
// Riportiamo le schede generali, anche se resettate a bozza
$this->pdo->go(<<<SQL
insert into b_npa_schede (
`codice_npa`,
`type`,
`id_scheda`,
`dati`,
`utente_modifica`,
`codice_lotto`,
`versione_scheda`,
`codice_scheda_guue`,
`timestamp_creazione`
)
select
:nuovo_codice,
`type`,
`id_scheda`,
`dati`,
`utente_modifica`,
`codice_lotto`,
:versione_scheda,
`codice_scheda_guue`,
NOW()
from b_npa_schede where codice_npa = :vecchio_codice and (codice_lotto = 0 OR codice_lotto IS NULL) and soft_delete != 'S';
SQL, [
":vecchio_codice" => $vecchio_codice,
":nuovo_codice" => $nuovo_codice,
":versione_scheda" => self::LATEST_SDK_VERSION,
//":guue_sent_notice_id" => uuidgen_guue(),
]);
$this->fascicolo = $new_fascicolo;
// Rimuovo eventuali riferimenti ai fascicoli degli OE
$this->pdo->go("UPDATE b_fvoe SET codice_operatore = -ABS(codice_operatore), stato = -1, uuid_partecipante = NULL WHERE codice_npa = :codice_npa", [":codice_npa" => $vecchio_codice]);
return true;
}
return false;
}
public function fetchCompatibleGUUECards(bool $multiple=true, bool $draft = false) : Array {
$filters = [
"codice_elemento" => $this->fascicolo["id_riferimento"],
"modulo" => $this->fascicolo["modulo_riferimento"],
"stato" => $draft ? ["IN", ["BOZZA", "DA TRASMETTERE"]] : ["IN", ["TRASMESSO", "PUBBLICATO"]],
"form" => ["IN", $this->metadataSchede[$this->scheda["id_scheda"]]["guue"]],
];
// Verifichiamo se c'è un lotto collegato nelle multilotto
if(count($this->getLots()) > 1) {
$codice_lotto_npa = $this->scheda["codice_lotto"];
if(!empty($codice_lotto_npa)) {
$lot = $this->getLots($codice_lotto_npa) ?? null;
if(!empty($lot)) {
$lot = reset($lot);
$filters["lotto_riferimento"] = ["OR", [$lot["codice"], null, 0]];
}
}
}
$allSchede = eFormsManager::fetchSchede(
["codice", "form"],
$filters,
true
);
$codice = $this->scheda["codice"];
$schede = array_filter($allSchede, function($scheda) use($codice) {
// Check che la scheda non sia già usata
$assignedANAC = NuovaPiattaformaAppalti::fetchSchede(
[
"s.codice",
"id_scheda"
],
[
"s.codice" => ["!=", $codice],
"codice_scheda_guue" => $scheda["codice"],
"id_riferimento" => $this->fascicolo["id_riferimento"],
"modulo_riferimento" => $this->fascicolo["modulo_riferimento"]
],
true,
false
);
return empty($assignedANAC);
});
if($multiple) return $schede;
return empty($schede) ? [] : reset($schede);
}
// Override utente per Cron
private $__cronOverrideRole = null;
private $__cronOverrideCF = null;
public function setCronOverrideUser(string $role, string $cf) {
$this->__cronOverrideRole = $role;
$this->__cronOverrideCF = $cf;
}
private function publishGUUEOnSuccessfulPublication() {
if(isset($this->scheda["codice_scheda_guue"])) {
$codice_guue = $this->scheda["codice_scheda_guue"];
} else {
$this->checkIfCardNeedsXMLDocument();
if ($this->check["eform"]["required"]) {
$codice_guue = $this->check["eform"]["codice"] ?? 0;
}
}
if(!empty($codice_guue)) {
$this->pdo->go("UPDATE b_guue SET stato = :stato WHERE codice = :codice", [":stato" => "PUBBLICATO", ":codice" => $codice_guue]);
}
}
public function resetGUUEonSoftDelete() {
if(isset($this->scheda["codice_scheda_guue"])) {
$codice_guue = $this->scheda["codice_scheda_guue"];
} else {
$this->checkIfCardNeedsXMLDocument();
if ($this->check["eform"]["required"]) {
$codice_guue = $this->check["eform"]["codice"] ?? 0;
}
}
if(!empty($codice_guue)) {
$this->pdo->go("UPDATE b_guue SET stato = :stato, notice_id = NULL WHERE codice = :codice", [":stato" => "BOZZA", ":codice" => $codice_guue]);
}
}
private $__givenInfo = [];
/**
* Ottiene la descrizione dei metadati di una scheda. Usato per GPT
*
* @param string $id_scheda
* @return string
*/
public function getInfoScheda(string $id_scheda) : string {
if(!empty($this->__givenInfo[$id_scheda])) return "";
$this->__givenInfo[$id_scheda] = True;
$metadata = $this->metadataSchede[$id_scheda] ?? null;
unset($metadata["order"]);
unset($metadata["multipla"]);
unset($metadata["settori"]);
unset($metadata["obbligatoria"]);
$retval = "";
foreach ($metadata as $id => $val) {
if(is_array($val)) {
$val = implode(",", $val);
$retval .= "$id => $val, ";
} else if(is_bool($val)) {
if($val) $retval .= "$id => sì, ";
} elseif(is_string($val)) {
$retval .= "$id => \"{$val}\", ";
} else {
$retval .= "$id => $val, ";
}
}
return "\n- Info sulla scheda {$id_scheda}: $retval" . PHP_EOL;
}
/**
* Verifica che l'utente corrente possa accedere
* alla presente istanza NPA.
*
* @return bool
*/
public function verifyUtente()
{
if(empty($_SESSION["utente"])) return false;
if(empty($this->fascicolo)) return false;
if(!$_SESSION["utente"]->permission("npa")) {
return false;
}
if (!empty($this->fascicolo["modulo_riferimento"]) && !empty($this->fascicolo["id_riferimento"])) {
if(!$_SESSION["utente"]->permission($this->fascicolo["modulo_riferimento"], $this->fascicolo["id_riferimento"])) {
return false;
}
}
return true;
}
/**
* Ritorna l'idAvviso dinamicamente
*
* @return string|null
*/
public function getANACNoticeID() : ?string {
if(empty($this->scheda["info"])) return NULL;
$info = unserialize($this->scheda["info"]);
return empty($info->idNuovoAvviso) ? $info->idAvviso : $info->idNuovoAvviso;
}
/**
* Crea una revisione (modifica/rettifica) di una scheda
*
* @param boolean $conserva_dati
* @param string $tipo
* @param string|null $message Viene popolato in caso di errore con il messaggio d'errore
* @param array|null $dati Viene popolato in caso di successo con la query per arrivare alla scheda
* @return bool
*/
public function createRevision(bool $conserva_dati, string $tipo, ?string &$message=null, ?array &$dati = null) : bool{
$message = "Operazione non valida";
if(empty($this->scheda)) return false;
if(!in_array($tipo, ["modifica", "rettifica"])) return false;
if($tipo === "modifica") {
if(!$this->isCardModifiable()) {
$message = "Questa scheda non può essere modificata.";
return false;
}
$tipo_codice = "codice_modifica";
} else {
if(!$this->isCardRectifiable()) {
$message = "Questa scheda non può essere rettificata.";
return false;
}
$tipo_codice = "codice_rettifica";
}
$nuovo_codice_guue = null;
if(!empty($this->scheda["codice_scheda_guue"])) {
$nuovo_codice_guue = eFormsManager::duplicaScheda($this->scheda["codice_scheda_guue"], true, $conserva_dati);
}
$salva = new salva();
$salva->nome_tabella = "b_npa_schede";
$salva->oggetto = $this->scheda;
unset($salva->oggetto["codice"]);
$salva->operazione = "INSERT";
unset($salva->oggetto["dati"]);
unset($salva->oggetto["codice_rettifica"]);
unset($salva->oggetto["codice_modifica"]);
if(isset($_SESSION["utente"])) {
$salva->oggetto["utente_modifica"] = $_SESSION["utente"]->codice;
}
$salva->ignore = ["timestamp_trasmissione", "timestamp_modifica", "uuid", "stato", "info", "pubblicazione"];
$salva->oggetto[$tipo_codice] = $this->scheda["codice"];
$salva->oggetto["timestamp_creazione"] = date("d/m/Y H:i:s");
if($nuovo_codice_guue !== null) {
$salva->oggetto["codice_scheda_guue"] = $nuovo_codice_guue;
} else {
$salva->ignore[] = "codice_scheda_guue";
}
if($conserva_dati) {
$salva->oggetto["dati"] = $this->scheda["dati"];
} else {
if(valid_json($this->scheda["dati"], $dati, true) && isset($dati["anacForm"]["stazioniAppaltanti"])) {
$salva->oggetto["dati"] = json_encode([
"anacForm" => [
"stazioniAppaltanti" => $dati["anacForm"]["stazioniAppaltanti"]
]
]);
}
}
$codice = $salva->save();
if(!empty($codice)) {
$dati = [
"codice" => $codice,
"scheda" => $salva->oggetto["id_scheda"],
"id_riferimento" => $this->fascicolo["id_riferimento"],
"modulo_riferimento" => $this->fascicolo["modulo_riferimento"],
];
return true;
}
$message = "Impossibile creare la {$tipo}";
return false;
}
/**
* Verifica che la scheda attuale rispetti ancora i requisiti di soglia dell'oggetto
*
* @return bool
*/
public function checkSoglia() {
if(empty($this->oggetto) || !method_exists($this->oggetto, "getSogliaGara")) return true;
$sogliaGara = $this->oggetto->getSogliaGara();
$metadata = $this->metadataSchede[$this->scheda["id_scheda"]];
if($sogliaGara == "soprasoglia" && !$metadata["soprasoglia"]) return false;
return true;
}
/**
* Verifica che non ci siano rettifiche o modifiche in corso di compilazione nel fascicolo
*
* @param array|null $filters Se indicato, indica filtri extra per cercare
* @param array $additionalCards Se specificato, indica le schede extra che vanno trattate come rettifiche
* @return boolean
*/
public function hasPendingRettificheOrModifiche(?array $filters = null, array $additionalCards = []) {
return self::hasPendingRettificheOrModificheStatic($this->fascicolo["codice"], $filters, $additionalCards );
}
/**
* Verifica che non ci siano rettifiche o modifiche in corso di compilazione nel fascicolo
*
* @param int $codice_npa
* @param array|null $filters Se indicato, indica filtri extra per cercare
* @param array $additionalCards Se specificato, indica le schede extra che vanno trattate come rettifiche
* @return boolean
*/
public static function hasPendingRettificheOrModificheStatic(int $codice_npa, ?array $filters = null, array $additionalCards = []) {
$filters = empty($filters) ? [] : $filters;
$filters["stato"] = ["<", 20]; // Solo pending
$filters["codice_npa"] = $codice_npa; // Solo di questo fascicolo
$orFilters = [
"codice_rettifica" => [">", 0],
"codice_modifica" => [">", 0],
];
if(!empty($additionalCards)) {
$orFilters["id_scheda"] = ["IN", $additionalCards];
}
$filters[uniqid("or_")] = $orFilters;
$rettifiche = self::fetchSchede(
["id_scheda"],
$filters
);
return !empty($rettifiche);
}
/**
* Verifica tra le schede trasmesse se sono in uso partecipanti o aggiudicatari
*
* @param boolean $aggiudicatari Se true ricerca schede con aggiudicatari, altrimenti schede con partecipanti
* @param boolean $unlockOnRettifica Se true e c'è una rettifica in corso non vengono bloccati i partecipanti
* @return boolean
*/
public function hasLockOnPartecipanti(bool $aggiudicatari = false, bool $unlockOnRettifica = true, ?int $codice_lotto_npa = null) {
return self::hasLockOnPartecipantiStatic($this->fascicolo["codice"], $aggiudicatari, $unlockOnRettifica, $codice_lotto_npa);
}
/**
* Ottiene l'ultima versione di un dato all'interno delle schede inviate
* @param string $dotPath
* @param bool $onlyCurrentLot
* @return array|null
*/
public function getAnacLastSent(string $dotPath, bool $onlyCurrentLot = true) : ?array {
$completedCards = $this->getLastCompletedCards($onlyCurrentLot);
foreach($completedCards as $scheda) {
$dati = json_decode($scheda["dati"], true);
$match = DotPath::get($dati, $dotPath, null);
if($match !== null) { return $match; }
}
return null;
}
/**
* Verifica tra le schede trasmesse se sono in uso partecipanti o aggiudicatari
*
* @param int $codice_npa Codice fascicolo NPA di riferimento
* @param boolean $aggiudicatari Se true ricerca schede con aggiudicatari, altrimenti schede con partecipanti
* @param boolean $unlockOnRettifica Se true e c'è una rettifica in corso non vengono bloccati i partecipanti
* @return boolean
*/
public static function hasLockOnPartecipantiStatic(int $codice_npa, bool $aggiudicatari = false, bool $unlockOnRettifica = true, $codice_lotto_npa = null) {
$filterSchede = [
"stato" => [">=", 20],
"codice_npa" => $codice_npa,
];
if(! empty($codice_lotto_npa)) { $filters["codice_lotto"] = ["OR", [$codice_lotto_npa, null, '', 0]]; }
$schedeTrasmesse = self::fetchSchede(["id_scheda"], $filterSchede);
if(empty($schedeTrasmesse)) { return false; }
// Ci servono solo gli idScheda
$schedeTrasmesse = array_unique(array_column($schedeTrasmesse, "id_scheda", null));
$metadataSchede = self::getCardsMetadata();
foreach($schedeTrasmesse as $scheda) {
if($scheda === "S1") { continue; } // Le S1 vivono nel loro mondo, isolate da tutti, beate loro.
$metadata = $metadataSchede[$scheda];
$shouldLock = $metadata["aggiudicatari"];
if(!$shouldLock && !$aggiudicatari) {
$shouldLock = $metadata["partecipanti"];
}
if($shouldLock) {
// Verifichiamo che non sia in corso una rettifica, in quel caso possiamo sbloccare
if($unlockOnRettifica) {
$hasPendingRettificheOrModifiche = self::hasPendingRettificheOrModificheStatic($codice_npa, ["id_scheda" => $scheda]);
if($hasPendingRettificheOrModifiche) return false;
}
return true;
}
}
return false;
}
private $formRequirements = [];
/**
* Verifica che la scheda corrente richieda partecipanti o aggiudicatari e setta continue su false se non dovessero essere presenti
*
* @return array
*/
public function verifyFormRequirements() : array{
if(!empty($this->formRequirements)) return $this->formRequirements;
$suggestions = $this->getSuggestions();
$idScheda = $this->scheda["id_scheda"];
$metadata = $this->metadataSchede[$idScheda];
$lottoCorrente = $this->scheda["codice_lotto"];
$retval = [
"continue" => true,
"aggiudicazioni" => $metadata["aggiudicatari"],
"partecipanti" => $metadata["partecipanti"]
];
$toCheck = [];
if($idScheda === "S4") {
$ultimiAggiudicatari = $this->getAnacLastSent("anacForm.aggiudicazioni", false);
if(!empty($ultimiAggiudicatari)) {
foreach($ultimiAggiudicatari as $aggiudicazione) {
$lotIdentifier = "LOT-" . str_pad($this->lot["numero"], 4, "0", STR_PAD_LEFT);
if(!isset($aggiudicazione["lotIdentifier"]) || $aggiudicazione["lotIdentifier"] === $lotIdentifier) {
$ultimiAggiudicatariInviati = array_unique(recursive_array_key_search_all("idPartecipante", $aggiudicazione));
}
}
}
$aggiudicatariCorrenti = array_unique(array_column($this->getPartecipanti(true, false), "uuid"));
$retval["S4"] = true;
$retval["continue"] = !empty(arrayDiff($aggiudicatariCorrenti, $ultimiAggiudicatariInviati));
} elseif($metadata["aggiudicatari"]) {
if($lottoCorrente == 0) {
foreach($this->getLots() as $lot) {
$this->setLotto($lot["codice_lotto_npa"]);
$agg = $this->getPartecipanti(true);
if(empty($agg)) {
$retval["non_aggiudicati"][] = $lot;
}
$this->unsetLotto();
}
$retval["continue"] = count($this->getLots()) > count($retval["non_aggiudicati"] ?? []);
} else {
$agg = $this->getPartecipanti(true);
if(empty($agg)) {
// Le soprasoglia possono partire senza aggiudicazioni
if($metadata['soprasoglia'] ?? false) {
if(!in_array($this->lot['stato'], [99, 95])) {
$retval["non_aggiudicati"][] = $this->lot;
}
// Evitiamo che l'utente possa compilare manualmente questa sezione
$this->locked_sections[] = "anacForm.aggiudicazioni.*.offertePresentate.*.";
$this->locked_repetitions["offertePresentate"] = true;
} else {
$retval["continue"] = false;
}
}
}
} elseif($metadata["partecipanti"]) {
$toCheck = DotPath::get($suggestions, "anacForm.elencoSoggetti", []);
}
foreach($toCheck as $lotto) {
if(empty($lotto["partecipanti"])) {
$retval["continue"] = false;
break;
}
}
$this->formRequirements = $retval;
return $retval;
}
}