Files
Odoo-Modules/fusion_plating/fusion_plating_jobs/models/account_move.py
gsinghpal 0d85063b5e feat(numbering): wire CoC/RCV/DLV/PU into parent-numbered mixin + rename counters
Per-model counter fields on sale.order renamed to x_fc_pn_*_count
to avoid collision with pre-existing compute fields of the same
short name in bridge_mrp / receiving / configurator (silent
compute-override was suppressing the storage). 4 child models
(fp.certificate, fp.receiving, fusion.plating.delivery,
fusion.plating.pickup.request) now derive names as PFX-<parent>
with -NN suffix from the 2nd onward.

fusion.plating.pickup.request gains a sale_order_id field
(optional) so pickups created against an SO get parent-derived
names, while standalone pickups (pre-SO) fall back to PU/YYYY/NNNN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 13:30:37 -04:00

124 lines
4.7 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
"""account.move overrides for Fusion Plating:
1. Block direct creation of out_invoice / out_refund for ALL users
including administrators. The only legal entry points are:
* sale.order._create_invoices() — sets context fp_from_so_invoice=True
* manual create() with invoice_origin matching an existing sale.order.name
2. Once a customer move is created via a legitimate path, derive its
name from the SO's parent number (IN-30000 / IN-30000-02 for
invoices, CN-30000 / CN-30000-02 for credit notes). Per the
2026-05-12 parent-number hierarchy design.
3. On post, link the invoice back to its fp.job's portal job (mark
complete, stamp invoice_ref). Pre-existing behaviour, preserved.
"""
import logging
from odoo import api, models
from odoo.exceptions import UserError
from odoo.tools.translate import _
_logger = logging.getLogger(__name__)
CUSTOMER_TYPES = ('out_invoice', 'out_refund')
class AccountMove(models.Model):
_inherit = ['account.move', 'fp.parent.numbered.mixin']
# =================================================================
# Parent-numbered mixin hooks
# =================================================================
def _fp_parent_sale_order(self):
"""Find linked SO via SO context flag (set by _create_invoices),
or fall back to invoice_origin name match."""
so_id = self.env.context.get('fp_invoice_source_so_id')
if so_id:
so = self.env['sale.order'].browse(so_id).exists()
if so:
return so
if self.invoice_origin:
return self.env['sale.order'].search(
[('name', '=', self.invoice_origin)], limit=1,
)
return self.env['sale.order']
def _fp_name_prefix(self):
return 'CN' if self.move_type == 'out_refund' else 'IN'
def _fp_parent_counter_field(self):
return 'x_fc_pn_cn_count' if self.move_type == 'out_refund' else 'x_fc_pn_invoice_count'
# =================================================================
# Create override: block off-flow + assign parent-derived name
# =================================================================
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self._fp_validate_customer_invoice(vals)
moves = super().create(vals_list)
for mv in moves:
if mv.move_type in CUSTOMER_TYPES:
mv._fp_assign_parent_name()
return moves
@api.model
def _fp_validate_customer_invoice(self, vals):
"""Refuse out_invoice / out_refund creation that didn't come
through the SO workflow. Applies to ALL users including admins."""
mtype = vals.get('move_type', 'entry')
if mtype not in CUSTOMER_TYPES:
return
if self.env.context.get('fp_from_so_invoice'):
return
origin = (vals.get('invoice_origin') or '').strip()
if origin and self.env['sale.order'].sudo().search_count(
[('name', '=', origin)]
):
return
raise UserError(_(
'Customer invoices and credit notes must be created from a '
'Sale Order. Open the originating SO and use the Create '
'Invoice / Add Credit Note action.\n\n'
'This rule applies to all users including administrators. '
'It is enforced to keep the parent-number audit trail '
'intact (see fusion_plating numbering policy).'
))
# =================================================================
# Post hook: link the invoice to its fp.job's portal job
# =================================================================
def action_post(self):
result = super().action_post()
for invoice in self.filtered(
lambda m: m.move_type in CUSTOMER_TYPES
):
invoice._fp_link_to_job()
return result
def _fp_link_to_job(self):
self.ensure_one()
if not self.invoice_origin:
return
Job = self.env['fp.job'].sudo()
SO = self.env['sale.order'].sudo()
so = SO.search([('name', '=', self.invoice_origin)], limit=1)
if not so:
return
job = Job.search([('sale_order_id', '=', so.id)], limit=1)
if not job or not job.portal_job_id:
return
portal = job.portal_job_id
if 'state' in portal._fields:
portal.state = 'complete'
if 'invoice_ref' in portal._fields:
portal.invoice_ref = self.name
_logger.info(
'Invoice %s linked to fp.job %s portal %s',
self.name, job.name, portal.name,
)