diff --git a/shipment_advice/models/shipment_advice.py b/shipment_advice/models/shipment_advice.py
index 5ff3d489..571922cf 100644
--- a/shipment_advice/models/shipment_advice.py
+++ b/shipment_advice/models/shipment_advice.py
@@ -155,6 +155,11 @@ def _default_warehouse_id(self):
compute="_compute_picking_ids",
string="Loaded transfers",
)
+ to_validate_picking_ids = fields.One2many(
+ comodel_name="stock.picking",
+ compute="_compute_picking_ids",
+ string="Transfers to validate",
+ )
loaded_pickings_count = fields.Integer(compute="_compute_count")
loaded_package_ids = fields.One2many(
comodel_name="stock.quant.package",
@@ -167,6 +172,17 @@ def _default_warehouse_id(self):
string="Package Levels",
)
loaded_packages_count = fields.Integer(compute="_compute_count")
+ line_to_load_ids = fields.One2many(
+ comodel_name="stock.move.line",
+ compute="_compute_line_to_load_ids",
+ help=(
+ "Lines to load in priority.\n"
+ "If the shipment is planned, it'll return the planned lines.\n"
+ "If the shipment is not planned, it'll return lines from transfers "
+ "partially loaded."
+ ),
+ )
+ lines_to_load_count = fields.Integer(compute="_compute_count")
carrier_ids = fields.Many2many(
comodel_name="delivery.carrier",
string="Related shipping methods",
@@ -195,6 +211,69 @@ def _default_warehouse_id(self):
def _default_run_in_queue_job(self):
return self.env.user.company_id.shipment_advice_run_in_queue_job
+ def _find_move_lines_domain(self, picking_type_ids=None):
+ """Returns the base domain to look for move lines for a given shipment."""
+ self.ensure_one()
+ domain = [
+ ("state", "in", ("assigned", "partially_available")),
+ ("picking_code", "=", self.shipment_type),
+ "|",
+ ("shipment_advice_id", "=", False),
+ ("shipment_advice_id", "=", self.id),
+ ]
+ # Restrict on picking types if provided
+ if picking_type_ids:
+ domain.insert(0, ("picking_id.picking_type_id", "in", picking_type_ids.ids))
+ else:
+ domain.insert(
+ 0,
+ ("picking_id.picking_type_id.warehouse_id", "=", self.warehouse_id.id),
+ )
+ # Shipment with planned content, restrict the search to it
+ if self.planned_move_ids:
+ domain.append(("move_id.shipment_advice_id", "=", self.id))
+ # Shipment without planned content, search for all unplanned moves
+ else:
+ domain.append(("move_id.shipment_advice_id", "=", False))
+ # Restrict to shipment carrier delivery types (providers)
+ if self.carrier_ids:
+ domain.extend(
+ [
+ "|",
+ (
+ "picking_id.carrier_id.delivery_type",
+ "in",
+ self.carrier_ids.mapped("delivery_type"),
+ ),
+ ("picking_id.carrier_id", "=", False),
+ ]
+ )
+ return domain
+
+ @api.depends("planned_move_ids")
+ @api.depends_context("shipment_picking_type_ids")
+ def _compute_line_to_load_ids(self):
+ picking_type_ids = self.env.context.get("shipment_picking_type_ids", [])
+ for shipment in self:
+ domain = shipment._find_move_lines_domain(picking_type_ids)
+ # Restrict to lines not loaded
+ domain.insert(0, ("shipment_advice_id", "=", False))
+ # Find lines to load from partially loaded transfers if the shipment
+ # is not planned.
+ if not shipment.planned_move_ids:
+ all_lines_to_load = self.env["stock.move.line"].search(domain)
+ all_pickings = all_lines_to_load.picking_id
+ loaded_lines = self.env["stock.move.line"].search(
+ [
+ ("picking_id", "in", all_pickings.ids),
+ ("id", "not in", all_lines_to_load.ids),
+ ("shipment_advice_id", "!=", False),
+ ]
+ )
+ pickings_partially_loaded = loaded_lines.picking_id
+ domain += [("picking_id", "in", pickings_partially_loaded.ids)]
+ shipment.line_to_load_ids = self.env["stock.move.line"].search(domain)
+
def _check_include_package_level(self, package_level):
"""Check if a package level should be listed in the shipment advice.
@@ -208,11 +287,25 @@ def _compute_total_load(self):
packages = shipment.loaded_move_line_ids.result_package_id
shipment.total_load = sum(packages.mapped("shipping_weight"))
- @api.depends("planned_move_ids", "loaded_move_line_ids")
+ @api.depends(
+ "planned_move_ids", "loaded_move_line_ids.picking_id.loaded_shipment_advice_ids"
+ )
def _compute_picking_ids(self):
for shipment in self:
shipment.planned_picking_ids = shipment.planned_move_ids.picking_id
shipment.loaded_picking_ids = shipment.loaded_move_line_ids.picking_id
+ # Transfers to validate are those having only the current shipment
+ # advice to process
+ to_validate_picking_ids = []
+ for picking in shipment.loaded_move_line_ids.picking_id:
+ shipments_to_process = picking.loaded_shipment_advice_ids.filtered(
+ lambda s: s.state not in ("done", "cancel")
+ )
+ if shipments_to_process == shipment:
+ to_validate_picking_ids.append(picking.id)
+ shipment.to_validate_picking_ids = self.env["stock.picking"].browse(
+ to_validate_picking_ids
+ )
@api.depends(
"loaded_move_line_ids.package_level_id.package_id",
@@ -233,7 +326,7 @@ def _compute_package_ids(self):
package_ids
)
- @api.depends("planned_picking_ids", "planned_move_ids")
+ @api.depends("planned_picking_ids", "planned_move_ids", "line_to_load_ids")
def _compute_count(self):
for shipment in self:
shipment.planned_pickings_count = len(shipment.planned_picking_ids)
@@ -243,6 +336,7 @@ def _compute_count(self):
shipment.loaded_move_line_without_package_ids
)
shipment.loaded_packages_count = len(shipment.loaded_package_ids)
+ shipment.lines_to_load_count = len(shipment.line_to_load_ids)
@api.depends("planned_picking_ids", "loaded_picking_ids")
def _compute_carrier_ids(self):
@@ -323,7 +417,7 @@ def _get_picking_to_process(self):
self.ensure_one()
if self.shipment_type == "incoming":
return self.planned_picking_ids
- return self.loaded_picking_ids
+ return self.to_validate_picking_ids
def _action_done(self):
# Validate transfers (create backorders for unprocessed lines)
@@ -353,13 +447,17 @@ def _action_done(self):
]
),
group(self.delayable(description=self.name)._unplan_undone_moves()),
- group(self.delayable(description=self.name)._postprocess_action_done()),
+ group(
+ self.delayable(description=self.name)._postprocess_action_done(
+ backorder_policy
+ )
+ ),
).delay()
return
for picking in pickings:
self._validate_picking(picking, backorder_policy)
self._unplan_undone_moves()
- self._postprocess_action_done()
+ self._postprocess_action_done(backorder_policy)
def _check_action_done_allowed(self):
for shipment in self:
@@ -402,12 +500,13 @@ def _unplan_undone_moves(self):
).filtered(lambda m: m.state not in ("cancel", "done") and not m.quantity_done)
moves_to_unplan.shipment_advice_id = False
- def _postprocess_action_done(self):
+ def _postprocess_action_done(self, backorder_policy):
self.ensure_one()
if self.state != "in_process":
return
if self._get_picking_to_process().filtered(
lambda p: p.state not in ("done", "cancel")
+ and backorder_policy != "leave_open"
):
self.write(
{
@@ -508,6 +607,13 @@ def button_open_loaded_packages(self):
action["domain"] = [("id", "in", self.loaded_package_ids.ids)]
return action
+ def button_open_to_load_move_lines(self):
+ action_xmlid = "stock.stock_move_line_action"
+ action = self.env["ir.actions.act_window"]._for_xml_id(action_xmlid)
+ action["domain"] = [("id", "in", self.line_to_load_ids.ids)]
+ action["context"] = {} # Disable filters
+ return action
+
def _domain_open_deliveries_in_progress(self):
self.ensure_one()
domain = []
diff --git a/shipment_advice/tests/__init__.py b/shipment_advice/tests/__init__.py
index c25e037d..42fe7e37 100644
--- a/shipment_advice/tests/__init__.py
+++ b/shipment_advice/tests/__init__.py
@@ -1,6 +1,7 @@
from . import test_shipment_advice
from . import test_shipment_advice_async
from . import test_shipment_advice_plan
+from . import test_shipment_advice_to_load
from . import test_shipment_advice_load
from . import test_shipment_advice_picking_values
from . import test_shipment_advice_unload
diff --git a/shipment_advice/tests/common.py b/shipment_advice/tests/common.py
index 2838418d..ab495c58 100644
--- a/shipment_advice/tests/common.py
+++ b/shipment_advice/tests/common.py
@@ -39,7 +39,7 @@ def setUpClass(cls):
cls.product_out1,
20,
)
- cls.package = cls.env["stock.quant.package"].create({"name": "PKG_OUT2"})
+ cls.package = cls.env["stock.quant.package"].create({"name": "PKG_OUT"})
cls._update_qty_in_location(
cls.picking_type_out.default_location_src_id,
cls.product_out2,
diff --git a/shipment_advice/tests/test_shipment_advice.py b/shipment_advice/tests/test_shipment_advice.py
index 9c5d7dcd..56348953 100644
--- a/shipment_advice/tests/test_shipment_advice.py
+++ b/shipment_advice/tests/test_shipment_advice.py
@@ -12,6 +12,34 @@ class TestShipmentAdvice(Common):
def setUpClass(cls):
super().setUpClass()
+ def _prepare_picking_with_two_packages(self):
+ # Prepare packages & products
+ package2 = self.env["stock.quant.package"].create({"name": "PKG_OUT2"})
+ package3 = self.env["stock.quant.package"].create({"name": "PKG_OUT3"})
+ self.env["stock.quant"]._update_available_quantity(
+ self.product_out2,
+ self.picking_type_out.default_location_src_id,
+ 5,
+ package_id=package2,
+ )
+ self.env["stock.quant"]._update_available_quantity(
+ self.product_out3,
+ self.picking_type_out.default_location_src_id,
+ 5,
+ package_id=package3,
+ )
+ # Prepare moves (belonging to the same transfer)
+ group = self.env["procurement.group"].create({})
+ move_product_out2_2 = self._create_move(
+ self.picking_type_out, self.product_out2, 5, group
+ )
+ self.assertEqual(move_product_out2_2.move_line_ids.package_id, package2)
+ move_product_out3_2 = self._create_move(
+ self.picking_type_out, self.product_out3, 5, group
+ )
+ self.assertEqual(move_product_out3_2.move_line_ids.package_id, package3)
+ return move_product_out2_2.picking_id
+
def test_shipment_advice_confirm(self):
self._check_sequence(self.shipment_advice_out)
with self.assertRaises(UserError):
@@ -78,8 +106,10 @@ def test_shipment_advice_incoming_done_partial(self):
self.assertEqual(backorder.state, "assigned")
def test_shipment_advice_done_full(self):
- """Validating a shipment (whatever the backorder policy is) should
- validate all fully loaded transfers.
+ """Validating a shipment validate all fully loaded related transfers.
+
+ Whatever the backorder policy is, and if the loaded transfers are linked
+ to only one in progress shipment.
"""
picking = self.move_product_out1.picking_id
self.progress_shipment_advice(self.shipment_advice_out)
@@ -108,7 +138,7 @@ def test_shipment_advice_done_backorder_policy_disabled(self):
# Validate the shipment => the transfer is still open
self.shipment_advice_out.action_done()
picking = package_level.picking_id
- self.assertEqual(self.shipment_advice_out.state, "error")
+ self.assertEqual(self.shipment_advice_out.state, "done")
# Check the transfer
self.assertTrue(
all(
@@ -118,6 +148,42 @@ def test_shipment_advice_done_backorder_policy_disabled(self):
)
self.assertEqual(picking.state, "assigned")
+ def test_multi_shipment_advice_done_backorder_policy_disabled(self):
+ """Load a transfer in multiple shipments and validate them with no BO policy.
+
+ The last shipment validated is then responsible of the the transfer validation.
+
+ 1. Load first package in one shipment advice
+ 2. Validate the first shipment advice: delivery order is not yet validated
+ 3. Load second package in another shipment advice
+ 4. Validate the second shipment advice: delivery order is now well validated
+ """
+ # Disable the backorder policy
+ company = self.shipment_advice_out.company_id
+ company.shipment_advice_outgoing_backorder_policy = "leave_open"
+ # Prepare a transfer to load in two shipment advices
+ shipment_advice_out2 = self.env["shipment.advice"].create(
+ {"shipment_type": "outgoing"}
+ )
+ picking = self._prepare_picking_with_two_packages()
+ line1, line2 = picking.move_line_ids
+ # Load first package in the first shipment advice
+ pl1 = line1.package_level_id
+ self.progress_shipment_advice(self.shipment_advice_out)
+ self.load_records_in_shipment(self.shipment_advice_out, pl1)
+ # Validate the first shipment advice: delivery order hasn't been validated
+ self.shipment_advice_out.action_done()
+ self.assertEqual(self.shipment_advice_out.state, "done")
+ self.assertEqual(picking.state, "assigned")
+ # Load second package in the second shipment advice
+ pl2 = line2.package_level_id
+ self.progress_shipment_advice(shipment_advice_out2)
+ self.load_records_in_shipment(shipment_advice_out2, pl2)
+ # Validate the second shipment advice: delivery order has now been validated
+ shipment_advice_out2.action_done()
+ self.assertEqual(shipment_advice_out2.state, "done")
+ self.assertEqual(picking.state, "done")
+
def test_shipment_advice_done_backorder_policy_enabled(self):
"""Validating a shipment with the backorder policy enabled should
validate partial transfers and create a backorder.
@@ -147,6 +213,35 @@ def test_shipment_advice_done_backorder_policy_enabled(self):
)
self.assertEqual(picking2.state, "assigned")
+ def test_assign_lines_to_multiple_shipment_advices(self):
+ """Assign lines of a transfer to different shipment advices.
+
+ 1. Load two packages in two different shipment advices
+ 2. Validate the first shipment advice: delivery order is not yet validated
+ 3. Validate the second shipment advice: delivery order is now well validated
+ """
+ # Prepare a transfer to load in two shipment advices
+ shipment_advice_out2 = self.env["shipment.advice"].create(
+ {"shipment_type": "outgoing"}
+ )
+ picking = self._prepare_picking_with_two_packages()
+ line1, line2 = picking.move_line_ids
+ # Load packages in different shipment advices
+ pl1 = line1.package_level_id
+ self.progress_shipment_advice(self.shipment_advice_out)
+ self.load_records_in_shipment(self.shipment_advice_out, pl1)
+ pl2 = line2.package_level_id
+ self.progress_shipment_advice(shipment_advice_out2)
+ self.load_records_in_shipment(shipment_advice_out2, pl2)
+ # Validate the first shipment advice: delivery order hasn't been validated
+ self.shipment_advice_out.action_done()
+ self.assertEqual(self.shipment_advice_out.state, "done")
+ self.assertEqual(picking.state, "assigned")
+ # Validate the second shipment advice: delivery order has now been validated
+ shipment_advice_out2.action_done()
+ self.assertEqual(shipment_advice_out2.state, "done")
+ self.assertEqual(picking.state, "done")
+
def test_shipment_advice_cancel(self):
self.progress_shipment_advice(self.shipment_advice_out)
self.shipment_advice_out.action_cancel()
diff --git a/shipment_advice/tests/test_shipment_advice_async.py b/shipment_advice/tests/test_shipment_advice_async.py
index c614f2f2..ba1d7e1b 100644
--- a/shipment_advice/tests/test_shipment_advice_async.py
+++ b/shipment_advice/tests/test_shipment_advice_async.py
@@ -148,7 +148,7 @@ def test_shipment_advice_done_backorder_policy_disabled(self):
self._asset_jobs_dependency(jobs)
trap.perform_enqueued_jobs()
picking = package_level.picking_id
- self.assertEqual(self.shipment_advice_out.state, "error")
+ self.assertEqual(self.shipment_advice_out.state, "done")
# Check the transfer
self.assertTrue(
all(
diff --git a/shipment_advice/tests/test_shipment_advice_to_load.py b/shipment_advice/tests/test_shipment_advice_to_load.py
new file mode 100644
index 00000000..e838bed4
--- /dev/null
+++ b/shipment_advice/tests/test_shipment_advice_to_load.py
@@ -0,0 +1,51 @@
+# Copyright 2023 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
+
+
+from .common import Common
+
+
+class TestShipmentAdviceToLoad(Common):
+ def test_shipment_advice_not_planned_lines_to_load(self):
+ self.progress_shipment_advice(self.shipment_advice_out)
+ # Load a transfer partially
+ self.load_records_in_shipment(
+ self.shipment_advice_out, self.move_product_out1.move_line_ids
+ )
+ self.assertEqual(
+ self.move_product_out1.move_line_ids.shipment_advice_id,
+ self.shipment_advice_out,
+ )
+ # Check the lines computed by the shipment advice that could be loaded
+ lines_to_load = (
+ self.move_product_out2.move_line_ids | self.move_product_out3.move_line_ids
+ )
+ self.assertFalse(lines_to_load.shipment_advice_id)
+ self.assertEqual(self.shipment_advice_out.line_to_load_ids, lines_to_load)
+
+ def test_shipment_advice_planned_lines_to_load(self):
+ self.progress_shipment_advice(self.shipment_advice_out)
+ # Plan a transfer in the shipment advice
+ picking = self.move_product_out2.picking_id
+ self.plan_records_in_shipment(self.shipment_advice_out, picking)
+ # Check the lines computed by the shipment advice that could be loaded
+ # (= all the lines of the planned transfer)
+ lines_to_load = picking.move_line_ids
+ self.assertFalse(lines_to_load.shipment_advice_id)
+ self.assertEqual(self.shipment_advice_out.line_to_load_ids, lines_to_load)
+ # Load some goods from the planned transfer
+ self.load_records_in_shipment(
+ self.shipment_advice_out, self.move_product_out1.move_line_ids
+ )
+ self.assertEqual(
+ self.move_product_out1.move_line_ids.shipment_advice_id,
+ self.shipment_advice_out,
+ )
+ # Check the lines computed by the shipment advice that could be loaded
+ # (= the remaining planned lines of the transfer not yet loaded)
+ self.shipment_advice_out.invalidate_recordset()
+ lines_to_load2 = (
+ self.move_product_out2.move_line_ids | self.move_product_out3.move_line_ids
+ )
+ self.assertFalse(lines_to_load2.shipment_advice_id)
+ self.assertEqual(self.shipment_advice_out.line_to_load_ids, lines_to_load2)
diff --git a/shipment_advice/views/shipment_advice.xml b/shipment_advice/views/shipment_advice.xml
index fcb4718e..0341856f 100644
--- a/shipment_advice/views/shipment_advice.xml
+++ b/shipment_advice/views/shipment_advice.xml
@@ -145,6 +145,20 @@
widget="statinfo"
/>
+