Data Mapping in ERP Migration: Synchronizing PlentyONE and Odoo Flawlessly

Data transport is the solved problem in an ERP migration. The unsolved problem is the semantic mapping: a field that PlentyONE manages as a simple property may require its own object relationship in Odoo's relational data model. If this translation is set up incorrectly, no error messages will appear, but rather incorrect links – which often only become visible weeks after go-live.

This article treats mapping as what it is: an architectural task with clear rules. Business keys instead of surrogate keys, documented responsibilities per entity, and validation before import. In addition, there are reference implementations for customers, prices, inventories, and orders, with which these rules can be implemented productively.


Why data mapping is the real migration risk

PlentyONE, Shopware, and Odoo each use their own, historically developed data models for customers, products, variants, and prices. A field that is a simple property in one system may require its own relational structure in the target system. Anyone who underestimates this mapping and transfers data "mechanically" 1:1 will, at best, produce import errors – and at worst, unnoticed incorrect assignments that only become apparent during operation.

This page deals with the mapping itself – i.e., the semantic assignment between the systems. PlentyONE Odoo Data Migration clarifies which data belongs in the migration scope; ERP Migration without Downtime shows how such a mapping is technically transferred to the productive go-live without errors.


Proprietary Structures vs. Relational Odoo Models

Odoo 19 uses a consistently relational model: objects such as customers, products, or orders consist of interconnected data records, not flat, self-contained tables. PlentyONE and Shopware structures therefore not only need to be copied, but also translated into this relational model – with correct foreign key relationships for customers, products, variants, and pricing logic.


Business Keys vs. Surrogate Keys

The central technical principle for any reputable ERP mapping is the strict separation between surrogate keys and business keys.

  • Surrogate keys are purely technical, system-generated internal IDs (such as the numeric database ID from PlentyONE). They should never be used as primary keys between systems, as Odoo generates its own sequences and collisions would otherwise be unavoidable.
  • Business keys are unique, technical identifiers that remain stable regardless of the system – for example, the article number (SKU), the EAN, the VAT ID, or the UUID from the Shopware frontend.

Therefore, mapping is done exclusively via business keys. These are stored in Odoo in dedicated custom fields (e.g., x_shopware_uuid ) to permanently ensure referential integrity. This decision is not an ad-hoc practice but is explicitly documented as an Architecture Decision Record in the underlying architecture: If surrogate keys are mapped instead, immediate assignment errors threaten when creating new objects, because immutable business keys like the UUID exclude this confusion from the outset.

A practical example illustrates the risk: If a customer is accidentally deleted in Odoo and then recreated, they automatically receive a new internal database ID. If this internal ID had been the reference between the systems, the connector would no longer be able to link the "new" customer with their previous order history. In contrast, the stable business key reference – such as the original PlentyONE customer number or the Shopware UUID – remains valid in this case as well.


Business Keys vs. Surrogate Keys

Product and variant mapping

Odoo structurally separates the parent product ( product.template ) from the specific variant ( product.product ). The mapping must reflect this separation exactly:

Source systemTarget in Odoo 19
Shopware Parentproduct.template
Shopware variantproduct.product

The SKU serves as a central, unchangeable business key throughout the entire process. If an SKU is missing, the import of this specific item is deliberately blocked by the ETL process instead of generating an unclear data record.

Categories are generally imported from top to bottom in order to build the category tree correctly. Since the Shopware frontend internally works consistently with UUIDs instead of sequential IDs, these UUIDs are permanently stored in the Odoo database in order to keep the relationship stable even after changes.

Reference Implementation: Customer Import

The customer module imports companies, contact persons, delivery and billing addresses, as well as price lists, tax data, and fiscal items in a clean parent-child structure. The order is deliberately fixed: first the parent company, then the associated contact persons. This structure prevents a contact person from being created in the system without a valid company assignment – an error that would immediately become apparent in downstream processes such as invoicing.

Reference Implementation: Price Import

To synchronize purchase prices, sales prices, tiered prices, and B2B price lists, imports are carried out exclusively in a resource-efficient manner using the write() or load() methods of the JSON-2 API. Price lists in Odoo are independent objects linked to customers or customer groups. Therefore, individual B2B conditions are not maintained as an additional field on the customer but are created and referenced as a separate price list. This makes it possible to reuse the same pricing logic later for new customers with comparable conditions without having to rebuild the mapping.

Reference Implementation: Inventory

Inventory synchronization transfers stock from the source system to the Odoo model stock.quant. This is done exclusively via an audit-proof inventory (referred to as "Inventory Adjustment" in Odoo terminology), followed by a strict validation of the target stock levels. A common technical error at this point is the attempt to create inventory levels directly using create() with arbitrary field values. Since Odoo 15, this direct write access has been blocked by the system – inventory levels may only be manipulated via the allowed inventory functions of stock.quant itself. This restriction is not an obstacle, but rather protects against precisely the kind of uncontrolled inventory changes that lead to negative or inexplicable inventory levels in historically grown systems.

Reference Implementation: Orders

When transferring historical orders, order items, and delivery and invoice statuses, an important rule applies: delivered, invoiced, and open quantities must be reconstructed based on the actual historical documents and delivery/invoice statuses. A direct equivalence of the delivered and invoiced quantity with the originally ordered quantity is only permissible if it corresponds to the historical state from a business perspective – for example, in the case of fully completed orders without partial delivery. Otherwise, the ERP system would incorrectly classify the order as fully completed, even though in reality a partial delivery or invoicing was still outstanding. The specific migration logic for this case must therefore always be checked on a version- and implementation-specific basis, and not automated across the board.


Customers, Categories, Manufacturers, and Taxes

Odoo manages companies and contacts in a common parent-child model: The company forms the parent, and the associated contacts form the child records. Based on the country of origin and VAT ID, the system automatically assigns the correct fiscal position during import (e.g., domestic, EU, third country) – a prerequisite for legally compliant invoice generation. The existing customer number from PlentyONE is retained as a unique reference.

A consequential error path in practice: If the fiscal position is assigned incorrectly – for example, because a country of origin was transferred incorrectly – the system calculates an incorrect tax logic on all subsequent invoices for this customer. This error often goes unnoticed until an external audit or a customer inquiry reveals it. Therefore, the validation of the fiscal position assignment belongs in every dry run, not just in the post-go-live check.


Validate mapping

Before any productive import, a systematic validation at the database level is required. Typical, directly verifiable sources of error:

Duplicate SKUs (critical error in variant assignment):

SELECT default_code, COUNT(*) FROM product_product GROUP BY default_code HAVING COUNT(*) > 1;

Products without a sales price:

SELECT id, name->>'en_US' AS name FROM product_template WHERE list_price IS NULL OR list_price = 0;

Negative stock levels (read-only check – correction is made exclusively via the system's permitted inventory functions, not via direct write access):

SELECT product_id, location_id, quantity FROM stock_quant WHERE quantity < 0;

These and other checks should be an integral part of every dry run – not a subsequent troubleshooting after go-live. Two additional checks also belong in this recipe book: the search for variants without a valid parent product and the check for duplicate customer numbers or email addresses, which indicate an accidental multiple creation in the source system. Experience shows that both error patterns are more frequent than a single faulty SKU, but can also be reliably identified using simple SQL queries on the target database.

A complete validation round before the productive import should cover at least the following categories:

Test categoryTypical error.Impact if undetected
SKU Uniquenessduplicate item numbersIncorrect variant assignment.
Parent Product ReferenceVariant without product.templateBlocked or faulty import.
Price completenessMissing sales price.Item not for sale
Inventory plausibilitynegative stockIncorrect availability display
Fiscal positionIncorrect country of origin/VAT IDincorrect tax calculation
Duplicate CustomersMultiple email addresses createdInconsistent order history.

Each of these categories can be checked with comparatively simple, repeatable queries directly on the target database – provided that these checks are a fixed, automated part of the dry run and not an occasional random sample.

We explain how a complete test migration and cutover process is structured in ERP Migration without Downtime. We describe the most common specific mapping pitfalls – from UUID conflicts to missing relations – in detail in PlentyONE Odoo Migration Errors.


Mapping is one of several project phases. Our overview of the migration from PlentyONE to Odoo describes the complete process from system analysis to go-live.


Frequently Asked Questions about Data Mapping

Why can’t we just use the PlentyONE ID as a reference in Odoo?

Because it is a surrogate key – a purely technical, system-generated ID that has no business meaning and can either collide or fail when the data record is created again. Odoo assigns its own internal IDs via its own sequences; only a stable business key such as the SKU or the Shopware UUID guarantees correct recognition across both systems.

What happens if an SKU is not unique in the source system?

The import of this specific item is deliberately blocked instead of creating an unclear or possibly incorrectly assigned data record. Duplicates must therefore be identified and cleaned up before the productive import.

Can we test the mapping step by step without resetting the target system each time?

Yes. The load() method works idempotently via external IDs: An existing data record is updated instead of duplicated when imported again. This allows repeated test runs against the same staging environment.

How long does a complete mapping project take for a medium-sized catalog?

This depends heavily on the complexity of the variant structure, the data quality, and the number of existing individual fields. A reliable time estimate is only possible after a technical analysis of your specific data structures.

We don’t know how cleanly our item numbers and customer data are actually maintained.

This can be measured in advance, not estimated: Duplicate item numbers, variants without a parent product, and missing mandatory fields are visible via simple queries at the database level – before the first productive import runs.


Sources

The concept of external IDs (XML-IDs) and their role in data import is based on the official Odoo documentation: Odoo 19 – Export and import data. The restriction of direct write access to stock.quant (since Odoo 15, via _get_inventory_fields_write() ) is documented in both the technical migration manual and the Odoo documentation on inventory adjustments.


Have the technical data mapping checked.

A technical analysis of your systems will show whether your existing data model can be cleanly mapped to Odoo via business keys, or where specific points of conflict lie with variants, customers, or pricing logic. This is precisely what we provide as part of our free, no-obligation e-commerce audits.

Have your data mapping technically reviewed – free of charge and without obligation.

Request a mapping analysis now

✔ Free ✔ Non-binding ✔ Concrete mapping logic instead of theory ✔ Answer within 24 hours

Christopher Einenkel

Senior Integration Architect & API Specialist
Specialist in ERP/PIM Integrations & Enterprise Interfaces

Christopher Einenkel is the strategic mind at HQ GmbH for advanced system integrations and complex API architectures. Whenever data synchronizations reach their limits or demanding enterprise systems such as Salesforce need to be connected to Shopify, he designs tailored, high-performance interface logic.

His track record includes the in-house development of proprietary enterprise solutions, including automated AI translation tools for product master data, seamless B2B procurement systems, as well as automated market evaluation and competitive monitoring solutions. In addition, he manages the technical implementation and API integration of digital software call center systems into existing CRM and ERP environments for our enterprise clients.

View full profile & interface certifications
Christopher Einenkel - Senior Integration Architect at HQ GmbH
↑