Files
Odoo-Modules/fusion_plating/fusion_plating_receiving/models/sale_order.py
gsinghpal dcd4955bb7 feat(jobs+receiving): confirm->receive flow, lock recipe, reset step, lock steps, fix bake gate
- Confirm->Receive (A): after a single interactive SO confirm, receiving's
  action_confirm returns action_view_receiving() so the user lands straight
  on the Receive Parts screen (opt-out via fp_no_receiving_redirect context).
- Lock recipe (1): recipe_id readonly on the WO form — stick to the
  order-entry recipe.
- Hide spec (2): customer_spec_id invisible on the WO form.
- Reset step (3): new fp.job.step.button_reset (operator-usable, audited) +
  an undo button next to Start. Resets to Ready, clears finish + sign-off,
  closes open timelogs, keeps start audit + move/CoC history.
- Lock steps (4): steps list create=false delete=false (no Add a line / no
  trash) — steps come from the recipe, only skippable, never deleted.
- Bake gate fix (5): _fp_missing_required_step_inputs now honours the node's
  collect_measurements master switch, matching the Record-Inputs wizard.
  collect_measurements=False + required prompts no longer blocks finish
  (wizard shows 0 rows, so the gate must too). Unblocks WO-30098 + 63 other
  affected nodes (bake steps).

Deployed + verified on entech (-u jobs; bake finishes, reset done->ready,
recipe readonly, spec hidden, steps locked, receiving redirect target OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:55:34 -04:00

106 lines
4.2 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 SaleOrder(models.Model):
_inherit = 'sale.order'
x_fc_receiving_ids = fields.One2many(
'fp.receiving', 'sale_order_id', string='Receiving Records',
)
x_fc_receiving_count = fields.Integer(
string='Receiving Count', compute='_compute_receiving_count',
)
x_fc_show_receive_parts_btn = fields.Boolean(
string='Show Receive Parts Button',
compute='_compute_show_receive_parts_btn',
help='True once the SO is confirmed and there is still at least '
'one receiving record that is not yet closed. Hidden again '
'when every receiving record has been closed.',
)
@api.depends('x_fc_receiving_ids')
def _compute_receiving_count(self):
for rec in self:
rec.x_fc_receiving_count = len(rec.x_fc_receiving_ids)
@api.depends('state', 'x_fc_receiving_ids.state')
def _compute_show_receive_parts_btn(self):
for rec in self:
if rec.state not in ('sale', 'done'):
rec.x_fc_show_receive_parts_btn = False
continue
rec.x_fc_show_receive_parts_btn = any(
r.state != 'closed' for r in rec.x_fc_receiving_ids
)
def action_confirm(self):
"""Override to auto-create receiving record on SO confirmation.
Per-line metadata (part catalog, part number) is sourced from
``sale.order.line.x_fc_part_catalog_id`` — NOT from the SO header.
The header field exists too but is rarely populated; the line
carries the authoritative part link in the configurator flow.
Each receiving line prefills ``received_qty`` to ``expected_qty``
so the racking crew only types when the count is off (mirrors
the header behaviour in fp_receiving.py:create).
"""
res = super().action_confirm()
for order in self:
if order.x_fc_receiving_ids:
continue
total_qty = sum(order.order_line.mapped('product_uom_qty'))
line_vals = []
for line in order.order_line:
part = (
line.x_fc_part_catalog_id
if 'x_fc_part_catalog_id' in line._fields else False
)
expected = int(line.product_uom_qty or 0)
line_vals.append((0, 0, {
'part_catalog_id': part.id if part else False,
'part_number': (part.part_number if part else '') or '',
'description': line.name or '',
'expected_qty': expected,
'received_qty': expected,
}))
self.env['fp.receiving'].create({
'sale_order_id': order.id,
'expected_qty': int(total_qty),
'line_ids': line_vals,
})
# Seamless flow: after a single interactive confirm, jump straight
# to the Receive Parts screen so the dock counts parts in right away
# (idiot-proof — no hunting for the smart button). Guarded to a
# single order + an opt-out context flag so batch / programmatic
# confirms (and tests) keep the native return value.
if (len(self) == 1
and not self.env.context.get('fp_no_receiving_redirect')
and self.x_fc_receiving_ids):
return self.action_view_receiving()
return res
def action_view_receiving(self):
"""Smart button action to view receiving records."""
self.ensure_one()
if self.x_fc_receiving_count == 1:
return {
'type': 'ir.actions.act_window',
'res_model': 'fp.receiving',
'res_id': self.x_fc_receiving_ids[0].id,
'view_mode': 'form',
'target': 'current',
}
return {
'type': 'ir.actions.act_window',
'res_model': 'fp.receiving',
'view_mode': 'list,form',
'domain': [('sale_order_id', '=', self.id)],
'target': 'current',
}