Add Distinta Base import tab: parse .xls/.xml export to CSV with preview

Reads the ERP-exported "Distinta Base" file (SpreadsheetML XML or real
binary .xls/.xlsx via ExcelDataReader), extracts Componente/Descrizione,
writes a CSV and previews it in a new tab alongside the existing CAD
search. Also ignore the exported Excel/CSV files dropped in the repo root.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5 committed 2026-09-16 10:22:16 +02:00
1 parent 82a69e0d20
commit f88639bf4b
4 files changed
+209 -3

No files matched your search

+6
View File
@@ -19,3 +19,9 @@ publish/
# Stato locale degli strumenti agentici
.claude/
# File Excel/XML della Distinta Base (esportati dal gestionale) e il CSV generato
/*.xls
/*.xlsx
/*.xls.xml
/*.csv
+2
View File
@@ -10,6 +10,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.12" />
<PackageReference Include="ExcelDataReader.DataSet" Version="3.9.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.0" />
</ItemGroup>
</Project>
+109
View File
@@ -0,0 +1,109 @@
using System.Text;
using System.Xml.Linq;
using ExcelDataReader;
namespace CadSearchGui;
/// <summary>
/// Converte il file "Distinta Base" esportato dal gestionale (SpreadsheetML
/// XML con estensione .xls/.xml, oppure .xls/.xlsx binario vero) nelle sole
/// colonne Componente/Descrizione in CSV.
/// </summary>
static class DistintaBaseImporter
{
private static readonly XNamespace Ss = "urn:schemas-microsoft-com:office:spreadsheet";
private const string NomeFoglio = "Distinta Base";
static DistintaBaseImporter()
{
Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
}
public static (string PercorsoCsv, List<(string Componente, string Descrizione)> Righe) ConvertiInCsv(string percorsoInput)
{
var righeGrezze = EImmagineSpreadsheetMl(percorsoInput)
? LeggiRigheSpreadsheetMl(percorsoInput)
: LeggiRigheExcelBinario(percorsoInput);
var righe = righeGrezze.Skip(1) // salta l'intestazione
.Where(r => r.Componente.Length > 0 || r.Descrizione.Length > 0)
.ToList();
var percorsoCsv = Path.Combine(
Path.GetDirectoryName(Path.GetFullPath(percorsoInput))!,
Path.GetFileNameWithoutExtension(percorsoInput) + ".csv");
using var writer = new StreamWriter(percorsoCsv, false, Encoding.UTF8);
writer.WriteLine("Componente;Descrizione");
foreach (var (componente, descrizione) in righe)
{
writer.WriteLine($"{EscapeCsv(componente)};{EscapeCsv(descrizione)}");
}
return (percorsoCsv, righe);
}
/// <summary>I file XML "SpreadsheetML" iniziano con l'intestazione XML testuale;
/// i file .xls/.xlsx binari veri iniziano invece con una firma binaria (OLE2 o ZIP).</summary>
private static bool EImmagineSpreadsheetMl(string percorso)
{
Span<byte> primiByte = stackalloc byte[5];
using var stream = File.OpenRead(percorso);
var letti = stream.Read(primiByte);
return letti >= 5 && Encoding.ASCII.GetString(primiByte) == "<?xml";
}
private static List<(string Componente, string Descrizione)> LeggiRigheSpreadsheetMl(string percorsoInput)
{
var doc = XDocument.Load(percorsoInput);
var foglio = doc.Descendants(Ss + "Worksheet")
.FirstOrDefault(w => (string?)w.Attribute(Ss + "Name") == NomeFoglio)
?? throw new InvalidDataException($"Foglio '{NomeFoglio}' non trovato nel file.");
var righeXml = foglio.Descendants(Ss + "Row").ToList();
if (righeXml.Count == 0)
throw new InvalidDataException($"Il foglio '{NomeFoglio}' non contiene righe.");
return righeXml.Select(LeggiColonneBeCSpreadsheetMl).ToList();
}
private static (string Componente, string Descrizione) LeggiColonneBeCSpreadsheetMl(XElement riga)
{
string b = "", c = "";
int colonna = 0;
foreach (var cella in riga.Elements(Ss + "Cell"))
{
// ss:Index salta le celle vuote iniziali: senza attributo la colonna è quella precedente + 1
var indice = cella.Attribute(Ss + "Index");
colonna = indice != null ? int.Parse(indice.Value) : colonna + 1;
if (colonna == 2) b = cella.Element(Ss + "Data")?.Value ?? "";
else if (colonna == 3) c = cella.Element(Ss + "Data")?.Value ?? "";
}
return (b, c);
}
private static List<(string Componente, string Descrizione)> LeggiRigheExcelBinario(string percorsoInput)
{
using var stream = File.Open(percorsoInput, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = ExcelReaderFactory.CreateReader(stream);
var dataSet = reader.AsDataSet();
var tabella = dataSet.Tables[NomeFoglio]
?? throw new InvalidDataException($"Foglio '{NomeFoglio}' non trovato nel file.");
var righe = new List<(string Componente, string Descrizione)>();
foreach (System.Data.DataRow riga in tabella.Rows)
{
var b = riga.Table.Columns.Count > 1 ? riga[1]?.ToString() ?? "" : "";
var c = riga.Table.Columns.Count > 2 ? riga[2]?.ToString() ?? "" : "";
righe.Add((b, c));
}
return righe;
}
private static string EscapeCsv(string valore)
{
if (valore.Contains(';') || valore.Contains('"') || valore.Contains('\n'))
return $"\"{valore.Replace("\"", "\"\"")}\"";
return valore;
}
}
+92 -3
View File
@@ -15,6 +15,8 @@ public partial class Form1 : Form
private readonly DataGridView _grid = new();
private readonly Label _lblStatus = new();
private readonly Label _lblDb = new();
private readonly DataGridView _gridImport = new();
private readonly Label _lblImportStatus = new();
private const string OpzionePrtAsm = "Solo prt/asm (predefinito)";
private const string OpzioneTuttiTipi = "Tutti i tipi";
@@ -120,9 +122,96 @@ public partial class Form1 : Form
_lblStatus.Text = "Pronto.";
statusPanel.Controls.Add(_lblStatus);
Controls.Add(_grid);
Controls.Add(statusPanel);
Controls.Add(topPanel);
var tabRicerca = new TabPage("Ricerca CAD");
tabRicerca.Controls.Add(_grid);
tabRicerca.Controls.Add(statusPanel);
tabRicerca.Controls.Add(topPanel);
var tabs = new TabControl { Dock = DockStyle.Fill };
tabs.TabPages.Add(BuildTabImportaDistinta());
tabs.TabPages.Add(tabRicerca);
Controls.Add(tabs);
}
private TabPage BuildTabImportaDistinta()
{
var tab = new TabPage("Importa Distinta Base") { AllowDrop = true };
var lblIstruzioni = new Label
{
Dock = DockStyle.Top,
Height = 60,
Padding = new Padding(8),
Text = "Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta "
+ "Base'), oppure scegli il file col pulsante sotto. Verrà generato un CSV con "
+ "le colonne Componente e Descrizione, con anteprima qui sotto.",
};
var btnSfoglia = new Button { Text = "Scegli file...", Dock = DockStyle.Top, Height = 32 };
btnSfoglia.Click += (_, _) => ScegliFileDistinta();
_lblImportStatus.Dock = DockStyle.Top;
_lblImportStatus.Height = 24;
_lblImportStatus.Padding = new Padding(8, 4, 8, 4);
_lblImportStatus.ForeColor = SystemColors.GrayText;
_gridImport.Dock = DockStyle.Fill;
_gridImport.ReadOnly = true;
_gridImport.AllowUserToAddRows = false;
_gridImport.AllowUserToDeleteRows = false;
_gridImport.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
_gridImport.Columns.Add("componente", "Componente");
_gridImport.Columns.Add("descrizione", "Descrizione");
tab.Controls.Add(_gridImport);
tab.Controls.Add(_lblImportStatus);
tab.Controls.Add(btnSfoglia);
tab.Controls.Add(lblIstruzioni);
tab.DragEnter += (_, e) =>
e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true
? DragDropEffects.Copy
: DragDropEffects.None;
tab.DragDrop += (_, e) =>
{
if (e.Data?.GetData(DataFormats.FileDrop) is string[] { Length: > 0 } file)
ImportaDistinta(file[0]);
};
return tab;
}
private void ScegliFileDistinta()
{
using var dialog = new OpenFileDialog
{
Filter = "File distinta base (*.xls;*.xml)|*.xls;*.xml|Tutti i file|*.*",
Title = "Seleziona il file Distinta Base",
};
if (dialog.ShowDialog(this) == DialogResult.OK)
ImportaDistinta(dialog.FileName);
}
private void ImportaDistinta(string percorso)
{
try
{
var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso);
_gridImport.Rows.Clear();
foreach (var (componente, descrizione) in righe)
{
_gridImport.Rows.Add(componente, descrizione);
}
_lblImportStatus.ForeColor = Color.DarkGreen;
_lblImportStatus.Text = $"CSV generato ({righe.Count} righe): {csv}";
}
catch (Exception ex)
{
_gridImport.Rows.Clear();
_lblImportStatus.ForeColor = Color.DarkRed;
_lblImportStatus.Text = $"Errore durante la conversione: {ex.Message}";
}
}
private void InizializzaDatabase()