diff --git a/purchase_duplicate_check/README.rst b/purchase_duplicate_check/README.rst new file mode 100644 index 00000000000..8e328cedf2a --- /dev/null +++ b/purchase_duplicate_check/README.rst @@ -0,0 +1,119 @@ +======================== +Purchase Duplicate Check +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7107035505d1a2624df4c1bce4fc742fd17319625c9a8ea54b4558c8e7c35107 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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%2Fpurchase--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/purchase-workflow/tree/16.0/purchase_duplicate_check + :alt: OCA/purchase-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/purchase-workflow-16-0/purchase-workflow-16-0-purchase_duplicate_check + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/purchase-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds the following features to the Purchase App: + +Pending RFQ or Purchase orders with associated incoming +stock picking not in the "Done" state for Product in PO Line: + +- RFQ Confirmation Wizard if there's existing RFQ for the same products +- Add "Pending orders" field to the PO Line +- Assign Acitivty for the responsible user to check the associated orders + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +This module helps prevent overordering of the same products by streamlining the control of Requests for Quotations (RFQs) and undelivered Purchase Orders (POs). + +Configuration +============= + +To Configure the Activity for a responsible person to check the repeating RFQ/PO: + +Go to Settings -> Purchase - > Orders + +- Enable the "Create Activity for Repeating Orders" Checkbox +- Select the activity type in the "Activity" Field +- Press the "Arrow button" to Configure the Default user for activity + +Usage +===== + +Pending RFQ for Product in PO Line: + +Go to the Purchase App: + +- Create a new order +- Press the "Add a Line" button to add purchase line +- Select a Product for the PO Line +- If there are pending RFQs for the same product in the "Draft"/"Sent" state, or Purchase orders with associated incoming stock picking not in the "Done" state it will be shown in the "Pending order" field +- Press the "Confirm Order" button to confirm the RFQ +- If there's a Pending RFQ's for the products in PO Lines, the Confirmation wizard with Pending RFQ's information will appear +- Press the "Confirm" button to process with the order or "Cancel" to get back to RFQ + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Cetmix + +Contributors +~~~~~~~~~~~~ + +* `Cetmix `_ + + * Ivan Sokolov + * Mikhail Lapin + * Maksim Shurupov + * Dinar Gabbasov + +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/purchase-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_duplicate_check/__init__.py b/purchase_duplicate_check/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/purchase_duplicate_check/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/purchase_duplicate_check/__manifest__.py b/purchase_duplicate_check/__manifest__.py new file mode 100644 index 00000000000..4fe330a8faa --- /dev/null +++ b/purchase_duplicate_check/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "Purchase Duplicate Check", + "version": "16.0.1.0.0", + "summary": "Prevents overordering in the Purchase app with a Confirmation Wizard, " + "'Pending Orders' field, and activity tracking for repeated orders.", + "author": "Cetmix, Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Inventory/Purchase", + "website": "https://github.com/OCA/purchase-workflow", + "depends": ["purchase_stock", "confirmation_wizard"], + "data": [ + "views/purchase_order_views.xml", + "views/res_config_settings_views.xml", + "wizard/confirmation_wizard_views.xml", + ], + "installable": True, + "application": False, +} diff --git a/purchase_duplicate_check/models/__init__.py b/purchase_duplicate_check/models/__init__.py new file mode 100644 index 00000000000..3cf91acfc7a --- /dev/null +++ b/purchase_duplicate_check/models/__init__.py @@ -0,0 +1,3 @@ +from . import purchase_order +from . import purchase_order_line +from . import res_config_settings diff --git a/purchase_duplicate_check/models/purchase_order.py b/purchase_duplicate_check/models/purchase_order.py new file mode 100644 index 00000000000..efff07d3d34 --- /dev/null +++ b/purchase_duplicate_check/models/purchase_order.py @@ -0,0 +1,55 @@ +from odoo import models + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + def _prepare_pending_orders_message(self, product_id): + """ + Prepare pending order line message + + :param product_id: product.product record id + :return str: message + """ + message_parts = [] + order_lines = self.order_line.filtered(lambda l: l.product_id.id == product_id) + for line in order_lines: + order = line.order_id + order_href = ( + f"{order.name}" + ) + type_ = order.state in ["draft", "sent"] and "RFQ" or "PO" + message_parts.append( + f"{type_}: {order_href}; date: {order.create_date.date()}; Qty: {line.product_qty};
" # noqa + ) + return "".join(message_parts) + + def _is_activity_enabled(self) -> bool: + """Check if activity for duplicated orders is enabled""" + return ( + self.env["ir.config_parameter"] + .sudo() + .get_param( + "purchase_duplicate_check.allow_create_activity_repeating_orders", False + ) + ) + + def _check_pending_order(self): + """Check for pending orders and trigger confirmation wizard if needed.""" + if self._is_activity_enabled() and not self._context.get( + "skip_rfq_confirmation" + ): + return ( + self.env["confirmation.wizard"] + .with_context(skip_rfq_confirmation=True) + .confirm_pending_order(self) + ) + + def button_confirm(self): + """ + Confirm the purchase order. + + :return: action or super + """ + action = self._check_pending_order() + return action or super().button_confirm() diff --git a/purchase_duplicate_check/models/purchase_order_line.py b/purchase_duplicate_check/models/purchase_order_line.py new file mode 100644 index 00000000000..91774bb735c --- /dev/null +++ b/purchase_duplicate_check/models/purchase_order_line.py @@ -0,0 +1,87 @@ +from odoo import _, fields, models + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + pending_order_ids = fields.Many2many( + "purchase.order", + string="Pending Orders", + compute="_compute_pending_order_ids", + ) + + def _compute_pending_order_ids(self): + product_lines = self.filtered(lambda rec: rec.product_type == "product") + other_lines = self - product_lines + + if other_lines: + other_lines.pending_order_ids = False + + if not product_lines: + return + + product_ids = tuple(product_lines.mapped("product_id")._origin.ids) + order_ids = tuple(product_lines.mapped("order_id")._origin.ids) + if not product_ids or not order_ids: + product_lines.pending_order_ids = False + return + query = """ + SELECT po.id, pol.product_id + FROM purchase_order po + JOIN purchase_order_line pol ON pol.order_id = po.id + LEFT JOIN stock_move sm ON sm.purchase_line_id = pol.id + LEFT JOIN stock_picking sp ON sp.id = sm.picking_id + + WHERE pol.product_id IN %s + AND po.id NOT IN %s + AND ( + po.state IN ('draft', 'sent') + OR ( + po.state NOT IN ('draft', 'sent') + AND sp.picking_type_id IN ( + SELECT id FROM stock_picking_type WHERE code = 'incoming' + ) + AND sp.state NOT IN ('done', 'cancel') + ) + ) + """ + self.env.cr.execute(query, (product_ids, order_ids)) + result = self.env.cr.fetchall() + product_orders_map = {} + for order_id, product_id in result: + if product_id not in product_orders_map: + product_orders_map[product_id] = [] + product_orders_map[product_id].append(order_id) + + for rec in product_lines: + rec.pending_order_ids = [ + (6, 0, product_orders_map.get(rec.product_id.id, [])) + ] + + def _get_order_confirm_message(self): + """Get order confirmation message for pending orders""" + message = "" + for line in self: + pending_orders = line.pending_order_ids + if not pending_orders: + continue + product_line_msg = pending_orders._prepare_pending_orders_message( + line.product_id.id + ) + message += f""" + Product {line.product_id.name}
+ {product_line_msg}
+ """ + return message + + def action_open_pending_orders(self): + """Action open pending purchase orders""" + self.ensure_one() + return { + "name": _("Pending Orders"), + "views": [[False, "tree"], [False, "form"]], + "res_model": "purchase.order", + "type": "ir.actions.act_window", + "domain": [("id", "in", self.pending_order_ids.ids)], + "context": {"create": False}, + } diff --git a/purchase_duplicate_check/models/res_config_settings.py b/purchase_duplicate_check/models/res_config_settings.py new file mode 100644 index 00000000000..ed65905c833 --- /dev/null +++ b/purchase_duplicate_check/models/res_config_settings.py @@ -0,0 +1,19 @@ +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + allow_create_activity_repeating_orders = fields.Boolean( + config_parameter="purchase_duplicate_check.allow_create_activity_repeating_orders" + ) + repeating_orders_activity_type_id = fields.Many2one( + comodel_name="mail.activity.type", + config_parameter="purchase_duplicate_check.repeating_orders_activity_type_id", + string="Activity", + ) + + @api.onchange("allow_create_activity_repeating_orders") + def _onchange_allow_create_activity_repeating_orders(self): + if not self.allow_create_activity_repeating_orders: + self.repeating_orders_activity_type_id = False diff --git a/purchase_duplicate_check/readme/CONFIGURE.rst b/purchase_duplicate_check/readme/CONFIGURE.rst new file mode 100644 index 00000000000..02c7d90681e --- /dev/null +++ b/purchase_duplicate_check/readme/CONFIGURE.rst @@ -0,0 +1,7 @@ +To Configure the Activity for a responsible person to check the repeating RFQ/PO: + +Go to Settings -> Purchase - > Orders + +- Enable the "Create Activity for Repeating Orders" Checkbox +- Select the activity type in the "Activity" Field +- Press the "Arrow button" to Configure the Default user for activity diff --git a/purchase_duplicate_check/readme/CONTEXT.rst b/purchase_duplicate_check/readme/CONTEXT.rst new file mode 100644 index 00000000000..c315d39b1ce --- /dev/null +++ b/purchase_duplicate_check/readme/CONTEXT.rst @@ -0,0 +1 @@ +This module helps prevent overordering of the same products by streamlining the control of Requests for Quotations (RFQs) and undelivered Purchase Orders (POs). diff --git a/purchase_duplicate_check/readme/CONTRIBUTORS.rst b/purchase_duplicate_check/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..e38b444fc8c --- /dev/null +++ b/purchase_duplicate_check/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* `Cetmix `_ + + * Ivan Sokolov + * Mikhail Lapin + * Maksim Shurupov + * Dinar Gabbasov diff --git a/purchase_duplicate_check/readme/DESCRIPTION.rst b/purchase_duplicate_check/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..75a4923e746 --- /dev/null +++ b/purchase_duplicate_check/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +This module adds the following features to the Purchase App: + +Pending RFQ or Purchase orders with associated incoming +stock picking not in the "Done" state for Product in PO Line: + +- RFQ Confirmation Wizard if there's existing RFQ for the same products +- Add "Pending orders" field to the PO Line +- Assign Acitivty for the responsible user to check the associated orders diff --git a/purchase_duplicate_check/readme/USAGE.rst b/purchase_duplicate_check/readme/USAGE.rst new file mode 100644 index 00000000000..3bd80a7e7a2 --- /dev/null +++ b/purchase_duplicate_check/readme/USAGE.rst @@ -0,0 +1,11 @@ +Pending RFQ for Product in PO Line: + +Go to the Purchase App: + +- Create a new order +- Press the "Add a Line" button to add purchase line +- Select a Product for the PO Line +- If there are pending RFQs for the same product in the "Draft"/"Sent" state, or Purchase orders with associated incoming stock picking not in the "Done" state it will be shown in the "Pending order" field +- Press the "Confirm Order" button to confirm the RFQ +- If there's a Pending RFQ's for the products in PO Lines, the Confirmation wizard with Pending RFQ's information will appear +- Press the "Confirm" button to process with the order or "Cancel" to get back to RFQ diff --git a/purchase_duplicate_check/static/description/index.html b/purchase_duplicate_check/static/description/index.html new file mode 100644 index 00000000000..f4267e18b4f --- /dev/null +++ b/purchase_duplicate_check/static/description/index.html @@ -0,0 +1,464 @@ + + + + + +Purchase Duplicate Check + + + +
+

Purchase Duplicate Check

+ + +

Beta License: AGPL-3 OCA/purchase-workflow Translate me on Weblate Try me on Runboat

+

This module adds the following features to the Purchase App:

+

Pending RFQ or Purchase orders with associated incoming +stock picking not in the “Done” state for Product in PO Line:

+
    +
  • RFQ Confirmation Wizard if there’s existing RFQ for the same products
  • +
  • Add “Pending orders” field to the PO Line
  • +
  • Assign Acitivty for the responsible user to check the associated orders
  • +
+

Table of contents

+ +
+

Use Cases / Context

+

This module helps prevent overordering of the same products by streamlining the control of Requests for Quotations (RFQs) and undelivered Purchase Orders (POs).

+
+
+

Configuration

+

To Configure the Activity for a responsible person to check the repeating RFQ/PO:

+

Go to Settings -> Purchase - > Orders

+
    +
  • Enable the “Create Activity for Repeating Orders” Checkbox
  • +
  • Select the activity type in the “Activity” Field
  • +
  • Press the “Arrow button” to Configure the Default user for activity
  • +
+
+
+

Usage

+

Pending RFQ for Product in PO Line:

+

Go to the Purchase App:

+
    +
  • Create a new order
  • +
  • Press the “Add a Line” button to add purchase line
  • +
  • Select a Product for the PO Line
  • +
  • If there are pending RFQs for the same product in the “Draft”/”Sent” state, or Purchase orders with associated incoming stock picking not in the “Done” state it will be shown in the “Pending order” field
  • +
  • Press the “Confirm Order” button to confirm the RFQ
  • +
  • If there’s a Pending RFQ’s for the products in PO Lines, the Confirmation wizard with Pending RFQ’s information will appear
  • +
  • Press the “Confirm” button to process with the order or “Cancel” to get back to RFQ
  • +
+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Cetmix
  • +
+
+
+

Contributors

+
    +
  • Cetmix
      +
    • Ivan Sokolov
    • +
    • Mikhail Lapin
    • +
    • Maksim Shurupov
    • +
    • Dinar Gabbasov
    • +
    +
  • +
+
+
+

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/purchase-workflow project on GitHub.

+

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

+
+
+
+ + diff --git a/purchase_duplicate_check/tests/__init__.py b/purchase_duplicate_check/tests/__init__.py new file mode 100644 index 00000000000..165f23ce4d5 --- /dev/null +++ b/purchase_duplicate_check/tests/__init__.py @@ -0,0 +1 @@ +from . import test_purchase_order diff --git a/purchase_duplicate_check/tests/test_purchase_order.py b/purchase_duplicate_check/tests/test_purchase_order.py new file mode 100644 index 00000000000..507e8ecb6af --- /dev/null +++ b/purchase_duplicate_check/tests/test_purchase_order.py @@ -0,0 +1,102 @@ +from odoo.tests import Form, TransactionCase + + +class TestPurchaseOrder(TransactionCase): + def setUp(self): + super().setUp() + self.partner = self.env["res.partner"].create({"name": "Vendor #1"}) + self.product_1 = self.env["product.product"].create( + {"name": "Product 1", "detailed_type": "product"} + ) + self.product_2 = self.env["product.product"].create( + {"name": "Product 2", "detailed_type": "product"} + ) + form = Form(self.env["purchase.order"]) + form.partner_id = self.partner + with form.order_line.new() as line: + line.product_id = self.product_1 + line.product_qty = 10.0 + self.order1 = form.save() + + self.activity_type_email = self.env.ref("mail.mail_activity_data_email") + self.env["ir.config_parameter"].sudo().set_param( + "purchase_duplicate_check.allow_create_activity_repeating_orders", True + ) + self.env["ir.config_parameter"].sudo().set_param( + "purchase_duplicate_check.repeating_orders_activity_type_id", + self.activity_type_email.id, + ) + + def _get_and_create_purchase_order(self): + form = Form(self.env["purchase.order"]) + form.partner_id = self.partner + with form.order_line.new() as line: + line.product_id = self.product_1 + line.product_qty = 5.0 + with form.order_line.new() as line: + line.product_id = self.product_2 + line.product_qty = 5.0 + return form.save() + + def test_prepare_pending_orders_message(self): + """Test flow where prepare message for purchase order""" + message = self.order1._prepare_pending_orders_message(self.product_2.id) + self.assertFalse(message, "Message must be empty") + expected_message = f"RFQ: {self.order1.name}; date: {self.order1.create_date.date()}; Qty: 10.0;
" # noqa + message = self.order1._prepare_pending_orders_message(self.product_1.id) + self.assertEqual(message, expected_message, "Messages must be the same") + + def test_check_pending_order(self): + """ + Test flow where check purchase order + by exists pending orders for order lines + """ + result = self.order1._check_pending_order() + self.assertIsNone(result, "Result should be None") + order2 = self._get_and_create_purchase_order() + result = order2.with_context(skip_rfq_confirmation=True)._check_pending_order() + self.assertIsNone(result, "Result should be None") + result = order2._check_pending_order() + self.assertIsInstance(result, dict, "Result should be dict") + + def test_button_confirm(self): + """ + Test flow where check confirmation wizard at + the confirmation purchase order + """ + order1 = self.order1 + order2 = self._get_and_create_purchase_order() + order3 = self._get_and_create_purchase_order() + + order3.with_context(skip_rfq_confirmation=True).button_confirm() + + self.assertEqual(order2.state, "draft", "Order should be draft") + line1, line2 = order2.order_line + self.assertEqual( + line1.pending_order_ids, + order1 | order3, + "Pending orders should be the same", + ) + self.assertEqual( + line2.pending_order_ids, order3, "Pending orders should be the same" + ) + + result = order2.button_confirm() + self.assertIsInstance(result, dict, "Result should be dict") + + wizard = self.env["confirmation.wizard"].browse(result["res_id"]) + self.assertEqual(wizard.res_ids, str(order2.ids), "Res IDS must be the same") + self.assertEqual(wizard.res_model, order1._name, "Res Model must be the same") + self.assertEqual( + wizard.callback_method, + "button_confirm", + "Callback method must be 'nutton_confirm'", + ) + + result = wizard.with_context(skip_rfq_confirmation=True).action_confirm() + self.assertTrue(result, "Result should be True") + self.assertEqual(order2.state, "purchase", "Order state must be 'purchase'") + activity = order2.activity_ids + self.assertEqual(len(activity), 1) + self.assertEqual(activity.user_id, self.env.user) + self.assertEqual(activity.activity_type_id, self.activity_type_email) diff --git a/purchase_duplicate_check/views/purchase_order_views.xml b/purchase_duplicate_check/views/purchase_order_views.xml new file mode 100644 index 00000000000..14d8b34e5e2 --- /dev/null +++ b/purchase_duplicate_check/views/purchase_order_views.xml @@ -0,0 +1,42 @@ + + + + + purchase.order.form.view + purchase.order + + + + + +