Chain-of-custody log: one row per Move Parts / Move Rack commit. FP/MOVE/YYYY/NNNN sequence (5-digit). Carries from/to step + tank, transfer type, qty, location, photo, rack, operator, datetime. Child model captures recorded transition-input values (Sub 12a's fp.step.template.transition.input snapshots → fp.job.step.move. input.value rows). Each row carries 5 typed value columns; the controller (Task 8) picks the right one based on input_type. Operators get read+write+create — they generate moves at runtime — but no unlink. Manager-only deletes for audit safety. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
4.1 KiB
Python
105 lines
4.1 KiB
Python
# -*- 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')
|