Files
Odoo-Modules/fusion_plating/fusion_plating/models/fp_tank_composition.py
gsinghpal 13e300d90e changes
2026-04-28 19:39:37 -04:00

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()