feat(fusion_plating_jobs): de-rack/bake area_kind name override + rack-load Phase 1
- _compute_area_kind: name-based override so de-rack/de-mask steps land in the De-Racking column and bake/oven steps in Baking, regardless of a mis-tagged recipe kind (fixed WO cards scattering into the wrong shop-floor columns). - fp.rack.load jobs extension: racking-step resolution by area_kind (not the corrupt kind), equal-split/override ops, fp.job qty_racked/unracked rollups, and independent rack movement (per-line moves) + de-racking unrack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
166
fusion_plating/fusion_plating_jobs/models/fp_job_rack.py
Normal file
166
fusion_plating/fusion_plating_jobs/models/fp_job_rack.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
#
|
||||
# Multi-rack splitting at Racking — Phase 1 jobs-module extension.
|
||||
# Core models live in fusion_plating/models/fp_rack_load.py. This file owns
|
||||
# everything that touches jobs-module fields (fp.job.step.area_kind,
|
||||
# fp.job.part_catalog_id) and the racking-step detection (_fp_is_racking_step).
|
||||
# Spec/plan: docs/superpowers/{specs,plans}/2026-06-03-racking-multi-rack-*.md
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class FpRackLoad(models.Model):
|
||||
_inherit = 'fp.rack.load'
|
||||
|
||||
current_area_kind = fields.Char(
|
||||
string='Current Area', compute='_compute_current_area_kind', store=True)
|
||||
|
||||
@api.depends('current_step_id.area_kind')
|
||||
def _compute_current_area_kind(self):
|
||||
for load in self:
|
||||
load.current_area_kind = load.current_step_id.area_kind or False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Racking-step resolution + the "total parts available to rack"
|
||||
# ------------------------------------------------------------------
|
||||
@api.model
|
||||
def _fp_racking_step_for(self, job):
|
||||
# Detect the racking step by area_kind == 'racking' (the corrected
|
||||
# classification), NOT _fp_is_racking_step() — the latter keys off the
|
||||
# step's kind, and de-racking steps are frequently mis-tagged
|
||||
# kind='racking' in the data, which would wrongly match De-Racking.
|
||||
return job.step_ids.filtered(lambda s: s.area_kind == 'racking')[:1]
|
||||
|
||||
@api.model
|
||||
def _fp_racking_total(self, job):
|
||||
step = self._fp_racking_step_for(job)
|
||||
if step and step.qty_at_step:
|
||||
return int(step.qty_at_step)
|
||||
return int(job.qty or 0)
|
||||
|
||||
@api.model
|
||||
def _fp_job_loads(self, job):
|
||||
"""Active (not unracked/cancelled) loads carrying this job's parts."""
|
||||
return self.search([
|
||||
('line_ids.job_id', '=', job.id),
|
||||
('state', 'in', ('loading', 'loaded', 'running')),
|
||||
], order='id')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Division API (operator's split + manual override)
|
||||
# ------------------------------------------------------------------
|
||||
@api.model
|
||||
def _fp_split_job(self, job, n):
|
||||
"""(Re)create n loads for `job`, equal split of the racking total.
|
||||
|
||||
Drops existing unmoved 'loading' loads first. Moved/assigned loads are
|
||||
left alone (can't re-split parts that already advanced)."""
|
||||
total = self._fp_racking_total(job)
|
||||
self._fp_job_loads(job).filtered(
|
||||
lambda l: l.state == 'loading' and not l.current_step_id).unlink()
|
||||
qtys = self._fp_equal_split(total, max(int(n), 1))
|
||||
loads = self.browse()
|
||||
for q in qtys:
|
||||
loads |= self.create({
|
||||
'line_ids': [(0, 0, {'job_id': job.id, 'qty': q})],
|
||||
})
|
||||
return loads
|
||||
|
||||
@api.model
|
||||
def _fp_ensure_seeded(self, job):
|
||||
"""Default state: one rack carrying all the parts."""
|
||||
if not self._fp_job_loads(job):
|
||||
self._fp_split_job(job, 1)
|
||||
return self._fp_job_loads(job)
|
||||
|
||||
@api.model
|
||||
def _fp_add_rack(self, job):
|
||||
return self._fp_split_job(job, len(self._fp_job_loads(job)) + 1)
|
||||
|
||||
@api.model
|
||||
def _fp_divide_equally(self, job):
|
||||
return self._fp_split_job(job, max(len(self._fp_job_loads(job)), 1))
|
||||
|
||||
def _fp_set_qty(self, qty):
|
||||
"""Manual override of a single load's quantity (must not exceed the
|
||||
job's available parts across all its loads)."""
|
||||
self.ensure_one()
|
||||
line = self.line_ids[:1]
|
||||
if not line:
|
||||
raise UserError(_('This rack has no work order line.'))
|
||||
job = line.job_id
|
||||
total = self._fp_racking_total(job)
|
||||
other = sum((self._fp_job_loads(job) - self).mapped('qty_total'))
|
||||
if other + int(qty) > total:
|
||||
raise UserError(
|
||||
_('Assigned %(a)s exceeds the %(t)s parts available to rack.')
|
||||
% {'a': other + int(qty), 't': total})
|
||||
line.qty = int(qty)
|
||||
|
||||
def _fp_remove_rack(self):
|
||||
self.ensure_one()
|
||||
if self.current_step_id:
|
||||
raise UserError(_('Cannot remove a rack that has already moved.'))
|
||||
self.unlink()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Independent movement + de-racking
|
||||
# ------------------------------------------------------------------
|
||||
def _fp_advance_to(self, to_step):
|
||||
"""Move these rack-loads to `to_step`: one move row per line (per WO),
|
||||
carrying the rack + line qty, then update position/state."""
|
||||
Move = self.env['fp.job.step.move']
|
||||
for load in self:
|
||||
from_step = load.current_step_id
|
||||
for line in load.line_ids:
|
||||
Move.create({
|
||||
'job_id': line.job_id.id,
|
||||
'from_step_id': from_step.id if from_step else False,
|
||||
'to_step_id': to_step.id,
|
||||
'qty_moved': line.qty,
|
||||
'rack_id': load.rack_id.id if load.rack_id else False,
|
||||
'transfer_type': 'step',
|
||||
'moved_by_user_id': self.env.user.id,
|
||||
})
|
||||
load.current_step_id = to_step
|
||||
load.state = 'running'
|
||||
|
||||
def _fp_unrack(self):
|
||||
"""De-Racking: free the physical rack; each line's parts continue in
|
||||
its own job's flow (the per-line moves already attributed qty)."""
|
||||
for load in self:
|
||||
load.state = 'unracked'
|
||||
if load.rack_id:
|
||||
load.rack_id.racking_state = 'empty'
|
||||
|
||||
|
||||
class FpRackLoadLine(models.Model):
|
||||
_inherit = 'fp.rack.load.line'
|
||||
|
||||
part_catalog_id = fields.Many2one(
|
||||
related='job_id.part_catalog_id', store=True, string='Part')
|
||||
|
||||
|
||||
class FpJob(models.Model):
|
||||
_inherit = 'fp.job'
|
||||
|
||||
rack_load_line_ids = fields.One2many(
|
||||
'fp.rack.load.line', 'job_id', string='Rack Loads')
|
||||
qty_racked = fields.Integer(
|
||||
string='Parts Racked', compute='_compute_qty_racked')
|
||||
qty_unracked = fields.Integer(
|
||||
string='Parts Unassigned', compute='_compute_qty_racked')
|
||||
|
||||
@api.depends('rack_load_line_ids.qty', 'rack_load_line_ids.load_id.state')
|
||||
def _compute_qty_racked(self):
|
||||
Load = self.env['fp.rack.load']
|
||||
for job in self:
|
||||
active = job.rack_load_line_ids.filtered(
|
||||
lambda l: l.load_id.state in ('loading', 'loaded', 'running'))
|
||||
job.qty_racked = sum(active.mapped('qty'))
|
||||
total = Load._fp_racking_total(job)
|
||||
job.qty_unracked = max(total - job.qty_racked, 0)
|
||||
Reference in New Issue
Block a user