707 lines
27 KiB
PHP
707 lines
27 KiB
PHP
<?php
|
|
namespace App\Filament\Pages\Fornitore;
|
|
|
|
use App\Filament\Pages\Contabilita\FattureElettronicheP7mRicevute;
|
|
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
|
use App\Models\Amministratore;
|
|
use App\Models\Fornitore;
|
|
use App\Models\Stabile;
|
|
use App\Models\TicketIntervento;
|
|
use App\Models\User;
|
|
use App\Modules\Contabilita\Models\FatturaFornitore;
|
|
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
|
|
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
|
|
use App\Services\FattureElettroniche\P7mExtractor;
|
|
use App\Support\ArchivioPaths;
|
|
use BackedEnum;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\FileUpload;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use UnitEnum;
|
|
use ZipArchive;
|
|
|
|
class Contabilita extends Page
|
|
{
|
|
use ResolvesOperatoreContext;
|
|
|
|
protected static ?string $navigationLabel = 'Contabilita';
|
|
|
|
protected static ?string $title = 'Contabilita fornitore';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-banknotes';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Fornitore';
|
|
|
|
protected static ?int $navigationSort = 3;
|
|
|
|
protected static ?string $slug = 'fornitore/contabilita';
|
|
|
|
protected string $view = 'filament.pages.fornitore.contabilita';
|
|
|
|
public ?Fornitore $fornitore = null;
|
|
|
|
public ?string $fornitoreLabel = null;
|
|
|
|
public bool $missingAdminContext = false;
|
|
|
|
public bool $contabilitaReady = false;
|
|
|
|
/** @var array{aperte:int,pagate:int,registrate:int,totale_aperto:float,totale_registrato:float} */
|
|
public array $stats = [
|
|
'aperte' => 0,
|
|
'pagate' => 0,
|
|
'registrate' => 0,
|
|
'totale_aperto' => 0.0,
|
|
'totale_registrato' => 0.0,
|
|
];
|
|
|
|
/** @var array<int,array<string,mixed>> */
|
|
public array $fattureRows = [];
|
|
|
|
/** @var array<int,array<string,mixed>> */
|
|
public array $queueRows = [];
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return $user instanceof User
|
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']);
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
[$fornitore] = $this->resolveOperatoreContext(allowAdminWithoutSupplier: true);
|
|
|
|
if (! $fornitore instanceof Fornitore) {
|
|
$this->missingAdminContext = true;
|
|
return;
|
|
}
|
|
|
|
$this->fornitore = $fornitore;
|
|
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
|
$this->loadData();
|
|
}
|
|
|
|
public function getTicketsUrl(): string
|
|
{
|
|
if ($this->fornitore instanceof Fornitore) {
|
|
return TicketOperativi::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
|
|
}
|
|
|
|
return TicketOperativi::getUrl(panel: 'admin-filament');
|
|
}
|
|
|
|
public function getCollaboratoriUrl(): string
|
|
{
|
|
if ($this->fornitore instanceof Fornitore) {
|
|
return Collaboratori::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
|
|
}
|
|
|
|
return Collaboratori::getUrl(panel: 'admin-filament');
|
|
}
|
|
|
|
public function getFattureRicevuteUrl(): string
|
|
{
|
|
return FattureElettronicheP7mRicevute::getUrl(panel: 'admin-filament');
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
if (! $this->fornitore instanceof Fornitore) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
Action::make('importa_fe_manuale')
|
|
->label('Importa FE manuale')
|
|
->icon('heroicon-o-arrow-up-tray')
|
|
->modalWidth('4xl')
|
|
->form([
|
|
FileUpload::make('fe_files')
|
|
->label('File FE (XML/P7M/ZIP)')
|
|
->directory(fn(): string => $this->getSupplierFeUploadDirectory())
|
|
->disk('local')
|
|
->acceptedFileTypes([
|
|
'.p7m',
|
|
'.xml',
|
|
'.zip',
|
|
'application/xml',
|
|
'text/xml',
|
|
'application/pkcs7-mime',
|
|
'application/x-pkcs7-mime',
|
|
'application/pkcs7-signature',
|
|
'application/x-pkcs7-signature',
|
|
'application/octet-stream',
|
|
'application/zip',
|
|
'application/x-zip-compressed',
|
|
])
|
|
->multiple()
|
|
->required(),
|
|
Select::make('importa_righe')
|
|
->label('Import righe')
|
|
->options([
|
|
'auto' => 'Auto (segue impostazione fornitore)',
|
|
'si' => 'Si (forza import righe)',
|
|
'no' => 'No (non importare righe)',
|
|
])
|
|
->default('auto')
|
|
->required(),
|
|
Toggle::make('force_update')
|
|
->label('Reimport / completa se gia presente')
|
|
->default(false),
|
|
])
|
|
->action(function (array $data): void {
|
|
if (! $this->fornitore instanceof Fornitore) {
|
|
Notification::make()->title('Fornitore non disponibile')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
if (! $this->supplierHasTaxIdentity()) {
|
|
Notification::make()
|
|
->title('Import FE bloccato')
|
|
->body('Il fornitore corrente non ha P.IVA o codice fiscale valorizzati: non posso verificare che l XML appartenga davvero a questa anagrafica.')
|
|
->warning()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
$paths = $data['fe_files'] ?? [];
|
|
$paths = is_array($paths) ? $paths : [$paths];
|
|
|
|
$parser = new FatturaElettronicaXmlParser();
|
|
$importer = new FatturaElettronicaImporter($parser);
|
|
$extractor = app(P7mExtractor::class);
|
|
|
|
$summary = [
|
|
'imported' => 0,
|
|
'duplicates' => 0,
|
|
'skipped' => 0,
|
|
'errors' => 0,
|
|
'messages' => [],
|
|
];
|
|
|
|
foreach ($paths as $path) {
|
|
$result = $this->importSupplierUploadedFeFile(
|
|
(string) $path,
|
|
$importer,
|
|
$parser,
|
|
$extractor,
|
|
[
|
|
'force_update' => (bool) ($data['force_update'] ?? false),
|
|
'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'),
|
|
]
|
|
);
|
|
|
|
$summary['imported'] += (int) ($result['imported'] ?? 0);
|
|
$summary['duplicates'] += (int) ($result['duplicates'] ?? 0);
|
|
$summary['skipped'] += (int) ($result['skipped'] ?? 0);
|
|
$summary['errors'] += (int) ($result['errors'] ?? 0);
|
|
$summary['messages'] = array_merge($summary['messages'], $result['messages'] ?? []);
|
|
}
|
|
|
|
$this->loadData();
|
|
|
|
$body = 'Importate: ' . $summary['imported'] . ' · Duplicate: ' . $summary['duplicates'] . ' · Saltate: ' . $summary['skipped'] . ' · Errori: ' . $summary['errors'];
|
|
if ($summary['messages'] !== []) {
|
|
$body .= "\n" . implode("\n", array_slice($summary['messages'], 0, 3));
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Import FE manuale completato')
|
|
->body($body)
|
|
->{$summary['errors'] > 0 ? 'warning' : 'success'}()
|
|
->send();
|
|
}),
|
|
];
|
|
}
|
|
|
|
protected function loadData(): void
|
|
{
|
|
if (! $this->fornitore instanceof Fornitore) {
|
|
return;
|
|
}
|
|
|
|
$this->queueRows = TicketIntervento::query()
|
|
->with(['ticket.stabile.amministratore'])
|
|
->where('fornitore_id', (int) $this->fornitore->id)
|
|
->whereIn('stato', ['in_verifica', 'fatturabile', 'fatturato'])
|
|
->orderByDesc('updated_at')
|
|
->limit(40)
|
|
->get()
|
|
->map(fn(TicketIntervento $intervento) => [
|
|
'ticket_id' => (int) $intervento->ticket_id,
|
|
'intervento_id' => (int) $intervento->id,
|
|
'stato' => (string) $intervento->stato,
|
|
'stabile' => (string) ($intervento->ticket?->stabile?->denominazione ?? '-'),
|
|
'amministratore' => (string) ($intervento->ticket?->stabile?->amministratore?->denominazione_studio ?? '-'),
|
|
'titolo' => (string) ($intervento->ticket?->titolo ?? '-'),
|
|
'aggiornato' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
|
|
'url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'),
|
|
])
|
|
->all();
|
|
|
|
if (! Schema::hasTable('contabilita_fatture_fornitori')) {
|
|
$this->contabilitaReady = false;
|
|
return;
|
|
}
|
|
|
|
$this->contabilitaReady = true;
|
|
|
|
$fatture = FatturaFornitore::query()
|
|
->with(['stabile.amministratore'])
|
|
->where('fornitore_id', (int) $this->fornitore->id)
|
|
->orderByDesc('data_documento')
|
|
->orderByDesc('id')
|
|
->limit(100)
|
|
->get();
|
|
|
|
$this->stats = [
|
|
'aperte' => $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->count(),
|
|
'pagate' => $fatture->where('stato', 'pagato')->count(),
|
|
'registrate' => $fatture->count(),
|
|
'totale_aperto' => (float) $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->sum('netto_da_pagare'),
|
|
'totale_registrato' => (float) $fatture->sum('totale'),
|
|
];
|
|
|
|
$this->fattureRows = $fatture
|
|
->map(fn(FatturaFornitore $fattura) => [
|
|
'id' => (int) $fattura->id,
|
|
'data_documento' => optional($fattura->data_documento)->format('d/m/Y') ?: '-',
|
|
'numero_documento' => (string) ($fattura->numero_documento ?? '-'),
|
|
'stato' => (string) ($fattura->stato ?? 'inserito'),
|
|
'stabile' => (string) ($fattura->stabile?->denominazione ?? '-'),
|
|
'amministratore' => (string) ($fattura->stabile?->amministratore?->denominazione_studio ?? '-'),
|
|
'descrizione' => (string) ($fattura->descrizione ?? ''),
|
|
'totale' => (float) ($fattura->totale ?? 0),
|
|
'netto' => (float) ($fattura->netto_da_pagare ?? 0),
|
|
])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $options
|
|
* @return array{imported:int,duplicates:int,skipped:int,errors:int,messages:array<int,string>}
|
|
*/
|
|
private function importSupplierUploadedFeFile(
|
|
string $path,
|
|
FatturaElettronicaImporter $importer,
|
|
FatturaElettronicaXmlParser $parser,
|
|
P7mExtractor $extractor,
|
|
array $options,
|
|
): array {
|
|
$summary = [
|
|
'imported' => 0,
|
|
'duplicates' => 0,
|
|
'skipped' => 0,
|
|
'errors' => 0,
|
|
'messages' => [],
|
|
];
|
|
|
|
$path = trim($path);
|
|
if ($path === '') {
|
|
return $summary;
|
|
}
|
|
|
|
try {
|
|
$filename = basename($path);
|
|
$lower = Str::lower($filename);
|
|
|
|
if (Str::endsWith($lower, '.zip')) {
|
|
$absZip = Storage::disk('local')->path($path);
|
|
if (! is_file($absZip)) {
|
|
throw new \RuntimeException('ZIP non trovato');
|
|
}
|
|
|
|
$tmpDir = 'tmp/fornitori-fe/' . now()->format('Y/m') . '/' . (string) Str::uuid();
|
|
Storage::disk('local')->makeDirectory($tmpDir);
|
|
$absTmp = Storage::disk('local')->path($tmpDir);
|
|
|
|
$zip = new ZipArchive();
|
|
$open = $zip->open($absZip);
|
|
if ($open !== true) {
|
|
throw new \RuntimeException('ZIP non valido');
|
|
}
|
|
|
|
$zip->extractTo($absTmp);
|
|
$zip->close();
|
|
|
|
$it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($absTmp));
|
|
foreach ($it as $fileInfo) {
|
|
if (! $fileInfo instanceof \SplFileInfo || ! $fileInfo->isFile()) {
|
|
continue;
|
|
}
|
|
|
|
$innerName = (string) $fileInfo->getFilename();
|
|
$innerLower = Str::lower($innerName);
|
|
if (! (Str::endsWith($innerLower, '.xml') || Str::endsWith($innerLower, '.p7m'))) {
|
|
continue;
|
|
}
|
|
|
|
$bytes = file_get_contents($fileInfo->getPathname());
|
|
if (! is_string($bytes) || $bytes === '') {
|
|
$summary['errors']++;
|
|
$summary['messages'][] = $filename . '::' . $innerName . ' - file vuoto';
|
|
continue;
|
|
}
|
|
|
|
$isP7m = Str::endsWith($innerLower, '.p7m');
|
|
$xml = $isP7m ? $extractor->extractXmlFromP7m($fileInfo->getPathname()) : $bytes;
|
|
$this->mergeSupplierImportResult(
|
|
$summary,
|
|
$this->importSupplierParsedXml(
|
|
is_string($xml) ? $xml : '',
|
|
$isP7m ? (pathinfo($innerName, PATHINFO_FILENAME) . '.xml') : $innerName,
|
|
$importer,
|
|
$parser,
|
|
$options,
|
|
$isP7m ? $innerName : null,
|
|
$isP7m ? $bytes : null
|
|
),
|
|
$filename . '::' . $innerName
|
|
);
|
|
}
|
|
|
|
Storage::disk('local')->deleteDirectory($tmpDir);
|
|
return $summary;
|
|
}
|
|
|
|
$bytes = Storage::disk('local')->get($path);
|
|
if (! is_string($bytes) || $bytes === '') {
|
|
throw new \RuntimeException('File vuoto');
|
|
}
|
|
|
|
$isP7m = Str::endsWith($lower, '.p7m');
|
|
$xml = $isP7m ? $extractor->extractXmlFromP7m(Storage::disk('local')->path($path)) : $bytes;
|
|
|
|
$this->mergeSupplierImportResult(
|
|
$summary,
|
|
$this->importSupplierParsedXml(
|
|
is_string($xml) ? $xml : '',
|
|
$isP7m ? (pathinfo($filename, PATHINFO_FILENAME) . '.xml') : $filename,
|
|
$importer,
|
|
$parser,
|
|
$options,
|
|
$isP7m ? $filename : null,
|
|
$isP7m ? $bytes : null
|
|
),
|
|
$filename
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$summary['errors']++;
|
|
$summary['messages'][] = basename($path) . ' - ' . $e->getMessage();
|
|
} finally {
|
|
try {
|
|
Storage::disk('local')->delete($path);
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $options
|
|
* @return array{status:string,message:string}
|
|
*/
|
|
private function importSupplierParsedXml(
|
|
string $xml,
|
|
string $xmlFilename,
|
|
FatturaElettronicaImporter $importer,
|
|
FatturaElettronicaXmlParser $parser,
|
|
array $options,
|
|
?string $p7mFilename = null,
|
|
?string $p7mBytes = null,
|
|
): array {
|
|
$xml = trim($xml);
|
|
if ($xml === '') {
|
|
return ['status' => 'error', 'message' => 'XML non valido o assente'];
|
|
}
|
|
|
|
if ($this->isSdINotificationDocument($xmlFilename, $xml)) {
|
|
return [
|
|
'status' => 'skipped',
|
|
'message' => 'Ricevuta/notifica SdI: non viene importata nella contabilita fornitore',
|
|
];
|
|
}
|
|
|
|
$parsed = $parser->parse($xml);
|
|
if (! $this->supplierMatchesParsedIdentity($parsed)) {
|
|
return ['status' => 'error', 'message' => 'Il file non appartiene al fornitore corrente'];
|
|
}
|
|
|
|
$stabileId = $this->resolveSupplierStabileIdFromParsed($parsed);
|
|
if ($stabileId <= 0) {
|
|
return ['status' => 'error', 'message' => 'Impossibile determinare uno stabile univoco per questa FE'];
|
|
}
|
|
|
|
$unique = (string) Str::uuid();
|
|
$ym = now()->format('Y/m');
|
|
$inboxBase = $this->supplierInboxBaseForStabile($stabileId, $ym);
|
|
$permanentP7mPath = null;
|
|
|
|
if ($p7mFilename !== null && is_string($p7mBytes) && $p7mBytes !== '') {
|
|
$permanentP7mPath = $inboxBase . '/' . $unique . '-' . $p7mFilename;
|
|
Storage::disk('local')->put($permanentP7mPath, $p7mBytes);
|
|
}
|
|
|
|
$permanentXmlPath = $inboxBase . '/' . $unique . '-' . $xmlFilename;
|
|
Storage::disk('local')->put($permanentXmlPath, $xml);
|
|
|
|
$result = $importer->importXml($xml, 0, $xmlFilename, [
|
|
'xml_path' => $permanentXmlPath,
|
|
'p7m_path' => $permanentP7mPath,
|
|
'nome_file_p7m' => $p7mFilename,
|
|
'force_update' => (bool) ($options['force_update'] ?? false),
|
|
'auto_repair_duplicate' => true,
|
|
'importa_righe' => (string) ($options['importa_righe'] ?? 'auto'),
|
|
'create_fornitore_if_missing' => false,
|
|
'allow_fallback_stabile' => false,
|
|
'force_stabile_id' => $stabileId,
|
|
'force_fornitore_id' => (int) $this->fornitore->id,
|
|
'skip_fornitore_sync' => true,
|
|
'user_id' => (int) Auth::id(),
|
|
]);
|
|
|
|
$status = (string) ($result['status'] ?? 'error');
|
|
$message = (string) ($result['message'] ?? 'Import FE non riuscito');
|
|
|
|
if ($status === 'duplicate' || $status === 'error') {
|
|
try {
|
|
Storage::disk('local')->delete($permanentXmlPath);
|
|
if ($permanentP7mPath !== null) {
|
|
Storage::disk('local')->delete($permanentP7mPath);
|
|
}
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
return [
|
|
'status' => $status,
|
|
'message' => $message,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $summary
|
|
* @param array<string, string> $result
|
|
*/
|
|
private function mergeSupplierImportResult(array &$summary, array $result, string $label): void
|
|
{
|
|
$status = (string) ($result['status'] ?? 'error');
|
|
$message = trim((string) ($result['message'] ?? ''));
|
|
|
|
if ($status === 'imported') {
|
|
$summary['imported']++;
|
|
return;
|
|
}
|
|
|
|
if ($status === 'duplicate') {
|
|
$summary['duplicates']++;
|
|
$summary['messages'][] = $label . ' - ' . ($message !== '' ? $message : 'duplicato');
|
|
return;
|
|
}
|
|
|
|
if ($status === 'skipped') {
|
|
$summary['skipped']++;
|
|
$summary['messages'][] = $label . ' - ' . ($message !== '' ? $message : 'saltato');
|
|
return;
|
|
}
|
|
|
|
$summary['errors']++;
|
|
$summary['messages'][] = $label . ' - ' . ($message !== '' ? $message : 'errore');
|
|
}
|
|
|
|
private function isSdINotificationDocument(string $xmlFilename, string $xml): bool
|
|
{
|
|
$filename = strtoupper(trim($xmlFilename));
|
|
if ($filename !== '' && preg_match('/_(RC|NS|MC|NE|EC|AT|DT|MT|SE|SM)_\d+\.XML$/', $filename) === 1) {
|
|
return true;
|
|
}
|
|
|
|
foreach ([
|
|
'<RicevutaConsegna',
|
|
'<NotificaScarto',
|
|
'<NotificaMancataConsegna',
|
|
'<NotificaEsito',
|
|
'<MetadatiInvioFile',
|
|
'<AttestazioneTrasmissioneFattura',
|
|
'<EsitoCommittente',
|
|
] as $needle) {
|
|
if (str_contains($xml, $needle)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function getSupplierFeUploadDirectory(): string
|
|
{
|
|
if (! $this->fornitore instanceof Fornitore) {
|
|
return 'tmp/fornitori-fe';
|
|
}
|
|
|
|
$adminCode = (string) ($this->fornitore->amministratore?->codice_amministratore ?: $this->fornitore->amministratore?->codice_univoco ?: 'ID-' . (int) $this->fornitore->amministratore_id);
|
|
$fornitoreCode = (string) ($this->fornitore->codice_fornitore ?: $this->fornitore->codice_univoco ?: 'ID-' . (int) $this->fornitore->id);
|
|
|
|
return 'amministratori/' . $adminCode . '/fornitori/' . $fornitoreCode . '/temp/upload/fatture-fe';
|
|
}
|
|
|
|
private function supplierHasTaxIdentity(): bool
|
|
{
|
|
if (! $this->fornitore instanceof Fornitore) {
|
|
return false;
|
|
}
|
|
|
|
return $this->normalizeTaxId((string) ($this->fornitore->partita_iva ?? '')) !== ''
|
|
|| $this->normalizeTaxId((string) ($this->fornitore->codice_fiscale ?? '')) !== ''
|
|
|| $this->normalizeTaxId((string) ($this->fornitore->rubrica?->partita_iva ?? '')) !== ''
|
|
|| $this->normalizeTaxId((string) ($this->fornitore->rubrica?->codice_fiscale ?? '')) !== '';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $parsed
|
|
*/
|
|
private function supplierMatchesParsedIdentity(array $parsed): bool
|
|
{
|
|
if (! $this->fornitore instanceof Fornitore) {
|
|
return false;
|
|
}
|
|
|
|
$invoiceIds = array_values(array_unique(array_filter([
|
|
$this->normalizeTaxId((string) ($parsed['fornitore_piva'] ?? '')),
|
|
$this->normalizeTaxId((string) ($parsed['fornitore_cf'] ?? '')),
|
|
$this->normalizeTaxId((string) ($parsed['destinatario_piva'] ?? '')),
|
|
$this->normalizeTaxId((string) ($parsed['destinatario_cf'] ?? '')),
|
|
])));
|
|
|
|
if ($invoiceIds === []) {
|
|
return false;
|
|
}
|
|
|
|
$supplierIds = array_values(array_filter([
|
|
$this->normalizeTaxId((string) ($this->fornitore->partita_iva ?? '')),
|
|
$this->normalizeTaxId((string) ($this->fornitore->codice_fiscale ?? '')),
|
|
$this->normalizeTaxId((string) ($this->fornitore->rubrica?->partita_iva ?? '')),
|
|
$this->normalizeTaxId((string) ($this->fornitore->rubrica?->codice_fiscale ?? '')),
|
|
]));
|
|
|
|
foreach ($invoiceIds as $invoiceId) {
|
|
if (in_array($invoiceId, $supplierIds, true)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $parsed
|
|
*/
|
|
private function resolveSupplierStabileIdFromParsed(array $parsed): int
|
|
{
|
|
$adminId = (int) ($this->fornitore?->amministratore_id ?? 0);
|
|
if ($adminId <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
$cf = $this->normalizeTaxId((string) ($parsed['destinatario_cf'] ?? ''));
|
|
if ($cf !== '') {
|
|
$match = Stabile::query()
|
|
->where('amministratore_id', $adminId)
|
|
->whereNotNull('codice_fiscale')
|
|
->whereRaw("REPLACE(UPPER(codice_fiscale), ' ', '') = ?", [$cf])
|
|
->where('attivo', true)
|
|
->get();
|
|
if ($match->count() === 1) {
|
|
return (int) $match->first()->id;
|
|
}
|
|
}
|
|
|
|
$codice = $this->normalizeTaxId((string) ($parsed['codice_destinatario'] ?? ''));
|
|
if ($codice !== '') {
|
|
$match = Stabile::query()
|
|
->where('amministratore_id', $adminId)
|
|
->whereNotNull('codice_destinatario_sdi')
|
|
->whereRaw("REPLACE(UPPER(codice_destinatario_sdi), ' ', '') = ?", [$codice])
|
|
->where('attivo', true)
|
|
->get();
|
|
if ($match->count() === 1) {
|
|
return (int) $match->first()->id;
|
|
}
|
|
}
|
|
|
|
$pec = trim(strtolower((string) ($parsed['pec_destinatario'] ?? '')));
|
|
if ($pec !== '') {
|
|
$match = Stabile::query()
|
|
->where('amministratore_id', $adminId)
|
|
->where(function ($query) use ($pec): void {
|
|
$query->whereRaw('LOWER(pec_condominio) = ?', [$pec]);
|
|
|
|
if (Schema::hasColumn('stabili', 'pec_amministratore')) {
|
|
$query->orWhereRaw('LOWER(pec_amministratore) = ?', [$pec]);
|
|
}
|
|
})
|
|
->where('attivo', true)
|
|
->get();
|
|
if ($match->count() === 1) {
|
|
return (int) $match->first()->id;
|
|
}
|
|
}
|
|
|
|
$destPiva = $this->normalizeTaxId((string) ($parsed['destinatario_piva'] ?? ''));
|
|
if ($destPiva !== '') {
|
|
$match = Stabile::query()
|
|
->select('stabili.id')
|
|
->join('amministratori', 'amministratori.id', '=', 'stabili.amministratore_id')
|
|
->where('stabili.amministratore_id', $adminId)
|
|
->where('stabili.attivo', true)
|
|
->whereRaw("REPLACE(UPPER(amministratori.partita_iva), ' ', '') = ?", [$destPiva])
|
|
->get();
|
|
if ($match->count() === 1) {
|
|
return (int) $match->first()->id;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private function supplierInboxBaseForStabile(int $stabileId, string $ym): string
|
|
{
|
|
$stabile = Stabile::query()
|
|
->with(['amministratore:id,codice_amministratore,codice_univoco'])
|
|
->select(['id', 'amministratore_id', 'codice_stabile', 'codice_univoco'])
|
|
->find($stabileId);
|
|
|
|
if ($stabile instanceof Stabile) {
|
|
$base = ArchivioPaths::stabileBase($stabile, $stabile->amministratore instanceof Amministratore ? $stabile->amministratore : null);
|
|
if (is_string($base) && $base !== '') {
|
|
return $base . '/fatture-ricevute/inbox/' . $ym;
|
|
}
|
|
}
|
|
|
|
$adminId = (int) ($stabile?->amministratore_id ?? 0);
|
|
return 'amministratori/ID-' . $adminId . '/stabili/ID-' . $stabileId . '/fatture-ricevute/inbox/' . $ym;
|
|
}
|
|
|
|
private function normalizeTaxId(string $value): string
|
|
{
|
|
$normalized = strtoupper(str_replace(' ', '', trim($value)));
|
|
if (str_starts_with($normalized, 'IT') && strlen($normalized) > 11) {
|
|
$normalized = substr($normalized, 2);
|
|
}
|
|
|
|
return preg_replace('/[^A-Z0-9]/', '', $normalized) ?? '';
|
|
}
|
|
}
|