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:
1 parent
82a69e0d20
commit
f88639bf4b
4 files changed
+209
-3
No files matched your search
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user