INTRODUCTION

Your Operators Are Tracking Production on Paper — And It's Costing You Hours Every Day

In most manufacturing plants running Odoo, the MRP module handles planning — bills of materials, routing, scheduling. But the moment a production order hits the shop floor, visibility drops to zero. Operators scribble start times on clipboards, quality inspectors walk between stations with paper checklists, and supervisors don't know a machine is down until someone walks over to tell them.

Odoo 19's Shop Floor module closes this gap. It provides a tablet-friendly, kiosk-mode interface designed specifically for operators — not back-office users. Workers see their assigned work orders, tap to start/pause/finish operations, log time automatically, run quality checks inline, and scan barcodes to confirm components. Supervisors get a real-time dashboard showing every work center's status, throughput, and bottlenecks.

This guide covers the complete setup: enabling Shop Floor, configuring work centers for tablet use, setting up time tracking, embedding quality control points, integrating barcode scanning, and building production dashboards. Every step includes the Python and XML you need, plus the gotchas we've hit deploying this for discrete and process manufacturers.

01

Enabling the Shop Floor Module and Configuring Basic Settings

The Shop Floor interface ships with the mrp module but requires explicit activation. It is available under Manufacturing > Shop Floor once the work center routing is configured. Here's how to enable it and set the foundational configuration.

Python — Enable Shop Floor in a custom module __manifest__.py
{
    'name': 'Shop Floor Customizations',
    'version': '19.0.1.0.0',
    'category': 'Manufacturing',
    'summary': 'Custom shop floor configurations for production tracking',
    'depends': [
        'mrp',
        'mrp_workorder',
        'quality_control',
        'stock_barcode',
    ],
    'data': [
        'security/ir.model.access.csv',
        'views/mrp_workcenter_views.xml',
        'views/shop_floor_templates.xml',
    ],
    'installable': True,
    'application': False,
    'license': 'LGPL-3',
}

Next, enable the required settings in Manufacturing > Configuration > Settings:

SettingLocationWhy It Matters
Work OrdersManufacturing > OperationsEnables routing-based work orders. Without this, production orders have no individual operations to track.
Quality ChecksQuality > Control PointsAllows quality inspections to be embedded directly into work order steps.
Barcode ScannerInventory > BarcodeEnables barcode scanning for component consumption and finished product registration.
Work Order DependenciesManufacturing > OperationsForces sequential operations — operators cannot start Step 3 until Step 2 is marked done.
XML — Enable settings via data file (for automated deployments)
<odoo>
  <data noupdate="1">
    <!-- Enable Work Orders -->
    <record id="config_enable_work_orders" model="res.config.settings">
      <field name="group_mrp_routings" eval="True"/>
    </record>

    <!-- Enable Quality Control -->
    <record id="config_enable_quality" model="res.config.settings">
      <field name="group_quality_control" eval="True"/>
    </record>

    <!-- Enable Barcode -->
    <record id="config_enable_barcode" model="res.config.settings">
      <field name="module_stock_barcode" eval="True"/>
    </record>
  </data>
</odoo>
Pro Tip: Module Dependencies

If you're on Odoo Community, the Shop Floor tablet interface and quality control integration require the Enterprise license. The mrp_workorder module (which powers Shop Floor) is Enterprise-only. Community users can still track work orders via the standard MRP backend, but the dedicated tablet UI and inline quality checks are not available.

02

Configuring Work Centers for Shop Floor Visibility

Work centers are the backbone of the Shop Floor module. Each work center represents a physical station (CNC machine, assembly bench, paint booth) and maps directly to a column in the Shop Floor interface. Operators see only the work orders assigned to their work center.

Python — Custom work center model extension
from odoo import models, fields, api


class MrpWorkcenter(models.Model):
    _inherit = 'mrp.workcenter'

    # ── Shop Floor Display Settings ──
    tablet_login_required = fields.Boolean(
        string='Require Operator Login',
        default=True,
        help='Force operators to scan their badge or enter PIN before starting work orders.',
    )
    display_priority = fields.Integer(
        string='Display Order',
        default=10,
        help='Controls the left-to-right order of work centers in the Shop Floor view.',
    )
    auto_time_tracking = fields.Boolean(
        string='Auto Time Tracking',
        default=True,
        help='Automatically start the timer when an operator begins a work order.',
    )
    allow_parallel_operations = fields.Boolean(
        string='Allow Parallel Operations',
        default=False,
        help='Allow operators to work on multiple work orders simultaneously at this center.',
    )
    kiosk_background_color = fields.Char(
        string='Kiosk Background Color',
        default='#1a1a2e',
        help='Hex color for the Shop Floor column header. Helps operators identify their station.',
    )

    # ── Capacity and OEE ──
    target_oee = fields.Float(
        string='Target OEE (%)',
        default=85.0,
        help='Overall Equipment Effectiveness target. Used in dashboard KPIs.',
    )
    planned_downtime_minutes = fields.Float(
        string='Planned Downtime (min/shift)',
        default=30.0,
        help='Scheduled breaks, changeovers, maintenance per 8-hour shift.',
    )

    @api.depends('order_ids', 'order_ids.duration_expected', 'order_ids.duration')
    def _compute_current_oee(self):
        """Compute real-time OEE for dashboard display."""
        for wc in self:
            completed = wc.order_ids.filtered(
                lambda o: o.state == 'done'
                and o.date_finished
                and o.date_finished >= fields.Datetime.now().replace(hour=0, minute=0, second=0)
            )
            if completed:
                expected = sum(completed.mapped('duration_expected'))
                actual = sum(completed.mapped('duration'))
                wc.current_oee = (expected / actual * 100) if actual > 0 else 0
            else:
                wc.current_oee = 0.0

    current_oee = fields.Float(
        compute='_compute_current_oee',
        string='Current OEE (%)',
    )
XML — Work center form view extension
<odoo>
  <record id="view_mrp_workcenter_form_shopfloor" model="ir.ui.view">
    <field name="name">mrp.workcenter.form.shopfloor</field>
    <field name="model">mrp.workcenter</field>
    <field name="inherit_id" ref="mrp.mrp_workcenter_view"/>
    <field name="arch" type="xml">
      <xpath expr="//page[@name='general']" position="after">
        <page string="Shop Floor" name="shop_floor">
          <group>
            <group string="Display Settings">
              <field name="tablet_login_required"/>
              <field name="display_priority"/>
              <field name="kiosk_background_color" widget="color"/>
            </group>
            <group string="Operator Settings">
              <field name="auto_time_tracking"/>
              <field name="allow_parallel_operations"/>
            </group>
          </group>
          <group string="OEE Targets">
            <field name="target_oee"/>
            <field name="planned_downtime_minutes"/>
            <field name="current_oee" readonly="1"/>
          </group>
        </page>
      </xpath>
    </field>
  </record>
</odoo>

With this configuration, each work center now carries the metadata the Shop Floor interface needs: display order, color coding, login requirements, and OEE tracking. The key relationship is Work Center → Routing → Bill of Materials. When a production order is created for a product with a routing, Odoo generates individual work orders for each operation and assigns them to the corresponding work center.

03

Setting Up the Tablet and Kiosk Interface for Shop Floor Operators

The Shop Floor view is accessed from Manufacturing > Shop Floor. It opens a full-screen, touch-optimized interface designed for tablets mounted at work stations. Each work center appears as a column, and work orders flow through as cards that operators can tap to start, pause, or complete.

Hardware Requirements

ComponentMinimum SpecRecommendedNotes
Tablet10" Android/iOS, 2GB RAMSamsung Galaxy Tab A8 / iPad 10th genMust run Chrome 90+ or Safari 15+ for full Shop Floor support.
MountVESA-compatible wall/pole mountRAM Mounts X-Grip with steel enclosureShop environments need shock-resistant, lockable enclosures.
Barcode ScannerBluetooth HID scannerZebra DS2278 / Honeywell Voyager 1602gMust pair via Bluetooth to the tablet. USB scanners need an OTG adapter.
NetworkWi-Fi with < 100ms latency to Odoo serverDedicated manufacturing VLAN with QoSShop Floor polls every 10 seconds. Unstable Wi-Fi causes "Connection Lost" errors.

Operator Authentication: PIN and Badge Login

Operators don't log into Odoo the way office users do. The Shop Floor interface supports two authentication methods: PIN entry and badge scanning. Each operator has a numeric PIN and/or a barcode badge. When they arrive at a work center, they scan their badge or type their PIN to "claim" the tablet session.

Python — Configure operator barcode and PIN on hr.employee
from odoo import models, fields


class HrEmployee(models.Model):
    _inherit = 'hr.employee'

    # These fields exist in base hr module, but here's how to set them programmatically
    def action_set_shop_floor_credentials(self):
        """Bulk-assign PINs and barcodes for shop floor operators."""
        for employee in self:
            if not employee.barcode:
                # Generate unique barcode: SF + employee ID zero-padded to 8 digits
                employee.barcode = f'SF{{employee.id:08d}}'
            if not employee.pin:
                # Generate 4-digit PIN from employee ID
                employee.pin = f'{{(employee.id * 7 + 1000) % 10000:04d}}'
XML — Add Shop Floor access group to manufacturing operators
<odoo>
  <data noupdate="0">
    <!-- Create a dedicated Shop Floor Operator group -->
    <record id="group_shop_floor_operator" model="res.groups">
      <field name="name">Shop Floor Operator</field>
      <field name="category_id" ref="base.module_category_manufacturing"/>
      <field name="implied_ids" eval="[(4, ref('mrp.group_mrp_user'))]"/>
      <field name="comment">
        Operators who use the Shop Floor tablet interface.
        Can start/pause/complete work orders and log time.
        Cannot modify routings, BOMs, or production planning.
      </field>
    </record>

    <!-- Restrict menu visibility -->
    <record id="mrp.menu_mrp_shop_floor" model="ir.ui.menu">
      <field name="groups_id" eval="[
        (4, ref('group_shop_floor_operator')),
        (4, ref('mrp.group_mrp_manager')),
      ]"/>
    </record>
  </data>
</odoo>

Kiosk Mode: Full-Screen Lock

For dedicated shop floor tablets, you want to lock the device into the Shop Floor interface so operators can't accidentally navigate to other Odoo modules or open the browser. This is handled at the device level, not in Odoo:

PlatformMethodSetup
AndroidGuided Access / Kiosk Browser AppInstall "Fully Kiosk Browser" (enterprise MDM) — set URL to https://erp.company.com/odoo/shop-floor, disable navigation bar, enable auto-restart on crash.
iPadGuided Access (built-in)Settings > Accessibility > Guided Access. Triple-click home button to lock into Safari. Disable touch on status bar area.
WindowsAssigned Access (Kiosk Mode)Windows Settings > Accounts > Assigned Access. Create a kiosk user locked to Edge with the Shop Floor URL.
Pro Tip: URL Shortcut

The direct URL for the Shop Floor interface is /odoo/shop-floor. You can append a work center filter: /odoo/shop-floor?work_center_id=5. This lets you pre-configure each tablet to show only its assigned work center, so operators never see irrelevant work orders.

04

Work Order Time Tracking: Automatic Timers, Pauses, and Labor Cost Allocation

When an operator taps "Start" on a work order card in the Shop Floor interface, Odoo creates a mrp.workcenter.productivity record — a time log entry that records the start timestamp, the operator, and the work center. When they tap "Pause" or "Done", the end timestamp is recorded. This gives you per-operation, per-operator time tracking with zero manual data entry.

Python — Extend time tracking with labor cost computation
from odoo import models, fields, api
from odoo.tools import float_round


class MrpWorkcenterProductivity(models.Model):
    _inherit = 'mrp.workcenter.productivity'

    operator_id = fields.Many2one(
        'hr.employee',
        string='Operator',
        help='Employee who performed this work order step.',
    )
    labor_cost = fields.Float(
        compute='_compute_labor_cost',
        string='Labor Cost',
        store=True,
        help='Computed from operator hourly rate × actual duration.',
    )

    @api.depends('duration', 'operator_id', 'operator_id.hourly_cost')
    def _compute_labor_cost(self):
        for record in self:
            if record.operator_id and record.duration:
                hours = record.duration / 60.0
                rate = record.operator_id.hourly_cost or record.workcenter_id.costs_hour
                record.labor_cost = float_round(hours * rate, precision_digits=2)
            else:
                record.labor_cost = 0.0


class MrpWorkorder(models.Model):
    _inherit = 'mrp.workorder'

    total_labor_cost = fields.Float(
        compute='_compute_total_labor_cost',
        string='Total Labor Cost',
        store=True,
    )
    efficiency_ratio = fields.Float(
        compute='_compute_efficiency_ratio',
        string='Efficiency (%)',
        help='Expected duration / Actual duration × 100',
    )

    @api.depends('time_ids.labor_cost')
    def _compute_total_labor_cost(self):
        for wo in self:
            wo.total_labor_cost = sum(wo.time_ids.mapped('labor_cost'))

    @api.depends('duration_expected', 'duration')
    def _compute_efficiency_ratio(self):
        for wo in self:
            if wo.duration > 0:
                wo.efficiency_ratio = float_round(
                    (wo.duration_expected / wo.duration) * 100,
                    precision_digits=1,
                )
            else:
                wo.efficiency_ratio = 0.0

Understanding the Time Tracking Data Model

ModelPurposeKey Fields
mrp.workorderA single operation within a production orderduration_expected, duration, state, workcenter_id
mrp.workcenter.productivityIndividual time log entries (start/stop pairs)date_start, date_end, duration, loss_id
mrp.workcenter.productivity.lossReason codes for non-productive timename, loss_type (productive / performance / quality / availability)
Python — Custom loss reasons for shop floor pauses
from odoo import models, fields


class MrpWorkcenterProductivityLoss(models.Model):
    _inherit = 'mrp.workcenter.productivity.loss'

    # Standard loss types from Odoo:
    # 'productive'   → Normal production time
    # 'performance'  → Slow cycle, reduced speed
    # 'quality'      → Rework, scrap
    # 'availability' → Downtime, breakdown, changeover


# Data file to create custom loss reasons
# Add to your module's data/mrp_data.xml:
#
# <record id="loss_material_shortage" model="mrp.workcenter.productivity.loss">
#     <field name="name">Material Shortage</field>
#     <field name="loss_type">availability</field>
#     <field name="sequence">10</field>
# </record>
#
# <record id="loss_tool_changeover" model="mrp.workcenter.productivity.loss">
#     <field name="name">Tool Changeover</field>
#     <field name="loss_type">availability</field>
#     <field name="sequence">20</field>
# </record>
#
# <record id="loss_machine_calibration" model="mrp.workcenter.productivity.loss">
#     <field name="name">Machine Calibration</field>
#     <field name="loss_type">performance</field>
#     <field name="sequence">30</field>
# </record>
Pro Tip: Loss Reasons Drive OEE

The loss_type field directly feeds Odoo's OEE (Overall Equipment Effectiveness) calculation. OEE = Availability × Performance × Quality. When an operator pauses a work order and selects "Machine Breakdown" (availability loss), it reduces the Availability component. "Slow Cycle" (performance loss) reduces Performance. "Rework" (quality loss) reduces Quality. If you don't configure meaningful loss reasons, your OEE numbers will be meaningless.

05

Embedding Quality Checks Directly into Work Order Steps

Odoo 19 lets you attach quality control points to specific operations in a routing. When an operator reaches that step in the Shop Floor interface, a quality check dialog appears before they can mark the operation as complete. This eliminates the "inspect later" problem — quality checks happen inline, in real-time, at the work center where the defect would have originated.

Python — Create quality control points programmatically
from odoo import models, api


class QualityPoint(models.Model):
    _inherit = 'quality.point'

    @api.model
    def create_shop_floor_checkpoints(self, product_tmpl_id, workcenter_id):
        """Create a standard set of quality checks for a work center.

        Called when setting up new products or routings.
        Returns the created quality points for further customization.
        """
        checks = [
            {
                'name': 'Visual Inspection',
                'product_tmpl_id': product_tmpl_id,
                'workcenter_id': workcenter_id,
                'test_type_id': self.env.ref('quality_control.test_type_passfail').id,
                'note': 'Check for surface defects, scratches, discoloration.',
                'sequence': 10,
            },
            {
                'name': 'Dimensional Check',
                'product_tmpl_id': product_tmpl_id,
                'workcenter_id': workcenter_id,
                'test_type_id': self.env.ref('quality_control.test_type_measure').id,
                'norm': 50.0,          # Target measurement
                'tolerance_min': 49.5,  # Lower tolerance
                'tolerance_max': 50.5,  # Upper tolerance
                'note': 'Measure width in mm using digital caliper.',
                'sequence': 20,
            },
            {
                'name': 'Component Verification',
                'product_tmpl_id': product_tmpl_id,
                'workcenter_id': workcenter_id,
                'test_type_id': self.env.ref('quality_control.test_type_passfail').id,
                'note': 'Scan barcode of each component to verify correct parts used.',
                'sequence': 30,
            },
            {
                'name': 'Photo Documentation',
                'product_tmpl_id': product_tmpl_id,
                'workcenter_id': workcenter_id,
                'test_type_id': self.env.ref('quality_control.test_type_picture').id,
                'note': 'Take a photo of the assembled unit before packaging.',
                'sequence': 40,
            },
        ]
        return self.create(checks)

Quality Check Types Available in Shop Floor

Check TypeOperator ActionPass/Fail LogicBest For
Pass/FailTap "Pass" or "Fail" buttonBinary — operator judgmentVisual inspections, go/no-go gauges
MeasureEnter a numeric valueAuto-pass if within tolerance rangeDimensions, weight, temperature, torque
Take PictureUse tablet camera to take a photoAlways passes (documentation only)Traceability, defect documentation, customer proof
TextEnter freeform textAlways passes (logging only)Operator notes, lot numbers, special instructions
XML — Quality alert automation: auto-create alert on failed check
<odoo>
  <!-- Server action: auto-create quality alert on failed check -->
  <record id="action_quality_check_failed_alert" model="ir.actions.server">
    <field name="name">Create Quality Alert on Failed Check</field>
    <field name="model_id" ref="quality_control.model_quality_check"/>
    <field name="state">code</field>
    <field name="code">
for check in records.filtered(lambda c: c.quality_state == 'fail'):
    env['quality.alert'].create({
        'name': f'FAIL: {check.point_id.name} - {check.product_id.name}',
        'product_id': check.product_id.id,
        'product_tmpl_id': check.product_id.product_tmpl_id.id,
        'workcenter_id': check.workcenter_id.id,
        'production_id': check.production_id.id,
        'team_id': check.point_id.team_id.id,
        'description': f'Failed quality check at {check.workcenter_id.name}.\n'
                       f'Check: {check.point_id.name}\n'
                       f'Operator: {check.write_uid.name}\n'
                       f'Work Order: {check.workorder_id.name}',
    })
    </field>
  </record>

  <!-- Automation rule: trigger on quality check state change -->
  <record id="rule_quality_check_failed" model="base.automation">
    <field name="name">Quality Check Failed → Create Alert</field>
    <field name="model_id" ref="quality_control.model_quality_check"/>
    <field name="trigger">on_write</field>
    <field name="trigger_field_ids" eval="[(4, ref('quality_control.field_quality_check__quality_state'))]"/>
    <field name="filter_domain">[('quality_state', '=', 'fail')]</field>
    <field name="action_server_ids" eval="[(4, ref('action_quality_check_failed_alert'))]"/>
  </record>
</odoo>
Pro Tip: Blocking vs. Non-Blocking Checks

Quality control points have a "Control Per" setting: per operation, per product, or per lot. For safety-critical products, set checks to per-unit and mark them as blocking — the operator physically cannot tap "Done" on the work order until every quality check passes. For high-volume, low-risk products, set checks to per-lot or per-operation to avoid slowing down the line.

06

Barcode Scanning Integration: Component Consumption and Finished Product Registration

Barcode scanning on the Shop Floor serves two purposes: confirming that operators use the correct components (incoming material verification) and registering finished products (output tracking with lot/serial numbers). Odoo 19's Shop Floor interface natively supports Bluetooth HID barcode scanners — no additional app or hardware driver required.

Python — Custom barcode handler for shop floor component scanning
from odoo import models, api, _
from odoo.exceptions import UserError


class MrpWorkorder(models.Model):
    _inherit = 'mrp.workorder'

    def action_scan_component_barcode(self, barcode):
        """Process a scanned barcode during a work order.

        Handles three barcode types:
        1. Product barcode → confirms correct component
        2. Lot/Serial barcode → assigns traceability
        3. Employee barcode → logs operator identity
        """
        self.ensure_one()

        # Check if it's an employee badge scan
        employee = self.env['hr.employee'].search([('barcode', '=', barcode)], limit=1)
        if employee:
            return self._handle_operator_scan(employee)

        # Check if it's a lot/serial number
        lot = self.env['stock.lot'].search([('name', '=', barcode)], limit=1)
        if lot:
            return self._handle_lot_scan(lot)

        # Check if it's a product barcode
        product = self.env['product.product'].search([('barcode', '=', barcode)], limit=1)
        if product:
            return self._handle_component_scan(product)

        raise UserError(_('Barcode "%s" not recognized. Scan a component, lot number, or employee badge.', barcode))

    def _handle_component_scan(self, product):
        """Verify scanned component matches expected BOM line."""
        expected_moves = self.move_raw_ids.filtered(
            lambda m: m.product_id == product and m.state not in ('done', 'cancel')
        )
        if not expected_moves:
            raise UserError(_(
                'Product "%s" is not a required component for this work order. '
                'Expected components: %s',
                product.display_name,
                ', '.join(self.move_raw_ids.mapped('product_id.display_name')),
            ))
        # Mark component as scanned/verified
        expected_moves[0].write({'is_scanned': True})
        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'title': _('Component Verified'),
                'message': _('%s scanned successfully.', product.display_name),
                'type': 'success',
                'sticky': False,
            },
        }

    def _handle_lot_scan(self, lot):
        """Assign scanned lot to the current work order's output."""
        if lot.product_id != self.product_id:
            raise UserError(_(
                'Lot "%s" belongs to product "%s", but this work order produces "%s".',
                lot.name, lot.product_id.display_name, self.product_id.display_name,
            ))
        self.finished_lot_id = lot
        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'title': _('Lot Assigned'),
                'message': _('Lot %s assigned to output.', lot.name),
                'type': 'success',
                'sticky': False,
            },
        }

    def _handle_operator_scan(self, employee):
        """Log operator identity for the current work order."""
        self.write({'employee_id': employee.id})
        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'title': _('Operator Logged'),
                'message': _('Welcome, %s.', employee.name),
                'type': 'info',
                'sticky': False,
            },
        }

Barcode Configuration Checklist

ItemWhere to SetFormat
Product BarcodesProduct > General Information > BarcodeEAN-13, UPC-A, Code128, or any string
Lot/Serial BarcodesAuto-generated or via Inventory > Lot/Serial NumbersTypically alphanumeric: LOT-2026-001234
Employee BadgesEmployees > HR Settings > Badge IDNumeric or alphanumeric. Prefix with "SF" to avoid collisions.
Work Center BarcodesManufacturing > Work Centers > CodeUsed for location scanning: WC-CNC-01
Pro Tip: Scanner Mode Matters

Make sure your Bluetooth barcode scanner is set to HID (keyboard emulation) mode, not SPP (serial port) mode. In HID mode, the scanner types the barcode as keystrokes — Odoo's Shop Floor interface captures this via a JavaScript keyboard listener. SPP mode requires a custom app and will not work with the browser-based Shop Floor. Also, configure the scanner to append an Enter key after each scan — this triggers the barcode processing in Odoo.

07

Production Dashboards: Real-Time Visibility for Supervisors and Plant Managers

While operators use the Shop Floor tablet interface, supervisors need a bird's-eye view of the entire production floor. Odoo 19 provides built-in work center dashboards under Manufacturing > Reporting, but for real-time shop floor monitoring, you'll want a custom dashboard that shows live status, throughput, and alerts.

Python — Dashboard data controller for real-time shop floor stats
from odoo import http, fields
from odoo.http import request
import json


class ShopFloorDashboard(http.Controller):

    @http.route('/shop_floor/dashboard/data', type='json', auth='user')
    def get_dashboard_data(self):
        """Return real-time shop floor KPIs for the dashboard."""
        Workorder = request.env['mrp.workorder']
        Workcenter = request.env['mrp.workcenter']
        today_start = fields.Datetime.now().replace(hour=0, minute=0, second=0)

        workcenters = Workcenter.search([])
        dashboard = []

        for wc in workcenters:
            # Current work orders
            in_progress = Workorder.search_count([
                ('workcenter_id', '=', wc.id),
                ('state', '=', 'progress'),
            ])
            pending = Workorder.search_count([
                ('workcenter_id', '=', wc.id),
                ('state', '=', 'ready'),
            ])
            completed_today = Workorder.search([
                ('workcenter_id', '=', wc.id),
                ('state', '=', 'done'),
                ('date_finished', '>=', today_start),
            ])

            # Compute today's throughput and efficiency
            total_expected = sum(completed_today.mapped('duration_expected'))
            total_actual = sum(completed_today.mapped('duration'))
            efficiency = (total_expected / total_actual * 100) if total_actual > 0 else 0

            dashboard.append({
                'id': wc.id,
                'name': wc.name,
                'code': wc.code,
                'in_progress': in_progress,
                'pending': pending,
                'completed_today': len(completed_today),
                'efficiency': round(efficiency, 1),
                'target_oee': wc.target_oee,
                'current_oee': round(wc.current_oee, 1),
                'status': 'active' if in_progress > 0 else ('idle' if pending > 0 else 'offline'),
            })

        # Overall plant KPIs
        all_today = Workorder.search([
            ('state', '=', 'done'),
            ('date_finished', '>=', today_start),
        ])

        return {
            'workcenters': dashboard,
            'plant_summary': {
                'total_completed': len(all_today),
                'total_in_progress': Workorder.search_count([('state', '=', 'progress')]),
                'total_pending': Workorder.search_count([('state', '=', 'ready')]),
                'avg_efficiency': round(
                    sum(d['efficiency'] for d in dashboard) / len(dashboard), 1
                ) if dashboard else 0,
            },
        }
XML — Dashboard action and menu entry
<odoo>
  <!-- Dashboard client action -->
  <record id="action_shop_floor_dashboard" model="ir.actions.client">
    <field name="name">Shop Floor Dashboard</field>
    <field name="tag">shop_floor_dashboard</field>
    <field name="context">{'auto_refresh': 30}</field>
  </record>

  <!-- Menu item under Manufacturing > Reporting -->
  <menuitem
    id="menu_shop_floor_dashboard"
    name="Shop Floor Dashboard"
    parent="mrp.menu_mrp_reporting"
    action="action_shop_floor_dashboard"
    sequence="5"
    groups="mrp.group_mrp_manager"
  />

  <!-- OEE Pivot view for historical analysis -->
  <record id="view_workcenter_productivity_pivot_oee" model="ir.ui.view">
    <field name="name">mrp.workcenter.productivity.pivot.oee</field>
    <field name="model">mrp.workcenter.productivity</field>
    <field name="arch" type="xml">
      <pivot string="OEE Analysis">
        <field name="workcenter_id" type="row"/>
        <field name="loss_id" type="col"/>
        <field name="duration" type="measure"/>
      </pivot>
    </field>
  </record>
</odoo>

Key Dashboard Metrics to Track

MetricFormulaTargetAction If Below Target
OEEAvailability × Performance × Quality> 85%Drill into which factor (A/P/Q) is dragging it down.
First Pass YieldUnits passed QC first time / Total units produced> 95%Review quality check failure reasons by work center.
Cycle Time Variance(Actual - Expected) / Expected × 100< 10%Investigate if expected times in routing are outdated.
Downtime %Non-productive time / Total shift time × 100< 15%Check loss reason breakdown — machine vs. material vs. changeover.
Pro Tip: TV Dashboard Mode

Mount a large TV on the shop floor wall and open the dashboard in a browser with ?auto_refresh=30 in the URL. Use Chromium kiosk mode (chromium --kiosk --incognito https://erp.company.com/shop_floor/dashboard) so it refreshes every 30 seconds. This gives everyone — operators, supervisors, visitors — instant visibility into production status without needing to access Odoo.

08

5 Shop Floor Deployment Mistakes That Kill Operator Adoption

1

Routing Durations Set to Zero — OEE Shows 0% or Infinity

When you create routing operations, the duration_expected field defaults to zero. Odoo uses this as the denominator in efficiency calculations. Zero expected time means the efficiency formula divides by zero, producing either 0% or infinite OEE. Supervisors see nonsense numbers on the dashboard and immediately stop trusting the system.

Our Fix

Before going live, run a time study on each operation. Set duration_expected to a realistic average. After 2 weeks of live data, update expected times using actual averages: UPDATE mrp_routing_workcenter SET time_cycle_manual = (SELECT AVG(duration) FROM mrp_workorder WHERE operation_id = mrp_routing_workcenter.id AND state = 'done').

2

Wi-Fi Dead Zones Cause "Connection Lost" Every 10 Seconds

The Shop Floor interface polls the server every 10 seconds via the bus (longpolling/WebSocket). If a tablet loses Wi-Fi for more than 10 seconds, it shows a "Connection Lost" banner and stops recording time. The operator keeps working, but the time log has a gap. Worse, if the reconnection happens mid-operation, the work order state can become inconsistent.

Our Fix

Do a Wi-Fi heat map of the shop floor before deployment. Every work center needs < 100ms latency and > -65 dBm signal strength. Add access points near metal machinery (which reflects/absorbs Wi-Fi). Use a dedicated manufacturing VLAN with QoS prioritization for Odoo traffic.

3

Operators Forget to Tap "Pause" — Time Logs Include Lunch Breaks

The most common data quality issue. An operator starts a work order at 8:00 AM, goes to lunch at noon without pausing, and finishes at 1:30 PM. The system logs 5.5 hours for a 4.5-hour operation. Over a week, this inflates labor costs by 10–15% and makes efficiency metrics unreliable.

Our Fix

Implement an auto-pause mechanism. Use a scheduled action that runs every 5 minutes: if a work order has been "in progress" for longer than 2x its expected duration without any activity (no barcode scans, no quality checks), auto-pause it with loss reason "Idle — Auto-paused." Also, add a cron that pauses all active work orders during configured break times.

4

Quality Checks Block the Line Because Inspectors Are Busy

If every quality check is set to "blocking" mode and your sole quality inspector is on break, the entire line stops. Operators see "Waiting for Quality Check" and can't proceed to the next operation. In a high-volume environment, a 15-minute delay at one station cascades into hours of lost production across downstream work centers.

Our Fix

Classify checks into tiers. Tier 1 (Safety/Regulatory): Always blocking — operator cannot proceed. Tier 2 (Quality): Blocking only for first article inspection; subsequent units in the same lot use "non-blocking" checks that can be completed later. Tier 3 (Documentation): Never blocking — photos and notes can be added after the operation.

5

Barcode Collisions Between Products, Lots, and Employee Badges

If a product barcode happens to match an employee badge number (both are just strings), scanning that barcode on the Shop Floor triggers the wrong action — the system logs an operator change instead of confirming a component. This is silent and corrupts both the operator log and the component verification.

Our Fix

Use prefixed barcode schemes. Employee badges: EMP-XXXXX. Products: standard EAN-13 or UPC-A. Lots: LOT-YYYY-XXXXXX. Work centers: WC-XXXX. The custom barcode handler checks prefixes first, eliminating ambiguity. Never allow free-form barcodes without a namespace prefix.

BUSINESS ROI

What Real-Time Shop Floor Tracking Saves Your Manufacturing Operation

The Shop Floor module replaces paper-based tracking, manual data entry, and supervisor walk-arounds with automated, real-time data collection. Here's the measurable impact we've seen across client deployments:

2-3 hrs/daySupervisor Time Saved

No more walking the floor to check status. Real-time dashboards show every work center's state, throughput, and bottlenecks from any screen.

15-25%More Accurate Labor Costing

Auto time tracking eliminates guesswork. Actual labor hours per product are recorded to the minute, making job costing reliable.

40-60%Faster Defect Detection

Inline quality checks catch defects at the source, not at final inspection. Rework costs drop because you find problems one step after they occur, not ten steps later.

Beyond the direct savings, the data collected by Shop Floor feeds into continuous improvement cycles. Once you have 30 days of time tracking data, you can identify: which operations consistently exceed expected times (routing problem or training gap?), which work centers have the highest downtime (maintenance schedule or equipment age?), and which quality checks fail most often (supplier issue or process issue?). This is the foundation of a data-driven lean manufacturing practice — and it starts with getting operators to tap "Start" on a tablet instead of writing on a clipboard.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to Odoo 19 Shop Floor module. Configure tablet kiosk interface, work order time tracking, inline quality checks, barcode scanning, and real-time production dashboards.

H2 Keywords

1. "Configuring Work Centers for Shop Floor Visibility"
2. "Work Order Time Tracking: Automatic Timers, Pauses, and Labor Cost Allocation"
3. "Embedding Quality Checks Directly into Work Order Steps"
4. "Barcode Scanning Integration: Component Consumption and Finished Product Registration"
5. "Production Dashboards: Real-Time Visibility for Supervisors and Plant Managers"

Stop Tracking Production on Clipboards

The Shop Floor module is one of Odoo 19's most impactful features for manufacturers — but only if it's configured correctly. The difference between a successful rollout and one that operators abandon within a week comes down to the details: reliable Wi-Fi, realistic routing times, non-ambiguous barcodes, and quality checks that help rather than block.

If you're planning to deploy Shop Floor in your manufacturing facility, we can help. We handle the full setup: work center configuration, routing time studies, quality control point design, barcode scheme architecture, tablet hardware selection, and operator training. Our deployments typically go live in 2-3 weeks with full operator adoption within the first month.

Book a Free Manufacturing Consultation