Guion de la visita a Super Service · todo copiar y pegar, en orden
| Qué | El dato |
|---|---|
| Servidor | Entrar como administrador. Sin eso, el paso 5 no se puede hacer |
| Sage | Abierto, con la empresa PANAMA LININGS, S.A. cargada. Suele exigir estar abierto para dejar leer por ODBC |
| Usuario de Sage | Peachtree |
| Contraseña de Sage | smart10 — si no entra, prueba Peachtree. Si tampoco, se cambia en Maintain → Users → User Security, botón Change |
| Office | https://api.m2goffice.com · almacén Principal |
| Token de Office | m2go_sk_9f0xvyp7_Rd-q-bFWZIyLQgkLf7krJg2RSTaAsfl41Ir95bgHN8SJKR3h4bkJchFSXJ6zw-JWYa creado y probado contra producción. Permiso: escribir inventario, nada más. |
En Sage, con la empresa abierta: File → Back Up… → marca Include company name in the backup file name → Back Up y lo guardas en el escritorio del servidor.
No es por desconfianza del procedimiento: es que si hoy se toca algo por error, con la copia se vuelve atrás en cinco minutos y sin llamar a nadie.
Abre PowerShell como administrador y pega esto tal cual. Solo comprueba que el servidor habla con Office: no manda ningún artículo.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
try {
Invoke-RestMethod -Uri "https://api.m2goffice.com/api/public/v1/inventario/sincronizar" -Method Post `
-ContentType "application/json" -Body "{}" -ErrorAction Stop
} catch {
$codigo = $_.Exception.Response.StatusCode.value__
if ($codigo -eq 401) { "BIEN: el servidor llega a Office (401 = falta el token, es lo esperado)" }
else { "OJO: respuesta inesperada -> $codigo. Avisa antes de seguir." }
}
Pega esto una vez en cada ventana de PowerShell que abras hoy. Crea el
comando Mandar, que publica cualquier salida en el tablón de esta página:
function Mandar($cosa) {
$t = ($cosa | Out-String)
Invoke-RestMethod -Uri "https://rbss.m2g.dev/api/pegar?de=servidor" -Method Post `
-Body ([Text.Encoding]::UTF8.GetBytes($t)) -ContentType "text/plain; charset=utf-8" | Out-Null
"Mandado al tablon."
}
A partir de ahí, cualquier cosa se me manda con Mandar $loquesea. Compruébalo:
Mandar "Hola, estoy en el servidor de Super Service"
La delata un fichero llamado CrystalReports.udl:
Get-ChildItem C:\, D:\ -Recurse -Filter "CrystalReports.udl" -EA 0 |
Select-Object -First 5 FullName, LastWriteTime
Apunta la ruta que salga (algo como
C:\Sage\Peachtree\Company\panamali).
Y comprueba que el motor de la base de datos está vivo:
Get-Service | Where-Object { $_.Name -match 'psql|pervasive|actian|zen' } |
Format-Table Name, Status, DisplayName -Auto
Running. Si están todos parados,
Sage no está realmente abierto.Ábrelo con este comando, no buscando «ODBC» en el menú de inicio (ese abre el de 64 bits, que no sirve):
Start-Process C:\Windows\SysWOW64\odbcad32.exe
Dentro de la ventana:
SAGE50_PANAMALININGS (el script lo busca por ese nombre).Comprobar que quedó registrado:
Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\ODBC\ODBC.INI\ODBC Data Sources' |
Select-Object * -Exclude PS* | Format-List
SAGE50_PANAMALININGS.Abre una ventana de PowerShell de 32 bits (es otro programa distinto):
Start-Process C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe
Y dentro de esa ventana nueva, cambia la contraseña y pega:
$clave = "smart10"
$cn = New-Object System.Data.Odbc.OdbcConnection("DSN=SAGE50_PANAMALININGS;UID=Peachtree;PWD=$clave;")
$cn.Open()
"Conectado: " + $cn.State
$cn.GetSchema("Tables") | Where-Object { $_.TABLE_NAME -match 'invent|item|stock' } |
Select-Object TABLE_NAME | Format-Table -Auto
Si conecta, saca la lista de columnas de la tabla de inventario:
$cmd = New-Object System.Data.Odbc.OdbcCommand("SELECT * FROM Inventory", $cn)
$r = $cmd.ExecuteReader()
0..($r.FieldCount-1) | ForEach-Object { "{0,-30} {1}" -f $r.GetName($_), $r.GetFieldType($_).Name }
$r.Close(); $cn.Close()
Y me la mandas sin copiar nada:
$cn.Open()
$cmd = New-Object System.Data.Odbc.OdbcCommand("SELECT * FROM Inventory", $cn)
$r = $cmd.ExecuteReader()
$columnas = 0..($r.FieldCount-1) | ForEach-Object { "{0,-30} {1}" -f $r.GetName($_), $r.GetFieldType($_).Name }
$r.Close(); $cn.Close()
Mandar $columnas
Con ella ajusto la consulta del paso siguiente en dos minutos y te la dejo en el tablón, lista para copiar. Sin eso, el envío sale con las columnas equivocadas.
Guarda esto como C:\SmartService\enviar-inventario.ps1. Solo se cambian
las cuatro primeras líneas (y la consulta, si te la he ajustado):
$Token = "m2go_sk_9f0xvyp7_Rd-q-bFWZIyLQgkLf7krJg2RSTaAsfl41Ir95bgHN8SJKR3h4bkJchFSXJ6zw-JW"
$Clave = "smart10"
$Dsn = "SAGE50_PANAMALININGS"
$Consulta = "SELECT * FROM Inventory" # te la ajusto yo con las columnas del paso 5
# ---------------- de aqui para abajo no se toca ----------------
$ErrorActionPreference = "Stop"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$Url = "https://api.m2goffice.com/api/public/v1/inventario/sincronizar"
$Log = "C:\SmartService\envios.log"
New-Item -ItemType Directory -Force -Path (Split-Path $Log) | Out-Null
function Escribe($t) { $l = "$(Get-Date -f 'yyyy-MM-dd HH:mm:ss') $t"; Write-Host $l; Add-Content $Log $l -Encoding UTF8 }
# 1. Leer de Sage (SOLO LECTURA)
$cn = New-Object System.Data.Odbc.OdbcConnection("DSN=$Dsn;UID=Peachtree;PWD=$Clave;")
$cn.Open()
$tabla = New-Object System.Data.DataTable
(New-Object System.Data.Odbc.OdbcDataAdapter($Consulta, $cn)).Fill($tabla) | Out-Null
$cn.Close()
Escribe "Leidos $($tabla.Rows.Count) articulos de Sage"
# 2. Traducir al formato de Office: codigo, nombre, cantidad, precio
function Campo($fila, $nombres) {
foreach ($n in $nombres) { if ($fila.Table.Columns.Contains($n)) { $v = $fila[$n]; if ($v -isnot [DBNull]) { return $v } } }
return $null
}
$Articulos = @()
foreach ($f in $tabla.Rows) {
$codigo = Campo $f @("Item ID","ItemID","Item_ID","codigo")
$nombre = Campo $f @("Item Description","Description","ItemDescription","nombre")
if (-not $codigo -or -not $nombre) { continue }
$cant = Campo $f @("Qty on Hand","Quantity On Hand","QtyOnHand","Stocking Qty","cantidad")
$prec = Campo $f @("Price Level 1","Sales Price","Price1","precio")
$Articulos += [pscustomobject]@{
codigo = "$codigo".Trim()
nombre = "$nombre".Trim()
cantidad = if ($cant -ne $null) { [int][math]::Round([double]$cant) } else { $null }
precio = if ($prec -ne $null) { ("{0:F2}" -f [double]$prec) } else { $null }
}
}
Escribe "Preparados $($Articulos.Count) articulos para Office"
# 3. Enviar en lotes de 500 (es el maximo que acepta Office)
$Ejecucion = Get-Date -Format 'yyyyMMdd-HHmmss'
$Tam = 500; $Lotes = [math]::Ceiling($Articulos.Count / $Tam)
for ($i = 0; $i -lt $Lotes; $i++) {
$Trozo = @($Articulos[($i*$Tam)..([math]::Min(($i+1)*$Tam, $Articulos.Count)-1)])
$Json = (@{ articulos = $Trozo } | ConvertTo-Json -Depth 6)
$Bytes = [Text.Encoding]::UTF8.GetBytes($Json)
try {
$r = Invoke-RestMethod -Uri $Url -Method Post -Body $Bytes `
-ContentType "application/json; charset=utf-8" -TimeoutSec 300 `
-Headers @{ "Authorization" = "Bearer $Token"; "Idempotency-Key" = "ss-$Ejecucion-$i" }
Escribe ("Lote {0}/{1}: recibidos {2}, creados {3}, actualizados {4}, rechazados {5}" -f ($i+1), $Lotes, $r.recibidos, $r.creados, $r.actualizados, $r.rechazados)
if ($r.rechazos) { $r.rechazos | ForEach-Object { Escribe (" RECHAZADO {0}: {1}" -f $_.codigo, $_.motivo) } }
} catch {
Escribe "ERROR en el lote $($i+1): $($_.Exception.Message)"
}
}
Escribe "Fin."
Y se ejecuta con el PowerShell de 32 bits:
C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File C:\SmartService\enviar-inventario.ps1
Y en cuanto termine, me mandas el resultado:
Mandar (Get-Content C:\SmartService\envios.log -Tail 40)
Con el log delante compruebo en Office cuántos artículos entraron de verdad, si algún campo llegó raro y si hay que corregir la consulta y repetir. Repetir no duplica: el código de Sage es la llave.
Desde PowerShell de administrador (este sí, el normal de 64 bits):
$exe = "C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe"
$accion = New-ScheduledTaskAction -Execute $exe `
-Argument "-NoProfile -ExecutionPolicy Bypass -File C:\SmartService\enviar-inventario.ps1"
$disparo = New-ScheduledTaskTrigger -Daily -At 7:00am
$ajustes = New-ScheduledTaskSettingsSet -StartWhenAvailable `
-MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 30)
Register-ScheduledTask -TaskName "SuperService - Inventario Sage 50" `
-Action $accion -Trigger $disparo -Settings $ajustes -User "SYSTEM" -RunLevel Highest -Force
Para lanzarla una vez y comprobar que la tarea en sí funciona:
Start-ScheduledTask -TaskName "SuperService - Inventario Sage 50"
Get-ScheduledTaskInfo -TaskName "SuperService - Inventario Sage 50"
| Lo que ves | Qué es |
|---|---|
401 al enviar | El token está mal pegado o le falta un trozo. Vuelve a copiarlo entero |
403 al enviar | El token es correcto pero sin permiso de inventario. Escríbeme: lo arreglo en un minuto |
409 o «repetida» | Se reutilizó la misma clave de envío. No pasa nada: vuelve a ejecutar el script (genera una nueva cada vez) |
| «Data source name not found» | Estás en el PowerShell de 64 bits, o el DSN se creó como «de usuario» |
| «Could not open connection» | Sage cerrado o el motor Pervasive parado |
| Nombres con acentos rotos | Avísame: se ajusta la lectura en el script |
| Muchos rechazos | Casi siempre es un artículo sin código o sin nombre en Sage. El log dice cuál y por qué; los demás entran igual |
| Momento | Qué hago con eso |
|---|---|
Paso 6 · Mandar $columnas | Te devuelvo la consulta ajustada a tus columnas, en el tablón |
Paso 7 · Mandar (Get-Content …) | Reviso en Office qué entró de verdad y te digo si repetimos |
| Paso 8 | Si el script falla con Sage cerrado, decidimos la hora de la sincronización |
| Cualquier error raro | Mandar $Error[0] y lo miro |