[IMP] stock_picking_batch_custom: keep the weighed quantity on the operator view

Basket Assembly is where the reverting quantity was noticed, so it depends
on stock_move_manual_quantity now and its two lists load `picked`: the web
client only saves the fields present in the arch, and without it the
onchange that freezes a hand-typed quantity never reaches the server.

Collecting a line does the same, minus the demand. It is the operator saying
the goods are in the basket, so the quantity has to survive the reservation
engine even when it was never retyped -- but only a quantity typed by hand
says what the demand should become, and a line collected at zero would
otherwise lose its demand and be cancelled on validation.

Drops views/stock_move_line_views.xml on the way. It declared a second
record under the id stock_picking_batch_views.xml already uses, so it was
loaded first and immediately overwritten: dead weight that would have turned
into a duplicate-field view had anyone renamed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
GitHub Copilot 2026-08-27 13:26:56 +02:00
parent d433e50f2f
commit 2e7549f857
7 changed files with 103 additions and 37 deletions

View file

@ -10,6 +10,9 @@
"website": "https://github.com/Criptomart",
"license": "AGPL-3",
"depends": [
# A quantity typed by the operator is the weighed one: it must not be
# re-reserved by the scheduler.
"stock_move_manual_quantity",
"stock_picking_batch",
# Ensure our related fields to sale/picking (home_delivery, pickup_slot_label)
# are available by depending on the Aplicoop website_sale extension.
@ -18,7 +21,6 @@
"data": [
"security/ir.model.access.csv",
"views/res_config_settings_views.xml",
"views/stock_move_line_views.xml",
"views/stock_picking_batch_views.xml",
],
"assets": {

View file

@ -47,6 +47,21 @@ class StockMoveLine(models.Model):
readonly=True,
)
def write(self, vals):
res = super().write(vals)
if vals.get("is_collected"):
# Collecting a line is the operator saying the goods are in the
# basket: its quantity must survive the reservation engine even if
# it was never retyped. The demand is left alone, only a quantity
# typed by hand adjusts it.
lines = self.filtered(
lambda line: not line.picked and line.state not in ("done", "cancel")
)
if lines:
lines.picked = True
lines._freeze_manual_quantity(align_demand=False)
return res
@api.depends("picking_id")
def _compute_consumer_group_id(self):
for line in self:

View file

@ -12,3 +12,8 @@ Uso
hecho y pendiente) y marca el check de recogido consolidado si corresponde.
4. Ordena o agrupa por categoría en cualquiera de las vistas según convenga.
5. La cantidad que teclea el operario (el peso real de la balanza) queda fijada:
se marca como *Picked* y ni el planificador ni la validación de otros
albaranes vuelven a modificarla. Marcar **Collected** protege igualmente la
cantidad de la línea. Ver ``stock_move_manual_quantity``.

View file

@ -1 +1,2 @@
from . import test_batch_summary # noqa: F401
from . import test_collected_picked # noqa: F401

View file

@ -0,0 +1,73 @@
# Copyright 2026 Criptomart
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import Command
from odoo.tests import TransactionCase
from odoo.tests import tagged
@tagged("-at_install", "post_install")
class TestCollectedPicked(TransactionCase):
"""Collecting a line protects its quantity from the reservation engine."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.stock_location = cls.env.ref("stock.stock_location_stock")
cls.customer_location = cls.env.ref("stock.stock_location_customers")
cls.picking_type = cls.env.ref("stock.picking_type_out")
cls.product = cls.env["product.product"].create(
{
"name": "Collected Apples",
"is_storable": True,
"uom_id": cls.env.ref("uom.product_uom_kgm").id,
"uom_po_id": cls.env.ref("uom.product_uom_kgm").id,
}
)
cls.env["stock.quant"]._update_available_quantity(
cls.product, cls.stock_location, 50.0
)
def _create_picking(self, demand):
picking = self.env["stock.picking"].create(
{
"picking_type_id": self.picking_type.id,
"location_id": self.stock_location.id,
"location_dest_id": self.customer_location.id,
"move_ids": [
Command.create(
{
"name": self.product.name,
"product_id": self.product.id,
"product_uom": self.product.uom_id.id,
"product_uom_qty": demand,
"location_id": self.stock_location.id,
"location_dest_id": self.customer_location.id,
}
)
],
}
)
picking.action_confirm()
picking.action_assign()
return picking
def test_collecting_a_line_marks_it_picked(self):
picking = self._create_picking(2.0)
line = picking.move_ids.move_line_ids
line.is_collected = True
self.assertTrue(line.picked)
self.assertTrue(picking.move_ids.picked)
def test_collecting_a_line_keeps_its_demand(self):
"""Only a hand-typed quantity lowers the demand, collecting does not."""
picking = self._create_picking(2.0)
move = picking.move_ids
move.move_line_ids.write({"quantity": 1.5, "picked": True})
move.move_line_ids.is_collected = True
self.assertEqual(move.product_uom_qty, 1.5)
self.assertTrue(move.picked)

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_move_line_tree_inherit_batch_custom" model="ir.ui.view">
<field name="name">stock.move.line.list.batch.custom</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock_picking_batch.view_move_line_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='picking_id']" position="attributes">
<attribute name="optional">hide</attribute>
</xpath>
<xpath expr="//field[@name='lot_id']" position="attributes">
<attribute name="optional">hide</attribute>
</xpath>
<xpath expr="//field[@name='lot_name']" position="attributes">
<attribute name="optional">hide</attribute>
</xpath>
<xpath expr="//field[@name='location_id']" position="attributes">
<attribute name="optional">hide</attribute>
</xpath>
<xpath expr="//field[@name='location_dest_id']" position="attributes">
<attribute name="optional">hide</attribute>
</xpath>
<xpath expr="//field[@name='product_id']" position="after">
<field name="product_categ_id" optional="hide"/>
</xpath>
<xpath expr="//field[@name='picking_id']" position="after">
<field name="picking_partner_id" optional="hide"/>
<field name="consumer_group_id" optional="show"/>
<field name="home_delivery" optional="show" widget="boolean_toggle"/>
</xpath>
<xpath expr="//field[@name='quantity']" position="after">
<field name="is_collected" optional="show" widget="boolean_toggle"/>
</xpath>
</field>
</record>
</odoo>

View file

@ -33,6 +33,8 @@
<field name="home_delivery" readonly="1" widget="boolean_toggle" optional="show"/>
<field name="consumer_group_id" readonly="1" optional="show"/>
<field name="quantity" string="Cant."/>
<!-- Loaded so `_onchange_quantity_manual` reaches the server. -->
<field name="picked" column_invisible="True"/>
<field name="is_collected" widget="boolean_toggle"/>
</list>
</field>
@ -91,6 +93,10 @@
<field name="consumer_group_id" readonly="1" optional="show"/>
<field name="is_collected" widget="boolean_toggle" optional="show"/>
</xpath>
<xpath expr="//field[@name='quantity']" position="after">
<!-- Loaded so `_onchange_quantity_manual` reaches the server. -->
<field name="picked" column_invisible="True"/>
</xpath>
<xpath expr="//field[@name='product_uom_id']" position="attributes">
<attribute name="optional">hide</attribute>
</xpath>