Add Sage Spain export artifacts
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
$scriptPath = Join-Path $PSScriptRoot "Export-SageSqlCsv.ps1"
|
||||
|
||||
& $scriptPath `
|
||||
-Database "Sage" `
|
||||
-ObjectName @(
|
||||
"dbo.CabeceraAlbaranCliente",
|
||||
"dbo.LineasAlbaranCliente",
|
||||
"dbo.EstadisVenta",
|
||||
"dbo.EstadisVentaTallas",
|
||||
"dbo.FacturasTB",
|
||||
"dbo.MovimientosFacturas",
|
||||
"dbo.Vis_RTDV_EfectosFactura"
|
||||
) `
|
||||
-FromDate "2025-01-01" `
|
||||
-ToDate "2026-01-01" `
|
||||
-MaxRowsPerObject 10000
|
||||
@@ -0,0 +1,15 @@
|
||||
$scriptPath = Join-Path $PSScriptRoot "Export-SageSqlCsv.ps1"
|
||||
|
||||
& $scriptPath `
|
||||
-Database "Sage" `
|
||||
-ObjectName @(
|
||||
"dbo.CabeceraAlbaranCliente",
|
||||
"dbo.LineasAlbaranCliente",
|
||||
"dbo.EstadisVenta",
|
||||
"dbo.EstadisVentaTallas",
|
||||
"dbo.FacturasTB",
|
||||
"dbo.MovimientosFacturas",
|
||||
"dbo.Vis_RTDV_EfectosFactura"
|
||||
) `
|
||||
-FromDate "2025-01-01" `
|
||||
-ToDate "2026-01-01"
|
||||
@@ -0,0 +1,215 @@
|
||||
param(
|
||||
[string]$ServerInstance = "localhost",
|
||||
[string]$Database = "Sage",
|
||||
[datetime]$FromDate = "2025-01-01",
|
||||
[datetime]$ToDate = "2026-01-01",
|
||||
[string]$OutputDirectory = (Join-Path $env:USERPROFILE "Desktop")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function New-Connection {
|
||||
$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
|
||||
$builder["Data Source"] = $ServerInstance
|
||||
$builder["Initial Catalog"] = $Database
|
||||
$builder["Integrated Security"] = $true
|
||||
$builder["TrustServerCertificate"] = $true
|
||||
$builder["Connect Timeout"] = 15
|
||||
return New-Object System.Data.SqlClient.SqlConnection($builder.ConnectionString)
|
||||
}
|
||||
|
||||
function Convert-ToCsvValue {
|
||||
param($Value)
|
||||
|
||||
if ($null -eq $Value -or $Value -is [System.DBNull]) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if ($Value -is [datetime]) {
|
||||
$text = $Value.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
}
|
||||
else {
|
||||
$text = [string]$Value
|
||||
}
|
||||
|
||||
$text = $text.Replace('"', '""')
|
||||
return '"' + $text + '"'
|
||||
}
|
||||
|
||||
function Export-QueryToCsv {
|
||||
param(
|
||||
[string]$Sql,
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$conn = New-Connection
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = $Sql
|
||||
$cmd.CommandTimeout = 0
|
||||
|
||||
$fromParameter = $cmd.Parameters.Add("@FromDate", [System.Data.SqlDbType]::Date)
|
||||
$fromParameter.Value = $FromDate.Date
|
||||
|
||||
$toParameter = $cmd.Parameters.Add("@ToDate", [System.Data.SqlDbType]::Date)
|
||||
$toParameter.Value = $ToDate.Date
|
||||
|
||||
$writer = New-Object System.IO.StreamWriter($Path, $false, [System.Text.Encoding]::UTF8)
|
||||
$rowCount = 0
|
||||
$salesSum = [decimal]0
|
||||
|
||||
try {
|
||||
$conn.Open()
|
||||
$reader = $cmd.ExecuteReader()
|
||||
|
||||
$headers = for ($i = 0; $i -lt $reader.FieldCount; $i++) {
|
||||
Convert-ToCsvValue $reader.GetName($i)
|
||||
}
|
||||
$writer.WriteLine(($headers -join ";"))
|
||||
|
||||
$salesIndex = -1
|
||||
for ($i = 0; $i -lt $reader.FieldCount; $i++) {
|
||||
if ($reader.GetName($i) -eq "SalesPriceValue") {
|
||||
$salesIndex = $i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
while ($reader.Read()) {
|
||||
$values = for ($i = 0; $i -lt $reader.FieldCount; $i++) {
|
||||
Convert-ToCsvValue $reader.GetValue($i)
|
||||
}
|
||||
$writer.WriteLine(($values -join ";"))
|
||||
$rowCount++
|
||||
|
||||
if ($salesIndex -ge 0 -and -not $reader.IsDBNull($salesIndex)) {
|
||||
$salesSum += [decimal]$reader.GetValue($salesIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$writer.Dispose()
|
||||
$conn.Dispose()
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Rows = $rowCount
|
||||
SalesPriceValueSum = $salesSum
|
||||
}
|
||||
}
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$runDirectory = Join-Path $OutputDirectory "Sage_Spain_Sales_Export_$timestamp"
|
||||
New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null
|
||||
|
||||
$csvPath = Join-Path $runDirectory "Spain_Sales_2025.csv"
|
||||
$summaryPath = Join-Path $runDirectory "Spain_Sales_2025_summary.txt"
|
||||
|
||||
$sql = @"
|
||||
SELECT
|
||||
'TRES' AS TSC,
|
||||
'Spanien' AS Land,
|
||||
'Sage' AS SourceSystem,
|
||||
c.CodigoEmpresa AS CompanyCode,
|
||||
c.EjercicioAlbaran AS DeliveryYear,
|
||||
c.SerieAlbaran AS DeliverySeries,
|
||||
c.NumeroAlbaran AS DeliveryNumber,
|
||||
c.EjercicioFactura AS InvoiceYear,
|
||||
c.SerieFactura AS InvoiceSeries,
|
||||
c.NumeroFactura AS InvoiceNumber,
|
||||
l.Orden AS PositionOnInvoice,
|
||||
l.LineasPosicion AS SourceLineId,
|
||||
l.CodigoArticulo AS Material,
|
||||
l.DescripcionArticulo AS Name,
|
||||
l.Descripcion2Articulo AS Description2,
|
||||
l.DescripcionLinea AS DescriptionLine,
|
||||
l.CodigoFamilia AS ProductGroup,
|
||||
l.CodigoSubfamilia AS ProductSubGroup,
|
||||
CAST(l.Unidades AS decimal(19, 6)) AS Quantity,
|
||||
c.CodigoCliente AS CustomerNumber,
|
||||
c.Nombre AS CustomerName,
|
||||
c.CodigoNacion AS CustomerCountryCode,
|
||||
c.Nacion AS CustomerCountry,
|
||||
CAST(l.PrecioCoste AS decimal(19, 6)) AS StandardCost,
|
||||
CAST(l.ImporteCoste AS decimal(19, 6)) AS StandardCostValue,
|
||||
'EUR' AS StandardCostCurrency,
|
||||
CAST(l.ImporteNeto AS decimal(19, 6)) AS SalesPriceValue,
|
||||
'EUR' AS SalesCurrency,
|
||||
'EUR' AS DocumentCurrency,
|
||||
'EUR' AS CompanyCurrency,
|
||||
c.CodigoDivisa AS SageCurrencyCode,
|
||||
CAST(c.BaseImponible AS decimal(19, 6)) AS DocumentNetAmount,
|
||||
CAST(c.TotalIva AS decimal(19, 6)) AS DocumentVatAmount,
|
||||
CAST(c.ImporteFactura AS decimal(19, 6)) AS DocumentGrossAmount,
|
||||
c.FechaFactura AS InvoiceDate,
|
||||
c.FechaAlbaran AS DeliveryDate,
|
||||
l.FechaRegistro AS LineRegistrationDate,
|
||||
c.EjercicioPedido AS OrderYear,
|
||||
c.SeriePedido AS OrderSeries,
|
||||
c.NumeroPedido AS OrderNumber,
|
||||
c.SuPedido AS PurchaseOrderNumber,
|
||||
c.CodigoExportacion_ AS Incoterms2020,
|
||||
c.CondicionExportacion_ AS IncotermsText,
|
||||
c.CodigoComisionista AS SalesResponsibleEmployee,
|
||||
c.StatusAbono AS CreditStatus,
|
||||
c.NoFacturable AS NonBillable,
|
||||
c.TipoNuevaFra AS InvoiceType,
|
||||
c.StatusFacturado AS BillingStatus,
|
||||
CASE
|
||||
WHEN c.TipoNuevaFra = 2 OR c.SerieFactura = 'REC' OR c.StatusAbono <> 0 THEN 'Credit Note'
|
||||
ELSE 'Invoice'
|
||||
END AS DocumentType
|
||||
FROM dbo.CabeceraAlbaranCliente c
|
||||
JOIN dbo.LineasAlbaranCliente l
|
||||
ON l.CodigoEmpresa = c.CodigoEmpresa
|
||||
AND l.EjercicioAlbaran = c.EjercicioAlbaran
|
||||
AND l.SerieAlbaran = c.SerieAlbaran
|
||||
AND l.NumeroAlbaran = c.NumeroAlbaran
|
||||
WHERE c.FechaFactura >= @FromDate
|
||||
AND c.FechaFactura < @ToDate
|
||||
ORDER BY
|
||||
c.FechaFactura,
|
||||
c.SerieFactura,
|
||||
c.NumeroFactura,
|
||||
l.Orden;
|
||||
"@
|
||||
|
||||
$result = Export-QueryToCsv -Sql $sql -Path $csvPath
|
||||
|
||||
@"
|
||||
Sage Spain Sales CSV export
|
||||
===========================
|
||||
|
||||
Created: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
||||
Server instance: $ServerInstance
|
||||
Database: $Database
|
||||
From date: $($FromDate.ToString("yyyy-MM-dd"))
|
||||
To date: $($ToDate.ToString("yyyy-MM-dd"))
|
||||
|
||||
Output:
|
||||
$csvPath
|
||||
|
||||
Rows:
|
||||
$($result.Rows)
|
||||
|
||||
SalesPriceValue sum:
|
||||
$($result.SalesPriceValueSum)
|
||||
|
||||
Source:
|
||||
dbo.CabeceraAlbaranCliente joined with dbo.LineasAlbaranCliente
|
||||
|
||||
Filter:
|
||||
CabeceraAlbaranCliente.FechaFactura >= FromDate
|
||||
CabeceraAlbaranCliente.FechaFactura < ToDate
|
||||
|
||||
Notes:
|
||||
- Currency is set to EUR because Sage exports EnEuros_=-1 and CodigoDivisa is empty in the analysed rows.
|
||||
- SalesPriceValue uses LineasAlbaranCliente.ImporteNeto.
|
||||
- DocumentNetAmount uses CabeceraAlbaranCliente.BaseImponible.
|
||||
- Credit notes are marked when TipoNuevaFra=2, SerieFactura='REC', or StatusAbono is non-zero.
|
||||
"@ | Set-Content -LiteralPath $summaryPath -Encoding UTF8
|
||||
|
||||
Write-Host "Created:"
|
||||
Write-Host " $csvPath"
|
||||
Write-Host " $summaryPath"
|
||||
Write-Host "Rows: $($result.Rows)"
|
||||
Write-Host "SalesPriceValue sum: $($result.SalesPriceValueSum)"
|
||||
@@ -0,0 +1,410 @@
|
||||
param(
|
||||
[string]$ServerInstance = "localhost",
|
||||
[string]$Database = "",
|
||||
[string[]]$ObjectName = @(),
|
||||
[datetime]$FromDate = "2025-01-01",
|
||||
[datetime]$ToDate = "2026-01-01",
|
||||
[string]$OutputDirectory = (Join-Path $env:USERPROFILE "Desktop"),
|
||||
[int]$SampleRows = 500,
|
||||
[int]$MaxRowsPerObject = 0,
|
||||
[switch]$DiscoverOnly,
|
||||
[switch]$ExportCandidates,
|
||||
[switch]$IncludeSystemDatabases
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function New-Connection {
|
||||
param([string]$DbName)
|
||||
|
||||
$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
|
||||
$builder["Data Source"] = $ServerInstance
|
||||
$builder["Initial Catalog"] = $DbName
|
||||
$builder["Integrated Security"] = $true
|
||||
$builder["TrustServerCertificate"] = $true
|
||||
$builder["Connect Timeout"] = 15
|
||||
return New-Object System.Data.SqlClient.SqlConnection($builder.ConnectionString)
|
||||
}
|
||||
|
||||
function Invoke-DataTable {
|
||||
param(
|
||||
[string]$DbName,
|
||||
[string]$Sql,
|
||||
[hashtable]$Parameters = @{}
|
||||
)
|
||||
|
||||
$conn = New-Connection $DbName
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = $Sql
|
||||
$cmd.CommandTimeout = 300
|
||||
|
||||
foreach ($key in $Parameters.Keys) {
|
||||
$param = $cmd.Parameters.Add("@$key", [System.Data.SqlDbType]::NVarChar, 4000)
|
||||
$param.Value = [string]$Parameters[$key]
|
||||
}
|
||||
|
||||
$table = New-Object System.Data.DataTable
|
||||
try {
|
||||
$conn.Open()
|
||||
$reader = $cmd.ExecuteReader()
|
||||
$table.Load($reader)
|
||||
}
|
||||
finally {
|
||||
$conn.Dispose()
|
||||
}
|
||||
|
||||
return $table
|
||||
}
|
||||
|
||||
function Convert-ToCsvValue {
|
||||
param($Value)
|
||||
|
||||
if ($null -eq $Value -or $Value -is [System.DBNull]) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if ($Value -is [datetime]) {
|
||||
$text = $Value.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
}
|
||||
else {
|
||||
$text = [string]$Value
|
||||
}
|
||||
|
||||
$text = $text.Replace('"', '""')
|
||||
return '"' + $text + '"'
|
||||
}
|
||||
|
||||
function Export-QueryToCsv {
|
||||
param(
|
||||
[string]$DbName,
|
||||
[string]$Sql,
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$conn = New-Connection $DbName
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = $Sql
|
||||
$cmd.CommandTimeout = 0
|
||||
|
||||
$writer = New-Object System.IO.StreamWriter($Path, $false, [System.Text.Encoding]::UTF8)
|
||||
$rowCount = 0
|
||||
|
||||
try {
|
||||
$conn.Open()
|
||||
$reader = $cmd.ExecuteReader()
|
||||
|
||||
$headers = for ($i = 0; $i -lt $reader.FieldCount; $i++) {
|
||||
Convert-ToCsvValue $reader.GetName($i)
|
||||
}
|
||||
$writer.WriteLine(($headers -join ";"))
|
||||
|
||||
while ($reader.Read()) {
|
||||
$values = for ($i = 0; $i -lt $reader.FieldCount; $i++) {
|
||||
Convert-ToCsvValue $reader.GetValue($i)
|
||||
}
|
||||
$writer.WriteLine(($values -join ";"))
|
||||
$rowCount++
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$writer.Dispose()
|
||||
$conn.Dispose()
|
||||
}
|
||||
|
||||
return $rowCount
|
||||
}
|
||||
|
||||
function Quote-NamePart {
|
||||
param([string]$Name)
|
||||
|
||||
return "[" + $Name.Replace("]", "]]") + "]"
|
||||
}
|
||||
|
||||
function Split-SqlObjectName {
|
||||
param([string]$Name)
|
||||
|
||||
$parts = $Name.Split(".", 2)
|
||||
if ($parts.Count -eq 1) {
|
||||
return [pscustomobject]@{ SchemaName = "dbo"; ObjectName = $parts[0] }
|
||||
}
|
||||
|
||||
return [pscustomobject]@{ SchemaName = $parts[0].Trim("[", "]"); ObjectName = $parts[1].Trim("[", "]") }
|
||||
}
|
||||
|
||||
function Get-UserDatabases {
|
||||
$sql = @"
|
||||
SELECT name
|
||||
FROM sys.databases
|
||||
WHERE state_desc = 'ONLINE'
|
||||
AND HAS_DBACCESS(name) = 1
|
||||
$(if ($IncludeSystemDatabases) { "" } else { "AND database_id > 4" })
|
||||
ORDER BY name;
|
||||
"@
|
||||
|
||||
Invoke-DataTable "master" $sql | ForEach-Object { $_.name }
|
||||
}
|
||||
|
||||
function Get-CandidateObjects {
|
||||
param([string]$DbName)
|
||||
|
||||
$sql = @"
|
||||
WITH object_columns AS (
|
||||
SELECT
|
||||
s.name AS SchemaName,
|
||||
o.name AS ObjectName,
|
||||
o.type_desc AS ObjectType,
|
||||
c.name AS ColumnName,
|
||||
t.name AS TypeName,
|
||||
c.max_length,
|
||||
c.precision,
|
||||
c.scale
|
||||
FROM sys.objects o
|
||||
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
||||
JOIN sys.columns c ON c.object_id = o.object_id
|
||||
JOIN sys.types t ON t.user_type_id = c.user_type_id
|
||||
WHERE o.type IN ('U', 'V')
|
||||
AND o.is_ms_shipped = 0
|
||||
),
|
||||
scored AS (
|
||||
SELECT
|
||||
SchemaName,
|
||||
ObjectName,
|
||||
ObjectType,
|
||||
SUM(CASE WHEN LOWER(ObjectName) LIKE '%fact%' OR LOWER(ObjectName) LIKE '%invoice%' OR LOWER(ObjectName) LIKE '%venta%' OR LOWER(ObjectName) LIKE '%sales%' OR LOWER(ObjectName) LIKE '%albar%' OR LOWER(ObjectName) LIKE '%pedido%' THEN 5 ELSE 0 END) +
|
||||
SUM(CASE WHEN LOWER(ColumnName) LIKE '%fecha%' OR LOWER(ColumnName) LIKE '%date%' THEN 2 ELSE 0 END) +
|
||||
SUM(CASE WHEN LOWER(ColumnName) LIKE '%cliente%' OR LOWER(ColumnName) LIKE '%customer%' THEN 2 ELSE 0 END) +
|
||||
SUM(CASE WHEN LOWER(ColumnName) LIKE '%articulo%' OR LOWER(ColumnName) LIKE '%item%' OR LOWER(ColumnName) LIKE '%producto%' THEN 2 ELSE 0 END) +
|
||||
SUM(CASE WHEN LOWER(ColumnName) LIKE '%importe%' OR LOWER(ColumnName) LIKE '%neto%' OR LOWER(ColumnName) LIKE '%total%' OR LOWER(ColumnName) LIKE '%amount%' THEN 3 ELSE 0 END) +
|
||||
SUM(CASE WHEN LOWER(ColumnName) LIKE '%cantidad%' OR LOWER(ColumnName) LIKE '%quantity%' OR LOWER(ColumnName) LIKE '%unidades%' THEN 2 ELSE 0 END) AS Score,
|
||||
COUNT(*) AS ColumnCount,
|
||||
STRING_AGG(CONVERT(nvarchar(max), ColumnName), ', ') WITHIN GROUP (ORDER BY ColumnName) AS Columns
|
||||
FROM object_columns
|
||||
GROUP BY SchemaName, ObjectName, ObjectType
|
||||
)
|
||||
SELECT TOP (80)
|
||||
DB_NAME() AS DatabaseName,
|
||||
SchemaName,
|
||||
ObjectName,
|
||||
ObjectType,
|
||||
Score,
|
||||
ColumnCount,
|
||||
Columns
|
||||
FROM scored
|
||||
WHERE Score > 0
|
||||
ORDER BY Score DESC, ObjectName;
|
||||
"@
|
||||
|
||||
Invoke-DataTable $DbName $sql
|
||||
}
|
||||
|
||||
function Get-DateColumns {
|
||||
param(
|
||||
[string]$DbName,
|
||||
[string]$SchemaName,
|
||||
[string]$ObjectNameValue
|
||||
)
|
||||
|
||||
$sql = @"
|
||||
SELECT c.name AS ColumnName
|
||||
FROM sys.objects o
|
||||
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
||||
JOIN sys.columns c ON c.object_id = o.object_id
|
||||
JOIN sys.types t ON t.user_type_id = c.user_type_id
|
||||
WHERE s.name = @schema
|
||||
AND o.name = @object
|
||||
AND (
|
||||
t.name IN ('date', 'datetime', 'datetime2', 'smalldatetime')
|
||||
OR LOWER(c.name) LIKE '%fecha%'
|
||||
OR LOWER(c.name) LIKE '%date%'
|
||||
)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN LOWER(c.name) LIKE '%fact%' OR LOWER(c.name) LIKE '%invoice%' THEN 0
|
||||
WHEN LOWER(c.name) LIKE '%fecha%' OR LOWER(c.name) LIKE '%date%' THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
c.column_id;
|
||||
"@
|
||||
|
||||
Invoke-DataTable $DbName $sql @{ schema = $SchemaName; object = $ObjectNameValue } |
|
||||
ForEach-Object { $_.ColumnName }
|
||||
}
|
||||
|
||||
function Build-SelectSql {
|
||||
param(
|
||||
[string]$SchemaName,
|
||||
[string]$ObjectNameValue,
|
||||
[string]$DateColumn,
|
||||
[int]$TopRows
|
||||
)
|
||||
|
||||
$topClause = if ($TopRows -gt 0) { "TOP ($TopRows)" } else { "" }
|
||||
$qualified = "$(Quote-NamePart $SchemaName).$(Quote-NamePart $ObjectNameValue)"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($DateColumn)) {
|
||||
return "SELECT $topClause * FROM $qualified;"
|
||||
}
|
||||
|
||||
$from = $FromDate.ToString("yyyy-MM-dd")
|
||||
$to = $ToDate.ToString("yyyy-MM-dd")
|
||||
$dateColumnSql = Quote-NamePart $DateColumn
|
||||
|
||||
return @"
|
||||
SELECT $topClause *
|
||||
FROM $qualified
|
||||
WHERE TRY_CONVERT(date, $dateColumnSql) >= CONVERT(date, '$from')
|
||||
AND TRY_CONVERT(date, $dateColumnSql) < CONVERT(date, '$to')
|
||||
ORDER BY TRY_CONVERT(date, $dateColumnSql);
|
||||
"@
|
||||
}
|
||||
|
||||
function Normalize-FileName {
|
||||
param([string]$Value)
|
||||
|
||||
return ($Value -replace '[\\/:*?"<>|]', '_')
|
||||
}
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$runDirectory = Join-Path $OutputDirectory "Sage_SQL_CSV_Export_$timestamp"
|
||||
New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null
|
||||
|
||||
$databases = if ([string]::IsNullOrWhiteSpace($Database)) {
|
||||
@(Get-UserDatabases)
|
||||
}
|
||||
else {
|
||||
@($Database)
|
||||
}
|
||||
|
||||
$summary = New-Object System.Collections.Generic.List[object]
|
||||
$allCandidates = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
foreach ($db in $databases) {
|
||||
Write-Host "Scanning database: $db"
|
||||
try {
|
||||
$candidates = @(Get-CandidateObjects $db)
|
||||
foreach ($candidate in $candidates) {
|
||||
$allCandidates.Add($candidate)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$summary.Add([pscustomobject]@{
|
||||
Database = $db
|
||||
Object = ""
|
||||
Action = "Discovery failed"
|
||||
Rows = 0
|
||||
File = ""
|
||||
Error = $_.Exception.Message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$candidatePath = Join-Path $runDirectory "candidate_objects.csv"
|
||||
if ($allCandidates.Count -gt 0) {
|
||||
$allCandidates | Export-Csv -LiteralPath $candidatePath -NoTypeInformation -Encoding UTF8 -Delimiter ";"
|
||||
}
|
||||
|
||||
if (-not $DiscoverOnly) {
|
||||
$objectsToExport = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
foreach ($name in $ObjectName) {
|
||||
if ([string]::IsNullOrWhiteSpace($name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Database)) {
|
||||
throw "When -ObjectName is used, pass -Database as well."
|
||||
}
|
||||
|
||||
$parsed = Split-SqlObjectName $name
|
||||
$objectsToExport.Add([pscustomobject]@{
|
||||
DatabaseName = $Database
|
||||
SchemaName = $parsed.SchemaName
|
||||
ObjectName = $parsed.ObjectName
|
||||
})
|
||||
}
|
||||
|
||||
if ($ExportCandidates) {
|
||||
foreach ($candidate in ($allCandidates | Sort-Object DatabaseName, @{Expression="Score"; Descending=$true} | Select-Object -First 25)) {
|
||||
$objectsToExport.Add([pscustomobject]@{
|
||||
DatabaseName = $candidate.DatabaseName
|
||||
SchemaName = $candidate.SchemaName
|
||||
ObjectName = $candidate.ObjectName
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($object in $objectsToExport) {
|
||||
$db = $object.DatabaseName
|
||||
$schema = $object.SchemaName
|
||||
$objectNameValue = $object.ObjectName
|
||||
|
||||
try {
|
||||
$dateColumn = @(Get-DateColumns $db $schema $objectNameValue | Select-Object -First 1)[0]
|
||||
$limit = if ($MaxRowsPerObject -gt 0) { $MaxRowsPerObject } elseif ($ObjectName.Count -gt 0) { 0 } else { $SampleRows }
|
||||
$sql = Build-SelectSql $schema $objectNameValue $dateColumn $limit
|
||||
$fileName = Normalize-FileName "$db.$schema.$objectNameValue.csv"
|
||||
$path = Join-Path $runDirectory $fileName
|
||||
Write-Host "Exporting $db.$schema.$objectNameValue -> $path"
|
||||
$rows = Export-QueryToCsv $db $sql $path
|
||||
|
||||
$summary.Add([pscustomobject]@{
|
||||
Database = $db
|
||||
Object = "$schema.$objectNameValue"
|
||||
Action = "Exported"
|
||||
Rows = $rows
|
||||
File = $path
|
||||
DateColumn = $dateColumn
|
||||
Error = ""
|
||||
})
|
||||
}
|
||||
catch {
|
||||
$summary.Add([pscustomobject]@{
|
||||
Database = $db
|
||||
Object = "$schema.$objectNameValue"
|
||||
Action = "Export failed"
|
||||
Rows = 0
|
||||
File = ""
|
||||
DateColumn = ""
|
||||
Error = $_.Exception.Message
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$summaryPath = Join-Path $runDirectory "export_summary.csv"
|
||||
$summary | Export-Csv -LiteralPath $summaryPath -NoTypeInformation -Encoding UTF8 -Delimiter ";"
|
||||
|
||||
$readmePath = Join-Path $runDirectory "README.txt"
|
||||
@"
|
||||
Sage SQL CSV export
|
||||
===================
|
||||
|
||||
Server instance: $ServerInstance
|
||||
Database filter: $(if ($Database) { $Database } else { "(all accessible user databases)" })
|
||||
From date: $($FromDate.ToString("yyyy-MM-dd"))
|
||||
To date: $($ToDate.ToString("yyyy-MM-dd"))
|
||||
|
||||
Files:
|
||||
- candidate_objects.csv: SQL tables/views that look relevant for sales/invoices.
|
||||
- export_summary.csv: export status and row counts.
|
||||
- *.csv: exported samples or selected full exports.
|
||||
|
||||
Recommended workflow:
|
||||
1. Run discovery first:
|
||||
.\Export-SageSqlCsv.ps1 -DiscoverOnly
|
||||
2. Send candidate_objects.csv to Trafag/IT for selection.
|
||||
3. Export selected objects:
|
||||
.\Export-SageSqlCsv.ps1 -Database "DATABASE_NAME" -ObjectName "schema.table_or_view"
|
||||
4. If the selected object is very large, add:
|
||||
-FromDate "2025-01-01" -ToDate "2026-01-01" -MaxRowsPerObject 100000
|
||||
|
||||
The script only reads data. It does not change SQL Server or Sage.
|
||||
"@ | Set-Content -LiteralPath $readmePath -Encoding UTF8
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Created folder:"
|
||||
Write-Host " $runDirectory"
|
||||
Write-Host ""
|
||||
Write-Host "Main files:"
|
||||
Write-Host " $candidatePath"
|
||||
Write-Host " $summaryPath"
|
||||
@@ -0,0 +1,334 @@
|
||||
param(
|
||||
[string[]]$SqlInstance = @(),
|
||||
[string]$OutputDirectory = (Join-Path $env:USERPROFILE "Desktop"),
|
||||
[switch]$ScanProgramFiles
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Add-Section {
|
||||
param([string]$Title)
|
||||
|
||||
$script:reportLines += ""
|
||||
$script:reportLines += "============================================================"
|
||||
$script:reportLines += $Title
|
||||
$script:reportLines += "============================================================"
|
||||
}
|
||||
|
||||
function Add-Line {
|
||||
param([string]$Text = "")
|
||||
|
||||
$script:reportLines += $Text
|
||||
}
|
||||
|
||||
function Read-RegistryValues {
|
||||
param([string]$Path)
|
||||
|
||||
try {
|
||||
if (Test-Path $Path) {
|
||||
return Get-ItemProperty -Path $Path -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Add-Line "Registry read failed: $Path :: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-SageUninstallEntries {
|
||||
$paths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
|
||||
foreach ($path in $paths) {
|
||||
Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
$_.DisplayName -and (
|
||||
$_.DisplayName -match "Sage" -or
|
||||
$_.Publisher -match "Sage"
|
||||
)
|
||||
} |
|
||||
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, InstallLocation, UninstallString, PSPath
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SageFileVersions {
|
||||
$roots = @(
|
||||
$env:ProgramFiles,
|
||||
${env:ProgramFiles(x86)},
|
||||
$env:ProgramData
|
||||
) | Where-Object { $_ -and (Test-Path $_) }
|
||||
|
||||
foreach ($root in $roots) {
|
||||
Get-ChildItem -LiteralPath $root -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -match "Sage" } |
|
||||
ForEach-Object {
|
||||
Get-ChildItem -LiteralPath $_.FullName -Recurse -File -Include *.exe,*.dll -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.VersionInfo.ProductName -match "Sage" -or $_.VersionInfo.CompanyName -match "Sage" -or $_.Name -match "Sage" } |
|
||||
Select-Object FullName,
|
||||
@{Name="FileVersion"; Expression={$_.VersionInfo.FileVersion}},
|
||||
@{Name="ProductVersion"; Expression={$_.VersionInfo.ProductVersion}},
|
||||
@{Name="ProductName"; Expression={$_.VersionInfo.ProductName}},
|
||||
@{Name="CompanyName"; Expression={$_.VersionInfo.CompanyName}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SqlInstancesFromRegistry {
|
||||
$instanceRoots = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL",
|
||||
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Microsoft SQL Server\Instance Names\SQL"
|
||||
)
|
||||
|
||||
$instances = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
foreach ($root in $instanceRoots) {
|
||||
$values = Read-RegistryValues $root
|
||||
if (-not $values) {
|
||||
continue
|
||||
}
|
||||
|
||||
$values.PSObject.Properties |
|
||||
Where-Object { $_.Name -notlike "PS*" } |
|
||||
ForEach-Object {
|
||||
$instanceName = $_.Name
|
||||
$instanceId = [string]$_.Value
|
||||
$setupPaths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instanceId\Setup",
|
||||
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Microsoft SQL Server\$instanceId\Setup"
|
||||
)
|
||||
|
||||
foreach ($setupPath in $setupPaths) {
|
||||
$setup = Read-RegistryValues $setupPath
|
||||
if ($setup) {
|
||||
$instances.Add([pscustomobject]@{
|
||||
InstanceName = $instanceName
|
||||
InstanceId = $instanceId
|
||||
Edition = $setup.Edition
|
||||
Version = $setup.Version
|
||||
PatchLevel = $setup.PatchLevel
|
||||
ProductCode = $setup.ProductCode
|
||||
SQLPath = $setup.SQLPath
|
||||
SetupPath = $setupPath
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $instances
|
||||
}
|
||||
|
||||
function Get-SqlServices {
|
||||
Get-CimInstance Win32_Service -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like "MSSQL*" -or $_.Name -like "SQLAgent*" -or $_.DisplayName -like "*SQL Server*" } |
|
||||
Select-Object Name, DisplayName, State, StartMode, PathName
|
||||
}
|
||||
|
||||
function Resolve-Sqlcmd {
|
||||
$cmd = Get-Command sqlcmd.exe -ErrorAction SilentlyContinue
|
||||
if ($cmd) {
|
||||
return $cmd.Source
|
||||
}
|
||||
|
||||
$candidates = @(
|
||||
"$env:ProgramFiles\Microsoft SQL Server\Client SDK\ODBC\*\Tools\Binn\sqlcmd.exe",
|
||||
"${env:ProgramFiles(x86)}\Microsoft SQL Server\Client SDK\ODBC\*\Tools\Binn\sqlcmd.exe",
|
||||
"$env:ProgramFiles\Microsoft SQL Server\*\Tools\Binn\sqlcmd.exe",
|
||||
"${env:ProgramFiles(x86)}\Microsoft SQL Server\*\Tools\Binn\sqlcmd.exe"
|
||||
)
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
$match = Get-ChildItem -Path $candidate -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($match) {
|
||||
return $match.FullName
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-SqlVersionQuery {
|
||||
param(
|
||||
[string]$SqlcmdPath,
|
||||
[string]$Instance
|
||||
)
|
||||
|
||||
$query = @"
|
||||
SET NOCOUNT ON;
|
||||
SELECT
|
||||
@@VERSION AS FullVersion,
|
||||
SERVERPROPERTY('ProductVersion') AS ProductVersion,
|
||||
SERVERPROPERTY('ProductLevel') AS ProductLevel,
|
||||
SERVERPROPERTY('Edition') AS Edition,
|
||||
SERVERPROPERTY('EngineEdition') AS EngineEdition,
|
||||
SERVERPROPERTY('MachineName') AS MachineName,
|
||||
SERVERPROPERTY('ServerName') AS ServerName,
|
||||
SERVERPROPERTY('InstanceName') AS InstanceName,
|
||||
SERVERPROPERTY('Collation') AS Collation;
|
||||
"@
|
||||
|
||||
$tempQuery = Join-Path $env:TEMP ("sql_version_query_{0}.sql" -f ([guid]::NewGuid().ToString("N")))
|
||||
Set-Content -LiteralPath $tempQuery -Value $query -Encoding UTF8
|
||||
|
||||
try {
|
||||
$output = & $SqlcmdPath -S $Instance -E -i $tempQuery -W -s "|" -b 2>&1
|
||||
[pscustomobject]@{
|
||||
Instance = $Instance
|
||||
Success = $LASTEXITCODE -eq 0
|
||||
Output = ($output -join [Environment]::NewLine)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
[pscustomobject]@{
|
||||
Instance = $Instance
|
||||
Success = $false
|
||||
Output = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $tempQuery -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
|
||||
|
||||
$reportPath = Join-Path $OutputDirectory "Sage_SQL_Environment_$timestamp.txt"
|
||||
$jsonPath = Join-Path $OutputDirectory "Sage_SQL_Environment_$timestamp.json"
|
||||
$reportLines = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
$computer = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
|
||||
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
|
||||
$sageEntries = @(Get-SageUninstallEntries)
|
||||
$sqlRegistryInstances = @(Get-SqlInstancesFromRegistry)
|
||||
$sqlServices = @(Get-SqlServices)
|
||||
$sageFileVersions = @()
|
||||
|
||||
if ($ScanProgramFiles) {
|
||||
$sageFileVersions = @(Get-SageFileVersions)
|
||||
}
|
||||
|
||||
$sqlcmdPath = Resolve-Sqlcmd
|
||||
$queryInstances = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($instance in $SqlInstance) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($instance)) {
|
||||
$queryInstances.Add($instance)
|
||||
}
|
||||
}
|
||||
|
||||
if ($queryInstances.Count -eq 0) {
|
||||
$machineName = $env:COMPUTERNAME
|
||||
foreach ($instance in $sqlRegistryInstances) {
|
||||
if ($instance.InstanceName -eq "MSSQLSERVER") {
|
||||
$queryInstances.Add("localhost")
|
||||
}
|
||||
else {
|
||||
$queryInstances.Add("localhost\$($instance.InstanceName)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$queryInstances = @($queryInstances | Select-Object -Unique)
|
||||
$sqlQueryResults = @()
|
||||
if ($sqlcmdPath -and $queryInstances.Count -gt 0) {
|
||||
foreach ($instance in $queryInstances) {
|
||||
$sqlQueryResults += Invoke-SqlVersionQuery -SqlcmdPath $sqlcmdPath -Instance $instance
|
||||
}
|
||||
}
|
||||
|
||||
Add-Section "Capture metadata"
|
||||
Add-Line "Timestamp: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")"
|
||||
Add-Line "Computer: $env:COMPUTERNAME"
|
||||
Add-Line "User: $env:USERDOMAIN\$env:USERNAME"
|
||||
Add-Line "Output text: $reportPath"
|
||||
Add-Line "Output json: $jsonPath"
|
||||
|
||||
Add-Section "Windows / machine"
|
||||
Add-Line "Manufacturer: $($computer.Manufacturer)"
|
||||
Add-Line "Model: $($computer.Model)"
|
||||
Add-Line "OS: $($os.Caption)"
|
||||
Add-Line "OS Version: $($os.Version)"
|
||||
Add-Line "OS Build: $($os.BuildNumber)"
|
||||
Add-Line "Install date: $($os.InstallDate)"
|
||||
|
||||
Add-Section "Sage entries from installed programs"
|
||||
if ($sageEntries.Count -eq 0) {
|
||||
Add-Line "No Sage uninstall entries found."
|
||||
}
|
||||
else {
|
||||
$sageEntries | Format-List | Out-String | ForEach-Object { Add-Line $_.TrimEnd() }
|
||||
}
|
||||
|
||||
Add-Section "Sage file versions"
|
||||
if (-not $ScanProgramFiles) {
|
||||
Add-Line "Skipped. Re-run with -ScanProgramFiles for file version scan."
|
||||
}
|
||||
elseif ($sageFileVersions.Count -eq 0) {
|
||||
Add-Line "No Sage file versions found under Program Files / ProgramData."
|
||||
}
|
||||
else {
|
||||
$sageFileVersions | Sort-Object ProductName, ProductVersion, FullName | Format-Table -AutoSize | Out-String | ForEach-Object { Add-Line $_.TrimEnd() }
|
||||
}
|
||||
|
||||
Add-Section "SQL Server instances from registry"
|
||||
if ($sqlRegistryInstances.Count -eq 0) {
|
||||
Add-Line "No SQL Server registry instances found."
|
||||
}
|
||||
else {
|
||||
$sqlRegistryInstances | Format-List | Out-String | ForEach-Object { Add-Line $_.TrimEnd() }
|
||||
}
|
||||
|
||||
Add-Section "SQL Server services"
|
||||
if ($sqlServices.Count -eq 0) {
|
||||
Add-Line "No SQL Server services found."
|
||||
}
|
||||
else {
|
||||
$sqlServices | Format-Table -AutoSize | Out-String | ForEach-Object { Add-Line $_.TrimEnd() }
|
||||
}
|
||||
|
||||
Add-Section "SQL Server live query"
|
||||
Add-Line "sqlcmd path: $(if ($sqlcmdPath) { $sqlcmdPath } else { "not found" })"
|
||||
if (-not $sqlcmdPath) {
|
||||
Add-Line "Cannot query SQL Server live because sqlcmd.exe was not found."
|
||||
}
|
||||
elseif ($queryInstances.Count -eq 0) {
|
||||
Add-Line "No SQL instances to query. Pass -SqlInstance server\instance if needed."
|
||||
}
|
||||
else {
|
||||
foreach ($result in $sqlQueryResults) {
|
||||
Add-Line ""
|
||||
Add-Line "Instance: $($result.Instance)"
|
||||
Add-Line "Success: $($result.Success)"
|
||||
Add-Line $result.Output
|
||||
}
|
||||
}
|
||||
|
||||
$data = [pscustomobject]@{
|
||||
CapturedAt = (Get-Date).ToString("o")
|
||||
ComputerName = $env:COMPUTERNAME
|
||||
UserName = "$env:USERDOMAIN\$env:USERNAME"
|
||||
Windows = [pscustomobject]@{
|
||||
Caption = $os.Caption
|
||||
Version = $os.Version
|
||||
BuildNumber = $os.BuildNumber
|
||||
InstallDate = $os.InstallDate
|
||||
}
|
||||
SageUninstallEntries = $sageEntries
|
||||
SageFileVersions = $sageFileVersions
|
||||
SqlRegistryInstances = $sqlRegistryInstances
|
||||
SqlServices = $sqlServices
|
||||
SqlcmdPath = $sqlcmdPath
|
||||
SqlQueryResults = $sqlQueryResults
|
||||
}
|
||||
|
||||
$reportLines | Set-Content -LiteralPath $reportPath -Encoding UTF8
|
||||
$data | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
|
||||
|
||||
Write-Host "Created:"
|
||||
Write-Host " $reportPath"
|
||||
Write-Host " $jsonPath"
|
||||
Reference in New Issue
Block a user