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.
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.
{
'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:
| Setting | Location | Why It Matters |
|---|---|---|
| Work Orders | Manufacturing > Operations | Enables routing-based work orders. Without this, production orders have no individual operations to track. |
| Quality Checks | Quality > Control Points | Allows quality inspections to be embedded directly into work order steps. |
| Barcode Scanner | Inventory > Barcode | Enables barcode scanning for component consumption and finished product registration. |
| Work Order Dependencies | Manufacturing > Operations | Forces sequential operations — operators cannot start Step 3 until Step 2 is marked done. |
<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> 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.
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.
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 (%)',
)<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.
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
| Component | Minimum Spec | Recommended | Notes |
|---|---|---|---|
| Tablet | 10" Android/iOS, 2GB RAM | Samsung Galaxy Tab A8 / iPad 10th gen | Must run Chrome 90+ or Safari 15+ for full Shop Floor support. |
| Mount | VESA-compatible wall/pole mount | RAM Mounts X-Grip with steel enclosure | Shop environments need shock-resistant, lockable enclosures. |
| Barcode Scanner | Bluetooth HID scanner | Zebra DS2278 / Honeywell Voyager 1602g | Must pair via Bluetooth to the tablet. USB scanners need an OTG adapter. |
| Network | Wi-Fi with < 100ms latency to Odoo server | Dedicated manufacturing VLAN with QoS | Shop 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.
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}}'<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:
| Platform | Method | Setup |
|---|---|---|
| Android | Guided Access / Kiosk Browser App | Install "Fully Kiosk Browser" (enterprise MDM) — set URL to https://erp.company.com/odoo/shop-floor, disable navigation bar, enable auto-restart on crash. |
| iPad | Guided Access (built-in) | Settings > Accessibility > Guided Access. Triple-click home button to lock into Safari. Disable touch on status bar area. |
| Windows | Assigned Access (Kiosk Mode) | Windows Settings > Accounts > Assigned Access. Create a kiosk user locked to Edge with the Shop Floor URL. |
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.
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.
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.0Understanding the Time Tracking Data Model
| Model | Purpose | Key Fields |
|---|---|---|
mrp.workorder | A single operation within a production order | duration_expected, duration, state, workcenter_id |
mrp.workcenter.productivity | Individual time log entries (start/stop pairs) | date_start, date_end, duration, loss_id |
mrp.workcenter.productivity.loss | Reason codes for non-productive time | name, loss_type (productive / performance / quality / availability) |
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> 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.
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.
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 Type | Operator Action | Pass/Fail Logic | Best For |
|---|---|---|---|
| Pass/Fail | Tap "Pass" or "Fail" button | Binary — operator judgment | Visual inspections, go/no-go gauges |
| Measure | Enter a numeric value | Auto-pass if within tolerance range | Dimensions, weight, temperature, torque |
| Take Picture | Use tablet camera to take a photo | Always passes (documentation only) | Traceability, defect documentation, customer proof |
| Text | Enter freeform text | Always passes (logging only) | Operator notes, lot numbers, special instructions |
<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>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.
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.
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
| Item | Where to Set | Format |
|---|---|---|
| Product Barcodes | Product > General Information > Barcode | EAN-13, UPC-A, Code128, or any string |
| Lot/Serial Barcodes | Auto-generated or via Inventory > Lot/Serial Numbers | Typically alphanumeric: LOT-2026-001234 |
| Employee Badges | Employees > HR Settings > Badge ID | Numeric or alphanumeric. Prefix with "SF" to avoid collisions. |
| Work Center Barcodes | Manufacturing > Work Centers > Code | Used for location scanning: WC-CNC-01 |
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.
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.
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,
},
}<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
| Metric | Formula | Target | Action If Below Target |
|---|---|---|---|
| OEE | Availability × Performance × Quality | > 85% | Drill into which factor (A/P/Q) is dragging it down. |
| First Pass Yield | Units 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. |
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.
5 Shop Floor Deployment Mistakes That Kill Operator Adoption
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.
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').
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.
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.
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.
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.
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.
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.
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.
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.
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:
No more walking the floor to check status. Real-time dashboards show every work center's state, throughput, and bottlenecks from any screen.
Auto time tracking eliminates guesswork. Actual labor hours per product are recorded to the minute, making job costing reliable.
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.
Optimization Metadata
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.
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"