diff --git a/fusion_plating/fusion_plating/data/fp_sequence_data.xml b/fusion_plating/fusion_plating/data/fp_sequence_data.xml
index 5d46cac3..6dfe542d 100644
--- a/fusion_plating/fusion_plating/data/fp_sequence_data.xml
+++ b/fusion_plating/fusion_plating/data/fp_sequence_data.xml
@@ -22,5 +22,13 @@
+
+
+ FP — Move Log
+ fp.job.step.move
+ FP/MOVE/%(year)s/
+ 5
+
+
diff --git a/fusion_plating/fusion_plating/models/__init__.py b/fusion_plating/fusion_plating/models/__init__.py
index bf11a616..d2ef444f 100644
--- a/fusion_plating/fusion_plating/models/__init__.py
+++ b/fusion_plating/fusion_plating/models/__init__.py
@@ -40,3 +40,4 @@ from . import fp_step_template_transition_input
# Sub 12b — Rack-aware moves + persistent labor reconciliation
from . import fp_rack_tag
+from . import fp_job_step_move
diff --git a/fusion_plating/fusion_plating/models/fp_job_step_move.py b/fusion_plating/fusion_plating/models/fp_job_step_move.py
new file mode 100644
index 00000000..d9b41347
--- /dev/null
+++ b/fusion_plating/fusion_plating/models/fp_job_step_move.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
+# Part of the Fusion Plating product family.
+
+from odoo import api, fields, models
+
+
+class FpJobStepMove(models.Model):
+ """Chain-of-custody log — one row per part-batch move.
+
+ Sub 12b: every Move Parts / Move Rack click commits one (or, for
+ rack moves, one-per-batch atomic) row here. Sub 12c walks these in
+ chronological order to render the customer CoC PDF.
+ """
+ _name = 'fp.job.step.move'
+ _description = 'Fusion Plating — Job Step Move (Chain-of-Custody)'
+ _inherit = ['mail.thread']
+ _order = 'move_datetime desc, id desc'
+
+ name = fields.Char(
+ string='Move Reference',
+ default=lambda self: self.env['ir.sequence'].next_by_code(
+ 'fp.job.step.move') or '/',
+ readonly=True, copy=False,
+ )
+ job_id = fields.Many2one('fp.job', string='Job',
+ required=True, ondelete='cascade', index=True)
+ from_step_id = fields.Many2one('fp.job.step', string='From Step',
+ ondelete='set null', index=True)
+ to_step_id = fields.Many2one('fp.job.step', string='To Step',
+ ondelete='set null', index=True, required=True)
+ from_tank_id = fields.Many2one('fusion.plating.tank',
+ related='from_step_id.tank_id', store=True)
+ to_tank_id = fields.Many2one('fusion.plating.tank', string='To Tank',
+ ondelete='set null')
+
+ transfer_type = fields.Selection([
+ ('step', 'Step'),
+ ('hold', 'Hold'),
+ ('scrap', 'Scrap'),
+ ('rework', 'Rework'),
+ ('split', 'Split'),
+ ('return', 'Return'),
+ ], string='Transfer Type', default='step', required=True)
+
+ qty_moved = fields.Integer(string='Qty Moved', required=True)
+ qty_available_at_move = fields.Integer(string='Qty Available')
+
+ to_location = fields.Selection([
+ ('global', 'Global'),
+ ('quarantine', 'Quarantine'),
+ ('staging_a', 'Staging A'),
+ ('staging_b', 'Staging B'),
+ ('shipping_dock', 'Shipping Dock'),
+ ('scrap_bin', 'Scrap Bin'),
+ ], string='To Location', default='global')
+
+ photo_evidence_id = fields.Many2one('ir.attachment',
+ string='Photo Evidence', ondelete='set null')
+ customer_wo_count = fields.Integer(string='# Customer WOs')
+
+ rack_id = fields.Many2one('fusion.plating.rack',
+ string='Rack', ondelete='set null', index=True)
+ unrack_after_move = fields.Boolean(string='Unrack After Move')
+
+ moved_by_user_id = fields.Many2one('res.users', string='Moved By',
+ default=lambda self: self.env.user, required=True)
+ move_datetime = fields.Datetime(string='Move Time',
+ default=fields.Datetime.now, required=True, index=True)
+
+ transition_input_value_ids = fields.One2many(
+ 'fp.job.step.move.input.value', 'move_id',
+ string='Transition Input Values',
+ )
+
+
+class FpJobStepMoveInputValue(models.Model):
+ """Captured value for one transition-input prompt.
+
+ Each row = one author-defined prompt × one move. Snapshot of what
+ the operator typed at move-time. Used by Sub 12c CoC report.
+ """
+ _name = 'fp.job.step.move.input.value'
+ _description = 'Fusion Plating — Captured Transition Input Value'
+ _order = 'move_id, id'
+
+ move_id = fields.Many2one('fp.job.step.move', string='Move',
+ required=True, ondelete='cascade', index=True)
+ template_input_id = fields.Many2one(
+ 'fp.step.template.transition.input',
+ string='Template Input', ondelete='set null',
+ help='What was originally asked (template-level reference).')
+ node_input_id = fields.Many2one(
+ 'fusion.plating.process.node.input',
+ string='Node Input', ondelete='set null',
+ help='Snapshot of the authored prompt at job-creation time.')
+
+ value_text = fields.Char(string='Text Value')
+ value_number = fields.Float(string='Number Value')
+ value_boolean = fields.Boolean(string='Yes/No Value')
+ value_date = fields.Datetime(string='Date Value')
+ value_attachment_id = fields.Many2one('ir.attachment',
+ string='Attachment Value', ondelete='set null')
diff --git a/fusion_plating/fusion_plating/security/ir.model.access.csv b/fusion_plating/fusion_plating/security/ir.model.access.csv
index a94c186d..6b78505e 100644
--- a/fusion_plating/fusion_plating/security/ir.model.access.csv
+++ b/fusion_plating/fusion_plating/security/ir.model.access.csv
@@ -73,3 +73,9 @@ access_fp_step_template_transition_input_manager,fp.step.template.transition.inp
access_fp_rack_tag_operator,fp.rack.tag.operator,model_fp_rack_tag,group_fusion_plating_operator,1,0,0,0
access_fp_rack_tag_supervisor,fp.rack.tag.supervisor,model_fp_rack_tag,group_fusion_plating_supervisor,1,1,1,1
access_fp_rack_tag_manager,fp.rack.tag.manager,model_fp_rack_tag,group_fusion_plating_manager,1,1,1,1
+access_fp_job_step_move_operator,fp.job.step.move.operator,model_fp_job_step_move,group_fusion_plating_operator,1,1,1,0
+access_fp_job_step_move_supervisor,fp.job.step.move.supervisor,model_fp_job_step_move,group_fusion_plating_supervisor,1,1,1,0
+access_fp_job_step_move_manager,fp.job.step.move.manager,model_fp_job_step_move,group_fusion_plating_manager,1,1,1,1
+access_fp_job_step_move_input_value_operator,fp.job.step.move.input.value.operator,model_fp_job_step_move_input_value,group_fusion_plating_operator,1,1,1,0
+access_fp_job_step_move_input_value_supervisor,fp.job.step.move.input.value.supervisor,model_fp_job_step_move_input_value,group_fusion_plating_supervisor,1,1,1,0
+access_fp_job_step_move_input_value_manager,fp.job.step.move.input.value.manager,model_fp_job_step_move_input_value,group_fusion_plating_manager,1,1,1,1