[IMP] stock_picking_batch_collect: collected checks on transfers, ordered quantity, delivery slips report

- 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>
This commit is contained in:
GitHub Copilot 2026-10-04 14:32:38 +02:00
parent 76dc355c6c
commit 9c81cb986d
Signed by: snt
GPG key ID: A9FD34930EADBE71
24 changed files with 630 additions and 39 deletions

View file

@ -2,6 +2,7 @@
# 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):
@ -19,21 +20,68 @@ class StockPicking(models.Model):
if res is not True:
return res
self._check_batch_collected_products()
self._check_collected_products()
return res
def _check_batch_collected_products(self):
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
)
processed_move_lines = batch_pickings.move_line_ids.filtered(
lambda line: (
line.move_id.state not in ("cancel", "done")
and line.product_id
and line.move_id.quantity > 0
)
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,
)
if not processed_move_lines:
continue
batch._check_all_products_collected(batch_pickings)
)