# Copyright 2026 Criptomart # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import models from odoo.exceptions import UserError class StockPicking(models.Model): _inherit = "stock.picking" def _pre_action_done_hook(self): """Run collected checks only after Odoo resolves backorders. This keeps the product summary checkbox informative during the initial batch validation click, but still enforces it once the user confirms the actual picking flow that will be processed. """ res = super()._pre_action_done_hook() if res is not True: return res self._check_collected_products() return res def _get_open_move_lines(self, scope): """Move lines still to process; ``processed`` keeps those with quantity.""" lines = self.move_line_ids.filtered( lambda line: line.move_id.state not in ("cancel", "done") and line.product_id ) if scope == "processed": lines = lines.filtered(lambda line: line.move_id.quantity > 0) return lines def _get_not_collected_move_lines(self, scope): return self._get_open_move_lines(scope).filtered( lambda line: not line.is_collected ) def _check_collected_products(self): for batch in self.batch_id: batch_pickings = self.filtered( lambda picking, batch=batch: picking.batch_id == batch ) if batch_pickings._get_open_move_lines("processed"): batch._check_all_products_collected(batch_pickings) loose_pickings = self.filtered( lambda picking: not picking.batch_id and picking.picking_type_id.collected_check_required ) for picking in loose_pickings: if picking._get_open_move_lines("processed"): picking._check_picking_collected() def _check_picking_collected(self): """Apply the batch restrictions to a transfer outside a batch. A single transfer has no product summary: the toggle of its Operations tab collects every line of the product, so both restrictions check its move lines, with the widest scope enabled. """ self.ensure_one() company = self.company_id scopes = set() if company.batch_detailed_restriction_enabled: scopes.add(company.batch_detailed_restriction_scope) if company.batch_summary_restriction_enabled: scopes.add(company.batch_summary_restriction_scope) if not scopes: return scope = "all" if "all" in scopes else "processed" not_collected = self._get_not_collected_move_lines(scope) if not not_collected: return product_names = ", ".join( sorted(set(not_collected.mapped("product_id.display_name"))) ) raise UserError( self.env._( "You must mark the operations of %(picking)s as collected " "before validating it.\nPending products: %(products)s", picking=self.name, products=product_names, ) )