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:
- Phase 0 (preparation): Odoo configuration, master data (customers, suppliers, products), accounting configuration. Parallel operation.
- Phase 1 (sales and purchasing): sales and purchase orders in Odoo; invoicing stays in the old system temporarily or in parallel.
- Phase 2 (warehouse): stock management and delivery orders in Odoo. The initial inventory is taken as the reference point.
- Phase 3 (accounting): accounting close in the old system and opening balances in Odoo. Full invoicing in Odoo.
- 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 field | Odoo field (res.partner) | Notes |
|---|---|---|
| Customer code | ref | Keep as internal reference |
| Legal name | name | Normalise upper/lower case |
| Tax ID | vat | Add ES prefix if missing |
| Address | street, city, zip, state_id, country_id | Normalise using INE municipality catalogue |
| Phone | phone / mobile | Verify E.164 format |
| Validate syntax; discard invalid ones | ||
| Price list / discount | property_product_pricelist | Create equivalent price list in Odoo |
| Payment method | property_payment_term_id | Create equivalent payment terms |
| Chart of accounts | property_account_receivable_id | Map 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:
- Export to Excel/CSV from the application itself: the simplest method for small volumes. Limited in flexibility and without access to all tables.
- 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:
- Communicate to the entire team that the rollback is being activated.
- Disable user access to Odoo (so they do not generate data that will later be inconsistent).
- Reactivate EuroWin/Sage with the data from Friday's backup.
- Document all movements made in Odoo before the rollback to reproduce them in the old system.
- 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
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Source data with more duplicates and errors than expected | High | High (delays the project) | Data audit in the first weeks, not at the last moment |
| Team resistance to change | Medium | High (incorrect system usage) | Involve the team from the design phase, not just in training |
| Data loss at cutover | Low | Critical | Verified backups before cutover, tested rollback plan |
| Integration with other tools fails (online shop, POS, logistics) | Medium | High | Inventory all integrations in the diagnosis phase, not at the end |
| Project extends indefinitely (scope creep) | High | Medium (cost) | Define a closed scope for the base migration; improvements in later phases |
| Paralysis from inability to access history | Medium | Low-Medium | Keep 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:
- 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.
- 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.
- 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.
- 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.
- 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.