Sage 50 → M2G Office

Guion de la visita a Super Service · todo copiar y pegar, en orden

Qué vamos a conseguir: que el inventario que Super Service lleva en Sage 50 aparezca en M2G Office y se actualice una vez al día, solo.
Todo lo que tocamos en Sage es de solo lectura. Ninguna consulta escribe nada en su contabilidad. Y en Office, la sincronización nunca borra: como mucho crea artículos de más, y eso se limpia después.
Abajo de esta página hay un tablón compartido. No hace falta copiar texto entre el servidor y tu Mac: desde el servidor se publica solo, con el comando del paso 3. Yo lo leo al momento y te contesto ahí mismo.
El orden de la visita
  1. Antes de tocar nada
  2. Copia de seguridad de la empresa en Sage
  3. Comprobar que el servidor llega a Office
  4. Encontrar la carpeta de datos de la empresa
  5. Crear el ODBC de 32 bits
  6. Probar la conexión y sacar las columnas HABLA CONMIGO
  7. Ejecutar el envío HABLA CONMIGO
  8. Programarlo una vez al día
  9. Si algo falla
  1. Antes de tocar nada

    5 minutos · en el servidor Windows de Super Service

    QuéEl dato
    ServidorEntrar como administrador. Sin eso, el paso 5 no se puede hacer
    SageAbierto, con la empresa PANAMA LININGS, S.A. cargada. Suele exigir estar abierto para dejar leer por ODBC
    Usuario de SagePeachtree
    Contraseña de Sagesmart10 — si no entra, prueba Peachtree. Si tampoco, se cambia en Maintain → Users → User Security, botón Change
    Officehttps://api.m2goffice.com · almacén Principal
    Token de Officem2go_sk_9f0xvyp7_Rd-q-bFWZIyLQgkLf7krJg2RSTaAsfl41Ir95bgHN8SJKR3h4bkJchFSXJ6zw-JW
    Ya creado y probado contra producción. Permiso: escribir inventario, nada más.
    Dos reglas para hoy: no cambies nada dentro de Sage salvo la contraseña si no entra, y no borres nada en Office. Si algo no encaja, para y escríbeme: se arregla en un minuto y sin riesgo.
  2. Copia de seguridad de la empresa en Sage

    5 minutos · aunque no vayamos a escribir nada

    En Sage, con la empresa abierta: File → Back Up… → marca Include company name in the backup file nameBack 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.

  3. Comprobar que el servidor llega a Office

    2 minutos · separa «no sale de la red» de «no sé leer Sage»

    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." }
    }
    Lo que tiene que salir: BIEN: el servidor llega a Office.
    Si sale cualquier otra cosa, para aquí. Es la red o el proxy de ellos, y seguir no sirve de nada.

    Y deja preparado el atajo para mandarme cosas

    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"
  4. Encontrar la carpeta de datos de la empresa

    3 minutos · el ODBC la necesita exacta

    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
    Tiene que haber al menos uno en Running. Si están todos parados, Sage no está realmente abierto.
  5. Crear el ODBC de 32 bits

    5 minutos · aquí es donde más se falla

    Á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:

    1. Pestaña DSN de sistema (System DSN). De sistema, no de usuario: si es de usuario, la tarea automática no lo verá.
    2. Agregar → elige Pervasive ODBC Engine Interface → Finalizar.
    3. Data Source Name: escribe exactamente SAGE50_PANAMALININGS (el script lo busca por ese nombre).
    4. Database Name: elige la base de la empresa (la de la ruta del paso 3).
    5. Aceptar dos veces.

    Comprobar que quedó registrado:

    Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\ODBC\ODBC.INI\ODBC Data Sources' |
      Select-Object * -Exclude PS* | Format-List
    Tiene que aparecer SAGE50_PANAMALININGS.
  6. Probar la conexión y sacar las columnas HABLA CONMIGO

    5 minutos · este es el paso que decide la consulta final

    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

    Para aquí: ya tengo la lista en el tablón

    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.

    Si falla la conexión, casi siempre es una de tres: Sage cerrado, estás en el PowerShell de 64 bits por error, o el DSN se creó «de usuario» en vez de «de sistema».
  7. Ejecutar el envío HABLA CONMIGO

    10 minutos · con la consulta que te devuelvo del paso 5

    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)

    Para aquí: déjame mirar qué entró

    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.

  8. Programarlo una vez al día

    3 minutos · SOLO cuando el paso 6 haya salido bien y yo lo haya revisado

    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
    La prueba que hay que hacer antes de irse: cierra Sage y ejecuta el script a mano otra vez. Si falla con Sage cerrado, la tarea de las 7:00 fallará todas las mañanas hasta que alguien abra Sage. En ese caso, o se deja una sesión abierta en el servidor, o movemos la hora a horario de oficina. Dímelo y lo decidimos.

    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"
  9. Si algo falla

    Lo que vesQué es
    401 al enviarEl token está mal pegado o le falta un trozo. Vuelve a copiarlo entero
    403 al enviarEl 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 rotosAvísame: se ajusta la lectura en el script
    Muchos rechazosCasi 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
    Lo que NO hay que hacer: borrar artículos en Office «para limpiar» — se pierden fotos e historial; eso lo hago yo con copia previa— ni tocar datos dentro de Sage.

Cuándo escribirme, resumido

MomentoQué hago con eso
Paso 6 · Mandar $columnasTe 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 8Si el script falla con Sage cerrado, decidimos la hora de la sincronización
Cualquier error raroMandar $Error[0] y lo miro

Tablón compartido

Pega aquí lo que salga en el servidor y lo leo al momento. Yo te contesto en esta misma lista. Cada nota tiene su botón Copiar (para llevártela al otro ordenador) y su para borrarla. Se actualiza solo cada pocos segundos.