# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) import logging from odoo import models, fields, api from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class StockPicking(models.Model): _inherit = 'stock.picking' # ── Sale Order link ── x_fi_sale_order_id = fields.Many2one( 'sale.order', string='Sale Order', compute='_compute_fi_sale_invoice', store=True) x_fi_sale_order_state = fields.Selection( related='x_fi_sale_order_id.state', string='SO Status', store=True, tracking=False) # ── Invoice tracking (customer) ── x_fi_invoice_ids = fields.Many2many( 'account.move', 'stock_picking_invoice_rel', 'picking_id', 'move_id', string='Invoices', compute='_compute_fi_sale_invoice', store=True) x_fi_invoice_count = fields.Integer( compute='_compute_fi_sale_invoice', store=True) x_fi_invoice_status = fields.Selection([ ('no', 'No Invoice'), ('invoiced', 'Invoiced'), ('paid', 'Paid'), ], string='Invoice Status', compute='_compute_fi_sale_invoice', store=True) # ── Purchase Order link ── x_fi_purchase_order_id = fields.Many2one( 'purchase.order', string='Purchase Order', compute='_compute_fi_purchase_bill', store=True) x_fi_purchase_order_state = fields.Selection( related='x_fi_purchase_order_id.state', string='PO Status', store=True, tracking=False) # ── Bill tracking (vendor) ── x_fi_bill_ids = fields.Many2many( 'account.move', 'stock_picking_bill_rel', 'picking_id', 'move_id', string='Vendor Bills', compute='_compute_fi_purchase_bill', store=True) x_fi_bill_count = fields.Integer( compute='_compute_fi_purchase_bill', store=True) x_fi_bill_status = fields.Selection([ ('no', 'No Bill'), ('billed', 'Billed'), ('paid', 'Paid'), ], string='Bill Status', compute='_compute_fi_purchase_bill', store=True) @api.depends('sale_id', 'sale_id.invoice_ids', 'sale_id.invoice_ids.payment_state', 'origin') def _compute_fi_sale_invoice(self): for pick in self: so = pick.sale_id if not so and pick.origin: so = self.env['sale.order'].search( [('name', '=', pick.origin)], limit=1) pick.x_fi_sale_order_id = so.id if so else False if so: invoices = so.invoice_ids.filtered( lambda m: m.state == 'posted' and m.move_type == 'out_invoice') pick.x_fi_invoice_ids = invoices pick.x_fi_invoice_count = len(invoices) if not invoices: pick.x_fi_invoice_status = 'no' elif all(inv.payment_state in ('paid', 'in_payment', 'reversed') for inv in invoices): pick.x_fi_invoice_status = 'paid' else: pick.x_fi_invoice_status = 'invoiced' else: pick.x_fi_invoice_ids = self.env['account.move'] pick.x_fi_invoice_count = 0 pick.x_fi_invoice_status = 'no' @api.depends('purchase_id', 'purchase_id.invoice_ids', 'purchase_id.invoice_ids.payment_state', 'origin') def _compute_fi_purchase_bill(self): PO = self.env['purchase.order'] for pick in self: po = pick.purchase_id if not po and pick.origin: po = PO.search([('name', '=', pick.origin)], limit=1) pick.x_fi_purchase_order_id = po.id if po else False if po: bills = po.invoice_ids.filtered( lambda m: m.state == 'posted' and m.move_type == 'in_invoice') pick.x_fi_bill_ids = bills pick.x_fi_bill_count = len(bills) if not bills: pick.x_fi_bill_status = 'no' elif all(b.payment_state in ('paid', 'in_payment', 'reversed') for b in bills): pick.x_fi_bill_status = 'paid' else: pick.x_fi_bill_status = 'billed' else: pick.x_fi_bill_ids = self.env['account.move'] pick.x_fi_bill_count = 0 pick.x_fi_bill_status = 'no' # ── Smart button actions ── def action_view_sale_order(self): self.ensure_one() if not self.x_fi_sale_order_id: return return { 'type': 'ir.actions.act_window', 'name': 'Sale Order', 'res_model': 'sale.order', 'view_mode': 'form', 'res_id': self.x_fi_sale_order_id.id, } def action_view_invoices(self): self.ensure_one() invoices = self.x_fi_invoice_ids if not invoices: return if len(invoices) == 1: return { 'type': 'ir.actions.act_window', 'name': 'Invoice', 'res_model': 'account.move', 'view_mode': 'form', 'res_id': invoices.id, } return { 'type': 'ir.actions.act_window', 'name': 'Invoices', 'res_model': 'account.move', 'view_mode': 'list,form', 'domain': [('id', 'in', invoices.ids)], } def action_view_purchase_order(self): self.ensure_one() if not self.x_fi_purchase_order_id: return return { 'type': 'ir.actions.act_window', 'name': 'Purchase Order', 'res_model': 'purchase.order', 'view_mode': 'form', 'res_id': self.x_fi_purchase_order_id.id, } def action_view_bills(self): self.ensure_one() bills = self.x_fi_bill_ids if not bills: return if len(bills) == 1: return { 'type': 'ir.actions.act_window', 'name': 'Vendor Bill', 'res_model': 'account.move', 'view_mode': 'form', 'res_id': bills.id, } return { 'type': 'ir.actions.act_window', 'name': 'Vendor Bills', 'res_model': 'account.move', 'view_mode': 'list,form', 'domain': [('id', 'in', bills.ids)], } # ── Booking warning on confirm ── def button_validate(self): Booking = self.env['fusion.inventory.booking'] for pick in self.filtered(lambda p: p.picking_type_code == 'outgoing'): for move in pick.move_ids: active_bookings = Booking.search([ ('product_id', '=', move.product_id.id), ('state', '=', 'active'), ('user_id', '!=', self.env.uid), ]) if active_bookings: bookers = ', '.join(active_bookings.mapped('user_id.name')) _logger.warning( 'Product %s is booked by %s but being delivered in %s', move.product_id.name, bookers, pick.name) return super().button_validate() # ── Serial Number Scan ── def action_scan_serial_numbers(self): self.ensure_one() wizard = self.env['fusion.serial.scan.wizard'].create({ 'picking_id': self.id, }) wizard._scan() return { 'type': 'ir.actions.act_window', 'name': 'Serial Number Scan Results', 'res_model': 'fusion.serial.scan.wizard', 'view_mode': 'form', 'res_id': wizard.id, 'target': 'new', }