Bug surfaced on WO-30043 (2026-05-20): operator walked every step
including a fully closed receiving record, then hit
"Quantity Received is blank — close the receiving record for
SO SO-30043 before completing this job." Receiving WAS closed.
Root cause: the 2026-05-18 cert-creation gate
(fp.job.button_mark_done) blocks on job.qty_received but nothing
populated it. fp.receiving carried the qty on its line records,
fp.job stayed at 0 indefinitely. Two disconnected records on the
same SO.
Fix: when fp.receiving._update_so_receiving_status runs (i.e. on
every state transition — counted / staged / closed / accepted /
resolved), also mirror each line's received_qty onto the matching
fp.job by (sale_order_id + part_catalog_id). Single-part SOs map
1-to-1; multi-part SOs spawn one job per line so the same join
still works.
Two defensive guards in the hook:
- Skip silently when fusion_plating_jobs not installed
(Job = env.get('fp.job') returns None).
- Skip silently when fp.job doesn't yet carry part_catalog_id /
qty_received (test scope, unusual install topology).
Drive-by during cleanup:
- fp_parent_numbered_mixin._fp_assign_parent_name: guard
so.x_fc_parent_number access with field-existence check. The
column lives in fusion_plating_jobs; downstream modules that
inherit the mixin (receiving) but don't depend on jobs were
hitting AttributeError on every fp.receiving.create at test
time. Falls through to the legacy sequence when the column
isn't there.
- fp_receiving_views.xml: legacy carrier_name Char field rendered
as a second carrier row labeled "Legacy Carrier" alongside the
proper x_fc_carrier_id M2O — operators saw two carrier fields
and got confused. Hide the legacy display (data stays in DB for
audit; migration 19.0.3.10.0 already matched it to a real
delivery.carrier).
Migration 19.0.3.19.0/post-migrate.py backfills qty_received from
closed receiving lines for any job stuck at 0 — fixes WO-30043
and two sibling jobs on entech.
Modules: fusion_plating 19.0.20.2.0, fusion_plating_receiving
19.0.3.19.0, fusion_plating_jobs 19.0.10.15.0.
All 19 tests green (TestCarrierFields 6, TestQtyReceivedPropagation 5
new, TestReceivingGate 8). Direct verification on entech: WO-30043
qty_received = 1, mark_done succeeds, delivery + cert auto-created.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
308 lines
16 KiB
XML
308 lines
16 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
||
<!--
|
||
Copyright 2026 Nexa Systems Inc.
|
||
License OPL-1 (Odoo Proprietary License v1.0)
|
||
Part of the Fusion Plating product family.
|
||
-->
|
||
<odoo>
|
||
|
||
<!-- ===== Receiving List View ===== -->
|
||
<record id="view_fp_receiving_list" model="ir.ui.view">
|
||
<field name="name">fp.receiving.list</field>
|
||
<field name="model">fp.receiving</field>
|
||
<field name="arch" type="xml">
|
||
<list string="Receiving"
|
||
decoration-warning="state == 'discrepancy'"
|
||
decoration-success="state == 'accepted'"
|
||
decoration-muted="state == 'resolved'"
|
||
default_order="received_date desc">
|
||
<field name="received_date"/>
|
||
<field name="name"/>
|
||
<field name="sale_order_id"/>
|
||
<field name="partner_id"/>
|
||
<field name="po_number"/>
|
||
<field name="expected_qty"/>
|
||
<field name="received_qty"/>
|
||
<field name="qty_match" widget="boolean"/>
|
||
<field name="state" widget="badge"
|
||
decoration-info="state == 'draft'"
|
||
decoration-warning="state in ('inspecting', 'discrepancy')"
|
||
decoration-success="state == 'accepted'"
|
||
decoration-muted="state == 'resolved'"/>
|
||
</list>
|
||
</field>
|
||
</record>
|
||
|
||
<!-- ===== Receiving Form View ===== -->
|
||
<record id="view_fp_receiving_form" model="ir.ui.view">
|
||
<field name="name">fp.receiving.form</field>
|
||
<field name="model">fp.receiving</field>
|
||
<field name="arch" type="xml">
|
||
<form string="Receiving">
|
||
<header>
|
||
<!-- Sub 8 — new primary flow: box count only -->
|
||
<button name="action_mark_counted"
|
||
string="Mark Counted"
|
||
type="object"
|
||
class="btn-primary"
|
||
invisible="state not in ('draft', 'inspecting')"/>
|
||
<button name="action_mark_staged"
|
||
string="Stage for Racking"
|
||
type="object"
|
||
class="btn-primary"
|
||
invisible="state != 'counted'"/>
|
||
<button name="action_close"
|
||
string="Close"
|
||
type="object"
|
||
invisible="state not in ('staged', 'accepted', 'resolved')"/>
|
||
<!-- Legacy actions (hidden by default; surfaces for old records) -->
|
||
<button name="action_accept"
|
||
string="Accept (legacy)"
|
||
type="object"
|
||
invisible="state != 'inspecting'"
|
||
groups="fusion_plating.group_fusion_plating_manager"/>
|
||
<button name="action_flag_discrepancy"
|
||
string="Flag Discrepancy (legacy)"
|
||
type="object"
|
||
class="btn-danger"
|
||
invisible="state != 'inspecting'"
|
||
groups="fusion_plating.group_fusion_plating_manager"/>
|
||
<button name="action_resolve"
|
||
string="Resolve (legacy)"
|
||
type="object"
|
||
invisible="state != 'discrepancy'"
|
||
groups="fusion_plating.group_fusion_plating_manager"/>
|
||
<button name="action_generate_outbound_label"
|
||
type="object"
|
||
string="Generate Outbound Label"
|
||
class="btn-primary"
|
||
icon="fa-print"
|
||
invisible="not x_fc_carrier_id or not x_fc_weight"/>
|
||
<button name="action_print_label"
|
||
type="object"
|
||
string="Print Label"
|
||
class="btn-secondary"
|
||
icon="fa-file-pdf-o"
|
||
invisible="not x_fc_outbound_shipment_id"/>
|
||
<field name="state" widget="statusbar"
|
||
statusbar_visible="draft,counted,staged,closed"/>
|
||
</header>
|
||
<sheet>
|
||
<div class="oe_button_box" name="button_box">
|
||
<button name="action_create_outbound_shipment"
|
||
type="object"
|
||
class="oe_stat_button"
|
||
icon="fa-truck">
|
||
<field name="x_fc_outbound_shipment_count"
|
||
widget="statinfo"
|
||
string="Outbound Shipment"/>
|
||
</button>
|
||
<button name="action_print_label"
|
||
type="object"
|
||
class="oe_stat_button"
|
||
icon="fa-print"
|
||
invisible="not x_fc_has_label">
|
||
<div class="o_stat_info">
|
||
<span class="o_stat_value">PDF</span>
|
||
<span class="o_stat_text">Print Label</span>
|
||
</div>
|
||
<field name="x_fc_has_label" invisible="1"/>
|
||
</button>
|
||
</div>
|
||
<div class="alert alert-info" role="alert">
|
||
<i class="fa fa-info-circle me-2"/>
|
||
<strong>Receiving = box count only.</strong>
|
||
Count the boxes the truck dropped off, set the number
|
||
below, and stage them for racking. The racking crew
|
||
opens the boxes and inspects each part — click
|
||
<strong>Racking Inspections</strong> above to jump
|
||
straight to the open inspection for this SO.
|
||
</div>
|
||
<div class="oe_title">
|
||
<h1>
|
||
<field name="name" readonly="1"/>
|
||
</h1>
|
||
</div>
|
||
<group>
|
||
<group>
|
||
<field name="sale_order_id"/>
|
||
<field name="partner_id"/>
|
||
<field name="po_number"/>
|
||
</group>
|
||
<group string="Box Count">
|
||
<field name="box_count_in"/>
|
||
</group>
|
||
</group>
|
||
<group>
|
||
<group string="Reception">
|
||
<field name="received_by_id"/>
|
||
<field name="received_date"/>
|
||
<field name="x_fc_carrier_id"
|
||
options="{'no_create': True}"/>
|
||
<field name="carrier_tracking"/>
|
||
<!--
|
||
Legacy carrier_name (Char) is retained
|
||
in the schema for audit but hidden from
|
||
the form. Migration 19.0.3.10.0 already
|
||
matched legacy text against
|
||
delivery.carrier.name; any unmatched
|
||
value still lives on the record. View
|
||
duplication confused operators (one
|
||
field labelled "Outbound Carrier", a
|
||
second readonly one labelled "Legacy
|
||
Carrier") so we drop the display.
|
||
-->
|
||
<field name="carrier_name" invisible="1"/>
|
||
</group>
|
||
</group>
|
||
<group string="Outbound Packaging"
|
||
invisible="not x_fc_carrier_id">
|
||
<group>
|
||
<label for="x_fc_weight"/>
|
||
<div class="o_row">
|
||
<field name="x_fc_weight"/>
|
||
<field name="x_fc_weight_uom" nolabel="1"/>
|
||
</div>
|
||
<label for="x_fc_length" string="Dimensions (L×W×H)"/>
|
||
<div class="o_row">
|
||
<field name="x_fc_length"
|
||
placeholder="L"/>
|
||
<span class="mx-1">×</span>
|
||
<field name="x_fc_width"
|
||
placeholder="W"/>
|
||
<span class="mx-1">×</span>
|
||
<field name="x_fc_height"
|
||
placeholder="H"/>
|
||
<field name="x_fc_dim_uom" nolabel="1"/>
|
||
</div>
|
||
</group>
|
||
<group>
|
||
<p class="text-muted" colspan="2">
|
||
Enter the weight and dimensions of the
|
||
packaging you'll use to ship the finished
|
||
parts back. The system reuses the same
|
||
boxes for the return shipment. Click
|
||
<strong>Generate Outbound Label</strong>
|
||
in the header once carrier + weight are
|
||
set.
|
||
</p>
|
||
</group>
|
||
<group string="Quantities (populated by racking crew)">
|
||
<field name="expected_qty" readonly="1"/>
|
||
<field name="received_qty" readonly="1"/>
|
||
<field name="qty_match" widget="boolean_toggle" readonly="1"/>
|
||
</group>
|
||
</group>
|
||
<notebook>
|
||
<page string="Multi-Piece Packages"
|
||
name="outbound_packages"
|
||
invisible="not x_fc_carrier_id">
|
||
<p class="text-muted">
|
||
For multi-box shipments, add one row per
|
||
box with its weight + dimensions. The
|
||
carrier API will return one tracking
|
||
number + one label per row.
|
||
<strong>Single-box flow:</strong> leave
|
||
this empty and the top-level weight/dim
|
||
fields above are used.
|
||
</p>
|
||
<field name="x_fc_outbound_package_ids">
|
||
<list editable="bottom">
|
||
<field name="sequence" widget="handle"/>
|
||
<field name="weight"/>
|
||
<field name="length"/>
|
||
<field name="width"/>
|
||
<field name="height"/>
|
||
<field name="tracking_number"
|
||
readonly="1"/>
|
||
<field name="label_attachment_id"
|
||
readonly="1"
|
||
widget="many2one_binary"/>
|
||
</list>
|
||
</field>
|
||
</page>
|
||
<page string="Receiving Lines" name="lines">
|
||
<field name="line_ids">
|
||
<list editable="bottom">
|
||
<field name="part_number"/>
|
||
<field name="description"/>
|
||
<field name="expected_qty"/>
|
||
<field name="received_qty"/>
|
||
<field name="condition"/>
|
||
<field name="notes"/>
|
||
</list>
|
||
</field>
|
||
</page>
|
||
<page string="Damage Log" name="damage">
|
||
<field name="damage_ids">
|
||
<list editable="bottom">
|
||
<field name="description"/>
|
||
<field name="severity" widget="badge"
|
||
decoration-info="severity == 'cosmetic'"
|
||
decoration-warning="severity == 'functional'"
|
||
decoration-danger="severity == 'rejected'"/>
|
||
<field name="action_required"/>
|
||
<field name="customer_notified"/>
|
||
<field name="resolved"/>
|
||
</list>
|
||
</field>
|
||
</page>
|
||
<page string="Photos" name="photos">
|
||
<field name="attachment_ids" widget="many2many_binary"/>
|
||
</page>
|
||
<page string="Notes" name="notes">
|
||
<field name="notes"
|
||
placeholder="Internal notes about this receiving..."/>
|
||
</page>
|
||
</notebook>
|
||
</sheet>
|
||
<chatter/>
|
||
</form>
|
||
</field>
|
||
</record>
|
||
|
||
<!-- ===== Receiving Search View ===== -->
|
||
<record id="view_fp_receiving_search" model="ir.ui.view">
|
||
<field name="name">fp.receiving.search</field>
|
||
<field name="model">fp.receiving</field>
|
||
<field name="arch" type="xml">
|
||
<search>
|
||
<field name="name"/>
|
||
<field name="sale_order_id"/>
|
||
<field name="partner_id"/>
|
||
<field name="po_number"/>
|
||
<separator/>
|
||
<filter string="Awaiting Parts" name="draft" domain="[('state', '=', 'draft')]"/>
|
||
<filter string="Inspecting" name="inspecting" domain="[('state', '=', 'inspecting')]"/>
|
||
<filter string="Discrepancy" name="discrepancy" domain="[('state', '=', 'discrepancy')]"/>
|
||
<filter string="Accepted" name="accepted" domain="[('state', '=', 'accepted')]"/>
|
||
<group>
|
||
<filter string="Customer" name="group_customer" context="{'group_by': 'partner_id'}"/>
|
||
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
|
||
<filter string="Received Date" name="group_received_date" context="{'group_by': 'received_date:month'}"/>
|
||
</group>
|
||
</search>
|
||
</field>
|
||
</record>
|
||
|
||
<!-- ===== Window Action ===== -->
|
||
<record id="action_fp_receiving" model="ir.actions.act_window">
|
||
<field name="name">Receiving</field>
|
||
<field name="res_model">fp.receiving</field>
|
||
<field name="view_mode">list,form</field>
|
||
<field name="search_view_id" ref="view_fp_receiving_search"/>
|
||
<field name="context">{'search_default_draft': 1}</field>
|
||
<field name="help" type="html">
|
||
<p class="o_view_nocontent_smiling_face">
|
||
No receiving records yet
|
||
</p>
|
||
<p>
|
||
Receiving records are created automatically when a sale order is
|
||
confirmed. They track quantity verification, condition inspection,
|
||
and damage logging for customer parts arriving at the shop.
|
||
</p>
|
||
</field>
|
||
</record>
|
||
|
||
</odoo>
|