# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. # # Sub 12 Phase A. Inverse Many2one fields on NCR, Hold and fp.receiving so # RMA can hang One2many counterparts off them. Plus a tiny override on # fp.receiving.create to flip a linked RMA into the `received` state and # trigger the auto-spawn rules. import logging from markupsafe import Markup from odoo import api, fields, models _logger = logging.getLogger(__name__) class FpNcrRmaLink(models.Model): _inherit = 'fusion.plating.ncr' rma_id = fields.Many2one( 'fusion.plating.rma', string='RMA', ondelete='set null', index=True, help='Return that triggered this NCR (auto-set by RMA receive).', ) class FpQualityHoldRmaLink(models.Model): _inherit = 'fusion.plating.quality.hold' rma_id = fields.Many2one( 'fusion.plating.rma', string='RMA', ondelete='set null', index=True, help='Return that placed these parts on hold.', ) class FpReceivingRmaLink(models.Model): _inherit = 'fp.receiving' rma_id = fields.Many2one( 'fusion.plating.rma', string='Linked RMA', ondelete='set null', index=True, help='If set, this receiving is the inbound for a customer return. ' 'When created, it transitions the RMA to `received` and may ' 'auto-spawn an NCR + Hold per the RMA toggles.', ) @api.model_create_multi def create(self, vals_list): records = super().create(vals_list) # Walk new records, mirror back to RMA, walk the receiving's own # state machine (draft → counted → staged → closed) so the linked # SO's x_fc_receiving_status updates, then fire the RMA receive # hook. Without this the receiving sat at draft and the SO read # 'not_received' even though the parts were physically at the shop. for rec in records: if not rec.rma_id: continue rma = rec.rma_id.sudo() # Mirror inbound link both ways. if not rma.inbound_receiving_id: rma.inbound_receiving_id = rec.id if rma.state in ('authorised', 'shipped_to_us'): # Use received_qty as qty_received fallback if not set. if not rma.qty_received and rec.received_qty: rma.qty_received = rec.received_qty # Walk the receiving's lifecycle to closed so SO status # updates. RMA receipts don't have a multi-day racking # delay (parts are already plated and being inspected for # the complaint, not racked for fresh plating), so we # fast-forward all three transitions in one shot. rec.sudo()._fp_rma_fast_close() rma._enter_received_state(receiving=rec) else: _logger.info( 'RMA %s linked to fp.receiving %s but state %s does ' 'not trigger auto-receive hook.', rma.name, rec.name, rma.state, ) return records def _fp_rma_fast_close(self): """Walk an RMA-bound receiving from draft to closed in one call. For RMA returns, the receiving's box-count → racking → close walk is purely administrative — the parts are already plated and the operator opens them on triage, not on intake. Fast-forwarding here keeps the SO's x_fc_receiving_status accurate without forcing the receiver to click three buttons in sequence. """ for rec in self: if not rec.box_count_in: # Best-effort default: 1 box if unknown. Real qty lives on # the RMA's qty_returned / qty_received. rec.box_count_in = 1 if rec.state == 'draft': rec.action_mark_counted() if rec.state == 'counted': rec.action_mark_staged() if rec.state == 'staged': rec.action_close() class AccountMoveRmaLink(models.Model): """Auto-link a credit note back to its RMA when the accountant confirms the reversal wizard. Looks up by invoice_origin matching an RMA's sale_order_id.name, scoped to RMAs in `resolving` state with resolution_type='refund' and no refund_invoice_id yet. Also flips the RMA from `resolving` to `resolved` once the credit note is linked — mirrors the auto-progression for replace/rework paths so the RMA doesn't get stuck after a refund. """ _inherit = 'account.move' @api.model_create_multi def create(self, vals_list): moves = super().create(vals_list) moves._fp_link_to_rma() return moves def write(self, vals): result = super().write(vals) if 'state' in vals and vals.get('state') == 'posted': self._fp_link_to_rma() return result def _fp_link_to_rma(self): Rma = self.env['fusion.plating.rma'].sudo() for move in self: if move.move_type != 'out_refund': continue if not move.invoice_origin: continue candidate = Rma.search([ ('sale_order_id.name', '=', move.invoice_origin), ('resolution_type', '=', 'refund'), ('refund_invoice_id', '=', False), ('state', 'in', ('resolving', 'triaged')), ], limit=1) if not candidate: continue candidate.refund_invoice_id = move.id candidate.state = 'resolved' candidate.message_post( body=Markup( 'Refund credit note %s linked back to this RMA. ' 'Marked Resolved.' ) % move.name, message_type='comment', subtype_xmlid='mail.mt_note', ) candidate._fire_rma_notification('rma_resolved')