- 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>
137 lines
4.8 KiB
Python
137 lines
4.8 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_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())
|