- The collected checks also apply to transfers outside a batch, opt-in per
operation type ("Require Collected Lines"), with the batch restrictions of
the settings. A per-product toggle in the transfer's Operations tab collects
all its lines.
- Read-only ordered quantity from the sale line on every batch and transfer
view, since the weighed demand overwrites it. Adds a dependency on
sale_stock; a migration fills it in the summary of open batches.
- "Delivery Slips" report on the batch: all its delivery slips in one PDF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
3.2 KiB
Python
87 lines
3.2 KiB
Python
# 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,
|
|
)
|
|
)
|