[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

@ -25,10 +25,12 @@ class StockPickingBatch(models.Model):
"move_line_ids.move_id.quantity",
"move_line_ids.move_id.product_uom",
"move_line_ids.move_id.state",
"move_line_ids.move_id.sale_line_id.product_uom_qty",
)
def _compute_summary_line_ids(self):
"""Aggregate move quantities per product and keep collected flag.
- Ordered: sale line quantity converted to product UoM, once per line.
- Demand: move.product_uom_qty converted to product UoM.
- Done: move.quantity converted to product UoM.
- Pending: demand - done.
@ -49,6 +51,7 @@ class StockPickingBatch(models.Model):
}
# Use regular dict to avoid accidental creation of empty entries
aggregates = {}
counted_sale_lines = set()
# Demand per move (count once per move)
for move in batch.move_ids:
@ -63,10 +66,22 @@ class StockPickingBatch(models.Model):
aggregates[product.id] = {
"product_id": product.id,
"product_uom_id": product_uom.id,
"qty_ordered": 0.0,
"qty_demanded": 0.0,
"qty_done": 0.0,
}
aggregates[product.id]["qty_demanded"] += demand
if move.sale_ordered_qty and move.sale_line_id.id not in (
counted_sale_lines
):
counted_sale_lines.add(move.sale_line_id.id)
aggregates[product.id][
"qty_ordered"
] += move.product_uom._compute_quantity(
move.sale_ordered_qty,
product_uom,
rounding_method="HALF-UP",
)
# Done per move line
for line in batch.move_line_ids:
@ -84,6 +99,7 @@ class StockPickingBatch(models.Model):
aggregates[product.id] = {
"product_id": product.id,
"product_uom_id": product_uom.id,
"qty_ordered": 0.0,
"qty_demanded": 0.0,
"qty_done": 0.0,
}
@ -108,6 +124,7 @@ class StockPickingBatch(models.Model):
existing_line.id,
{
"product_uom_id": values["product_uom_id"],
"qty_ordered": values["qty_ordered"],
"qty_demanded": values["qty_demanded"],
"qty_done": values["qty_done"],
"is_collected": is_collected,
@ -122,6 +139,7 @@ class StockPickingBatch(models.Model):
{
"product_id": values["product_id"],
"product_uom_id": values["product_uom_id"],
"qty_ordered": values["qty_ordered"],
"qty_demanded": values["qty_demanded"],
"qty_done": values["qty_done"],
"is_collected": is_collected,
@ -151,30 +169,16 @@ class StockPickingBatch(models.Model):
def _get_not_collected_summary_lines(self, batch_pickings, scope):
summary_lines = self.summary_line_ids
if scope == "processed":
processed_product_ids = set(
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
)
).mapped("product_id.id")
)
processed_products = batch_pickings._get_open_move_lines(
"processed"
).product_id
summary_lines = summary_lines.filtered(
lambda line: line.product_id.id in processed_product_ids
lambda line: line.product_id in processed_products
)
return summary_lines.filtered(lambda line: not line.is_collected)
def _get_not_collected_detailed_lines(self, batch_pickings, scope):
detailed_lines = batch_pickings.move_line_ids.filtered(
lambda line: line.move_id.state not in ("cancel", "done")
and line.product_id
)
if scope == "processed":
detailed_lines = detailed_lines.filtered(
lambda line: line.move_id.quantity > 0
)
return detailed_lines.filtered(lambda line: not line.is_collected)
return batch_pickings._get_not_collected_move_lines(scope)
def _check_all_products_collected(self, pickings=None):
"""Validate collected restrictions based on tab configuration."""
@ -285,6 +289,11 @@ class StockPickingBatchSummaryLine(models.Model):
string="Unit of Measure",
required=True,
)
qty_ordered = fields.Float(
string="Ordered Quantity",
digits="Product Unit of Measure",
default=0.0,
)
qty_demanded = fields.Float(
string="Demanded Quantity",
digits="Product Unit of Measure",