GuideOdoo IndustryMarch 13, 2026

Odoo 19 for Fashion & Apparel:
Variant Management, Size Grids & Seasonal Planning

INTRODUCTION

Your SKU Count Just Exploded and Your ERP Wasn't Built for It

A single garment style in fashion doesn't stay a single product for long. One women's cotton blouse becomes 5 colors × 7 sizes = 35 SKUs. Add a second fabric option and you're at 70. Multiply by 120 styles per seasonal collection and your catalog has 8,400 active variants — before accounting for last season's carryover styles that are still selling.

Generic ERPs handle this poorly. They either flatten variants into independent products (making size-run reorders impossible) or bolt on attribute matrices as an afterthought (creating performance bottlenecks at 10,000+ variants). Fashion companies end up managing size grids in spreadsheets, seasonal buy plans in Excel, and warehouse allocation in emails. The ERP becomes a bookkeeping system instead of an operational backbone.

Odoo 19 handles fashion natively — if you configure it correctly. This guide covers the complete setup: product variant architecture for color/size matrices, size grid templates that enforce consistent sizing across categories, seasonal collection planning with PLM, multi-warehouse allocation for retail and wholesale channels, ecommerce integration with variant selectors, and returns management for size exchanges. Every configuration is drawn from live fashion deployments.

01

Configuring Product Variants for Color/Size Matrices in Odoo 19

Odoo's product variant system uses attributes and attribute values to generate variant combinations automatically. For fashion, you need two primary attributes — Color and Size — configured with specific settings that most generic guides skip.

Attribute Configuration

The critical setting is Variant Creation Mode. Fashion products must use "Instantly" for Color (customers need to see all colors in the catalog) but can use "Dynamically" for Size if you only want to create size variants when they're first ordered or received. For most apparel companies, we recommend "Instantly" for both — the variant count is predictable and you need all size/color combinations visible for purchase planning.

Python — fashion_product/models/product_attribute.py
from odoo import models, fields, api


class ProductAttribute(models.Model):
    _inherit = 'product.attribute'

    is_fashion_attribute = fields.Boolean(
        string='Fashion Attribute',
        help='Marks this attribute for fashion-specific logic'
    )
    attribute_category = fields.Selection([
        ('color', 'Color'),
        ('size', 'Size'),
        ('fabric', 'Fabric'),
        ('fit', 'Fit'),
    ], string='Fashion Category')


class ProductAttributeValue(models.Model):
    _inherit = 'product.attribute.value'

    color_hex = fields.Char(
        string='Hex Code',
        help='HTML color for swatches, e.g. #2C3E50'
    )
    size_sort_order = fields.Integer(
        string='Size Sort Order',
        help='Controls display order: XS=10, S=20, M=30...'
    )
    size_code = fields.Char(
        string='Size Code',
        help='Short code for barcode labels, e.g. M, L, 38'
    )

The size_sort_order field solves a persistent UI problem: Odoo sorts attribute values alphabetically by default, which puts "L" before "M" and "XL" before "XS". A numeric sort order ensures XS → S → M → L → XL → XXL everywhere — variant selectors, POS, ecommerce dropdowns, and purchase order lines.

Variant Naming Convention

By default, Odoo generates variant names like "Cotton Blouse (Blue, M)". Fashion companies need structured SKU codes for barcodes, EDI, and wholesale catalogs. Override the variant reference generation:

Python — fashion_product/models/product_product.py
class ProductProduct(models.Model):
    _inherit = 'product.product'

    @api.depends(
        'product_tmpl_id.default_code',
        'product_template_attribute_value_ids'
    )
    def _compute_default_code(self):
        for product in self:
            tmpl_code = (
                product.product_tmpl_id.default_code or ''
            )
            if not tmpl_code:
                continue
            parts = [tmpl_code]
            for ptav in product \
                    .product_template_attribute_value_ids \
                    .sorted('attribute_id'):
                attr = ptav.attribute_id
                val = ptav.product_attribute_value_id
                if attr.attribute_category == 'color':
                    parts.append(
                        val.name[:3].upper()
                    )
                elif attr.attribute_category == 'size':
                    parts.append(
                        val.size_code or val.name
                    )
            product.default_code = '-'.join(parts)
    # Result: BLS-001-BLU-M, BLS-001-BLU-L, etc.
SettingRecommended ValueWhy
Variant Creation ModeInstantly (both Color & Size)All combinations needed for buy planning and wholesale catalogs
Display Type (Color)ColorEnables swatch picker on ecommerce and POS
Display Type (Size)RadioCustomers see all sizes at once without opening a dropdown
Attribute ScopeGlobal (not per-product)Ensures "Navy" means the same hex code across all products
Don't Use "No Variant" for Fashion Attributes

Some guides suggest "No Variant Creation" for secondary attributes like Fabric to reduce variant count. In fashion, this breaks inventory tracking — you can't track stock of "Cotton" vs "Linen" versions of the same blouse if they don't have distinct variants. If the attribute affects physical inventory, it must generate a variant.

02

Size Grid Templates: Enforcing Consistent Sizing Across Product Categories

A fashion catalog doesn't have one size range — it has many. Women's tops run XS through XXL. Denim runs 24 through 36 (waist) with inseam options. Footwear runs EU 35 through 46. Without size grid templates, product managers manually assign size values to every new product, leading to inconsistencies: one product uses "Extra Small" while another uses "XS", and your ecommerce filter shows both as separate options.

Python — fashion_size_grid/models/size_grid.py
from odoo import models, fields, api


class FashionSizeGrid(models.Model):
    _name = 'fashion.size.grid'
    _description = 'Size Grid Template'

    name = fields.Char(required=True)  # e.g. "Women Tops"
    category = fields.Selection([
        ('tops', 'Tops & Blouses'),
        ('bottoms', 'Bottoms & Pants'),
        ('dresses', 'Dresses'),
        ('footwear', 'Footwear'),
        ('accessories', 'Accessories'),
    ], required=True)
    size_line_ids = fields.One2many(
        'fashion.size.grid.line', 'grid_id',
        string='Size Lines'
    )
    product_categ_id = fields.Many2one(
        'product.category',
        string='Default Product Category'
    )

    def action_apply_to_template(self, product_tmpl):
        """Apply this size grid to a product template.
        Creates the attribute line with all grid sizes.
        """
        size_attr = self.env.ref(
            'fashion_size_grid.attribute_size'
        )
        existing = product_tmpl.attribute_line_ids.filtered(
            lambda l: l.attribute_id == size_attr
        )
        size_values = self.size_line_ids.mapped(
            'attribute_value_id'
        )
        if existing:
            existing.write({
                'value_ids': [(6, 0, size_values.ids)]
            })
        else:
            product_tmpl.write({
                'attribute_line_ids': [(0, 0, {
                    'attribute_id': size_attr.id,
                    'value_ids': [(6, 0, size_values.ids)],
                })]
            })


class FashionSizeGridLine(models.Model):
    _name = 'fashion.size.grid.line'
    _description = 'Size Grid Line'
    _order = 'sequence'

    grid_id = fields.Many2one(
        'fashion.size.grid', ondelete='cascade'
    )
    sequence = fields.Integer(default=10)
    attribute_value_id = fields.Many2one(
        'product.attribute.value', required=True,
        domain="[('attribute_id.attribute_category',"
               " '=', 'size')]"
    )
    measurement_chest = fields.Float('Chest (cm)')
    measurement_waist = fields.Float('Waist (cm)')
    measurement_hips = fields.Float('Hips (cm)')
    measurement_length = fields.Float('Length (cm)')

The measurement fields on each grid line serve a dual purpose: they populate the size chart on your ecommerce product page and they feed into quality control checks during manufacturing. When a QC inspector measures a finished garment, Odoo compares the measurement against the grid template tolerance — flagging items that fall outside ±1.5cm of spec.

Size Conversion Tables

International customers need size equivalents. A size "M" in the US maps to "38" in Italy, "10" in the UK, and "170/88A" in China. Store conversions on the grid line and surface them on the ecommerce product page:

Your SizeUSUKEU/ITChest (cm)Waist (cm)
XS263480-8460-64
S4-68-1036-3884-8864-68
M8-1012-1440-4288-9268-72
L12-1416-1844-4692-9672-76
XL16204896-10076-80
Size Grids Are Not Optional for Wholesale

Wholesale buyers order in size runs — a predefined ratio like 1:2:3:3:2:1 across XS through XL. If your product template is missing sizes or has them in the wrong order, the buyer's purchase order doesn't match your catalog. Size grid templates ensure every product in a category has the exact same size range in the exact same order. This is a prerequisite for EDI with major retailers.

03

Seasonal Collection Planning: From Line Sheet to Purchase Order

Fashion operates on a seasonal calendar: Spring/Summer (SS) and Autumn/Winter (AW), each planned 6-9 months in advance. The planning cycle starts with a line sheet (the collection catalog), progresses through buy planning (how many units per style/color/size), and ends with purchase orders to manufacturers. Odoo doesn't have a native "season" concept, but the data model supports it with minimal customization.

Python — fashion_season/models/season.py
class FashionSeason(models.Model):
    _name = 'fashion.season'
    _description = 'Fashion Season'
    _order = 'year desc, season_type desc'

    name = fields.Char(compute='_compute_name', store=True)
    season_type = fields.Selection([
        ('SS', 'Spring/Summer'),
        ('AW', 'Autumn/Winter'),
        ('CR', 'Cruise/Resort'),
        ('PF', 'Pre-Fall'),
    ], required=True)
    year = fields.Integer(required=True, default=2026)
    date_design_start = fields.Date('Design Start')
    date_sample_deadline = fields.Date('Sample Deadline')
    date_buy_open = fields.Date('Buy Window Opens')
    date_buy_close = fields.Date('Buy Window Closes')
    date_delivery_start = fields.Date('First Delivery')
    date_delivery_end = fields.Date('Last Delivery')
    date_markdown_start = fields.Date('Markdown Start')
    state = fields.Selection([
        ('design', 'In Design'),
        ('sampling', 'Sampling'),
        ('buying', 'Buy Open'),
        ('production', 'In Production'),
        ('delivering', 'Delivering'),
        ('selling', 'In Season'),
        ('markdown', 'Markdown'),
        ('archived', 'Archived'),
    ], default='design')
    product_tmpl_ids = fields.Many2many(
        'product.template',
        string='Collection Styles'
    )

    @api.depends('season_type', 'year')
    def _compute_name(self):
        for rec in self:
            rec.name = '%s %s' % (
                rec.season_type or '', rec.year or ''
            )

The season model tracks the eight key milestone dates that govern the fashion calendar. Each date triggers downstream workflows:

  • Design Start → Sample Deadline: PLM workflows for tech packs, fabric sourcing, and fit samples
  • Buy Window Open → Close: Sales team enters pre-orders from wholesale accounts using size-run templates
  • Production → Delivery: Purchase orders to manufacturers with delivery windows aligned to the season calendar
  • Markdown Start: Triggers automated pricelist rules that apply progressive discounts (30% → 50% → 70%) based on remaining inventory levels

Size-Run Buy Planning

Wholesale buyers don't order "50 units of the blue blouse." They order size runs — a ratio that distributes units across sizes based on historical sell-through. A typical women's tops ratio is 1:2:3:3:2:1 (XS:S:M:L:XL:XXL). A buy of 120 units at this ratio becomes: XS=10, S=20, M=30, L=30, XL=20, XXL=10.

StyleColorTotal UnitsXSSMLXLXXL
BLS-001Navy120102030302010
BLS-001White180153045453015
DRS-042Black605101515105
Adjust Size Ratios by Channel

Ecommerce sells more extreme sizes (XS, XXL) than brick-and-mortar because online customers can't try on. A retail store ratio might be 0:1:3:3:1:0 while ecommerce is 2:3:4:4:3:2 for the same style. Use different size-run templates per sales channel and apply them during buy planning to avoid over-allocating extreme sizes to stores that won't sell them.

04

PLM for Fashion: Managing the Design-to-Production Pipeline

Odoo 19's PLM module tracks product lifecycle stages, but fashion needs an extended workflow: concept → tech pack → first sample → fit review → production sample → approved → in production. Each stage has specific deliverables, approval gates, and document requirements.

The tech pack is the central document in garment manufacturing — a detailed specification covering fabric, trims, construction, measurements, colorways, and packaging. In Odoo, attach tech packs as versioned documents on the product template using PLM's Engineering Change Order (ECO) workflow:

  • Fabric & Trim BoM: Bill of materials listing shell fabric yardage, lining, buttons, zippers, labels, and hang tags — all with supplier and lead time data
  • Construction Spec: Stitch type, seam allowance, wash instructions — attached as a PDF on the ECO
  • Measurement Spec: Linked to the size grid template, with tolerances for QC inspection
  • Colorway Approval: Lab-dip references stored as attachments with Pantone codes in a custom field
XML — fashion_plm/views/eco_view.xml
<record id="view_eco_form_fashion"
        model="ir.ui.view">
  <field name="name">mrp.eco.form.fashion</field>
  <field name="model">mrp.eco</field>
  <field name="inherit_id"
         ref="mrp_plm.mrp_eco_form_view"/>
  <field name="arch" type="xml">
    <xpath expr="//field[@name='tag_ids']"
           position="after">
      <field name="season_id"
             options="{'no_create': True}"/>
      <field name="tech_pack_status"
             widget="statusbar"
             statusbar_visible=
               "concept,techpack,sample,approved"/>
    </xpath>
    <xpath expr="//page[@name='misc']"
           position="inside">
      <group string="Garment Specifications">
        <field name="pantone_ref"/>
        <field name="fabric_composition"/>
        <field name="target_fob_price"/>
        <field name="target_retail_price"/>
        <field name="target_margin_pct"/>
      </group>
    </xpath>
  </field>
</record>

The margin fields on the ECO are critical for fashion buying. Buyers evaluate each style's viability based on Initial Markup (IMU): the ratio of retail price to FOB cost. A healthy IMU for mid-market fashion is 2.5x–3.0x. By embedding FOB target cost and retail price on the ECO, the buying team can filter styles by margin during the line review — killing underperforming styles before they consume production capacity.

Version Control for Tech Packs Is Non-Negotiable

We've seen factories produce 500 units of a garment using revision 2 of the tech pack when revision 4 was the approved version. The factory's email had the old attachment. PLM's ECO versioning ensures the approved tech pack is always accessible from the product record, with a clear audit trail showing who approved which revision and when. Factories pull specs from the Odoo vendor portal — never from email attachments.

05

Multi-Warehouse Allocation: Distributing Inventory Across Retail, Wholesale & Ecommerce

Fashion companies operate multiple channels with competing inventory needs: flagship stores need visual merchandising quantities (all sizes, all colors on the floor), wholesale accounts need bulk allocation for pre-booked orders, and ecommerce needs a shared pool for long-tail availability. Without a structured allocation model, the warehouse team plays favorites — and the ecommerce channel always loses.

Odoo 19's multi-warehouse and inter-warehouse transfer features handle this with a hub-and-spoke model:

WarehousePurposeReplenishmentPriority
Central DCReceives all inbound from manufacturersPO from suppliersN/A (hub)
Wholesale ReservePre-allocated for confirmed wholesale ordersInternal transfer from DC based on SO1 (highest)
Retail Stores (x N)Floor stock + backroomReplenishment rules (min/max per size)2
Ecommerce FulfillmentPick/pack for online ordersShared pool from remaining DC stock3

Configure replenishment rules per variant per warehouse for retail stores. A flagship store might stock 2 units of each size for a core style (min=1, max=2), while a smaller store stocks only the core size range M–XL. When a sale depletes stock below the minimum, Odoo generates an internal transfer from the Central DC automatically.

The ecommerce warehouse uses the remaining DC stock as a virtual available pool. Configure it with route_ids that pull from Central DC on demand. This means every unit not allocated to wholesale or retail is automatically available for ecommerce orders — no manual allocation spreadsheets.

Beware Overselling on Shared Stock

If ecommerce shares stock with retail replenishment, both channels can claim the same unit simultaneously. A customer places an online order for the last Navy/M unit at the same moment a retail replenishment transfer is created. One of them will fail at picking. Solution: configure a safety stock buffer on ecommerce availability — show "In Stock" only when DC quantity exceeds the sum of all pending retail replenishment transfers plus a configurable threshold (we recommend 3 units per variant).

06

Ecommerce Integration and Size-Based Returns Management

Fashion ecommerce has a unique problem: return rates of 30-40%, driven primarily by size and fit issues. Generic return workflows treat every return as a refund. Fashion needs size exchanges as a first-class operation — keeping the revenue while shipping the correct size.

Configure the Odoo 19 ecommerce product page to minimize size-related returns before they happen:

  • Size chart integration: Pull measurement data from the size grid template and display it as an expandable table on the product page
  • Fit recommendation engine: A simple computed field that maps the customer's saved measurements (from their portal profile) to the nearest size in the grid
  • Color-accurate photography: Use the color_hex field to generate swatch selectors that match product photography exactly
  • Stock-by-size visibility: Show "Only 2 left" per size variant to reduce cart abandonment on popular sizes

Size Exchange Workflow

When a customer requests a return for "wrong size," the return workflow should default to exchange rather than refund. The exchange creates a new delivery order for the correct size variant while the return receipt processes the original:

Python — fashion_returns/models/sale_return.py
class SaleReturnWizard(models.TransientModel):
    _name = 'fashion.return.wizard'
    _description = 'Fashion Size Exchange Wizard'

    sale_order_id = fields.Many2one('sale.order')
    original_product_id = fields.Many2one(
        'product.product', string='Original Item'
    )
    exchange_product_id = fields.Many2one(
        'product.product', string='Exchange For',
        domain="[('product_tmpl_id', '=',"
               " original_tmpl_id)]"
    )
    original_tmpl_id = fields.Many2one(
        related='original_product_id.product_tmpl_id'
    )
    return_reason = fields.Selection([
        ('too_small', 'Too Small'),
        ('too_large', 'Too Large'),
        ('wrong_fit', 'Wrong Fit/Style'),
        ('defective', 'Defective'),
        ('other', 'Other'),
    ], required=True)

    def action_process_exchange(self):
        self.ensure_one()
        # Create return picking for original
        return_picking = self.sale_order_id \
            ._create_return_picking(
                self.original_product_id
            )
        # Create new SO line for exchange item
        if self.exchange_product_id:
            self.env['sale.order.line'].create({
                'order_id': self.sale_order_id.id,
                'product_id':
                    self.exchange_product_id.id,
                'product_uom_qty': 1,
                'price_unit': 0,  # No charge
                'name': 'Size Exchange: %s -> %s'
                    % (
                    self.original_product_id
                        .display_name,
                    self.exchange_product_id
                        .display_name,
                ),
            })
            self.sale_order_id.action_confirm()
        # Log return reason for analytics
        self.env['fashion.return.reason'].create({
            'product_tmpl_id':
                self.original_tmpl_id.id,
            'from_size': self.original_product_id
                .product_template_attribute_value_ids
                .filtered(
                    lambda v: v.attribute_id
                    .attribute_category == 'size'
                ).name,
            'reason': self.return_reason,
            'date': fields.Date.today(),
        })
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'stock.picking',
            'res_id': return_picking.id,
            'view_mode': 'form',
        }

The fashion.return.reason model is deceptively important. When you aggregate return reasons by product template and size over a season, patterns emerge: if 60% of returns for style DRS-042 are "Too Small," the size chart is wrong or the factory is cutting below spec. This data feeds back into the size grid measurements and QC tolerances — closing the loop between returns and production.

Track Exchange Conversion Rates

Not all returns need to be revenue lost. A well-designed exchange flow converts 40-60% of size returns into exchanges rather than refunds. Track the exchange-to-refund ratio by product category. If a category has a high refund rate despite exchange availability, the problem is likely product quality or misleading photography — not just sizing.

07

3 Fashion ERP Mistakes That Kill Sell-Through Rates

1

Variant Explosion Slows Down the Entire System

You add a third attribute — say "Length" (Regular, Petite, Tall) — to a style that already has 6 colors and 7 sizes. That single attribute turns 42 variants into 126 variants per product template. Multiply by 200 styles and you have 25,200 active variants. The product search becomes slow, the POS takes 8 seconds to load a product card, and the warehouse app times out on stock moves. Odoo's variant system works, but it was not designed for unconstrained attribute multiplication.

Our Fix

Cap attributes at two per product template (Color + Size). Handle "Length" and "Fit" as separate product templates that share a common style code. BLS-001-REG and BLS-001-PET are different templates with different BoMs but the same base style. This keeps variant count under 50 per template and maintains system performance. Use a style_code field on product.template to group related templates for reporting.

2

No Season Field on Inventory Means No Aging Visibility

Without a season tag on stock, the inventory valuation report shows total quantity on hand with no way to distinguish current-season stock (full price) from last-season stock (needs markdown). The merchandising team asks "how much AW25 inventory do we still have?" and accounting can't answer without exporting to a spreadsheet and cross-referencing purchase order dates. Markdown decisions are delayed, sell-through drops, and end-of-season write-downs spike.

Our Fix

Add a season_id field to product.template and use it as a filter dimension in all inventory reports. Configure automated actions that flag products where season_id.date_markdown_start has passed but quantity on hand exceeds a threshold. The merchandising team gets a weekly digest of aging inventory by season, category, and warehouse — with suggested markdown percentages based on weeks-of-supply.

3

Ecommerce Shows "Out of Stock" When Only One Size Is Gone

A customer searches for your best-selling black dress. Size M is sold out. Instead of showing the product with M grayed out, the product disappears from search results entirely because Odoo's default ecommerce stock filter checks the product template level, not individual variants. You lose sales of sizes S, L, and XL to customers who never saw the product.

Our Fix

Override the ecommerce product visibility logic to show a product as "In Stock" if any variant has availability > 0. On the product detail page, show each size's stock status individually — "In Stock," "Low Stock" (fewer than 3 units), or "Out of Stock" — and allow customers to set a back-in-stock notification for sold-out sizes. This keeps products visible in search results and captures demand signals for replenishment planning.

BUSINESS ROI

What Proper Fashion ERP Configuration Saves Your Business

Fashion margins are thin and unforgiving — a 5% improvement in sell-through rate can double net profit. Here's what changes when your ERP actually understands fashion:

25%Higher Full-Price Sell-Through

Seasonal planning with proper buy ratios and channel-specific allocation ensures the right sizes reach the right channels at launch. Less overstock means fewer markdowns.

50%Fewer Size-Related Returns

Size grids with measurements on product pages, fit recommendations, and return reason analytics reduce the #1 cause of fashion ecommerce returns. Each avoided return saves $12-18 in shipping and handling.

3 WeeksFaster Collection Launch

PLM-driven design-to-production pipeline with size grid templates eliminates manual product setup. A 150-style collection that took 4 weeks to set up now takes 1 week with templated attributes and automated variant generation.

The compound effect is what matters most. Better buy planning reduces overstock. Less overstock means fewer markdowns. Fewer markdowns protect brand value. Protected brand value supports full-price sell-through next season. Fashion ERP isn't about efficiency — it's about the margin cascade that determines whether a brand survives or doesn't.

SEO NOTES

Optimization Metadata

Meta Desc

Configure Odoo 19 for fashion & apparel. Covers product variant matrices, size grid templates, seasonal collection planning, PLM for garment design, multi-warehouse allocation, and returns management.

H2 Keywords

1. "Configuring Product Variants for Color/Size Matrices in Odoo 19"
2. "Size Grid Templates: Enforcing Consistent Sizing Across Product Categories"
3. "Seasonal Collection Planning: From Line Sheet to Purchase Order"
4. "PLM for Fashion: Managing the Design-to-Production Pipeline"

Your ERP Should Think in Seasons, Sizes, and Sell-Through

Fashion is one of the most operationally complex verticals to run on an ERP. The combination of high variant counts, seasonal calendars, channel-specific allocation, and return rates that would bankrupt other industries demands a system configured with intention — not a generic install with a few extra fields bolted on.

If you're running a fashion or apparel brand on Odoo and struggling with variant management, seasonal planning, or return rates — we can help. We run fashion ERP readiness assessments covering product architecture, size grid configuration, seasonal workflow design, warehouse allocation strategy, and ecommerce optimization. The assessment takes 3-5 days and produces a prioritized implementation roadmap.

Book a Free Fashion ERP Assessment