The move lists already show the demand, the move line ones do not: the detailed operations of the batch and of the transfer, and the operator (Basket Assembly) view. Adds a read-only column with the demand of the line's move, in the unit of the line, next to the ordered quantity. Closes #21 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
6.5 KiB
Python
181 lines
6.5 KiB
Python
# Copyright 2026 Criptomart
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
|
|
from odoo import Command
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests import TransactionCase
|
|
from odoo.tests import tagged
|
|
|
|
|
|
@tagged("-at_install", "post_install")
|
|
class TestPickingCollected(TransactionCase):
|
|
"""Collected checks and ordered quantities on transfers outside a batch."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.stock_location = cls.env.ref("stock.stock_location_stock")
|
|
cls.picking_type_out = cls.env.ref("stock.picking_type_out")
|
|
cls.picking_type_out.collected_check_required = True
|
|
cls.env.company.write(
|
|
{
|
|
"batch_detailed_restriction_enabled": True,
|
|
"batch_detailed_restriction_scope": "processed",
|
|
"batch_summary_restriction_enabled": False,
|
|
}
|
|
)
|
|
kg = cls.env.ref("uom.product_uom_kgm")
|
|
cls.product = cls.env["product.product"].create(
|
|
{
|
|
"name": "Collected Pears",
|
|
"is_storable": True,
|
|
"uom_id": kg.id,
|
|
"uom_po_id": kg.id,
|
|
}
|
|
)
|
|
cls.env["stock.quant"]._update_available_quantity(
|
|
cls.product, cls.stock_location, 50.0
|
|
)
|
|
cls.partner = cls.env["res.partner"].create({"name": "Pear Lover"})
|
|
|
|
def _create_sale_picking(self, qty=2.0):
|
|
order = self.env["sale.order"].create(
|
|
{
|
|
"partner_id": self.partner.id,
|
|
"order_line": [
|
|
Command.create(
|
|
{"product_id": self.product.id, "product_uom_qty": qty}
|
|
)
|
|
],
|
|
}
|
|
)
|
|
order.action_confirm()
|
|
picking = order.picking_ids
|
|
picking.action_assign()
|
|
return picking
|
|
|
|
def test_loose_picking_blocks_until_collected(self):
|
|
picking = self._create_sale_picking()
|
|
|
|
with self.assertRaisesRegex(UserError, self.product.display_name):
|
|
picking.button_validate()
|
|
|
|
picking.move_ids.is_collected = True
|
|
picking.button_validate()
|
|
|
|
self.assertEqual(picking.state, "done")
|
|
|
|
def test_picking_type_without_check_is_not_blocked(self):
|
|
self.picking_type_out.collected_check_required = False
|
|
picking = self._create_sale_picking()
|
|
|
|
picking.button_validate()
|
|
|
|
self.assertEqual(picking.state, "done")
|
|
|
|
def test_batch_picking_follows_the_batch_restriction(self):
|
|
"""The batch summary flag is not required by the loose transfer check."""
|
|
self.env.company.batch_summary_restriction_enabled = True
|
|
picking = self._create_sale_picking()
|
|
batch = self.env["stock.picking.batch"].create(
|
|
{"picking_ids": [Command.set(picking.ids)]}
|
|
)
|
|
batch.action_confirm()
|
|
picking.move_ids.is_collected = True
|
|
|
|
with self.assertRaisesRegex(UserError, "product summary"):
|
|
batch.action_done()
|
|
|
|
def test_collecting_the_move_collects_its_lines(self):
|
|
picking = self._create_sale_picking()
|
|
move = picking.move_ids
|
|
self.assertFalse(move.is_collected)
|
|
|
|
move.is_collected = True
|
|
|
|
self.assertTrue(all(move.move_line_ids.mapped("is_collected")))
|
|
self.assertTrue(move.picked)
|
|
|
|
move.move_line_ids.is_collected = False
|
|
|
|
self.assertFalse(move.is_collected)
|
|
|
|
def test_ordered_quantity_survives_weighing(self):
|
|
picking = self._create_sale_picking(2.0)
|
|
move = picking.move_ids
|
|
|
|
move.move_line_ids.write({"quantity": 1.5, "picked": True})
|
|
|
|
self.assertEqual(move.product_uom_qty, 1.5)
|
|
self.assertEqual(move.sale_ordered_qty, 2.0)
|
|
self.assertEqual(move.move_line_ids.sale_ordered_qty, 2.0)
|
|
|
|
def test_move_line_demand_follows_the_move(self):
|
|
picking = self._create_sale_picking(2.0)
|
|
move = picking.move_ids
|
|
line = move.move_line_ids
|
|
|
|
self.assertEqual(line.move_demand_qty, 2.0)
|
|
|
|
gram = self.env.ref("uom.product_uom_gram")
|
|
line.product_uom_id = gram
|
|
self.assertEqual(line.move_demand_qty, 2000.0)
|
|
|
|
move.product_uom_qty = 3.0
|
|
self.assertEqual(line.move_demand_qty, 3000.0)
|
|
|
|
def test_move_line_demand_without_sale(self):
|
|
"""Unlike the ordered quantity, the demand needs no sale order."""
|
|
picking = self.env["stock.picking"].create(
|
|
{
|
|
"picking_type_id": self.picking_type_out.id,
|
|
"location_id": self.stock_location.id,
|
|
"location_dest_id": self.env.ref("stock.stock_location_customers").id,
|
|
"move_ids": [
|
|
Command.create(
|
|
{
|
|
"name": self.product.name,
|
|
"product_id": self.product.id,
|
|
"product_uom_qty": 4.0,
|
|
"product_uom": self.product.uom_id.id,
|
|
"location_id": self.stock_location.id,
|
|
"location_dest_id": self.env.ref(
|
|
"stock.stock_location_customers"
|
|
).id,
|
|
}
|
|
)
|
|
],
|
|
}
|
|
)
|
|
picking.action_confirm()
|
|
picking.action_assign()
|
|
line = picking.move_ids.move_line_ids
|
|
|
|
self.assertEqual(line.move_demand_qty, 4.0)
|
|
self.assertEqual(line.sale_ordered_qty, 0.0)
|
|
|
|
def test_batch_summary_ordered_quantity(self):
|
|
pickings = self._create_sale_picking(2.0) | self._create_sale_picking(3.0)
|
|
batch = self.env["stock.picking.batch"].create(
|
|
{"picking_ids": [Command.set(pickings.ids)]}
|
|
)
|
|
pickings[0].move_ids.move_line_ids.write({"quantity": 1.5, "picked": True})
|
|
|
|
batch._compute_summary_line_ids()
|
|
|
|
line = batch.summary_line_ids
|
|
self.assertEqual(line.qty_ordered, 5.0)
|
|
self.assertEqual(line.qty_demanded, 4.5)
|
|
|
|
def test_report_prints_every_delivery_slip(self):
|
|
pickings = self._create_sale_picking(2.0) | self._create_sale_picking(3.0)
|
|
batch = self.env["stock.picking.batch"].create(
|
|
{"picking_ids": [Command.set(pickings.ids)]}
|
|
)
|
|
|
|
html, _report_format = self.env["ir.actions.report"]._render_qweb_html(
|
|
"stock_picking_batch_collect.report_batch_deliveryslip", batch.ids
|
|
)
|
|
|
|
for picking in pickings:
|
|
self.assertIn(picking.name, html.decode())
|