From 3ffe6de8a7578870a183b8b19d369232b05adb08 Mon Sep 17 00:00:00 2001 From: Alessandro Uffreduzzi Date: Wed, 6 Dec 2023 15:22:17 +0100 Subject: [PATCH] [FIX]purchase_sale_inter_company: handle pickings with lots/serials Previously an intercompany picking with tracked products would simply throw an error. With this fix, the method searches for a lot in the destination company that matches the one in the source company (same name and same product). A new lot is created by duplicating the original, if none is found. --- purchase_sale_inter_company/README.rst | 20 ++- purchase_sale_inter_company/__manifest__.py | 1 + .../models/res_company.py | 1 - .../models/res_config.py | 1 - .../models/stock_picking.py | 37 +++++- .../readme/CONTRIBUTORS.rst | 5 +- .../readme/DESCRIPTION.rst | 2 + .../static/description/index.html | 12 +- .../tests/test_inter_company_purchase_sale.py | 124 +++++++++++++++++- 9 files changed, 187 insertions(+), 16 deletions(-) diff --git a/purchase_sale_inter_company/README.rst b/purchase_sale_inter_company/README.rst index 97062a75fc3..c2520e31b5b 100644 --- a/purchase_sale_inter_company/README.rst +++ b/purchase_sale_inter_company/README.rst @@ -7,7 +7,7 @@ Inter Company Module for Purchase to Sale Order !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:b044056bea6e953f2621dd5f72974a86ca2e3926a1e4dfedf25659e77241a539 + !! source digest: sha256:869d25c929f4db8beedddc0d5443401d7b0bb36f86ecb9154504afdae0bea861 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -32,6 +32,8 @@ This module is useful if there are multiple companies in the same Odoo database It allows to create a sale order in company A from a purchase order in company B, and keep delivery/receipt pickings synced, including backorders. +When Company A sends a product tracked by lot or serial number, a new lot/serial number with the same name is created in Company B to match it, if one doesn't already exist. + **Table of contents** .. contents:: @@ -95,9 +97,12 @@ Contributors * Mourad EL HADJ MIMOUNE * `PyTech SRL `_: - * Alessandro Uffreduzzi + * Alessandro Uffreduzzi + * Ooops404 + * Francesco Foresti + Maintainers ~~~~~~~~~~~ @@ -111,6 +116,17 @@ 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. +.. |maintainer-aleuffre| image:: https://github.com/aleuffre.png?size=40px + :target: https://github.com/aleuffre + :alt: aleuffre +.. |maintainer-renda-dev| image:: https://github.com/renda-dev.png?size=40px + :target: https://github.com/renda-dev + :alt: renda-dev + +Current `maintainers `__: + +|maintainer-aleuffre| |maintainer-renda-dev| + This module is part of the `OCA/multi-company `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_sale_inter_company/__manifest__.py b/purchase_sale_inter_company/__manifest__.py index 16204417300..4cf0fb527de 100644 --- a/purchase_sale_inter_company/__manifest__.py +++ b/purchase_sale_inter_company/__manifest__.py @@ -10,6 +10,7 @@ "category": "Purchase Management", "website": "https://github.com/OCA/multi-company", "author": "Odoo SA, Akretion, Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["aleuffre", "renda-dev"], "license": "AGPL-3", "installable": True, "depends": [ diff --git a/purchase_sale_inter_company/models/res_company.py b/purchase_sale_inter_company/models/res_company.py index 3bb91265c3e..853652baa9e 100644 --- a/purchase_sale_inter_company/models/res_company.py +++ b/purchase_sale_inter_company/models/res_company.py @@ -7,7 +7,6 @@ class ResCompany(models.Model): - _inherit = "res.company" so_from_po = fields.Boolean( diff --git a/purchase_sale_inter_company/models/res_config.py b/purchase_sale_inter_company/models/res_config.py index 7e785ec1209..875f5a919d2 100644 --- a/purchase_sale_inter_company/models/res_config.py +++ b/purchase_sale_inter_company/models/res_config.py @@ -7,7 +7,6 @@ class InterCompanyRulesConfig(models.TransientModel): - _inherit = "res.config.settings" so_from_po = fields.Boolean( diff --git a/purchase_sale_inter_company/models/stock_picking.py b/purchase_sale_inter_company/models/stock_picking.py index db3436a0b23..25c4d295ff4 100644 --- a/purchase_sale_inter_company/models/stock_picking.py +++ b/purchase_sale_inter_company/models/stock_picking.py @@ -21,19 +21,42 @@ def _action_done(self): purchase.picking_ids.write({"intercompany_picking_id": pick.id}) if not pick.intercompany_picking_id and purchase.picking_ids[0]: pick.write({"intercompany_picking_id": purchase.picking_ids[0]}) - for move_line in pick.move_line_ids: - sale_line_id = move_line.move_id.sale_line_id - po_move_lines = sale_line_id.auto_purchase_line_id.move_ids.mapped( + for move in pick.move_lines: + move_lines = move.move_line_ids + po_move_lines = move.sale_line_id.auto_purchase_line_id.move_ids.mapped( "move_line_ids" ) - if not po_move_lines: + if not len(move_lines) == len(po_move_lines): raise UserError( _( - "There's no corresponding line in PO %s for assigning " - "qty from %s for product %s" + "Mismatch between move lines with the " + "corresponding PO %s for assigning " + "quantities and lots from %s for product %s" ) - % (purchase.name, pick.name, move_line.product_id.name) + % (purchase.name, pick.name, move.product_id.name) ) + # check and assign lots here + for ml, po_ml in zip(move_lines, po_move_lines): + lot_id = ml.lot_id + if not lot_id: + continue + # search if the same lot exists in destination company + dest_lot_id = ( + self.env["stock.production.lot"] + .sudo() + .search( + [ + ("product_id", "=", lot_id.product_id.id), + ("name", "=", lot_id.name), + ("company_id", "=", po_ml.company_id.id), + ], + limit=1, + ) + ) + if not dest_lot_id: + # if it doesn't exist, create it by copying from original company + dest_lot_id = lot_id.copy({"company_id": po_ml.company_id.id}) + po_ml.lot_id = dest_lot_id return super()._action_done() def button_validate(self): diff --git a/purchase_sale_inter_company/readme/CONTRIBUTORS.rst b/purchase_sale_inter_company/readme/CONTRIBUTORS.rst index 0d7d472e621..06fd50fc432 100644 --- a/purchase_sale_inter_company/readme/CONTRIBUTORS.rst +++ b/purchase_sale_inter_company/readme/CONTRIBUTORS.rst @@ -9,5 +9,8 @@ * Mourad EL HADJ MIMOUNE * `PyTech SRL `_: - * Alessandro Uffreduzzi + * Alessandro Uffreduzzi + * Ooops404 + + * Francesco Foresti diff --git a/purchase_sale_inter_company/readme/DESCRIPTION.rst b/purchase_sale_inter_company/readme/DESCRIPTION.rst index 0f9002a495b..97575a30a0a 100644 --- a/purchase_sale_inter_company/readme/DESCRIPTION.rst +++ b/purchase_sale_inter_company/readme/DESCRIPTION.rst @@ -1,3 +1,5 @@ This module is useful if there are multiple companies in the same Odoo database and those companies sell goods or services among themselves. It allows to create a sale order in company A from a purchase order in company B, and keep delivery/receipt pickings synced, including backorders. + +When Company A sends a product tracked by lot or serial number, a new lot/serial number with the same name is created in Company B to match it, if one doesn't already exist. diff --git a/purchase_sale_inter_company/static/description/index.html b/purchase_sale_inter_company/static/description/index.html index d55bc73f71c..462cdd695d4 100644 --- a/purchase_sale_inter_company/static/description/index.html +++ b/purchase_sale_inter_company/static/description/index.html @@ -367,11 +367,12 @@

Inter Company Module for Purchase to Sale Order

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:b044056bea6e953f2621dd5f72974a86ca2e3926a1e4dfedf25659e77241a539 +!! source digest: sha256:869d25c929f4db8beedddc0d5443401d7b0bb36f86ecb9154504afdae0bea861 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/multi-company Translate me on Weblate Try me on Runboat

This module is useful if there are multiple companies in the same Odoo database and those companies sell goods or services among themselves.

It allows to create a sale order in company A from a purchase order in company B, and keep delivery/receipt pickings synced, including backorders.

+

When Company A sends a product tracked by lot or serial number, a new lot/serial number with the same name is created in Company B to match it, if one doesn’t already exist.

Table of contents

@@ -445,6 +449,8 @@

Maintainers

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.

+

Current maintainers:

+

aleuffre renda-dev

This module is part of the OCA/multi-company project on GitHub.

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

diff --git a/purchase_sale_inter_company/tests/test_inter_company_purchase_sale.py b/purchase_sale_inter_company/tests/test_inter_company_purchase_sale.py index e8d572d4f82..512ec1be00a 100644 --- a/purchase_sale_inter_company/tests/test_inter_company_purchase_sale.py +++ b/purchase_sale_inter_company/tests/test_inter_company_purchase_sale.py @@ -53,6 +53,8 @@ def _create_purchase_order(cls, partner, product_id=None): @classmethod def setUpClass(cls): super().setUpClass() + cls.lot_obj = cls.env["stock.production.lot"] + cls.quant_obj = cls.env["stock.quant"] # no job: avoid issue if account_invoice_inter_company_queued is installed cls.env = cls.env(context={"test_queue_job_no_delay": 1}) @@ -61,11 +63,19 @@ def setUpClass(cls): cls.consumable_product = cls.env["product.product"].create( { "name": "Consumable Product", - "type": "product", + "type": "consu", "categ_id": cls.env.ref("product.product_category_all").id, "qty_available": 100, } ) + cls.stockable_product_serial = cls.env["product.product"].create( + { + "name": "Stockable Product Tracked by Serial", + "type": "product", + "tracking": "serial", + "categ_id": cls.env.ref("product.product_category_all").id, + } + ) # if partner_multi_company or product_multi_company is installed # We have to do that because the default method added a company @@ -109,6 +119,32 @@ def setUpClass(cls): {"currency_id": cls.env.ref("base.USD").id} ) + # Add quants for product tracked by serial to supplier + cls.serial_1 = cls._create_serial_and_quant( + cls.stockable_product_serial, "111", cls.company_b + ) + cls.serial_2 = cls._create_serial_and_quant( + cls.stockable_product_serial, "222", cls.company_b + ) + cls.serial_3 = cls._create_serial_and_quant( + cls.stockable_product_serial, "333", cls.company_b + ) + + @classmethod + def _create_serial_and_quant(cls, product, name, company): + lot = cls.lot_obj.create( + {"product_id": product.id, "name": name, "company_id": company.id} + ) + cls.quant_obj.create( + { + "product_id": product.id, + "location_id": cls.warehouse_a.lot_stock_id.id, + "quantity": 1, + "lot_id": lot.id, + } + ) + return lot + def _approve_po(self, purchase_id): """Confirm the PO in company A and return the related sale of Company B""" @@ -275,3 +311,89 @@ def test_sync_picking(self): # A backorder should have been made for both self.assertTrue(len(sale.picking_ids) > 1) self.assertEqual(len(purchase.picking_ids), len(sale.picking_ids)) + + def test_sync_picking_lot(self): + """ + Test that the lot is synchronized on the moves + by searching or creating a new lot in the company of destination + """ + # lot 3 already exists in company_a + serial_3_company_a = self._create_serial_and_quant( + self.stockable_product_serial, "333", self.company_a + ) + self.company_a.sync_picking = True + self.company_b.sync_picking = True + + purchase = self._create_purchase_order( + self.partner_company_b, self.stockable_product_serial + ) + sale = self._approve_po(purchase) + + # validate the SO picking + po_picking_id = purchase.picking_ids + so_picking_id = sale.picking_ids + + so_move = so_picking_id.move_lines + so_move.move_line_ids = [ + ( + 0, + 0, + { + "location_id": so_move.location_id.id, + "location_dest_id": so_move.location_dest_id.id, + "product_id": self.stockable_product_serial.id, + "product_uom_id": self.stockable_product_serial.uom_id.id, + "qty_done": 1, + "lot_id": self.serial_1.id, + "picking_id": so_picking_id.id, + }, + ), + ( + 0, + 0, + { + "location_id": so_move.location_id.id, + "location_dest_id": so_move.location_dest_id.id, + "product_id": self.stockable_product_serial.id, + "product_uom_id": self.stockable_product_serial.uom_id.id, + "qty_done": 1, + "lot_id": self.serial_2.id, + "picking_id": so_picking_id.id, + }, + ), + ( + 0, + 0, + { + "location_id": so_move.location_id.id, + "location_dest_id": so_move.location_dest_id.id, + "product_id": self.stockable_product_serial.id, + "product_uom_id": self.stockable_product_serial.uom_id.id, + "qty_done": 1, + "lot_id": self.serial_3.id, + "picking_id": so_picking_id.id, + }, + ), + ] + so_picking_id.button_validate() + + so_lots = so_move.mapped("move_line_ids.lot_id") + po_lots = po_picking_id.mapped("move_lines.move_line_ids.lot_id") + self.assertEqual( + len(so_lots), + len(po_lots), + msg="There aren't the same number of lots on both moves", + ) + self.assertNotEqual( + so_lots, po_lots, msg="The lots of the moves should be different objects" + ) + self.assertEqual( + so_lots.mapped("name"), + po_lots.mapped("name"), + msg="The lots should have the same name in both moves", + ) + self.assertIn( + serial_3_company_a, + po_lots, + msg="Serial 333 already existed, a new one shouldn't have been created", + )