[FIX] stock_picking_batch_custom: prevent product_id null error on summary lines
- Use regular dict instead of defaultdict to avoid empty entries - Make summary_line_ids readonly=True to prevent UI from inserting empty lines - Add SQL constraint CHECK(product_id IS NOT NULL) as safeguard - Use boolean_toggle widget for is_collected field - Fix tests to use TransactionCase and invalidate_recordset - Add test for empty batch + add pickings + confirm flow
This commit is contained in:
parent
ad8b759643
commit
3eeca66551
5 changed files with 304 additions and 45 deletions
|
|
@ -1,8 +1,6 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
|
@ -17,7 +15,7 @@ class StockPickingBatch(models.Model):
|
|||
string="Product Summary",
|
||||
compute="_compute_summary_line_ids",
|
||||
store=True,
|
||||
readonly=False,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
|
|
@ -38,36 +36,57 @@ class StockPickingBatch(models.Model):
|
|||
"""
|
||||
|
||||
for batch in self:
|
||||
previous_collected = {
|
||||
line.product_id.id: line.is_collected for line in batch.summary_line_ids
|
||||
}
|
||||
aggregates = defaultdict(
|
||||
lambda: {
|
||||
"product_id": False,
|
||||
"product_uom_id": False,
|
||||
"qty_demanded": 0.0,
|
||||
"qty_done": 0.0,
|
||||
}
|
||||
)
|
||||
# Base: limpiar si no hay líneas
|
||||
if not batch.move_line_ids:
|
||||
batch.summary_line_ids = [fields.Command.clear()]
|
||||
continue
|
||||
|
||||
previous_collected = {
|
||||
line.product_id.id: line.is_collected
|
||||
for line in batch.summary_line_ids
|
||||
if line.product_id
|
||||
}
|
||||
# Use regular dict to avoid accidental creation of empty entries
|
||||
aggregates = {}
|
||||
|
||||
# Demand per move (count once per move)
|
||||
for move in batch.move_ids:
|
||||
if move.state == "cancel" or not move.product_id:
|
||||
continue
|
||||
|
||||
product = move.product_id
|
||||
product_uom = product.uom_id
|
||||
demand = move.product_uom._compute_quantity(
|
||||
move.product_uom_qty, product_uom, rounding_method="HALF-UP"
|
||||
)
|
||||
done = move.product_uom._compute_quantity(
|
||||
move.quantity, product_uom, rounding_method="HALF-UP"
|
||||
)
|
||||
if product.id not in aggregates:
|
||||
aggregates[product.id] = {
|
||||
"product_id": product.id,
|
||||
"product_uom_id": product_uom.id,
|
||||
"qty_demanded": 0.0,
|
||||
"qty_done": 0.0,
|
||||
}
|
||||
aggregates[product.id]["qty_demanded"] += demand
|
||||
|
||||
entry = aggregates[product.id]
|
||||
entry["product_id"] = product.id
|
||||
entry["product_uom_id"] = product_uom.id
|
||||
entry["qty_demanded"] += demand
|
||||
entry["qty_done"] += done
|
||||
# Done per move line
|
||||
for line in batch.move_line_ids:
|
||||
move = line.move_id
|
||||
if move and move.state == "cancel":
|
||||
continue
|
||||
product = line.product_id
|
||||
if not product:
|
||||
continue
|
||||
product_uom = product.uom_id
|
||||
done = line.product_uom_id._compute_quantity(
|
||||
line.quantity, product_uom, rounding_method="HALF-UP"
|
||||
)
|
||||
if product.id not in aggregates:
|
||||
aggregates[product.id] = {
|
||||
"product_id": product.id,
|
||||
"product_uom_id": product_uom.id,
|
||||
"qty_demanded": 0.0,
|
||||
"qty_done": 0.0,
|
||||
}
|
||||
aggregates[product.id]["qty_done"] += done
|
||||
|
||||
commands = []
|
||||
products_in_totals = set()
|
||||
|
|
@ -76,9 +95,12 @@ class StockPickingBatch(models.Model):
|
|||
}
|
||||
|
||||
for product_id, values in aggregates.items():
|
||||
# Double-check: skip if product_id is not valid
|
||||
if not product_id or not values.get("product_id"):
|
||||
continue
|
||||
products_in_totals.add(product_id)
|
||||
existing_line = existing_by_product.get(product_id)
|
||||
values["is_collected"] = previous_collected.get(product_id, False)
|
||||
is_collected = previous_collected.get(product_id, False)
|
||||
if existing_line:
|
||||
commands.append(
|
||||
fields.Command.update(
|
||||
|
|
@ -87,12 +109,24 @@ class StockPickingBatch(models.Model):
|
|||
"product_uom_id": values["product_uom_id"],
|
||||
"qty_demanded": values["qty_demanded"],
|
||||
"qty_done": values["qty_done"],
|
||||
"is_collected": values["is_collected"],
|
||||
"is_collected": is_collected,
|
||||
},
|
||||
)
|
||||
)
|
||||
else:
|
||||
commands.append(fields.Command.create(values))
|
||||
# Ensure all required fields are present before create
|
||||
if values["product_id"] and values["product_uom_id"]:
|
||||
commands.append(
|
||||
fields.Command.create(
|
||||
{
|
||||
"product_id": values["product_id"],
|
||||
"product_uom_id": values["product_uom_id"],
|
||||
"qty_demanded": values["qty_demanded"],
|
||||
"qty_done": values["qty_done"],
|
||||
"is_collected": is_collected,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
obsolete_lines = [
|
||||
fields.Command.unlink(line.id)
|
||||
|
|
@ -100,7 +134,10 @@ class StockPickingBatch(models.Model):
|
|||
if line.product_id.id not in products_in_totals
|
||||
]
|
||||
|
||||
batch.summary_line_ids = commands + obsolete_lines
|
||||
if commands or obsolete_lines:
|
||||
batch.summary_line_ids = commands + obsolete_lines
|
||||
else:
|
||||
batch.summary_line_ids = [fields.Command.clear()]
|
||||
|
||||
|
||||
class StockPickingBatchSummaryLine(models.Model):
|
||||
|
|
@ -108,16 +145,22 @@ class StockPickingBatchSummaryLine(models.Model):
|
|||
_description = "Batch Product Summary Line"
|
||||
_order = "product_categ_id, product_id"
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"product_required",
|
||||
"CHECK(product_id IS NOT NULL)",
|
||||
"Product is required for summary lines.",
|
||||
),
|
||||
]
|
||||
|
||||
batch_id = fields.Many2one(
|
||||
comodel_name="stock.picking.batch",
|
||||
string="Batch",
|
||||
ondelete="cascade",
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
comodel_name="product.product",
|
||||
string="Product",
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue