diff --git a/stock_picking_group_by_partner_by_carrier/README.rst b/stock_picking_group_by_partner_by_carrier/README.rst new file mode 100644 index 000000000000..9e3b172e5ee1 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/README.rst @@ -0,0 +1,104 @@ +=========================================== +Stock Picking: group by partner and carrier +=========================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/13.0/stock_picking_group_by_partner_by_carrier + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-13-0/stock-logistics-workflow-13-0-stock_picking_group_by_partner_by_carrier + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/154/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module can be used if your customers expect that several different orders +they passed will be shipped in a single delivery order. + +With this module installed, when a sale order is confirmed, the stock moves for +the lines of the sale order can be placed in an existing delivery order that +shares the same delivery address and carrier (or lack thereof). + +Sale orders with a Shipping Policy set to 'When all products are ready' always +get their own shipping. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module you need to enable grouping on the picking types for which you want grouping. + +If you want to enable this for the shippings of a warehouse: + +* be sure that in the settings of the Inventory app, you checked "Manage Push + and Pull inventory flows" +* enable "debug mode" +* go to the warehouse for which you want grouping and check the setting "Group + Shippings" + + +You can also enable this for individual picking types by checking the setting +"Group Pickings" on the picking type view. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Camptocamp: + * Alexandre Fayolle + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_picking_group_by_partner_by_carrier/__init__.py b/stock_picking_group_by_partner_by_carrier/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_picking_group_by_partner_by_carrier/__manifest__.py b/stock_picking_group_by_partner_by_carrier/__manifest__.py new file mode 100644 index 000000000000..779ff8a69625 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2020 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Stock Picking: group by partner and carrier", + "Summary": "Group sales deliveries moves in 1 picking per partner and carrier", + "version": "13.0.1.0.0", + "development_status": "alpha", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-workflow", + "category": "Warehouse Management", + "depends": ["sale_stock", "delivery"], + "data": [ + "views/stock_picking_type.xml", + "views/stock_warehouse.xml", + "report/report_delivery_slip.xml", + ], + "installable": True, + "license": "AGPL-3", +} diff --git a/stock_picking_group_by_partner_by_carrier/models/__init__.py b/stock_picking_group_by_partner_by_carrier/models/__init__.py new file mode 100644 index 000000000000..02fc654ba0d5 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/models/__init__.py @@ -0,0 +1,6 @@ +from . import procurement_group +from . import sale_order +from . import stock_move +from . import stock_picking +from . import stock_picking_type +from . import stock_warehouse diff --git a/stock_picking_group_by_partner_by_carrier/models/procurement_group.py b/stock_picking_group_by_partner_by_carrier/models/procurement_group.py new file mode 100644 index 000000000000..662cce0885f5 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/models/procurement_group.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ProcurementGroup(models.Model): + _inherit = "procurement.group" + + carrier_id = fields.Many2one("delivery.carrier", string="Delivery Method") diff --git a/stock_picking_group_by_partner_by_carrier/models/sale_order.py b/stock_picking_group_by_partner_by_carrier/models/sale_order.py new file mode 100644 index 000000000000..116403b9e164 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/models/sale_order.py @@ -0,0 +1,23 @@ +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + picking_ids = fields.Many2many("stock.picking", string="Transfers", copy=False) + + def action_cancel(self): + for sale_order in self: + # change the context so we can intercept this in StockPicking.action_cancel + super( + SaleOrder, sale_order.with_context(cancel_sale_id=sale_order.id) + ).action_cancel() + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + def _prepare_procurement_group_vals(self): + vals = super()._prepare_procurement_group_vals() + vals["carrier_id"] = self.order_id.carrier_id.id + return vals diff --git a/stock_picking_group_by_partner_by_carrier/models/stock_move.py b/stock_picking_group_by_partner_by_carrier/models/stock_move.py new file mode 100644 index 000000000000..b3652562a423 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/models/stock_move.py @@ -0,0 +1,94 @@ +import re +from collections import namedtuple + +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _assign_picking(self): + return super( + StockMove, self.with_context(picking_no_overwrite_partner_origin=1) + )._assign_picking() + + def _assign_picking_post_process(self, new=False): + res = super()._assign_picking_post_process(new=new) + if not new: + picking = self.mapped("picking_id") + picking.ensure_one() + sales = self.mapped("sale_line_id.order_id") + for sale in sales: + pattern = r"\b%s\b" % sale.name + if not re.search(pattern, picking.origin): + picking.origin += " " + sale.name + picking.message_post_with_view( + "mail.message_origin_link", + values={"self": picking, "origin": sale}, + subtype_id=self.env.ref("mail.mt_note").id, + ) + return res + + def _search_picking_for_assignation(self): + # totally reimplement this one to add a hook to change the domain + self.ensure_one() + picking = self.env["stock.picking"].search( + self._domain_search_picking_for_assignation(), limit=1 + ) + return picking + + def _domain_search_picking_handle_move_type(self): + """Hook to handle the move type. Can be overloaded by other modules. + By default the move type is taken from the procurement group. + """ + # avoid mixing picking policies + return [("move_type", "=", self.group_id.move_type)] + + def _domain_search_picking_for_assignation(self): + states = ("draft", "confirmed", "waiting", "partially_available", "assigned") + if ( + not self.picking_type_id.group_pickings + or self.group_id.sale_id.picking_policy == "one" + ): + # use the normal domain from the stock module + domain = [ + ("group_id", "=", self.group_id.id), + ] + else: + domain = [ + # same partner + ("partner_id", "=", self.group_id.partner_id.id), + # don't search on the procurement.group + ] + domain += self._domain_search_picking_handle_move_type() + # same carrier only for outgoing transfers + if self.picking_type_id.code == "outgoing": + domain += [ + ("carrier_id", "=", self.group_id.carrier_id.id), + ] + else: + domain += [("carrier_id", "=", False)] + domain += [ + ("location_id", "=", self.location_id.id), + ("location_dest_id", "=", self.location_dest_id.id), + ("picking_type_id", "=", self.picking_type_id.id), + ("printed", "=", False), + ("immediate_transfer", "=", False), + ("state", "in", states), + ] + if self.env.context.get("picking_no_copy_if_can_group"): + # we are in the context of the creation of a backorder: + # don't consider the current move's picking + domain.append(("id", "!=", self.picking_id.id)) + return domain + + def _key_assign_picking(self): + return ( + self.sale_line_id.order_id.partner_shipping_id, + PickingPolicy(id=self.sale_line_id.order_id.picking_policy), + ) + super()._key_assign_picking() + + +# we define a named tuple because the code in module stock expects the values in +# the tuple returned by _key_assign_picking to be records with an id attribute +PickingPolicy = namedtuple("PickingPolicy", ["id"]) diff --git a/stock_picking_group_by_partner_by_carrier/models/stock_picking.py b/stock_picking_group_by_partner_by_carrier/models/stock_picking.py new file mode 100644 index 000000000000..27d1a13e9dfb --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/models/stock_picking.py @@ -0,0 +1,111 @@ +from collections import namedtuple +from itertools import groupby + +from odoo import api, fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + sale_ids = fields.Many2many("sale.order", compute="_compute_sale_ids", store=True) + # don't copy the printed state of a picking otherwise the backorder of a + # printed picking becomes printed + printed = fields.Boolean(copy=False) + + @api.depends("move_lines.group_id.sale_id") + def _compute_sale_ids(self): + for rec in self: + rec.sale_ids = rec.mapped("move_lines.group_id.sale_id") + + def write(self, values): + if self.env.context.get("picking_no_overwrite_partner_origin"): + written_fields = set(values.keys()) + if written_fields == {"partner_id", "origin"}: + values = {} + return super().write(values) + + def action_cancel(self): + cancel_sale_id = self.env.context.get("cancel_sale_id") + if cancel_sale_id: + moves = self.mapped("move_lines").filtered( + lambda m: m.group_id.sale_id.id == cancel_sale_id + and m.state not in ("done", "cancel") + ) + moves.with_context(cancel_sale_id=False)._action_cancel() + return True + else: + return super().action_cancel() + + def _create_backorder(self): + return super( + StockPicking, self.with_context(picking_no_copy_if_can_group=1) + )._create_backorder() + + def copy(self, defaults=None): + if self.env.context.get("picking_no_copy_if_can_group") and self.move_lines: + # we are in the process of the creation of a backorder. If we can + # find a suitable picking, then use it instead of copying the one + # we are creating a backorder from + picking = self.move_lines[0]._search_picking_for_assignation() + if picking: + return picking + return super( + StockPicking, self.with_context(picking_no_copy_if_can_group=0) + ).copy(defaults) + + def do_something(self): + return "bla bla" + + def get_delivery_report_lines(self): + self.ensure_one() + if self.state != "done": + moves = self.move_lines.filtered("product_uom_qty").sorted( + lambda m: m.sale_line_id.order_id + ) + if len(moves.mapped("sale_line_id.order_id")) > 1: + sales_and_moves = [] + for sale, sale_moves in groupby( + moves, lambda m: m.sale_line_id.order_id + ): + sales_and_moves.append( + MockedMove( + product_id=False, + description_picking=sale.name, + product_uom_qty=0, + product_uom=False, + lot_name="", + ) + ) + for move in sale_moves: + sales_and_moves.append(move) + return sales_and_moves + else: + return moves + else: + moves = self.move_lines.sorted(lambda m: m.sale_line_id.order_id) + if len(moves.mapped("sale_line_id.order_id")) > 1: + sales_and_moves = [] + for sale, sale_moves in groupby( + moves, lambda m: m.sale_line_id.order_id + ): + sales_and_moves.append( + MockedMove( + product_id=False, + description_picking=sale.name, + product_uom_qty=0, + product_uom=False, + lot_name="", + ) + ) + for move in sale_moves: + for move_line in move.move_line_ids: + sales_and_moves.append(move_line) + return sales_and_moves + else: + return self.move_line_ids + + +MockedMove = namedtuple( + "MockedMove", + ["product_id", "description_picking", "product_uom_qty", "product_uom", "lot_name"], +) diff --git a/stock_picking_group_by_partner_by_carrier/models/stock_picking_type.py b/stock_picking_group_by_partner_by_carrier/models/stock_picking_type.py new file mode 100644 index 000000000000..3b4a580a0ede --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/models/stock_picking_type.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class StockPickingType(models.Model): + _inherit = "stock.picking.type" + + group_pickings = fields.Boolean( + "Group pickings", + help="Group pickings for the same partner and carrier. " + "Pickings with shipping policy set to " + "'When all products are ready' are never grouped.", + ) diff --git a/stock_picking_group_by_partner_by_carrier/models/stock_warehouse.py b/stock_picking_group_by_partner_by_carrier/models/stock_warehouse.py new file mode 100644 index 000000000000..ecb5e45a52a4 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/models/stock_warehouse.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class StockWarehouse(models.Model): + _inherit = "stock.warehouse" + + group_shippings = fields.Boolean( + "Group shippings", + related="out_type_id.group_pickings", + readonly=False, + help="Group shippings for the same partner and carrier. " + "Shippings with shipping policy set to " + "'When all products are ready' are never grouped.", + ) diff --git a/stock_picking_group_by_partner_by_carrier/readme/CONTRIBUTORS.rst b/stock_picking_group_by_partner_by_carrier/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..ec6fdec6dbd7 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Camptocamp: + * Alexandre Fayolle diff --git a/stock_picking_group_by_partner_by_carrier/readme/DESCRIPTION.rst b/stock_picking_group_by_partner_by_carrier/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..8382fe5193d4 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +This module can be used if your customers expect that several different orders +they passed will be shipped in a single delivery order. + +With this module installed, when a sale order is confirmed, the stock moves for +the lines of the sale order can be placed in an existing delivery order that +shares the same delivery address and carrier (or lack thereof). + +Sale orders with a Shipping Policy set to 'When all products are ready' always +get their own shipping. diff --git a/stock_picking_group_by_partner_by_carrier/readme/USAGE.rst b/stock_picking_group_by_partner_by_carrier/readme/USAGE.rst new file mode 100644 index 000000000000..e711e1ce49be --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/readme/USAGE.rst @@ -0,0 +1,13 @@ +To use this module you need to enable grouping on the picking types for which you want grouping. + +If you want to enable this for the shippings of a warehouse: + +* be sure that in the settings of the Inventory app, you checked "Manage Push + and Pull inventory flows" +* enable "debug mode" +* go to the warehouse for which you want grouping and check the setting "Group + Shippings" + + +You can also enable this for individual picking types by checking the setting +"Group Pickings" on the picking type view. diff --git a/stock_picking_group_by_partner_by_carrier/report/report_delivery_slip.xml b/stock_picking_group_by_partner_by_carrier/report/report_delivery_slip.xml new file mode 100644 index 000000000000..c3214cc1f231 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/report/report_delivery_slip.xml @@ -0,0 +1,93 @@ + + + + diff --git a/stock_picking_group_by_partner_by_carrier/static/description/index.html b/stock_picking_group_by_partner_by_carrier/static/description/index.html new file mode 100644 index 000000000000..5b2207d31754 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/static/description/index.html @@ -0,0 +1,447 @@ + + + + + + +Stock Picking: group by partner and carrier + + + +
+

Stock Picking: group by partner and carrier

+ + +

Alpha License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runbot

+

This module can be used if your customers expect that several different orders +they passed will be shipped in a single delivery order.

+

With this module installed, when a sale order is confirmed, the stock moves for +the lines of the sale order can be placed in an existing delivery order that +shares the same delivery address and carrier (or lack thereof).

+

Sale orders with a Shipping Policy set to ‘When all products are ready’ always +get their own shipping.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Usage

+

To use this module you need to enable grouping on the picking types for which you want grouping.

+

If you want to enable this for the shippings of a warehouse:

+
    +
  • be sure that in the settings of the Inventory app, you checked “Manage Push +and Pull inventory flows”
  • +
  • enable “debug mode”
  • +
  • go to the warehouse for which you want grouping and check the setting “Group +Shippings”
  • +
+

You can also enable this for individual picking types by checking the setting +“Group Pickings” on the picking type view.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/stock-logistics-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/stock_picking_group_by_partner_by_carrier/tests/__init__.py b/stock_picking_group_by_partner_by_carrier/tests/__init__.py new file mode 100644 index 000000000000..77329395a51a --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/tests/__init__.py @@ -0,0 +1 @@ +from . import test_grouping diff --git a/stock_picking_group_by_partner_by_carrier/tests/test_grouping.py b/stock_picking_group_by_partner_by_carrier/tests/test_grouping.py new file mode 100644 index 000000000000..512cb9e86535 --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/tests/test_grouping.py @@ -0,0 +1,383 @@ +from odoo.tests import tagged + +from odoo.addons.sale.tests.test_sale_common import TestSale + + +@tagged("post_install", "-at_install") +class TestSaleStock(TestSale): + def setUp(self): + super().setUp() + self.carrier1 = self.env["delivery.carrier"].create( + { + "name": "My Test Carrier", + "product_id": self.env.ref("delivery.product_product_delivery").id, + } + ) + self.carrier2 = self.env["delivery.carrier"].create( + { + "name": "My Other Test Carrier", + "product_id": self.env.ref("delivery.product_product_delivery").id, + } + ) + self.env.ref("stock.warehouse0").group_shippings = True + + def _get_new_sale_order(self, amount=10.0, partner=None, carrier=None): + """ Creates and returns a sale order with one default order line. + + :param float amount: quantity of product for the order line (10 by default) + """ + if partner is None: + partner = self.env.ref("base.res_partner_1") + if carrier is None: + carrier_id = False + else: + carrier_id = carrier.id + product = self.env.ref("product.product_delivery_01") + sale_order_vals = { + "partner_id": partner.id, + "partner_invoice_id": partner.id, + "partner_shipping_id": partner.id, + "carrier_id": carrier_id, + "order_line": [ + ( + 0, + 0, + { + "name": product.name, + "product_id": product.id, + "product_uom_qty": amount, + "product_uom": product.uom_id.id, + "price_unit": product.list_price, + }, + ) + ], + "pricelist_id": self.env.ref("product.list0").id, + } + sale_order = self.env["sale.order"].create(sale_order_vals) + return sale_order + + def test_sale_stock_merge_same_partner_no_carrier(self): + """2 sale orders for the same partner, without carrier + + -> the pickings are merged""" + so1 = self._get_new_sale_order() + so2 = self._get_new_sale_order(amount=11) + so1.action_confirm() + so2.action_confirm() + self.assertTrue(so1.picking_ids) + self.assertEqual(so1.picking_ids, so2.picking_ids) + + def test_sale_stock_merge_same_carrier(self): + """2 sale orders for the same partner, with same carrier + + -> the pickings are merged""" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so1.action_confirm() + so2.action_confirm() + # there is a picking for the sales, and it is shared + self.assertTrue(so1.picking_ids) + self.assertEqual(so1.picking_ids, so2.picking_ids) + # the origin of the picking mentions both sales names + self.assertTrue(so1.name in so1.picking_ids[0].origin) + self.assertTrue(so2.name in so1.picking_ids[0].origin) + + def test_sale_stock_no_merge_different_carrier(self): + """2 sale orders for the same partner, with different carriers + + -> the pickings are not merged""" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier2) + so1.action_confirm() + so2.action_confirm() + self.assertEqual(so1.picking_ids.carrier_id, self.carrier1) + self.assertEqual(so2.picking_ids.carrier_id, self.carrier2) + self.assertNotEqual(so1.picking_ids, so2.picking_ids) + self.assertTrue(so1.name in so1.picking_ids[0].origin) + self.assertTrue(so2.name in so2.picking_ids[0].origin) + + def test_sale_stock_no_merge_carrier_set_only_on_one(self): + """2 sale orders for the same partner, one with the other without + + -> the pickings are not merged""" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so2 = self._get_new_sale_order(amount=11, carrier=None) + so1.action_confirm() + so2.action_confirm() + self.assertEqual(so1.picking_ids.carrier_id, self.carrier1) + self.assertFalse(so2.picking_ids.carrier_id) + self.assertNotEqual(so1.picking_ids, so2.picking_ids) + + def test_sale_stock_no_merge_same_carrier_picking_policy_one(self): + """2 sale orders for the same partner, with same carrier, deliver at + once picking policy + + -> the pickings are not merged + + """ + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.picking_policy = "one" + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.picking_policy = "one" + so1.action_confirm() + so2.action_confirm() + # there is a picking for each the sales, different + self.assertTrue(so1.picking_ids) + self.assertTrue(so2.picking_ids) + self.assertNotEqual(so1.picking_ids, so2.picking_ids) + # the origin of the picking mentions both sales names + self.assertTrue(so1.name in so1.picking_ids[0].origin) + self.assertTrue(so2.name in so2.picking_ids[0].origin) + + def test_sale_stock_no_merge_same_carrier_mixed_picking_policy(self): + """2 sale orders for the same partner, with same carrier, deliver at once + picking policy for the 1st sale order. + + -> the pickings are not merged + + """ + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.picking_policy = "one" + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so1.action_confirm() + so2.action_confirm() + # there is a picking for each the sales, different + self.assertTrue(so1.picking_ids) + self.assertTrue(so2.picking_ids) + self.assertNotEqual(so1.picking_ids, so2.picking_ids) + # the origin of the picking mentions both sales names + self.assertTrue(so1.name in so1.picking_ids[0].origin) + self.assertTrue(so2.name in so2.picking_ids[0].origin) + + def test_printed_pick_no_merge(self): + """1st sale order ship is printed, 2nd sale order not merged""" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so1.picking_ids.do_print_picking() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + self.assertNotEqual(so1.picking_ids, so2.picking_ids) + + def test_backorder_picking_merge(self): + """1st sale order ship is printed, 2nd sale order not merged. + Partial delivery of so1 + + -> backorder is merged with so2 picking + + """ + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so1.picking_ids.do_print_picking() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + pick = so1.picking_ids + move = pick.move_lines[0] + move.quantity_done = 5 + pick.with_context(cancel_backorder=False).action_done() + self.assertTrue(so2.picking_ids & so1.picking_ids) + self.assertEqual(so2.picking_ids.sale_ids, so1 + so2) + + def test_cancelling_sale_order1(self): + """1st sale order is cancelled + + -> picking is still todo with only 1 stock move todo""" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + self.assertTrue(so1.picking_ids) + self.assertTrue(so2.picking_ids) + self.assertEqual(so1.picking_ids, so2.picking_ids) + so1.action_cancel() + self.assertNotEqual(so1.picking_ids.state, "cancel") + moves = so1.picking_ids.move_lines + so1_moves = moves.filtered(lambda m: m.sale_line_id.order_id == so1) + so2_moves = moves.filtered(lambda m: m.sale_line_id.order_id == so2) + self.assertEqual(so1_moves.mapped("state"), ["cancel"]) + self.assertEqual(so2_moves.mapped("state"), ["confirmed"]) + self.assertEqual(so1.state, "cancel") + self.assertEqual(so2.state, "sale") + + def test_cancelling_sale_order1_before_create_order2(self): + """1st sale order is cancelled + + -> picking is still todo with only 1 stock move todo""" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so1.action_cancel() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + self.assertTrue(so1.picking_ids) + self.assertTrue(so2.picking_ids) + self.assertFalse(so1.picking_ids & so2.picking_ids) + + def test_cancelling_sale_order2(self): + """2nd sale order is cancelled + + -> picking is still todo with only 1 stock move todo""" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + self.assertTrue(so1.picking_ids) + self.assertTrue(so2.picking_ids) + self.assertEqual(so1.picking_ids, so2.picking_ids) + so2.action_cancel() + self.assertNotEqual(so1.picking_ids.state, "cancel") + moves = so1.picking_ids.move_lines + so1_moves = moves.filtered(lambda m: m.sale_line_id.order_id == so1) + so2_moves = moves.filtered(lambda m: m.sale_line_id.order_id == so2) + self.assertEqual(so1_moves.mapped("state"), ["confirmed"]) + self.assertEqual(so2_moves.mapped("state"), ["cancel"]) + self.assertEqual(so1.state, "sale") + self.assertEqual(so2.state, "cancel") + + def test_delivery_multi_step(self): + """the warehouse uses pick + ship + + -> shippings are grouped, pickings are not""" + warehouse = self.env.ref("stock.warehouse0") + warehouse.delivery_steps = "pick_ship" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + self.assertEqual(len(so1.picking_ids), 2) + self.assertEqual(len(so2.picking_ids), 2) + # ship should be shared between so1 and so2 + ships = so1.picking_ids & so2.picking_ids + self.assertEqual(len(ships), 1) + self.assertEqual(ships.picking_type_id, warehouse.out_type_id) + # but not picks + self.assertTrue(so1.picking_ids - so2.picking_ids) + self.assertTrue(so2.picking_ids - so1.picking_ids) + picks = (so1.picking_ids - so2.picking_ids) | ( + so2.picking_ids - so1.picking_ids + ) + + self.assertEqual(len(picks), 2) + self.assertEqual(picks.mapped("picking_type_id"), warehouse.pick_type_id) + + def test_delivery_multi_step_group_pick(self): + """the warehouse uses pick + ship (with grouping enabled on pick) + + -> shippings are grouped, as well as pickings""" + warehouse = self.env.ref("stock.warehouse0") + warehouse.delivery_steps = "pick_ship" + warehouse.pick_type_id.group_pickings = True + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + self.assertEqual(len(so1.picking_ids), 2) + self.assertEqual(len(so2.picking_ids), 2) + # ship & pick should be shared between so1 and so2 + transfers = so1.picking_ids & so2.picking_ids + self.assertEqual(len(transfers), 2) + ships = transfers.filtered(lambda o: o.picking_type_id == warehouse.out_type_id) + picks = transfers.filtered( + lambda o: o.picking_type_id == warehouse.pick_type_id + ) + self.assertEqual(len(ships), 1) + self.assertEqual(len(picks), 1) + self.assertFalse(so1.picking_ids - so2.picking_ids) + + def test_delivery_multi_step_group_pick_pack(self): + """the warehouse uses pick + pack + ship (with grouping enabled on pack) + + -> shippings are grouped, as well as pickings""" + warehouse = self.env.ref("stock.warehouse0") + warehouse.delivery_steps = "pick_pack_ship" + warehouse.pick_type_id.group_pickings = False + warehouse.pack_type_id.group_pickings = True + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + self.assertEqual(len(so1.picking_ids), 3) + self.assertEqual(len(so2.picking_ids), 3) + # ship & pack should be shared between so1 and so2, but not pick + all_transfers = so1.picking_ids | so2.picking_ids + common_transfers = so1.picking_ids & so2.picking_ids + self.assertEqual(len(all_transfers), 4) + self.assertEqual(len(common_transfers), 2) + ships = all_transfers.filtered( + lambda o: o.picking_type_id == warehouse.out_type_id + ) + packs = all_transfers.filtered( + lambda o: o.picking_type_id == warehouse.pack_type_id + ) + picks = all_transfers.filtered( + lambda o: o.picking_type_id == warehouse.pick_type_id + ) + self.assertEqual(len(ships), 1) + self.assertEqual(len(packs), 1) + self.assertEqual(len(picks), 2) + self.assertTrue(so1.picking_ids - so2.picking_ids) + + def test_delivery_multi_step_cancel_so1(self): + """the warehouse uses pick + ship. Cancel SO1 + + -> shippings are grouped, pickings are not""" + warehouse = self.env.ref("stock.warehouse0") + warehouse.delivery_steps = "pick_ship" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + ships = so1.picking_ids & so2.picking_ids + so1.action_cancel() + self.assertEqual(ships.state, "waiting") + pick1 = so1.picking_ids - ships + self.assertEqual(pick1.state, "cancel") + pick2 = so2.picking_ids - ships + self.assertEqual(pick2.state, "confirmed") + + def test_delivery_multi_step_cancel_so2(self): + """the warehouse uses pick + ship. Cancel SO2 + + -> shippings are grouped, pickings are not""" + warehouse = self.env.ref("stock.warehouse0") + warehouse.delivery_steps = "pick_ship" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + ships = so1.picking_ids & so2.picking_ids + so2.action_cancel() + self.assertEqual(ships.state, "waiting") + pick1 = so1.picking_ids - ships + self.assertEqual(pick1.state, "confirmed") + pick2 = so2.picking_ids - ships + self.assertEqual(pick2.state, "cancel") + + def test_delivery_multi_step_cancel_so1_create_so3(self): + """the warehouse uses pick + ship. Cancel SO1, create SO3 + + -> shippings are grouped, pickings are not""" + warehouse = self.env.ref("stock.warehouse0") + warehouse.delivery_steps = "pick_ship" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + ships = so1.picking_ids & so2.picking_ids + so1.action_cancel() + so3 = self._get_new_sale_order(amount=12, carrier=self.carrier1) + so3.action_confirm() + self.assertTrue(ships in so3.picking_ids) + pick3 = so3.picking_ids - ships + self.assertEqual(len(pick3), 1) + self.assertEqual(pick3.state, "confirmed") + + def test_delivery_mult_step_cancelling_sale_order1_before_create_order2(self): + """1st sale order is cancelled + + -> picking is still todo with only 1 stock move todo""" + so1 = self._get_new_sale_order(carrier=self.carrier1) + so1.action_confirm() + so1.action_cancel() + so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1) + so2.action_confirm() + self.assertTrue(so1.picking_ids) + self.assertTrue(so2.picking_ids) + self.assertFalse(so1.picking_ids & so2.picking_ids) diff --git a/stock_picking_group_by_partner_by_carrier/views/stock_picking_type.xml b/stock_picking_group_by_partner_by_carrier/views/stock_picking_type.xml new file mode 100644 index 000000000000..85622b8bfd0c --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/views/stock_picking_type.xml @@ -0,0 +1,13 @@ + + + + Operation Types + stock.picking.type + + + + + + + + diff --git a/stock_picking_group_by_partner_by_carrier/views/stock_warehouse.xml b/stock_picking_group_by_partner_by_carrier/views/stock_warehouse.xml new file mode 100644 index 000000000000..5652546b942f --- /dev/null +++ b/stock_picking_group_by_partner_by_carrier/views/stock_warehouse.xml @@ -0,0 +1,13 @@ + + + + stock.warehouse + stock.warehouse + + + + + + + +