217 lines
7.0 KiB
Python
217 lines
7.0 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 FpTankComposition(models.Model):
|
|
"""A defined chemistry composition for a tank (e.g. "Composition A",
|
|
"High-P Mix", "Strike Solution").
|
|
|
|
A tank can carry multiple authored compositions; one is "active" at any
|
|
given time. Switching compositions is a tracked, audit-logged event so
|
|
the shop has a chronological record of "what did this tank actually
|
|
contain on Tuesday afternoon?"
|
|
|
|
Each composition has its own ingredient list with per-chemical
|
|
percentages. Changes to ingredients are also chatter-tracked.
|
|
"""
|
|
_name = 'fusion.plating.tank.composition'
|
|
_description = 'Fusion Plating — Tank Composition'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_order = 'tank_id, sequence, name'
|
|
|
|
name = fields.Char(
|
|
string='Composition',
|
|
required=True,
|
|
tracking=True,
|
|
)
|
|
code = fields.Char(
|
|
string='Code',
|
|
tracking=True,
|
|
help='Short identifier — "A", "B", "C".',
|
|
)
|
|
sequence = fields.Integer(
|
|
string='Sequence',
|
|
default=10,
|
|
)
|
|
tank_id = fields.Many2one(
|
|
'fusion.plating.tank',
|
|
string='Tank',
|
|
required=True,
|
|
ondelete='cascade',
|
|
tracking=True,
|
|
)
|
|
facility_id = fields.Many2one(
|
|
related='tank_id.facility_id',
|
|
store=True,
|
|
readonly=True,
|
|
)
|
|
is_active = fields.Boolean(
|
|
string='Currently Active',
|
|
compute='_compute_is_active',
|
|
help='True when this composition is the tank\'s active composition.',
|
|
)
|
|
description = fields.Text(
|
|
string='Description',
|
|
)
|
|
notes = fields.Html(
|
|
string='Notes',
|
|
)
|
|
ingredient_ids = fields.One2many(
|
|
'fusion.plating.tank.composition.ingredient',
|
|
'composition_id',
|
|
string='Ingredients',
|
|
copy=True,
|
|
tracking=True,
|
|
)
|
|
total_percentage = fields.Float(
|
|
string='Total %',
|
|
compute='_compute_total_percentage',
|
|
store=True,
|
|
)
|
|
active = fields.Boolean(default=True)
|
|
|
|
@api.depends('ingredient_ids', 'ingredient_ids.percentage')
|
|
def _compute_total_percentage(self):
|
|
for rec in self:
|
|
rec.total_percentage = sum(rec.ingredient_ids.mapped('percentage'))
|
|
|
|
@api.depends('tank_id', 'tank_id.active_composition_id')
|
|
def _compute_is_active(self):
|
|
for rec in self:
|
|
rec.is_active = rec.tank_id.active_composition_id.id == rec.id
|
|
|
|
def action_set_active(self):
|
|
"""Mark this composition as the tank's active composition. Logs to
|
|
both this composition's chatter and the tank's chatter so the audit
|
|
trail captures who flipped the switch and when.
|
|
"""
|
|
self.ensure_one()
|
|
old = self.tank_id.active_composition_id
|
|
if old.id == self.id:
|
|
return True
|
|
self.tank_id.active_composition_id = self.id
|
|
msg_tank = _(
|
|
'Active composition changed: %(old)s → %(new)s by %(user)s'
|
|
) % {
|
|
'old': old.display_name or _('(none)'),
|
|
'new': self.display_name,
|
|
'user': self.env.user.name,
|
|
}
|
|
self.tank_id.message_post(body=msg_tank)
|
|
self.message_post(body=_('Activated by %s') % self.env.user.name)
|
|
return True
|
|
|
|
|
|
class FpTankCompositionIngredient(models.Model):
|
|
"""A single chemical entry in a tank composition.
|
|
|
|
Free-form chemical name (no FK to fusion.plating.chemical because core
|
|
must not depend on the safety module). Percentage is the share of the
|
|
composition; total % roll-up lives on the parent composition.
|
|
"""
|
|
_name = 'fusion.plating.tank.composition.ingredient'
|
|
_description = 'Fusion Plating — Tank Composition Ingredient'
|
|
_order = 'composition_id, sequence, id'
|
|
|
|
composition_id = fields.Many2one(
|
|
'fusion.plating.tank.composition',
|
|
string='Composition',
|
|
required=True,
|
|
ondelete='cascade',
|
|
index=True,
|
|
)
|
|
tank_id = fields.Many2one(
|
|
related='composition_id.tank_id',
|
|
store=True,
|
|
readonly=True,
|
|
)
|
|
sequence = fields.Integer(
|
|
string='Sequence',
|
|
default=10,
|
|
)
|
|
name = fields.Char(
|
|
string='Chemical',
|
|
required=True,
|
|
)
|
|
percentage = fields.Float(
|
|
string='Percentage',
|
|
digits=(6, 3),
|
|
required=True,
|
|
help='Share of this composition, in percent.',
|
|
)
|
|
uom = fields.Selection(
|
|
[
|
|
('pct', '% by Volume'),
|
|
('pct_w', '% by Weight'),
|
|
('g_l', 'g/L'),
|
|
('ml_l', 'mL/L'),
|
|
('oz_gal', 'oz/gal'),
|
|
],
|
|
string='Unit',
|
|
default='pct',
|
|
)
|
|
notes = fields.Char(
|
|
string='Notes',
|
|
)
|
|
|
|
# Mirror create/write/unlink to the parent composition's chatter so
|
|
# ingredient changes show up in the audit log even though this row
|
|
# doesn't carry mail.thread itself (kept lean for repeater UX).
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
records = super().create(vals_list)
|
|
for rec in records:
|
|
rec.composition_id.message_post(body=_(
|
|
'Ingredient added: %(name)s — %(pct)s %(uom)s'
|
|
) % {
|
|
'name': rec.name,
|
|
'pct': rec.percentage,
|
|
'uom': dict(rec._fields['uom'].selection).get(rec.uom, rec.uom),
|
|
})
|
|
return records
|
|
|
|
def write(self, vals):
|
|
# Capture before-state per-record so we can describe the diff.
|
|
snapshots = {
|
|
rec.id: {
|
|
'name': rec.name,
|
|
'percentage': rec.percentage,
|
|
'uom': rec.uom,
|
|
}
|
|
for rec in self
|
|
}
|
|
result = super().write(vals)
|
|
for rec in self:
|
|
before = snapshots.get(rec.id) or {}
|
|
changed = []
|
|
if 'name' in vals and before.get('name') != rec.name:
|
|
changed.append(_('name: %s → %s') % (before.get('name'), rec.name))
|
|
if 'percentage' in vals and before.get('percentage') != rec.percentage:
|
|
changed.append(_('percentage: %s → %s') % (
|
|
before.get('percentage'), rec.percentage,
|
|
))
|
|
if 'uom' in vals and before.get('uom') != rec.uom:
|
|
changed.append(_('unit: %s → %s') % (before.get('uom'), rec.uom))
|
|
if changed:
|
|
rec.composition_id.message_post(body=_(
|
|
'Ingredient %(name)s updated — %(changes)s'
|
|
) % {
|
|
'name': rec.name,
|
|
'changes': '; '.join(changed),
|
|
})
|
|
return result
|
|
|
|
def unlink(self):
|
|
for rec in self:
|
|
rec.composition_id.message_post(body=_(
|
|
'Ingredient removed: %(name)s — %(pct)s'
|
|
) % {
|
|
'name': rec.name,
|
|
'pct': rec.percentage,
|
|
})
|
|
return super().unlink()
|