223 lines
7.7 KiB
Python
223 lines
7.7 KiB
Python
# -*- 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',
|
|
}
|