- group_fp_office_user now implies base.group_partner_manager so every office/ manager role can create contacts (Technicians excluded). Fixes the live "create a company contact, it doesn't show" report (AccessError on save). - New fp.rack.load + fp.rack.load.line models (multi-rack split at Racking, Phase 1) with sequence, ACLs, equal-split math, and tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
95 lines
3.6 KiB
Python
95 lines
3.6 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.
|
|
#
|
|
# Multi-rack splitting + WO grouping at Racking — Phase 1 core models.
|
|
# Spec: docs/superpowers/specs/2026-06-03-racking-multi-rack-wo-grouping-design.md
|
|
# Plan: docs/superpowers/plans/2026-06-03-racking-multi-rack-phase1.md
|
|
#
|
|
# This file (core module) deliberately depends only on CORE fields. The
|
|
# racking-step-aware division ops, the fp.job rollups, movement, and the
|
|
# current_area_kind compute live in fusion_plating_jobs/models/fp_job_rack.py
|
|
# (that module owns fp.job.step.area_kind, fp.job.part_catalog_id, and
|
|
# _fp_is_racking_step).
|
|
|
|
from odoo import api, fields, models, _
|
|
|
|
|
|
class FpRackLoad(models.Model):
|
|
_name = 'fp.rack.load'
|
|
_description = 'Rack Load (parts on one physical rack)'
|
|
_inherit = ['mail.thread']
|
|
_order = 'id desc'
|
|
|
|
name = fields.Char(
|
|
string='Reference', required=True, copy=False, index=True,
|
|
default=lambda self: _('New'))
|
|
rack_id = fields.Many2one(
|
|
'fusion.plating.rack', string='Rack', tracking=True)
|
|
line_ids = fields.One2many(
|
|
'fp.rack.load.line', 'load_id', string='Work Orders')
|
|
qty_total = fields.Integer(
|
|
string='Total Parts', compute='_compute_qty_total', store=True)
|
|
current_step_id = fields.Many2one(
|
|
'fp.job.step', string='Current Step', tracking=True)
|
|
state = fields.Selection([
|
|
('loading', 'Loading'),
|
|
('loaded', 'Loaded'),
|
|
('running', 'Running'),
|
|
('unracked', 'Unracked'),
|
|
('cancelled', 'Cancelled'),
|
|
], string='State', default='loading', required=True, tracking=True)
|
|
tag_ids = fields.Many2many('fp.rack.tag', string='Tags')
|
|
company_id = fields.Many2one(
|
|
'res.company', string='Company',
|
|
default=lambda self: self.env.company)
|
|
|
|
@api.depends('line_ids.qty')
|
|
def _compute_qty_total(self):
|
|
for load in self:
|
|
load.qty_total = sum(load.line_ids.mapped('qty'))
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
if vals.get('name', _('New')) == _('New'):
|
|
vals['name'] = (
|
|
self.env['ir.sequence'].next_by_code('fp.rack.load')
|
|
or _('New'))
|
|
return super().create(vals_list)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Pure division math (no DB) — verifiable in isolation.
|
|
# ------------------------------------------------------------------
|
|
@api.model
|
|
def _fp_equal_split(self, total, n):
|
|
"""Split ``total`` parts across ``n`` racks as evenly as possible.
|
|
|
|
Remainder goes to the FIRST racks (spec D4): 100/3 -> [34, 33, 33].
|
|
Returns a list of n ints summing to total. n < 1 -> [].
|
|
"""
|
|
n = int(n)
|
|
if n < 1:
|
|
return []
|
|
base, rem = divmod(int(total), n)
|
|
return [base + 1 if i < rem else base for i in range(n)]
|
|
|
|
_qty_total_non_negative = models.Constraint(
|
|
'CHECK (qty_total >= 0)',
|
|
'Rack load total quantity cannot be negative.')
|
|
|
|
|
|
class FpRackLoadLine(models.Model):
|
|
_name = 'fp.rack.load.line'
|
|
_description = 'Rack Load Line (one work order on a rack)'
|
|
|
|
load_id = fields.Many2one(
|
|
'fp.rack.load', string='Rack Load', required=True, ondelete='cascade')
|
|
job_id = fields.Many2one('fp.job', string='Work Order', required=True)
|
|
qty = fields.Integer(string='Parts', required=True, default=0)
|
|
|
|
_qty_non_negative = models.Constraint(
|
|
'CHECK (qty >= 0)',
|
|
'Rack load line quantity cannot be negative.')
|