Migrating from EuroWin or Sage to Odoo: migration guide for distribution

Real case Cymit Química: strategy, data mapping, ETL, cutover and go-live when migrating from EuroWin and Access to Odoo in a distribution company with more than two million references.

Why migrate from EuroWin or Sage to Odoo in distribution

EuroWin and Sage are two of the most widely deployed ERPs in Spanish SMEs in the distribution, trade and light manufacturing sectors. They were tools that solved the problems of the nineties and two thousands very well: accounting, invoicing, purchase and sales orders, warehouse management. But in 2026 they have aged in a way that has no patch-level solution: their architecture prevents real integration with e-commerce platforms, catalogue automation, real-time business visibility, and the scalability that modern distribution operations demand.

The migration I describe in this guide is not theoretical. I personally led the migration of Cymit Química from EuroWin + Microsoft Access + CRM Visual to Odoo when I was CEO of the company. The result was revenue growth from 2 M€ to 4 M€ and, ultimately, the acquisition of the company by the PALEX Group. What follows is the real methodology I used, with the specific problems that arose and how we solved them.

Prior diagnosis: why many migrations fail

The most frequent failure in ERP migrations is not technical: it is planning. The projects I have seen fail share common patterns:

  • Underestimating the quality of source data: EuroWin and Sage store years of manually entered data, with inconsistencies, duplicates, poorly classified customers and products with broken references. Cleaning that data takes longer than the technical migration itself.
  • Trying to migrate everything at once without a rollback plan: the "big bang" without a safety net is the recipe for disaster in companies with daily operations that cannot stop.
  • Not involving the operational team from the start: end users know the business particularities that no external consultant will discover by reading documentation. Their input in data mapping is essential.
  • Migrating historical data that nobody will use: migrating 10 years of warehouse movement history has a high cost and questionable value. Defining with business criteria what historical data is needed saves weeks of work.

Migration strategy: big bang vs phased

Big bang

Big bang migration consists of migrating all data and switching to Odoo on a single date. The old system is frozen (no more operations are accepted), the full migration is executed and Odoo is activated as the sole operating system.

Advantages: there is no dual-operation period; the complexity of keeping two systems synchronised disappears on cutover day. It is simpler to manage operationally once executed.

Risks: if something fails on cutover day (corrupt data, integration that does not work, users who do not know how to operate the new system), all operations stop. It requires a clear and tested rollback plan.

When it is appropriate: small or medium-sized companies (up to ~30 users) with relatively bounded product catalogues, where the operational risk of a short outage is acceptable.

Phased migration

Phased migration activates Odoo modules incrementally, keeping the old system for unmigrated areas until each phase is consolidated.

A typical order for a distribution company:

  1. Phase 0 (preparation): Odoo configuration, master data (customers, suppliers, products), accounting configuration. Parallel operation.
  2. Phase 1 (sales and purchasing): sales and purchase orders in Odoo; invoicing stays in the old system temporarily or in parallel.
  3. Phase 2 (warehouse): stock management and delivery orders in Odoo. The initial inventory is taken as the reference point.
  4. Phase 3 (accounting): accounting close in the old system and opening balances in Odoo. Full invoicing in Odoo.
  5. Phase 4 (old system shutdown): historical data migrated or archived for reference, old system deactivated.

When it is appropriate: larger companies, complex operations with many integrations, or when the team needs learning time without the pressure of the business already depending on Odoo.

The decision at Cymit

At Cymit Química we opted for a hybrid approach: phases for core operations (sales, purchasing, warehouse) but with a defined and firm cutover date for accounting. The reason was that we had three disconnected systems (EuroWin, Access, CRM Visual) and a dual-operation period across three simultaneous fronts could not be stretched indefinitely. Defining phases but with concrete deadlines was what allowed us to complete the migration without it turning into an endless project.

Data mapping: from EuroWin and Sage to Odoo

Customers and contacts

EuroWin stores customers in its own tables with system-specific fields that have no direct counterpart in Odoo. The most common problems:

  • Duplicates: the same customer may have two records (one created by the sales team, another by billing) with slight variations in name or tax ID. Deduplication is manual or semi-automatic and requires human decision.
  • Customers with multiple addresses: in EuroWin the address is a field on the customer; in Odoo the model allows multiple contacts (parent/child) with different addresses. You need to decide how that structure is mapped.
  • Classifications and price lists: customer price lists (discounts, special prices) must be migrated to Odoo price lists, which have a more powerful but also more complex structure.

Indicative customer field mapping:

EuroWin / Sage fieldOdoo field (res.partner)Notes
Customer coderefKeep as internal reference
Legal namenameNormalise upper/lower case
Tax IDvatAdd ES prefix if missing
Addressstreet, city, zip, state_id, country_idNormalise using INE municipality catalogue
Phonephone / mobileVerify E.164 format
EmailemailValidate syntax; discard invalid ones
Price list / discountproperty_product_pricelistCreate equivalent price list in Odoo
Payment methodproperty_payment_term_idCreate equivalent payment terms
Chart of accountsproperty_account_receivable_idMap to Odoo chart of accounts

Products and catalogue

The product catalogue is usually the most complex asset to migrate in distribution companies. At Cymit, with more than two million references, it was the highest-volume work in the project. The critical points:

  • Internal reference vs supplier reference: EuroWin usually stores the supplier reference as the main reference. Odoo separates the internal reference (default_code) from supplier references (product.supplierinfo). You need to decide what becomes what.
  • Categories and catalogue structure: EuroWin's category hierarchy rarely maps directly to what you need in Odoo. It is an opportunity to redesign the catalogue taxonomy.
  • Units of measure: Odoo supports multiple UoMs with conversion; EuroWin and Sage have variable support. Products sold in boxes but stored in units require specific configuration.
  • Prices and price lists: sales and cost prices in EuroWin must be mapped to Odoo pricelists, which work differently (rules based on categories, quantities, dates).

Opening stock

The opening inventory is the starting point for the warehouse in Odoo. The options are:

  • Take the current stock from the old ERP as the opening inventory: the on-hand quantity for each product is migrated to Odoo via an inventory adjustment. This is the fastest method but assumes the old system's stock is correct.
  • Carry out a physical count before cutover: more laborious but eliminates accumulated discrepancies. Recommended if there are reasons to doubt the reliability of stock in EuroWin or Sage.

At Cymit we did a selective count: high-value or high-turnover products were physically recounted; the rest were taken from the system with an estimated correction factor. Discrepancies were adjusted during the first month of operation in Odoo.

Accounting and history

The accounting migration is the most delicate and the one requiring the most business judgement. The key decisions are:

  • Opening date: the opening balance in Odoo must match the closing balance of the old system. It is usually migrated at the close of a fiscal year to simplify reconciliation.
  • Invoice history: invoices issued before the cutover date are not migrated as Odoo documents (very costly and of little use). They are kept in the old system in read-only mode. Only outstanding receivable/payable balances (open items) are migrated as opening entries.
  • Chart of accounts: the EuroWin or Sage chart of accounts must be mapped to the Spanish accounting plan in Odoo. Odoo includes the Spanish localisation (l10n_es) with the complete PGC; the work consists of mapping the old system's accounts to their PGC equivalents.

ETL: extraction, transformation and loading

Extraction from EuroWin

EuroWin has no modern API. The extraction paths are:

  1. Export to Excel/CSV from the application itself: the simplest method for small volumes. Limited in flexibility and without access to all tables.
  2. Direct access to the EuroWin database: EuroWin stores its data in a SQL database (SQL Server or similar, depending on the version). With read-only access to that DB, all data can be extracted with SQL. This is the recommended path for complete migrations.

Example of customer extraction from the EuroWin DB (approximate structure, varies by version):

-- Extraer clientes de EuroWin (SQL Server)
SELECT
    c.CodCliente        AS codigo_externo,
    c.RazonSocial       AS nombre,
    c.CIF               AS cif,
    c.Direccion         AS calle,
    c.CodPostal         AS cp,
    c.Poblacion         AS ciudad,
    c.Telefono          AS telefono,
    c.Email             AS email,
    c.CodFormaPago      AS forma_pago_codigo,
    c.CodTarifa         AS tarifa_codigo,
    c.Observaciones     AS notas_internas
FROM
    Clientes c
WHERE
    c.Activo = 1
ORDER BY
    c.CodCliente;

Cleaning and deduplication

The slowest and most critical step in the ETL process is data cleaning. A Python script for the most common tasks:

import pandas as pd
import re

# Cargar el CSV exportado de EuroWin
df = pd.read_csv('clientes_eurowin.csv', encoding='latin-1', sep=';')

# 1. Normalizar CIF: añadir prefijo ES si falta
def normalizar_cif(cif):
    if pd.isna(cif) or str(cif).strip() == '':
        return False
    cif = str(cif).strip().upper().replace(' ', '').replace('-', '')
    if not cif.startswith('ES'):
        cif = 'ES' + cif
    return cif

df['vat'] = df['cif'].apply(normalizar_cif)

# 2. Normalizar nombre: Title Case, quitar espacios dobles
df['name'] = df['nombre'].str.strip().str.title()
df['name'] = df['name'].str.replace(r'\s+', ' ', regex=True)

# 3. Validar emails
EMAIL_REGEX = re.compile(r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$')
df['email_valido'] = df['email'].apply(
    lambda x: str(x).strip().lower() if pd.notna(x) and EMAIL_REGEX.match(str(x).strip()) else False
)

# 4. Detectar duplicados por CIF (el identificador más fiable)
duplicados_cif = df[df.duplicated(subset=['vat'], keep=False) & (df['vat'] != False)]
print(f'Clientes con CIF duplicado: {len(duplicados_cif)}')
duplicados_cif.to_csv('revisar_duplicados_cif.csv', index=False)

# 5. Detectar duplicados por nombre (fuzzy: nombres muy similares)
# Requiere: pip install rapidfuzz
from rapidfuzz import fuzz
nombres = df['name'].dropna().unique().tolist()
posibles_duplicados = []
for i, n1 in enumerate(nombres):
    for n2 in nombres[i+1:]:
        if fuzz.ratio(n1, n2) > 85:
            posibles_duplicados.append({'nombre_1': n1, 'nombre_2': n2, 'similitud': fuzz.ratio(n1, n2)})

pd.DataFrame(posibles_duplicados).to_csv('revisar_duplicados_nombre.csv', index=False)
print(f'Pares de nombres con similitud >85%: {len(posibles_duplicados)}')

print('\nResumen:')
print(f'  Total clientes exportados: {len(df)}')
print(f'  Clientes con CIF válido: {(df['vat'] != False).sum()}')
print(f'  Clientes con email válido: {(df['email_valido'] != False).sum()}')

Loading into Odoo via XML-RPC API

Odoo has a complete XML-RPC API that allows records to be created and updated programmatically. It is the recommended path for loading migration data (rather than manual CSV imports, which are error-prone and not traceable):

import xmlrpc.client
import pandas as pd

# Conexión a Odoo
ODOO_URL = 'https://odoo.tudominio.com'
DB = 'odoo_produccion'
USER = 'admin@tudominio.com'
PASSWORD = 'la_password_del_admin'

common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
uid = common.authenticate(DB, USER, PASSWORD, {})
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')

def crear_cliente_odoo(row, pricelist_map, payment_term_map):
    '''Crea un cliente en Odoo desde una fila del DataFrame limpio.'''
    vals = {
        'name': row['name'],
        'customer_rank': 1,
        'ref': str(row['codigo_externo']),
        'street': row.get('calle', ''),
        'zip': str(row.get('cp', '')),
        'city': row.get('ciudad', ''),
        'country_id': 69,    # España — ID verificar en tu instancia
        'lang': 'es_ES',
    }
    if row['vat']:
        vals['vat'] = row['vat']
    if row['email_valido']:
        vals['email'] = row['email_valido']
    if row.get('telefono'):
        vals['phone'] = str(row['telefono'])
    # Mapear tarifa y forma de pago usando los diccionarios de mapeo
    tarifa_codigo = row.get('tarifa_codigo')
    if tarifa_codigo and tarifa_codigo in pricelist_map:
        vals['property_product_pricelist'] = pricelist_map[tarifa_codigo]
    pago_codigo = row.get('forma_pago_codigo')
    if pago_codigo and pago_codigo in payment_term_map:
        vals['property_payment_term_id'] = payment_term_map[pago_codigo]

    partner_id = models.execute_kw(
        DB, uid, PASSWORD,
        'res.partner', 'create', [vals]
    )
    return partner_id

# Ejecutar la carga con manejo de errores
df_limpio = pd.read_csv('clientes_listos_para_odoo.csv')
errores = []
ok = 0

for idx, row in df_limpio.iterrows():
    try:
        pid = crear_cliente_odoo(row, pricelist_map={}, payment_term_map={})
        ok += 1
        if ok % 100 == 0:
            print(f'Cargados {ok} clientes...')
    except Exception as e:
        errores.append({'fila': idx, 'codigo': row['codigo_externo'], 'error': str(e)})

print(f'\nCarga completada: {ok} clientes OK, {len(errores)} errores.')
pd.DataFrame(errores).to_csv('errores_carga_clientes.csv', index=False)

Validation and reconciliation

Loading the data is not the end. Validation is the step that determines whether the migration is truly correct or whether there are problems that will only surface weeks later in production.

The minimum checks after loading:

  • Record count: the number of customers, products and suppliers in Odoo must match that of the source system, minus the records removed during cleaning. Document the difference and justify it.
  • Accounting balance reconciliation: the total accounts receivable balance in Odoo must match that of the old system at the cutover date. Any difference must have an explanation (manual entry, rounding difference, etc.).
  • Inventory verification: the total inventory value in Odoo must match that of the old system. Sampling of high-value products to verify individually.
  • Full cycle test: create a test order in Odoo from start to finish (order → delivery → invoice → payment) with real data to verify that the flow works correctly before go-live.
  • Validation with key users: the heads of sales, warehouse and accounting should review their data in Odoo before cutover. They will detect issues that no automated script will find.

Cutover plan

Cutover is the moment of maximum risk and maximum planning. A good cutover plan has minute-by-minute detail of what is going to happen, who executes it and what check confirms that step is done correctly.

Week before cutover

  • Freeze changes in the old system (no new customers or products are created in EuroWin/Sage).
  • Run the complete migration in the pre-production environment to verify that the scripts work without errors.
  • Final training for key users (heads of each area).
  • Prepare the rollback plan: what to do if on Monday morning Odoo is not working correctly.

The cutover weekend

# Plan de corte — ejemplo Cymit Química (simplificado)

Viernes 18:00 — Cierre operativo en EuroWin
  - Verificar que no hay pedidos pendientes de procesar
  - Hacer dump final de EuroWin (backup completo)
  - Tomar inventario de stock final

Viernes 20:00 — Extracción de datos finales
  - Extraer clientes, productos, stock, saldos contables
  - Ejecutar scripts de limpieza y transformación
  - Generar ficheros de carga para Odoo

Sábado 08:00 — Carga en Odoo producción
  - Cargar maestros (clientes, proveedores, productos)
  - Ajuste de inventario inicial
  - Apertura de saldos contables
  - Verificar conteos y reconciliaciones

Sábado 14:00 — Validación con usuarios clave
  - Responsable de ventas verifica sus clientes y tarifas
  - Responsable de almacén verifica stock
  - Contable verifica saldos de apertura

Sábado 18:00 — Decisión GO / NO-GO
  - Si todo cuadra: GO (Odoo en producción el lunes)
  - Si hay problemas críticos: NO-GO (EuroWin sigue activo, analizar)

Domingo — Buffer para correcciones menores
  - Resolver incidencias detectadas el sábado
  - Preparar soporte reforzado para el lunes

Lunes 08:00 — Go-live
  - Odoo es el único sistema operativo
  - Soporte técnico disponible todo el día

Rollback plan

The rollback plan must be documented and executable without depending on the migration consultant. If on Monday morning there is a critical problem in Odoo that prevents operations, the steps must be clear:

  1. Communicate to the entire team that the rollback is being activated.
  2. Disable user access to Odoo (so they do not generate data that will later be inconsistent).
  3. Reactivate EuroWin/Sage with the data from Friday's backup.
  4. Document all movements made in Odoo before the rollback to reproduce them in the old system.
  5. Root cause analysis of the problem and correction plan before the next cutover attempt.

Training and change management

At Cymit, the biggest obstacle was not technical: it was resistance to change from a team that had been working with EuroWin for years and knew its shortcuts and limitations. Training cannot be a PDF and a two-hour session.

What worked:

  • Training by role, not by feature: instead of teaching "this is Odoo", teach "this is how you process a sales order in your new system". Each role has a specific workflow that needs to be trained until it is automatic.
  • Training environment with anonymised real data: users learn better when they see their own customers and products, even if anonymised. A generic training environment does not prepare you for reality.
  • Champion users by area: identify one person in each area (sales, warehouse, accounting) who trains more deeply and acts as the first point of contact for their colleagues. This dramatically reduces dependence on external support.
  • Reinforced support for the first 30 days: the period immediately after go-live is where 80% of questions are concentrated. Having available and fast support during that month makes the difference between a migration that "went well" and one that "was a disaster".

Main risks and how to mitigate them

RiskProbabilityImpactMitigation
Source data with more duplicates and errors than expectedHighHigh (delays the project)Data audit in the first weeks, not at the last moment
Team resistance to changeMediumHigh (incorrect system usage)Involve the team from the design phase, not just in training
Data loss at cutoverLowCriticalVerified backups before cutover, tested rollback plan
Integration with other tools fails (online shop, POS, logistics)MediumHighInventory all integrations in the diagnosis phase, not at the end
Project extends indefinitely (scope creep)HighMedium (cost)Define a closed scope for the base migration; improvements in later phases
Paralysis from inability to access historyMediumLow-MediumKeep EuroWin/Sage in read-only mode for 12 months after cutover

Lessons learned at Cymit

Five years after the process, and having seen other migrations since then, the lessons I would bring to any similar project are:

  1. The quality of source data determines 70% of the project's success. Invest time auditing it at the beginning, not at the end. Every data quality problem you discover in production costs ten times more than detecting it earlier.
  2. Do not migrate what you do not use. The invoice history from 8 years ago that nobody looks at can stay in the old system in archive mode. Migrating for completeness prolongs the project without adding real value.
  3. Define the success criteria before you start. "The migration is fine" is ambiguous. "The accounts receivable balance matches the previous balance with a tolerance of €1, customer and product counts are within 2% of justified variation and a complete sales-warehouse-invoicing cycle has been successfully processed" is a verifiable criterion.
  4. Go-live day is not the end: it is the beginning. The first 90 days are the period of highest risk and greatest need for support. Plan resources for that period before starting the project.
  5. The platform you build must be acquirable. At Cymit, the clean architecture in Odoo was part of the valuation argument in the M&A process with PALEX. If a corporate transaction is on your horizon, technology is part of the price.

Are you considering migrating from EuroWin, Sage or another legacy ERP to Odoo?

Request a free migration audit

Integrating AI into Odoo: lead scoring and predictive models with Python
Complete technical guide to connecting machine learning models to Odoo CRM: from data pipeline design to real-time scoring of commercial opportunities