Files
Odoo-Modules/fusion_plating/fusion_plating/models/fp_bath_log_line.py
gsinghpal d3dd6376a6 feat(fusion_plating): quote-to-cash infra, notifications, wizards, Tier 1 plating features
Quote-to-cash PDF reports (portrait + landscape variants, 16 new actions):
- Quotation / Sales Order, Work Order Traveller, Packing Slip, Bill of Lading,
  Certificate of Conformance (portrait added), Invoice, Payment Receipt
- Shared fp_portrait_styles + fp_landscape_styles base templates

Workflow gap fixes (fusion_plating_bridge_mrp):
- Auto-assign recipe from SO coating config in MrpProduction.action_confirm
- Auto-create draft CoC (fp.certificate) on MrpProduction.button_mark_done

Notifications overhaul (fusion_plating_notifications v2.0):
- Expanded TRIGGER_EVENTS to 7 (added quote_sent, mo_complete, shipped, payment_received)
- Shared _dispatch method replaces three duplicated send helpers
- Auto-attach PDF reports per template config (quote, SO, CoC, invoice, receipt, BoL)
- Rebuilt 7 email templates with fusion_claims accent-bar design
  (info/success color-coded, theme-safe, 600px max-width)
- New hooks: MrpProduction done, FpDelivery mark_delivered, AccountPayment post,
  SaleOrder action_quotation_send

Wizards (fusion_plating_configurator):
- fp.direct.order.wizard — skip quotation for repeat customers with PO in hand;
  optional new-revision drawing upload bumps fp.part.catalog revision and links
  new rev to the SO; creates + confirms the SO in one step
- fp.part.catalog.import.wizard — 3-step CSV import with dry-run preview,
  tolerant parsing (customer by name/email/xmlid, human-readable selections),
  duplicate detection, create-missing-customers option, single transaction commit
- Partner form stat buttons: Direct Order, Import Parts
- CSV template download button

Tier 1 practical plating features:
- T1.1 Hydrogen bake window enforcement (fp.coating.config.requires_bake_relief,
  auto-create fusion.plating.bake.window on plating WO finish, FpDelivery lockout
  when window is open)
- T1.2 Bath replenishment rules + pending suggestion queue
  (fusion.plating.bath.replenishment.rule + .suggestion, hook on bath log line
  create, operator Apply / Dismiss actions)
- T1.3 Rack/fixture library (fusion.plating.rack with MTO counter, strip
  schedule, lifecycle: active → needs_strip → stripping → retired)
- T1.4 Rework / strip-and-replate MOs (x_fc_is_rework, x_fc_original_production_id,
  Create Rework stat button on completed MOs)
- T1.5 Parts location (x_fc_current_location computed on mrp.production —
  "In progress: Alkaline Clean" / "Queued: Bake Oven" / "Ready to Ship")

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:41:12 -04:00

159 lines
5.5 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 FpBathLogLine(models.Model):
"""A single parameter reading on a bath log.
Each line = one titration result or one sensor reading. Target ranges
are pulled from the bath's per-bath overrides if present, otherwise
from the parameter's defaults on fusion.plating.bath.parameter.
Status is computed per line (ok / warning / out_of_spec) and rolled
up to the parent log.
"""
_name = 'fusion.plating.bath.log.line'
_description = 'Fusion Plating — Bath Log Reading'
_order = 'log_id, sequence, id'
log_id = fields.Many2one(
'fusion.plating.bath.log',
string='Log',
required=True,
ondelete='cascade',
index=True,
)
bath_id = fields.Many2one(
related='log_id.bath_id',
store=True,
readonly=True,
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
parameter_id = fields.Many2one(
'fusion.plating.bath.parameter',
string='Parameter',
required=True,
ondelete='restrict',
)
parameter_code = fields.Char(
related='parameter_id.code',
store=True,
readonly=True,
)
uom = fields.Char(
related='parameter_id.uom',
readonly=True,
)
value = fields.Float(
string='Value',
required=True,
)
target_min = fields.Float(
string='Target Min',
compute='_compute_targets',
store=True,
)
target_max = fields.Float(
string='Target Max',
compute='_compute_targets',
store=True,
)
status = fields.Selection(
[
('ok', 'OK'),
('warning', 'Warning'),
('out_of_spec', 'Out of Spec'),
],
string='Status',
compute='_compute_status',
store=True,
)
notes = fields.Char(
string='Notes',
)
# ==========================================================================
@api.depends('parameter_id', 'log_id.bath_id')
def _compute_targets(self):
"""Resolve target range: per-bath override first, parameter default second."""
for rec in self:
tmin = tmax = 0.0
if rec.log_id.bath_id and rec.parameter_id:
override = rec.log_id.bath_id.target_line_ids.filtered(
lambda t: t.parameter_id.id == rec.parameter_id.id
)[:1]
if override:
tmin, tmax = override.target_min, override.target_max
else:
tmin = rec.parameter_id.target_min
tmax = rec.parameter_id.target_max
rec.target_min = tmin
rec.target_max = tmax
@api.depends('value', 'target_min', 'target_max', 'parameter_id.warning_tolerance')
def _compute_status(self):
for rec in self:
if rec.target_min == 0.0 and rec.target_max == 0.0:
rec.status = 'ok'
continue
v, lo, hi = rec.value, rec.target_min, rec.target_max
if v < lo or v > hi:
rec.status = 'out_of_spec'
continue
tol_pct = (rec.parameter_id.warning_tolerance or 0.0) / 100.0
span = max(hi - lo, 1e-9)
if tol_pct > 0 and (v - lo < span * tol_pct or hi - v < span * tol_pct):
rec.status = 'warning'
else:
rec.status = 'ok'
# ------------------------------------------------------------------
# T1.2 — Auto-suggest replenishment on every log line
# ------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
lines = super().create(vals_list)
lines._spawn_replenishment_suggestions()
return lines
def _spawn_replenishment_suggestions(self):
"""For every out-of-spec reading, run the matching replenishment
rule and create a pending suggestion the operator can apply."""
Rule = self.env['fusion.plating.bath.replenishment.rule']
Suggestion = self.env['fusion.plating.bath.replenishment.suggestion']
for line in self:
if not line.parameter_id or not line.log_id.bath_id:
continue
bath = line.log_id.bath_id
rules = Rule._find_rules(bath, line.parameter_id.id)
for rule in rules:
dose = rule._compute_dose(
line.value, line.target_min, line.target_max, bath.volume,
)
if dose <= 0:
continue
Suggestion.create({
'bath_id': bath.id,
'log_line_id': line.id,
'rule_id': rule.id,
'parameter_id': line.parameter_id.id,
'current_value': line.value,
'target_min': line.target_min,
'target_max': line.target_max,
'product_name': rule.product_name,
'dose_amount': dose,
'dose_uom': rule.dose_uom,
'state': 'pending',
})
bath.message_post(
body=f'Replenishment suggested: add {dose} {rule.dose_uom} '
f'of {rule.product_name} ({line.parameter_id.name} '
f'reading: {line.value})',
)