157 lines
5.9 KiB
Python
157 lines
5.9 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.
|
|
#
|
|
# 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 <b>%s</b> linked back to this RMA. '
|
|
'Marked Resolved.'
|
|
) % move.name,
|
|
message_type='comment',
|
|
subtype_xmlid='mail.mt_note',
|
|
)
|
|
candidate._fire_rma_notification('rma_resolved')
|