Improve bank movements and e-invoice import workflow
This commit is contained in:
parent
1c09aae635
commit
6ac70ce38e
|
|
@ -4,6 +4,7 @@ # Changelog
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- Reworked the banking operations hub so the topbar stays full-width, the sidebar stays fixed below it, the movements tab remains focused on imported/manual bank movements, and archive or gestione structure moved into a dedicated tab. The same slice also added first-pass QIF cleanup metadata for CBILL, SIA, commissions and provider hints, plus stronger bank-to-supplier matching and ACQ assignment for water invoices extracted from FE/PDF data.
|
||||
- Fixed document archive visibility queries to work safely even when single-user ACL columns are not yet present in older databases, and added the missing `documenti.allowed_user_ids` schema field for per-document access control.
|
||||
- Restored a cleaner two-column layout in Post-it Gestione boards and hid visual noise from calls that terminate on internal response group `601` while keeping the underlying records in the system.
|
||||
- Added actionable banking reconciliation for tenant rents and supplier payments directly from the MPS movement queue, with automatic operational matching metadata and supplier RA register synchronization when a withholding-bearing supplier invoice is paid.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -994,6 +994,54 @@ private function parseDescrizioneEstesa(string $text): array
|
|||
$res['periodo_rate'] = trim($m[1]);
|
||||
}
|
||||
|
||||
if (preg_match('/\b(Altri\s+pagamenti|Bonifici?|Addebiti?|Prelievi?|Commissioni?)\b/i', $text, $m)) {
|
||||
$res['payment_group'] = strtoupper(trim((string) $m[1]));
|
||||
}
|
||||
|
||||
if (preg_match('/\bFav\.?\s+([^\n]+?)(?=\s+(?:Cbill|CBILL|SIA\b|Da\s+Digital\s+Banking|Di\s+Cui\s+Commissioni|T[\-\+0-9\.,]|$))/i', $text, $m)) {
|
||||
$provider = trim((string) $m[1]);
|
||||
if ($provider !== '') {
|
||||
$res['payment_provider'] = $provider;
|
||||
if (empty($res['beneficiario'])) {
|
||||
$res['beneficiario'] = $provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/\bCbill\b\s*(?:N\.)?\s*\.?\s*([0-9]{9,})/i', $text, $m)) {
|
||||
$res['cbill'] = trim((string) $m[1]);
|
||||
}
|
||||
|
||||
if (preg_match('/\bSIA\b[:\s]*([A-Z0-9]{3,10})/i', $text, $m)) {
|
||||
$res['sia'] = strtoupper(trim((string) $m[1]));
|
||||
} elseif (preg_match('/\b([A-Z][A-Z0-9]{3,6})\b(?=\s+Cbill\b)/i', $text, $m)) {
|
||||
$candidate = strtoupper(trim((string) $m[1]));
|
||||
if (! in_array($candidate, ['CBILL', 'FAV'], true)) {
|
||||
$res['sia'] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/Di\s+Cui\s+Commissioni:\s*([0-9\.,]+)\s*Euro/i', $text, $m)) {
|
||||
$commissione = $this->parseItalianDecimal($m[1] ?? null);
|
||||
if ($commissione !== null) {
|
||||
$res['commissioni_euro'] = $commissione;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/\bDa\s+Digital\s+Banking\b/i', $text)) {
|
||||
$res['payment_channel'] = 'digital_banking';
|
||||
}
|
||||
|
||||
$providerUpper = strtoupper(trim((string) ($res['payment_provider'] ?? $res['beneficiario'] ?? '')));
|
||||
if ($providerUpper !== '') {
|
||||
if (str_contains($providerUpper, 'ACEA ATO2') || str_contains($providerUpper, 'ACEA ACQUA')) {
|
||||
$res['utility_type'] = 'acqua';
|
||||
$res['suggested_voce_spesa_code'] = 'ACQ';
|
||||
} elseif (str_contains($providerUpper, 'A2A')) {
|
||||
$res['utility_type'] = 'utenza';
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($res) && str_contains($text, 'Bonifico a Vostro favore')) {
|
||||
$res['descrizione_raw'] = $text;
|
||||
}
|
||||
|
|
@ -1040,4 +1088,17 @@ private function matchFornitoreId(string $name): ?int
|
|||
|
||||
return $rows->count() === 1 ? (int) $rows->first()->id : null;
|
||||
}
|
||||
|
||||
private function parseItalianDecimal(mixed $value): ?float
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = str_replace(['.', ' '], ['', ''], $value);
|
||||
$value = str_replace(',', '.', $value);
|
||||
|
||||
return is_numeric($value) ? (float) $value : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -256,8 +256,8 @@ public function downloadAndImport(
|
|||
Storage::disk('local')->makeDirectory($errorsDir);
|
||||
$errorsLogPath = $errorsDir . '/' . $uuid . '-download-errors.jsonl';
|
||||
|
||||
// Salva i file scaricati in una cartella stabile XML dentro lo stabile.
|
||||
$xmlBase = $stabileBase . '/XML';
|
||||
// Salva gli XML in una struttura leggibile e annuale: fatture_xml/ANNO.
|
||||
$xmlBase = ArchivioPaths::fattureXmlYearPath($stabile, $year, $amministratore) ?: ($stabileBase . '/fatture_xml/' . $year);
|
||||
Storage::disk('local')->makeDirectory($xmlBase);
|
||||
|
||||
foreach ($files as $filePath) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
use App\Modules\Contabilita\Models\FatturaFornitore;
|
||||
use App\Modules\Contabilita\Models\RegolaPrimaNota;
|
||||
use App\Services\Catalog\FornitoreProductCatalogService;
|
||||
use App\Services\Consumi\AcquaPdfTextParser;
|
||||
use App\Support\ArchivioPaths;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
|
|
@ -531,9 +532,6 @@ private function updateContabilitaFornituraDaPdf(FatturaElettronica $fattura): v
|
|||
}
|
||||
|
||||
$existing = is_array($fatturaContabile->dati_fornitura) ? $fatturaContabile->dati_fornitura : [];
|
||||
if (count($existing) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pdfPath = is_string($fattura->allegato_pdf_path ?? null) ? trim((string) $fattura->allegato_pdf_path) : '';
|
||||
if ($pdfPath === '') {
|
||||
|
|
@ -550,26 +548,56 @@ private function updateContabilitaFornituraDaPdf(FatturaElettronica $fattura): v
|
|||
}
|
||||
|
||||
$fullPath = $disk->path($resolvedPath);
|
||||
$data = $this->extractGasFornituraDataFromPdf($fullPath);
|
||||
if (! is_array($data) || $data === []) {
|
||||
$text = $this->extractPdfText($fullPath);
|
||||
if (! is_string($text) || trim($text) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$fatturaContabile->dati_fornitura = $data;
|
||||
$fatturaContabile->save();
|
||||
$gasData = $this->extractGasFornituraDataFromPdfText($text);
|
||||
$waterData = $this->extractAcquaFornituraDataFromPdfText($text);
|
||||
$paymentData = $this->extractPaymentReferenceDataFromPdfText($text, $fattura);
|
||||
|
||||
$data = is_array($waterData) && $waterData !== [] ? $waterData : $gasData;
|
||||
if (! is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
if ($paymentData !== []) {
|
||||
$data['pagamento'] = array_merge(is_array($data['pagamento'] ?? null) ? $data['pagamento'] : [], $paymentData);
|
||||
}
|
||||
|
||||
$merged = $this->mergeFornituraPayload($existing, $data);
|
||||
if ($merged === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (($merged['type'] ?? $merged['tipo'] ?? null) === 'acqua') {
|
||||
$this->enrichWaterInvoiceFromParsedData($fattura, $merged);
|
||||
$this->assignWaterExpenseCode($fatturaContabile, $merged);
|
||||
}
|
||||
|
||||
if ($merged !== $existing) {
|
||||
$fatturaContabile->dati_fornitura = $merged;
|
||||
$fatturaContabile->save();
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
private function extractGasFornituraDataFromPdf(string $fullPath): ?array
|
||||
{
|
||||
$text = $this->extractPdfText($fullPath);
|
||||
return is_string($text) ? $this->extractGasFornituraDataFromPdfText($text) : null;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
private function extractGasFornituraDataFromPdfText(string $text): ?array
|
||||
{
|
||||
try {
|
||||
$parser = new Parser();
|
||||
$text = $parser->parseFile($fullPath)->getText();
|
||||
$text = trim($text);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! is_string($text) || trim($text) === '') {
|
||||
if ($text === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -636,6 +664,183 @@ private function extractGasFornituraDataFromPdf(string $fullPath): ?array
|
|||
return $payload !== [] ? $payload : null;
|
||||
}
|
||||
|
||||
private function extractPdfText(string $fullPath): ?string
|
||||
{
|
||||
try {
|
||||
$parser = new Parser();
|
||||
$text = $parser->parseFile($fullPath)->getText();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = is_string($text) ? trim($text) : '';
|
||||
return $text !== '' ? $text : null;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
private function extractAcquaFornituraDataFromPdfText(string $text): ?array
|
||||
{
|
||||
try {
|
||||
$parsed = app(AcquaPdfTextParser::class)->parse($text);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [];
|
||||
$contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [];
|
||||
$consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : [];
|
||||
$generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : [];
|
||||
$quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : [];
|
||||
|
||||
$hasIdentifiers = count(array_filter([
|
||||
$codici['utenza'] ?? null,
|
||||
$codici['cliente'] ?? null,
|
||||
$codici['contratto'] ?? null,
|
||||
$contatore['matricola'] ?? null,
|
||||
$generale['numero_fattura_pdf'] ?? null,
|
||||
], static fn($value): bool => is_string($value) ? trim($value) !== '' : $value !== null)) > 0;
|
||||
|
||||
if (! $hasIdentifiers && $consumi === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'type' => 'acqua',
|
||||
'voce_spesa_code' => 'ACQ',
|
||||
'codici' => [
|
||||
'utenza' => $codici['utenza'] ?? null,
|
||||
'cliente' => $codici['cliente'] ?? null,
|
||||
'contratto' => $codici['contratto'] ?? null,
|
||||
],
|
||||
'contatore' => [
|
||||
'matricola' => $contatore['matricola'] ?? null,
|
||||
],
|
||||
'generale' => $generale,
|
||||
'quadro_dettaglio' => $quadro,
|
||||
'consumi' => array_values($consumi),
|
||||
];
|
||||
|
||||
return array_filter($payload, fn($value) => $value !== null && $value !== '');
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function extractPaymentReferenceDataFromPdfText(string $text, FatturaElettronica $fattura): array
|
||||
{
|
||||
$provider = trim((string) ($fattura->fornitore_denominazione ?? ''));
|
||||
$cbill = null;
|
||||
$sia = null;
|
||||
|
||||
if (preg_match('/\bCBILL\b(?:\s*N\.)?\s*\.?\s*([0-9]{9,})/i', $text, $m)) {
|
||||
$cbill = trim((string) ($m[1] ?? ''));
|
||||
}
|
||||
if (preg_match('/\bSIA\b[:\s]*([A-Z0-9]{3,10})/i', $text, $m)) {
|
||||
$sia = strtoupper(trim((string) ($m[1] ?? '')));
|
||||
} elseif (preg_match('/\b([A-Z][A-Z0-9]{3,6})\b(?=\s+CBILL\b)/i', $text, $m)) {
|
||||
$candidate = strtoupper(trim((string) ($m[1] ?? '')));
|
||||
if (! in_array($candidate, ['CBILL', 'FAV'], true)) {
|
||||
$sia = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'provider' => $provider !== '' ? $provider : null,
|
||||
'cbill' => $cbill,
|
||||
'sia' => $sia,
|
||||
];
|
||||
|
||||
return array_filter($payload, fn($value) => $value !== null && $value !== '');
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $existing @param array<string, mixed> $incoming @return array<string, mixed> */
|
||||
private function mergeFornituraPayload(array $existing, array $incoming): array
|
||||
{
|
||||
foreach ($incoming as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$existing[$key] = $this->mergeFornituraPayload(is_array($existing[$key] ?? null) ? $existing[$key] : [], $value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value !== null && $value !== '') {
|
||||
$existing[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $existing;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $payload */
|
||||
private function enrichWaterInvoiceFromParsedData(FatturaElettronica $fattura, array $payload): void
|
||||
{
|
||||
$consumi = is_array($payload['consumi'] ?? null) ? $payload['consumi'] : [];
|
||||
if ($consumi === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sum = 0.0;
|
||||
$hasNumeric = false;
|
||||
foreach ($consumi as $consumo) {
|
||||
if (! is_array($consumo) || ! is_numeric($consumo['valore'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
$sum += (float) $consumo['valore'];
|
||||
$hasNumeric = true;
|
||||
}
|
||||
|
||||
$reference = null;
|
||||
$first = is_array($consumi[0] ?? null) ? $consumi[0] : [];
|
||||
if (is_string($first['dal'] ?? null) && is_string($first['al'] ?? null) && $first['dal'] !== '' && $first['al'] !== '') {
|
||||
$reference = $first['dal'] . ' - ' . $first['al'];
|
||||
}
|
||||
|
||||
$dirty = false;
|
||||
if ($hasNumeric && (! is_numeric($fattura->consumo_valore) || (float) $fattura->consumo_valore <= 0)) {
|
||||
$fattura->consumo_valore = $sum;
|
||||
$dirty = true;
|
||||
}
|
||||
if ((! is_string($fattura->consumo_unita ?? null) || trim((string) $fattura->consumo_unita) === '') && $hasNumeric) {
|
||||
$fattura->consumo_unita = 'mc';
|
||||
$dirty = true;
|
||||
}
|
||||
if ($reference !== null && (! is_string($fattura->consumo_riferimento ?? null) || trim((string) $fattura->consumo_riferimento) === '')) {
|
||||
$fattura->consumo_riferimento = $reference;
|
||||
$dirty = true;
|
||||
}
|
||||
if (! is_string($fattura->consumo_raw ?? null) || trim((string) $fattura->consumo_raw) === '') {
|
||||
$fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$dirty = true;
|
||||
}
|
||||
|
||||
if ($dirty) {
|
||||
$fattura->save();
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $payload */
|
||||
private function assignWaterExpenseCode(FatturaFornitore $fatturaContabile, array $payload): void
|
||||
{
|
||||
if (($payload['voce_spesa_code'] ?? null) !== 'ACQ') {
|
||||
return;
|
||||
}
|
||||
if (! Schema::hasTable('voci_spesa') || ! Schema::hasTable('contabilita_fatture_fornitori_righe') || ! Schema::hasColumn('contabilita_fatture_fornitori_righe', 'voce_spesa_id')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$voceId = DB::table('voci_spesa')
|
||||
->where('stabile_id', (int) $fatturaContabile->stabile_id)
|
||||
->where('codice', 'ACQ')
|
||||
->orderBy('id')
|
||||
->value('id');
|
||||
|
||||
if (! $voceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fatturaContabile->righe()
|
||||
->whereNull('voce_spesa_id')
|
||||
->where('descrizione', '!=', 'Riepilogo FE / arrotondamenti')
|
||||
->update(['voce_spesa_id' => (int) $voceId]);
|
||||
}
|
||||
|
||||
/** @param array<int, string> $lines */
|
||||
private function findValueAfterLabel(array $lines, string $labelPattern, int $maxLookahead = 6, ?string $valuePattern = null): ?string
|
||||
{
|
||||
|
|
@ -1140,21 +1345,28 @@ private function postProcess(FatturaElettronica $fattura, string $xml, array $ex
|
|||
|
||||
private function archiveBaseForStabile(int $stabileId, string $ym): string
|
||||
{
|
||||
$year = null;
|
||||
try {
|
||||
$year = (int) Carbon::parse($ym . '/01')->format('Y');
|
||||
} catch (\Throwable) {
|
||||
$year = (int) now()->format('Y');
|
||||
}
|
||||
|
||||
$stabile = Stabile::query()
|
||||
->with(['amministratore:id,codice_amministratore,codice_univoco'])
|
||||
->select(['id', 'amministratore_id', 'codice_stabile', 'codice_univoco'])
|
||||
->find($stabileId);
|
||||
|
||||
if ($stabile) {
|
||||
$base = ArchivioPaths::stabileBase($stabile, $stabile->amministratore);
|
||||
$base = ArchivioPaths::fattureXmlYearPath($stabile, $year, $stabile->amministratore);
|
||||
if (is_string($base) && $base !== '') {
|
||||
return $base . '/fatture-ricevute/' . $ym;
|
||||
return $base;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback ultra-conservativo
|
||||
$adminId = (int) ($stabile?->amministratore_id ?: 0);
|
||||
return 'amministratori/ID-' . $adminId . '/stabili/ID-' . (int) $stabileId . '/fatture-ricevute/' . $ym;
|
||||
return 'amministratori/ID-' . $adminId . '/stabili/ID-' . (int) $stabileId . '/fatture_xml/' . $year;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Models\Documento;
|
||||
use App\Models\FatturaElettronica;
|
||||
use App\Models\GestioneContabile;
|
||||
use App\Models\Stabile;
|
||||
use App\Support\ArchivioPaths;
|
||||
use Dompdf\Dompdf;
|
||||
|
|
@ -40,7 +41,12 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
|||
$stabileFolder = $stabileCode ?: ('ID-' . (int) $fattura->stabile_id);
|
||||
|
||||
$stabileBase = ArchivioPaths::stabileBase($stabile, $stabile->amministratore);
|
||||
$base = (is_string($stabileBase) && $stabileBase !== '')
|
||||
$gestione = $this->resolveGestioneForYear($stabile, $anno);
|
||||
$pdfBase = ArchivioPaths::fatturePdfGestionePath($stabile, $gestione, $anno, 'ordinaria', $stabile->amministratore)
|
||||
?: $this->basePath($adminFolder, $stabileFolder, $anno);
|
||||
$xmlBase = ArchivioPaths::fattureXmlYearPath($stabile, $anno, $stabile->amministratore)
|
||||
?: $this->basePath($adminFolder, $stabileFolder, $anno);
|
||||
$legacyBase = (is_string($stabileBase) && $stabileBase !== '')
|
||||
? ($stabileBase . '/documenti/' . $anno . '/fatture-ricevute')
|
||||
: $this->basePath($adminFolder, $stabileFolder, $anno);
|
||||
|
||||
|
|
@ -50,7 +56,9 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
|||
}
|
||||
|
||||
// Recupero soft dal base path (file già spostati manualmente).
|
||||
$this->recoverMissingPathsFromBase($fattura, $base);
|
||||
$this->recoverMissingPathsFromBase($fattura, $legacyBase);
|
||||
$this->recoverMissingPathsFromBase($fattura, $pdfBase);
|
||||
$this->recoverMissingPathsFromBase($fattura, $xmlBase);
|
||||
|
||||
// Ripara PDF se salvato in vecchio schema con ID numerici.
|
||||
$newBase = (is_string($stabileBase) && $stabileBase !== '')
|
||||
|
|
@ -170,8 +178,8 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
|||
|| (is_string($fattura->xml_path) && $fattura->xml_path !== '' && Storage::disk('local')->exists($fattura->xml_path));
|
||||
if (! $hasPdfNow && $hasXmlNow) {
|
||||
$targetName = $protocollo . ' ' . $dateForName . ' ' . $sanFornitore . ' ' . $sanNumero . '.pdf';
|
||||
$targetPath = $base . '/' . $targetName;
|
||||
$this->ensureDir($base);
|
||||
$targetPath = $pdfBase . '/' . $targetName;
|
||||
$this->ensureDir($pdfBase);
|
||||
|
||||
// Evita overwrite: se esiste già, lo riusiamo.
|
||||
if (! Storage::disk('local')->exists($targetPath)) {
|
||||
|
|
@ -188,8 +196,8 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
|||
|
||||
if (is_string($fattura->allegato_pdf_path) && $fattura->allegato_pdf_path !== '' && Storage::disk('local')->exists($fattura->allegato_pdf_path)) {
|
||||
$targetName = $protocollo . ' ' . $dateForName . ' ' . $sanFornitore . ' ' . $sanNumero . '.pdf';
|
||||
$targetPath = $base . '/' . $targetName;
|
||||
$this->ensureDir($base);
|
||||
$targetPath = $pdfBase . '/' . $targetName;
|
||||
$this->ensureDir($pdfBase);
|
||||
if ($fattura->allegato_pdf_path !== $targetPath) {
|
||||
Storage::disk('local')->move($fattura->allegato_pdf_path, $targetPath);
|
||||
$fattura->allegato_pdf_path = $targetPath;
|
||||
|
|
@ -199,8 +207,8 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
|||
|
||||
if (is_string($fattura->xml_path) && $fattura->xml_path !== '' && Storage::disk('local')->exists($fattura->xml_path)) {
|
||||
$targetName = $protocollo . ' ' . $dateForName . ' ' . $sanFornitore . ' ' . $sanNumero . '.xml';
|
||||
$targetPath = $base . '/' . $targetName;
|
||||
$this->ensureDir($base);
|
||||
$targetPath = $xmlBase . '/' . $targetName;
|
||||
$this->ensureDir($xmlBase);
|
||||
if ($fattura->xml_path !== $targetPath) {
|
||||
Storage::disk('local')->move($fattura->xml_path, $targetPath);
|
||||
$fattura->xml_path = $targetPath;
|
||||
|
|
@ -271,6 +279,35 @@ public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $u
|
|||
});
|
||||
}
|
||||
|
||||
private function resolveGestioneForYear(Stabile $stabile, int $year): ?GestioneContabile
|
||||
{
|
||||
if (! Schema::hasTable('gestioni_contabili')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = GestioneContabile::query()
|
||||
->where('stabile_id', (int) $stabile->id)
|
||||
->where('anno_gestione', $year);
|
||||
|
||||
$ordinaria = (clone $query)
|
||||
->where('tipo_gestione', 'ordinaria')
|
||||
->orderByDesc('gestione_attiva')
|
||||
->orderByRaw("CASE WHEN stato = 'aperta' THEN 0 ELSE 1 END")
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if ($ordinaria) {
|
||||
return $ordinaria;
|
||||
}
|
||||
|
||||
return $query
|
||||
->orderByRaw("CASE WHEN tipo_gestione = 'straordinaria' THEN 0 ELSE 1 END")
|
||||
->orderByDesc('gestione_attiva')
|
||||
->orderByRaw("CASE WHEN stato = 'aperta' THEN 0 ELSE 1 END")
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
private function getDocumentoProtocolColumn(): ?string
|
||||
{
|
||||
static $cached = null;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
namespace App\Support;
|
||||
|
||||
use App\Models\Amministratore;
|
||||
use App\Models\DatiBancari;
|
||||
use App\Models\GestioneContabile;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\User;
|
||||
|
|
@ -66,6 +68,91 @@ public static function stabileBase(Stabile $stabile, ?Amministratore $admin = nu
|
|||
return self::adminBase($admin) . '/stabili/' . $folder;
|
||||
}
|
||||
|
||||
public static function gestioneFolderName(?GestioneContabile $gestione, ?int $fallbackYear = null, ?string $fallbackType = 'ordinaria'): string
|
||||
{
|
||||
$year = $fallbackYear ?: (int) now()->format('Y');
|
||||
$type = $fallbackType ?: 'ordinaria';
|
||||
|
||||
if ($gestione instanceof GestioneContabile) {
|
||||
$year = (int) ($gestione->anno_gestione ?: $year);
|
||||
$type = (string) ($gestione->tipo_gestione ?: $type);
|
||||
}
|
||||
|
||||
$prefix = match (strtolower(trim($type))) {
|
||||
'straordinaria', 'straordinario', 's' => 'S',
|
||||
'riscaldamento', 'r', 'acqua', 'a' => 'R',
|
||||
default => 'O',
|
||||
};
|
||||
|
||||
return $prefix . $year;
|
||||
}
|
||||
|
||||
public static function fattureXmlYearPath(Stabile $stabile, int|string $year, ?Amministratore $admin = null): ?string
|
||||
{
|
||||
$base = self::stabileBase($stabile, $admin);
|
||||
if (! is_string($base) || $base === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $base . '/fatture_xml/' . (int) $year;
|
||||
}
|
||||
|
||||
public static function fatturePdfGestionePath(Stabile $stabile, ?GestioneContabile $gestione = null, ?int $fallbackYear = null, ?string $fallbackType = 'ordinaria', ?Amministratore $admin = null): ?string
|
||||
{
|
||||
$base = self::stabileBase($stabile, $admin);
|
||||
if (! is_string($base) || $base === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $base . '/fatture/' . self::gestioneFolderName($gestione, $fallbackYear, $fallbackType);
|
||||
}
|
||||
|
||||
public static function bankAccountFolderName(DatiBancari $conto): string
|
||||
{
|
||||
$bankLabel = strtolower(trim((string) ($conto->denominazione_banca ?? '')));
|
||||
|
||||
$bankCode = match (true) {
|
||||
str_contains($bankLabel, 'monte') || str_contains($bankLabel, 'paschi') || str_contains($bankLabel, 'mps') => 'MPS',
|
||||
str_contains($bankLabel, 'intesa') => 'INTESA',
|
||||
str_contains($bankLabel, 'unicredit') => 'UNICREDIT',
|
||||
str_contains($bankLabel, 'bper') => 'BPER',
|
||||
str_contains($bankLabel, 'bcc') => 'BCC',
|
||||
default => strtoupper(self::sanitizeFolder((string) ($conto->denominazione_banca ?: 'BANCA'))),
|
||||
};
|
||||
|
||||
if ($bankCode === '') {
|
||||
$bankCode = 'BANCA';
|
||||
}
|
||||
|
||||
$typeCode = strtolower(trim((string) ($conto->tipo_conto ?? 'corrente'))) === 'cassa'
|
||||
? 'CA'
|
||||
: 'CC';
|
||||
|
||||
$identifier = preg_replace('/\D+/', '', (string) ($conto->numero_conto ?? '')) ?? '';
|
||||
if ($identifier === '') {
|
||||
$identifier = self::sanitizeFolder((string) ($conto->legacy_cod_cassa ?? ''));
|
||||
}
|
||||
if ($identifier === '') {
|
||||
$iban = strtoupper(preg_replace('/[^A-Z0-9]+/', '', (string) ($conto->iban ?? '')) ?? '');
|
||||
$identifier = $iban !== '' ? substr($iban, -8) : '';
|
||||
}
|
||||
if ($identifier === '') {
|
||||
$identifier = 'ID' . (int) $conto->id;
|
||||
}
|
||||
|
||||
return $bankCode . '-' . $typeCode . '-' . $identifier;
|
||||
}
|
||||
|
||||
public static function bankYearPath(Stabile $stabile, DatiBancari $conto, int|string $year, ?Amministratore $admin = null): ?string
|
||||
{
|
||||
$base = self::stabileBase($stabile, $admin);
|
||||
if (! is_string($base) || $base === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $base . '/banca_cc/' . self::bankAccountFolderName($conto) . '/' . (int) $year;
|
||||
}
|
||||
|
||||
public static function fornitoreBase(Fornitore $fornitore, ?Amministratore $admin = null): ?string
|
||||
{
|
||||
$code = (string) ($fornitore->codice_univoco ?: '');
|
||||
|
|
|
|||
|
|
@ -29,6 +29,23 @@
|
|||
'ritenute_da_versare_codice' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE', '2010'),
|
||||
'ritenute_da_versare_descrizione' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_DESC', 'Erario c/ritenute da versare'),
|
||||
'ritenute_da_versare_tipo' => env('CONTABILITA_CONTO_RITENUTE_DA_VERSARE_TIPO', 'Passività'),
|
||||
|
||||
// Bucket patrimoniali e di quadratura da usare nella riconciliazione operativa.
|
||||
'fondo_riserva_codice' => env('CONTABILITA_CONTO_FONDO_RISERVA', '3100'),
|
||||
'fondo_riserva_descrizione' => env('CONTABILITA_CONTO_FONDO_RISERVA_DESC', 'Fondo di riserva'),
|
||||
'fondo_riserva_tipo' => env('CONTABILITA_CONTO_FONDO_RISERVA_TIPO', 'Patrimonio Netto'),
|
||||
|
||||
'accantonamenti_codice' => env('CONTABILITA_CONTO_ACCANTONAMENTI', '3110'),
|
||||
'accantonamenti_descrizione' => env('CONTABILITA_CONTO_ACCANTONAMENTI_DESC', 'Accantonamenti da destinare'),
|
||||
'accantonamenti_tipo' => env('CONTABILITA_CONTO_ACCANTONAMENTI_TIPO', 'Patrimonio Netto'),
|
||||
|
||||
'rimborsi_da_distribuire_codice' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE', '2105'),
|
||||
'rimborsi_da_distribuire_descrizione' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE_DESC', 'Rimborsi da distribuire'),
|
||||
'rimborsi_da_distribuire_tipo' => env('CONTABILITA_CONTO_RIMBORSI_DISTRIBUIRE_TIPO', 'Passività'),
|
||||
|
||||
'rimborsi_da_utilizzare_codice' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE', '1115'),
|
||||
'rimborsi_da_utilizzare_descrizione' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_DESC', 'Rimborsi da utilizzare'),
|
||||
'rimborsi_da_utilizzare_tipo' => env('CONTABILITA_CONTO_RIMBORSI_UTILIZZARE_TIPO', 'Attività'),
|
||||
],
|
||||
|
||||
'fatture_elettroniche' => [
|
||||
|
|
|
|||
|
|
@ -23,6 +23,21 @@ .fi-main-ctn>main {
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.fi-sidebar {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
align-self: flex-start !important;
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.fi-sidebar-header {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
z-index: 20 !important;
|
||||
}
|
||||
|
||||
.fi-main {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
|
|
@ -40,6 +55,52 @@ .netgescon-footer {
|
|||
.fi-sidebar-nav,
|
||||
.fi-sidebar-nav-groups {
|
||||
row-gap: calc(var(--spacing) * 1.5) !important;
|
||||
overflow-y: auto !important;
|
||||
max-height: calc(100vh - 5rem) !important;
|
||||
max-height: calc(100dvh - 5rem) !important;
|
||||
}
|
||||
|
||||
@media (min-width: 64rem) {
|
||||
.fi-body.fi-body-has-navigation {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-sidebar {
|
||||
position: fixed !important;
|
||||
top: 4.5rem !important;
|
||||
left: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 20rem !important;
|
||||
height: calc(100vh - 4.5rem) !important;
|
||||
height: calc(100dvh - 4.5rem) !important;
|
||||
z-index: 40 !important;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.2) !important;
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-topbar {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
margin-left: 0 !important;
|
||||
z-index: 50 !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-main-ctn {
|
||||
margin-left: 0 !important;
|
||||
width: 100% !important;
|
||||
padding-top: 4.5rem !important;
|
||||
padding-left: 20rem !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-main-ctn > main {
|
||||
height: calc(100vh - 4.5rem) !important;
|
||||
height: calc(100dvh - 4.5rem) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fi-sidebar-group-btn,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,70 @@
|
|||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="{{ asset('css/netgescon-admin.css') }}">
|
||||
<style>
|
||||
.fi-sidebar {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
align-self: flex-start !important;
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.fi-sidebar-header {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
z-index: 20 !important;
|
||||
}
|
||||
|
||||
.fi-sidebar-nav,
|
||||
.fi-sidebar-nav-groups {
|
||||
row-gap: calc(var(--spacing) * 1.5) !important;
|
||||
overflow-y: auto !important;
|
||||
max-height: calc(100vh - 5rem) !important;
|
||||
max-height: calc(100dvh - 5rem) !important;
|
||||
}
|
||||
|
||||
@media (min-width: 64rem) {
|
||||
.fi-body.fi-body-has-navigation {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-sidebar {
|
||||
position: fixed !important;
|
||||
top: 4.5rem !important;
|
||||
left: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 20rem !important;
|
||||
height: calc(100vh - 4.5rem) !important;
|
||||
height: calc(100dvh - 4.5rem) !important;
|
||||
z-index: 40 !important;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.2) !important;
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-topbar {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
margin-left: 0 !important;
|
||||
z-index: 50 !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-main-ctn {
|
||||
margin-left: 0 !important;
|
||||
width: 100% !important;
|
||||
padding-top: 4.5rem !important;
|
||||
padding-left: 20rem !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-main-ctn > main {
|
||||
height: calc(100vh - 4.5rem) !important;
|
||||
height: calc(100dvh - 4.5rem) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fi-sidebar-group-btn,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@
|
|||
$ricQueue = $this->getRiconciliazioneQueue();
|
||||
$ricCurrent = $this->getRiconciliazioneCurrent();
|
||||
$ricCandidates = $this->getRiconciliazioneCandidates();
|
||||
$archiveSummary = $this->getArchivioOperativoSummary();
|
||||
$gestioniRilevate = $this->getGestioniContabiliRilevate();
|
||||
$ricBuckets = $this->getRiconciliazioneBucketRows();
|
||||
$tipoEstrattoOptions = [
|
||||
'estratto_conto' => 'Estratto conto',
|
||||
'saldo_banca' => 'Saldo banca',
|
||||
'riconciliazione' => 'Riconciliazione',
|
||||
'altro' => 'Altro',
|
||||
];
|
||||
@endphp
|
||||
|
||||
@if(! $stabile)
|
||||
|
|
@ -73,6 +82,7 @@
|
|||
<x-filament::tabs.item :active="$this->hubTab === 'conti'" wire:click="goToHubTab('conti')">Conti</x-filament::tabs.item>
|
||||
<x-filament::tabs.item :active="$this->hubTab === 'saldi'" wire:click="goToHubTab('saldi')">Saldi e estratti</x-filament::tabs.item>
|
||||
<x-filament::tabs.item :active="$this->hubTab === 'movimenti'" wire:click="goToHubTab('movimenti')">Movimenti</x-filament::tabs.item>
|
||||
<x-filament::tabs.item :active="$this->hubTab === 'struttura'" wire:click="goToHubTab('struttura')">Struttura e gestioni</x-filament::tabs.item>
|
||||
<x-filament::tabs.item :active="$this->hubTab === 'riconciliazione'" wire:click="goToHubTab('riconciliazione')">Riconciliazione</x-filament::tabs.item>
|
||||
</x-filament::tabs>
|
||||
|
||||
|
|
@ -159,6 +169,7 @@
|
|||
<tr>
|
||||
<th class="py-2 pr-4">Data saldo</th>
|
||||
<th class="py-2 pr-4">Periodo</th>
|
||||
<th class="py-2 pr-4">Esito</th>
|
||||
<th class="py-2 pr-4">Tipo</th>
|
||||
<th class="py-2 pr-4 text-right">Saldo</th>
|
||||
<th class="py-2 pr-4">Documento</th>
|
||||
|
|
@ -174,20 +185,109 @@
|
|||
<div>{{ $row['periodo_breve'] }}</div>
|
||||
<div class="text-xs text-slate-500">{{ $row['periodo'] }}</div>
|
||||
</td>
|
||||
<td class="py-3 pr-4 align-top">
|
||||
@if(($row['quadratura_status'] ?? 'neutral') === 'ok')
|
||||
<div class="inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-semibold text-emerald-700">
|
||||
<x-heroicon-o-check-circle class="h-4 w-4" />
|
||||
OK
|
||||
</div>
|
||||
@elseif(($row['quadratura_status'] ?? 'neutral') === 'anchor')
|
||||
<div class="inline-flex items-center gap-1 rounded-full bg-sky-100 px-2.5 py-1 text-xs font-semibold text-sky-700">
|
||||
<x-heroicon-o-flag class="h-4 w-4" />
|
||||
Partenza
|
||||
</div>
|
||||
@elseif(($row['quadratura_status'] ?? 'neutral') === 'mismatch')
|
||||
<div class="inline-flex items-center gap-1 rounded-full bg-amber-100 px-2.5 py-1 text-xs font-semibold text-amber-700">
|
||||
<x-heroicon-o-exclamation-triangle class="h-4 w-4" />
|
||||
Verifica
|
||||
</div>
|
||||
@else
|
||||
<div class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-600">
|
||||
<x-heroicon-o-minus-circle class="h-4 w-4" />
|
||||
N/D
|
||||
</div>
|
||||
@endif
|
||||
<div class="mt-1 text-xs text-slate-500">{{ $row['quadratura_label'] ?? '—' }}</div>
|
||||
@if(($row['quadratura_status'] ?? null) === 'mismatch' && array_key_exists('quadratura_delta', $row))
|
||||
<div class="mt-1 text-xs font-medium text-rose-700">
|
||||
Scarto: € {{ number_format((float) ($row['quadratura_delta'] ?? 0), 2, ',', '.') }}
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 pr-4">{{ $row['tipo'] }}</td>
|
||||
<td class="py-3 pr-4 text-right font-semibold">€ {{ number_format((float) $row['saldo'], 2, ',', '.') }}</td>
|
||||
<td class="py-3 pr-4">
|
||||
@if($row['documento_url'] && $row['documento_label'])
|
||||
<a href="{{ $row['documento_url'] }}" target="_blank" class="text-primary-600 hover:underline">{{ $row['documento_label'] }}</a>
|
||||
<button type="button" wire:click="openSaldoDocumento({{ $row['id'] }})" class="text-primary-600 hover:underline">{{ $row['documento_label'] }}</button>
|
||||
@else
|
||||
<span class="text-slate-400">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-slate-600">{{ $row['note'] !== '' ? $row['note'] : '—' }}</td>
|
||||
<td class="py-3 pr-0 text-right">
|
||||
<x-filament::button size="sm" color="gray" wire:click="openSaldoPeriodo({{ $row['id'] }})">Apri movimenti</x-filament::button>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<x-filament::button size="sm" color="gray" wire:click="openSaldoPeriodo({{ $row['id'] }})">Apri movimenti</x-filament::button>
|
||||
<x-filament::button size="sm" color="warning" wire:click="startEditingSaldo({{ $row['id'] }})">Modifica</x-filament::button>
|
||||
@if(($this->confirmingDeleteSaldoId ?? null) === $row['id'])
|
||||
<x-filament::button size="sm" color="danger" wire:click="deleteSaldo({{ $row['id'] }})">Conferma elimina</x-filament::button>
|
||||
<x-filament::button size="sm" color="gray" wire:click="cancelDeleteSaldo">Annulla</x-filament::button>
|
||||
@else
|
||||
<x-filament::button size="sm" color="danger" wire:click="requestDeleteSaldo({{ $row['id'] }})">Elimina</x-filament::button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@if(($this->editingSaldoId ?? null) === $row['id'])
|
||||
<tr class="bg-amber-50/60">
|
||||
<td colspan="8" class="py-4 pr-0">
|
||||
<div class="rounded-xl border border-amber-200 bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Modifica saldo / estratto</div>
|
||||
<div class="mt-1 text-xs text-slate-600">Puoi correggere data saldo, periodo, tipo e note del record gia memorizzato.</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||
<label class="text-xs text-slate-600">
|
||||
<span class="mb-1 block">Data saldo</span>
|
||||
<input type="date" wire:model.defer="editingSaldoData.data_saldo" class="w-full rounded-lg border-gray-300 text-sm" />
|
||||
</label>
|
||||
<label class="text-xs text-slate-600">
|
||||
<span class="mb-1 block">Periodo dal</span>
|
||||
<input type="date" wire:model.defer="editingSaldoData.periodo_da" class="w-full rounded-lg border-gray-300 text-sm" />
|
||||
</label>
|
||||
<label class="text-xs text-slate-600">
|
||||
<span class="mb-1 block">Periodo al</span>
|
||||
<input type="date" wire:model.defer="editingSaldoData.periodo_a" class="w-full rounded-lg border-gray-300 text-sm" />
|
||||
</label>
|
||||
<label class="text-xs text-slate-600">
|
||||
<span class="mb-1 block">Saldo ufficiale</span>
|
||||
<input type="text" wire:model.defer="editingSaldoData.saldo" class="w-full rounded-lg border-gray-300 text-sm" />
|
||||
</label>
|
||||
<label class="text-xs text-slate-600">
|
||||
<span class="mb-1 block">Tipo riferimento</span>
|
||||
<select wire:model.defer="editingSaldoData.tipo_estratto" class="w-full rounded-lg border-gray-300 text-sm">
|
||||
<option value="">Saldo registrato</option>
|
||||
@foreach($tipoEstrattoOptions as $tipoValue => $tipoLabel)
|
||||
<option value="{{ $tipoValue }}">{{ $tipoLabel }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-700">
|
||||
<input type="checkbox" wire:model.defer="editingSaldoData.is_partenza_contabile" class="rounded border-gray-300 text-primary-600" />
|
||||
<span>Usa questo saldo come partenza contabile</span>
|
||||
</label>
|
||||
<label class="text-xs text-slate-600 lg:col-span-2">
|
||||
<span class="mb-1 block">Note</span>
|
||||
<input type="text" wire:model.defer="editingSaldoData.note" class="w-full rounded-lg border-gray-300 text-sm" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap justify-end gap-2">
|
||||
<x-filament::button size="sm" color="gray" wire:click="cancelEditingSaldo">Annulla</x-filament::button>
|
||||
<x-filament::button size="sm" color="primary" wire:click="saveEditingSaldo">Salva modifiche</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -198,6 +298,80 @@
|
|||
@endforelse
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@elseif($this->hubTab === 'struttura')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Struttura operativa banca / FE</x-slot>
|
||||
<x-slot name="description">Separiamo la struttura documentale e le gestioni dal tab movimenti, così l’operatore vede subito il mastrino e gli import.</x-slot>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
||||
<div class="rounded-xl border bg-slate-50 p-4">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-slate-500">Struttura archivio banca</div>
|
||||
<div class="mt-2 text-sm font-semibold text-slate-900">{{ $archiveSummary['banca_folder'] ?? 'banca_cc/<conto>/<anno>' }}</div>
|
||||
<div class="mt-2 text-xs text-slate-600">Qui finiscono estratti, saldi, file importati e documenti conto in modo uniforme su tutti gli stabili.</div>
|
||||
<div class="mt-3 rounded-lg border bg-white p-3 text-xs text-slate-700">
|
||||
<div><span class="font-semibold">Base stabile:</span> {{ $archiveSummary['base_folder'] ?? '—' }}</div>
|
||||
<div class="mt-1"><span class="font-semibold">Anno operativo:</span> {{ $archiveSummary['reference_year'] ?? '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-slate-50 p-4">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-slate-500">Gestioni contabili</div>
|
||||
<div class="mt-2 text-sm font-semibold text-slate-900">{{ $archiveSummary['gestione_label'] ?? 'Gestione da associare' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-600">Le gestioni storiche restano qui, fuori dal mastrino, così il tab movimenti resta leggibile anche con molte annualità.</div>
|
||||
<div class="mt-3 max-h-80 space-y-2 overflow-y-auto pr-1">
|
||||
@forelse($gestioniRilevate as $gestione)
|
||||
<div class="rounded-lg border bg-white px-3 py-2 text-xs text-slate-700">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-semibold">{{ $gestione['folder'] }}</span>
|
||||
<span class="rounded-full px-2 py-0.5 {{ $gestione['active'] ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-100 text-slate-600' }}">{{ $gestione['stato'] !== '' ? $gestione['stato'] : 'attiva' }}</span>
|
||||
</div>
|
||||
<div class="mt-1">{{ $gestione['label'] }}</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed bg-white px-3 py-3 text-xs text-slate-500">Nessuna gestione ordinaria/straordinaria rilevata.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-slate-50 p-4">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-slate-500">Struttura FE pronta</div>
|
||||
<div class="mt-2 space-y-2 text-xs text-slate-700">
|
||||
<div class="rounded-lg border bg-white px-3 py-2">
|
||||
<div class="font-semibold text-slate-900">{{ $archiveSummary['fatture_xml_folder'] ?? 'fatture_xml/<anno>' }}</div>
|
||||
<div class="mt-1">XML originali per anno, indipendenti dal PDF.</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white px-3 py-2">
|
||||
<div class="font-semibold text-slate-900">{{ $archiveSummary['fatture_pdf_folder'] ?? 'fatture/<gestione>' }}</div>
|
||||
<div class="mt-1">PDF protocollati per gestione, pronti per ACL atomici su anno e tipo gestione.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1.1fr)_minmax(320px,0.9fr)]">
|
||||
<div class="rounded-lg border bg-slate-50 p-3">
|
||||
<div class="text-sm font-semibold text-slate-900">Criteri di riconciliazione e quadratura</div>
|
||||
<div class="mt-1 text-xs text-slate-600">Questi bucket restano visibili come riferimento operativo, ma fuori dal tab movimenti.</div>
|
||||
<div class="mt-3 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
@foreach($ricBuckets as $bucket)
|
||||
<div class="rounded-lg border bg-white p-3 text-sm">
|
||||
<div class="font-semibold text-slate-900">{{ $bucket['title'] }}</div>
|
||||
<div class="mt-1 text-xs text-slate-600">{{ $bucket['detail'] }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-slate-50 p-3">
|
||||
<div class="text-sm font-semibold text-slate-900">Regole operative di struttura</div>
|
||||
<div class="mt-3 space-y-2 text-xs text-slate-700">
|
||||
<div class="rounded-lg border bg-white px-3 py-2">I movimenti banca restano sempre sul <span class="font-semibold">conto selezionato</span> e vanno associati a una <span class="font-semibold">gestione</span>.</div>
|
||||
<div class="rounded-lg border bg-white px-3 py-2">Per questo stabile trattiamo solo <span class="font-semibold">ordinaria</span> e <span class="font-semibold">straordinaria</span>; il riscaldamento non entra nel flusso.</div>
|
||||
<div class="rounded-lg border bg-white px-3 py-2">Le FE acqua possono confluire sulla voce <span class="font-semibold">ACQ</span> quando il PDF e il pagamento banca danno un aggancio coerente.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@elseif($this->hubTab === 'riconciliazione')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Riconciliazione guidata</x-slot>
|
||||
|
|
@ -255,6 +429,8 @@
|
|||
<x-filament::button size="sm" color="primary" wire:click="goToHubTab('movimenti')">Apri nel mastrino</x-filament::button>
|
||||
@if($ricCurrent['has_registrazione'])
|
||||
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-medium text-emerald-700">Ha già una prima nota</span>
|
||||
@elseif(! empty($ricCurrent['match_label']))
|
||||
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-medium text-emerald-700">{{ $ricCurrent['match_label'] }}</span>
|
||||
@else
|
||||
<span class="rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-700">Da collegare</span>
|
||||
@endif
|
||||
|
|
@ -266,9 +442,9 @@
|
|||
<div class="text-sm font-semibold text-slate-900">Candidati automatici</div>
|
||||
<div class="mt-1 text-xs text-slate-600">
|
||||
@if($ricCurrent['importo'] >= 0)
|
||||
Match su incassi registrati per importo, data e testo della causale.
|
||||
Match su canoni affitto e incassi registrati per importo, data e testo della causale.
|
||||
@else
|
||||
Match su fatture fornitore per importo, data e denominazione del fornitore.
|
||||
Match su fatture fornitore contabili per importo netto, data e denominazione del fornitore.
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
@ -288,6 +464,19 @@
|
|||
<span>Importo: € {{ number_format((float) $candidate['importo'], 2, ',', '.') }}</span>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-slate-600">{{ $candidate['motivo'] }}</div>
|
||||
@if(($candidate['azione'] ?? null) === 'riconcilia_affitto')
|
||||
<div class="mt-3">
|
||||
<x-filament::button size="sm" color="success" wire:click="riconciliaCanoneAffitto({{ (int) $candidate['canone_dovuto_id'] }})">
|
||||
Chiudi canone affitto
|
||||
</x-filament::button>
|
||||
</div>
|
||||
@elseif(($candidate['azione'] ?? null) === 'riconcilia_pagamento_fornitore')
|
||||
<div class="mt-3">
|
||||
<x-filament::button size="sm" color="success" wire:click="riconciliaPagamentoFornitore({{ (int) $candidate['fattura_fornitore_id'] }})">
|
||||
Registra pagamento fornitore
|
||||
</x-filament::button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed bg-slate-50 p-4 text-sm text-slate-500">Nessun candidato convincente. In questo caso l’operatore lavora dal mastrino e conferma manualmente il collegamento.</div>
|
||||
|
|
@ -297,7 +486,7 @@
|
|||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
@else
|
||||
@elseif($this->hubTab === 'movimenti')
|
||||
<x-filament::section>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
|
|
@ -305,16 +494,23 @@
|
|||
<div class="mt-1 text-xs text-gray-600">
|
||||
Seleziona il conto o la cassa corretta e importa il file nel formato relativo. Il collegamento al conto resta esplicito, senza inferenze dal file.
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-slate-700">Per i file <span class="font-semibold">.qif</span> puliamo i movimenti nei gruppi operativi e leggiamo <span class="font-semibold">CBILL</span>, <span class="font-semibold">SIA</span>, commissioni e fornitore favorito per proporre subito il collegamento con fatture e prima nota.</div>
|
||||
@if($contoImport)
|
||||
<div class="mt-2 text-xs text-gray-700">
|
||||
Conto selezionato: <span class="font-medium">{{ $contoImport['label'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<x-filament::button type="button" color="primary" wire:click="mountAction('importa_estratto_unificato')">
|
||||
Importa estratto
|
||||
</x-filament::button>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::button type="button" color="gray" wire:click="mountAction('aggiungi_movimento_manuale')">
|
||||
Nuovo movimento manuale
|
||||
</x-filament::button>
|
||||
<x-filament::button type="button" color="primary" wire:click="mountAction('importa_estratto_unificato')">
|
||||
Importa estratto
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section>
|
||||
|
|
@ -404,10 +600,16 @@
|
|||
<div class="font-medium text-slate-900">{{ $card['tipo'] }}</div>
|
||||
<div class="text-xs text-slate-500">{{ $card['data'] ?? '—' }}</div>
|
||||
</div>
|
||||
@if(!empty($card['is_partenza_contabile']))
|
||||
<div class="mt-2 inline-flex items-center gap-1 rounded-full bg-sky-100 px-2.5 py-1 text-[11px] font-semibold text-sky-700">
|
||||
<x-heroicon-o-flag class="h-3.5 w-3.5" />
|
||||
Partenza contabile
|
||||
</div>
|
||||
@endif
|
||||
<div class="mt-2 text-lg font-semibold text-slate-900">€ {{ number_format((float) ($card['saldo'] ?? 0), 2, ',', '.') }}</div>
|
||||
<div class="mt-1 text-xs text-slate-600">{{ $card['periodo'] }}</div>
|
||||
@if(!empty($card['documento_url']) && !empty($card['documento_label']))
|
||||
<a href="{{ $card['documento_url'] }}" target="_blank" class="mt-2 inline-flex text-xs font-medium text-indigo-700 hover:underline">{{ $card['documento_label'] }}</a>
|
||||
<button type="button" wire:click="openSaldoDocumento({{ $card['id'] }})" class="mt-2 inline-flex text-xs font-medium text-indigo-700 hover:underline">{{ $card['documento_label'] }}</button>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
|
|
@ -423,4 +625,26 @@
|
|||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($this->pdfViewerUrl)
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4">
|
||||
<div class="flex h-[92vh] w-full max-w-7xl flex-col overflow-hidden rounded-2xl bg-white shadow-2xl">
|
||||
<div class="flex items-center justify-between border-b px-5 py-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">{{ $this->pdfViewerTitle ?? 'Documento PDF' }}</div>
|
||||
<div class="text-xs text-slate-500">Viewer PDF inline standard per saldi, estratti e documenti collegati.</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if($this->pdfViewerOpenUrl)
|
||||
<a href="{{ $this->pdfViewerOpenUrl }}" target="_blank" rel="noopener" class="inline-flex items-center rounded-md bg-slate-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-700">Apri in nuova scheda</a>
|
||||
@endif
|
||||
<x-filament::button type="button" color="gray" size="sm" wire:click="closePdfViewer">Chiudi</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 bg-slate-100 p-4">
|
||||
@include('filament.modals.pdf-viewer', ['url' => $this->pdfViewerUrl, 'openUrl' => $this->pdfViewerOpenUrl, 'title' => $this->pdfViewerTitle])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-filament-panels::page>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user