diff --git a/sale_order_confirm_partial/README.rst b/sale_order_confirm_partial/README.rst new file mode 100644 index 00000000000..4606102cc46 --- /dev/null +++ b/sale_order_confirm_partial/README.rst @@ -0,0 +1,127 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=============================== +Sale Order Partial Confirmation +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5c1808472b6a1de28c62bb966291f6168117f1661076334a1e5b8b8587880f6a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/16.0/sale_order_confirm_partial + :alt: OCA/sale-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_order_confirm_partial + :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/sale-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to select which sale order lines you would like to +confirm. It also can create a new quotation in the "Cancel" state with +all the unconfirmed lines from the original quotation in case you need +to keep that information. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +Sometimes you need to confirm only selected lines of a Sales Order. Eg +your customer has asked you a quotation for a desk, five chairs and a +sofa. However in the end he decides to order only a desk and 2 chairs. + +Configuration +============= + +Go to "Sales -> Configuration -> Settings" and scroll down to the +"Quotation & Orders" section. Enable the "Partial Confirmation" +checkbox. Enable the "Save Unconfirmed Items" checkbox if you want to +create a quotation with all the unconfirmed items. Define a suffix for +the unconfirmed items quotation in the "Unconfirmed Quotation Suffix" +field. If no suffix is defined then a standard "-U" suffix will be used. +Eg "SO00023-U". + +Usage +===== + +Open a quotation and click the "Confirm" button. A wizard will pop-up +with the following options available: + +- "All Items" +- "Selected Items" + +Choose "Selected Items", set the lines quantities you want to confirm in +the list below and click "Confirm". If you don't want to confirm +specific lines at all, just set their quantity to 0. Note, that +confirmed quantity cannot be negative or greater than ordered quantity +of related SO line. If you put invalid quantity, you'll be notified via +error message. Quotation will be updated according to your selection and +confirmed. + +If you have enabled "Save Unconfirmed Items" in the settings a new +quotation in the "Cancel" state will be created with all the lines that +were not confirmed. Internal note "Created from will be posted in the +chatter of the unconfirmed lines quotation. Internal note "Unconfirmed +lines are saved in will be posted in the chatter of the confirmed sales +order. + +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 OÜ https://cetmix.com + +- Ivan Sokolov + +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/sale-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_order_confirm_partial/__init__.py b/sale_order_confirm_partial/__init__.py new file mode 100644 index 00000000000..16879da2b66 --- /dev/null +++ b/sale_order_confirm_partial/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import models +from . import wizard diff --git a/sale_order_confirm_partial/__manifest__.py b/sale_order_confirm_partial/__manifest__.py new file mode 100644 index 00000000000..737319f83b9 --- /dev/null +++ b/sale_order_confirm_partial/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Sale Order Partial Confirmation", + "summary": "Select sales order lines to be confirmed", + "author": "Cetmix, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sale-workflow", + "category": "Sales", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "depends": ["sale"], + "data": [ + "security/ir.model.access.csv", + "views/res_config_settings_views.xml", + "wizard/sale_order_confirm_partial_views.xml", + ], + "installable": True, +} diff --git a/sale_order_confirm_partial/models/__init__.py b/sale_order_confirm_partial/models/__init__.py new file mode 100644 index 00000000000..cc53f390ff1 --- /dev/null +++ b/sale_order_confirm_partial/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import res_config_settings +from . import sale_order diff --git a/sale_order_confirm_partial/models/res_config_settings.py b/sale_order_confirm_partial/models/res_config_settings.py new file mode 100644 index 00000000000..bec54b45d89 --- /dev/null +++ b/sale_order_confirm_partial/models/res_config_settings.py @@ -0,0 +1,26 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + sale_order_confirm_partial_enabled = fields.Boolean( + string="Partial Confirmation", + config_parameter="sale_order_confirm_partial.enabled", + ) + so_confirm_partial_save_unconfirmed = fields.Boolean( + string="Save Unconfirmed Items", + config_parameter="sale_order_confirm_partial.save_unconfirmed", + help=( + "If enabled a new quotation in the 'Cancel' " + "state will be created with all the lines " + "that were not confirmed." + ), + ) + so_confirm_partial_unconfirmed_suffix = fields.Char( + string="Unconfirmed Quotation Suffix", + config_parameter="sale_order_confirm_partial.unconfirmed_suffix", + default="-U", + ) diff --git a/sale_order_confirm_partial/models/sale_order.py b/sale_order_confirm_partial/models/sale_order.py new file mode 100644 index 00000000000..1f76877eaf4 --- /dev/null +++ b/sale_order_confirm_partial/models/sale_order.py @@ -0,0 +1,149 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import _, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def action_confirm(self): + """ + If partial confirmation feature is enabled, + instead of standard SO confirmation at first we need + to open the partial confirmation wizard for current SO. + + If feature is disabled or 'standard_confirm_proceed' context key + was specified as True, then we proceed with standard confirmation. + """ + + force_confirm = self.env.context.get("standard_confirm_proceed", False) + partial_confirm_enabled = ( + self.env["ir.config_parameter"] + .sudo() + .get_param( + "sale_order_confirm_partial.enabled", + False, + ) + ) + if force_confirm or not partial_confirm_enabled: + return super().action_confirm() + action = self.env["ir.actions.act_window"]._for_xml_id( + "sale_order_confirm_partial.sale_order_confirm_partial_action" + ) + action.update( + { + "context": { + "default_sale_order_id": self.id, + } + } + ) + return action + + def _process_unconfirmed_quotation(self, confirmed_lines): + """ + If 'Save Unconfirmed Items' feature is enabled, + then create a new quotation in the 'Cancel' state + and move all unconfirmed lines to it. + + Quantities of those lines should be reduced + depending on confirmed quantities. + + If after reduction the quantity of any line + is less than or equal to 0, then remove that line. + + Args: + confirmed_lines: sale.order.confirm.partial.line recordset. + Lines to be confirmed. + + Returns: + sale.order: Created unconfirmed quotation + None: If feature is disabled or no unconfirmed lines + """ + self.ensure_one() + ICPsudo = self.env["ir.config_parameter"].sudo() + save_unconfirmed = ICPsudo.get_param( + "sale_order_confirm_partial.save_unconfirmed", False + ) + if not save_unconfirmed: + return + # If all lines are confirmed and confirmed quantity + # is equal to ordered quantity, then no need to create + # unconfirmed quotation. + if all( + [ + line.confirmed_qty == line.so_line_id.product_uom_qty + for line in confirmed_lines + ] + ) and len(confirmed_lines) == len(self.order_line): + return + unconfirmed_suffix = ICPsudo.get_param( + "sale_order_confirm_partial.unconfirmed_suffix", "-U" + ) + unconfirmed_order = self.copy( + { + "name": self.name + unconfirmed_suffix, + "order_line": False, + } + ) + unconfirmed_lines = self.env["sale.order.line"] + for line in self.order_line: + confirmed_line = confirmed_lines.filtered(lambda x: x.so_line_id == line) + if confirmed_line.confirmed_qty == line.product_uom_qty: + continue + unconfirmed_lines |= line.copy( + { + "order_id": unconfirmed_order.id, + "product_uom_qty": line.product_uom_qty + - confirmed_line.confirmed_qty, + } + ) + unconfirmed_order.state = "cancel" + unconfirmed_order.message_post( + body=_( + "Created from " + "%(name)s" + ) + % {"id": self.id, "name": self.name}, + partner_id=self.env.ref("base.partner_root").id, + subtype_id=self.env.ref("mail.mt_note").id, + ) + return unconfirmed_order + + def _update_order_lines_qty(self, confirmed_lines): + """ + Update quantities of order lines depending + on confirmed quantities in selected lines. + + Args: + confirmed_lines: sale.order.confirm.partial.line recordset. + """ + self.ensure_one() + for line in confirmed_lines: + line.so_line_id.product_uom_qty = line.confirmed_qty + + def action_confirm_partial(self, confirmed_lines): + """ + Confirms only selected lines of the SO. + + If 'Save Unconfirmed Items' feature is enabled, + then it creates also a new quotation in the 'Cancel' state + containing all unconfirmed lines and quantities. + + Args: + confirmed_lines: sale.order.confirm.partial.line recordset. + Lines to be confirmed. + """ + self.ensure_one() + unconfirmed_quotation = self._process_unconfirmed_quotation(confirmed_lines) + self._update_order_lines_qty(confirmed_lines) + self.with_context(standard_confirm_proceed=True).action_confirm() + if unconfirmed_quotation: + self.message_post( + body=_( + "Unconfirmed lines are saved in " + "%(name)s" + ) + % {"id": unconfirmed_quotation.id, "name": unconfirmed_quotation.name}, + partner_id=self.env.ref("base.partner_root").id, + subtype_id=self.env.ref("mail.mt_note").id, + ) diff --git a/sale_order_confirm_partial/readme/CONFIGURE.md b/sale_order_confirm_partial/readme/CONFIGURE.md new file mode 100644 index 00000000000..044453ec969 --- /dev/null +++ b/sale_order_confirm_partial/readme/CONFIGURE.md @@ -0,0 +1,4 @@ +Go to "Sales -> Configuration -> Settings" and scroll down to the "Quotation & Orders" section. +Enable the "Partial Confirmation" checkbox. +Enable the "Save Unconfirmed Items" checkbox if you want to create a quotation with all the unconfirmed items. +Define a suffix for the unconfirmed items quotation in the "Unconfirmed Quotation Suffix" field. If no suffix is defined then a standard "-U" suffix will be used. Eg "SO00023-U". \ No newline at end of file diff --git a/sale_order_confirm_partial/readme/CONTEXT.md b/sale_order_confirm_partial/readme/CONTEXT.md new file mode 100644 index 00000000000..b8333657496 --- /dev/null +++ b/sale_order_confirm_partial/readme/CONTEXT.md @@ -0,0 +1,2 @@ +Sometimes you need to confirm only selected lines of a Sales Order. +Eg your customer has asked you a quotation for a desk, five chairs and a sofa. However in the end he decides to order only a desk and 2 chairs. \ No newline at end of file diff --git a/sale_order_confirm_partial/readme/CONTRIBUTORS.md b/sale_order_confirm_partial/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..7576fb0b34f --- /dev/null +++ b/sale_order_confirm_partial/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +Cetmix OÜ https://cetmix.com + +- Ivan Sokolov diff --git a/sale_order_confirm_partial/readme/DESCRIPTION.md b/sale_order_confirm_partial/readme/DESCRIPTION.md new file mode 100644 index 00000000000..cbf3d1e5127 --- /dev/null +++ b/sale_order_confirm_partial/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to select which sale order lines you would like to confirm. It also can create a new quotation in the "Cancel" state with all the unconfirmed lines from the original quotation in case you need to keep that information. \ No newline at end of file diff --git a/sale_order_confirm_partial/readme/USAGE.md b/sale_order_confirm_partial/readme/USAGE.md new file mode 100644 index 00000000000..a1115521612 --- /dev/null +++ b/sale_order_confirm_partial/readme/USAGE.md @@ -0,0 +1,14 @@ +Open a quotation and click the "Confirm" button. +A wizard will pop-up with the following options available: + +- "All Items" +- "Selected Items" + +Choose "Selected Items", set the lines quantities you want to confirm in the list below and click "Confirm". +If you don't want to confirm specific lines at all, just set their quantity to 0. +Note, that confirmed quantity cannot be negative or greater than ordered quantity of related SO line. If you put invalid quantity, you'll be notified via error message. +Quotation will be updated according to your selection and confirmed. + +If you have enabled "Save Unconfirmed Items" in the settings a new quotation in the "Cancel" state will be created with all the lines that were not confirmed. +Internal note "Created from will be posted in the chatter of the unconfirmed lines quotation. +Internal note "Unconfirmed lines are saved in will be posted in the chatter of the confirmed sales order. \ No newline at end of file diff --git a/sale_order_confirm_partial/security/ir.model.access.csv b/sale_order_confirm_partial/security/ir.model.access.csv new file mode 100644 index 00000000000..43a5d8c229c --- /dev/null +++ b/sale_order_confirm_partial/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_order_confirm_partial_wizard,access_sale_order_confirm_partial_wizard,model_sale_order_confirm_partial,sales_team.group_sale_salesman,1,1,1,1 +access_sale_order_confirm_partial_line,access_sale_order_confirm_partial_line,model_sale_order_confirm_partial_line,sales_team.group_sale_salesman,1,1,1,1 diff --git a/sale_order_confirm_partial/static/description/icon.png b/sale_order_confirm_partial/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/sale_order_confirm_partial/static/description/icon.png differ diff --git a/sale_order_confirm_partial/static/description/index.html b/sale_order_confirm_partial/static/description/index.html new file mode 100644 index 00000000000..97bd22a8d34 --- /dev/null +++ b/sale_order_confirm_partial/static/description/index.html @@ -0,0 +1,474 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Sale Order Partial Confirmation

+ +

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

+

This module allows to select which sale order lines you would like to +confirm. It also can create a new quotation in the “Cancel” state with +all the unconfirmed lines from the original quotation in case you need +to keep that information.

+

Table of contents

+ +
+

Use Cases / Context

+

Sometimes you need to confirm only selected lines of a Sales Order. Eg +your customer has asked you a quotation for a desk, five chairs and a +sofa. However in the end he decides to order only a desk and 2 chairs.

+
+
+

Configuration

+

Go to “Sales -> Configuration -> Settings” and scroll down to the +“Quotation & Orders” section. Enable the “Partial Confirmation” +checkbox. Enable the “Save Unconfirmed Items” checkbox if you want to +create a quotation with all the unconfirmed items. Define a suffix for +the unconfirmed items quotation in the “Unconfirmed Quotation Suffix” +field. If no suffix is defined then a standard “-U” suffix will be used. +Eg “SO00023-U”.

+
+
+

Usage

+

Open a quotation and click the “Confirm” button. A wizard will pop-up +with the following options available:

+
    +
  • “All Items”
  • +
  • “Selected Items”
  • +
+

Choose “Selected Items”, set the lines quantities you want to confirm in +the list below and click “Confirm”. If you don’t want to confirm +specific lines at all, just set their quantity to 0. Note, that +confirmed quantity cannot be negative or greater than ordered quantity +of related SO line. If you put invalid quantity, you’ll be notified via +error message. Quotation will be updated according to your selection and +confirmed.

+

If you have enabled “Save Unconfirmed Items” in the settings a new +quotation in the “Cancel” state will be created with all the lines that +were not confirmed. Internal note “Created from will be posted in the +chatter of the unconfirmed lines quotation. Internal note “Unconfirmed +lines are saved in will be posted in the chatter of the confirmed sales +order.

+
+
+

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 OÜ https://cetmix.com

+
    +
  • Ivan Sokolov
  • +
+
+
+

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

+

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

+
+
+
+
+ + diff --git a/sale_order_confirm_partial/tests/__init__.py b/sale_order_confirm_partial/tests/__init__.py new file mode 100644 index 00000000000..048cfeafcbc --- /dev/null +++ b/sale_order_confirm_partial/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_sale_order_confirm_partial diff --git a/sale_order_confirm_partial/tests/test_sale_order_confirm_partial.py b/sale_order_confirm_partial/tests/test_sale_order_confirm_partial.py new file mode 100644 index 00000000000..8455ff11a1d --- /dev/null +++ b/sale_order_confirm_partial/tests/test_sale_order_confirm_partial.py @@ -0,0 +1,217 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import exceptions +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestSaleOrderConfirmPartial(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env["ir.config_parameter"].set_param( + "sale_order_confirm_partial.enabled", True + ) + cls.sale_order = cls.env["sale.order"].create( + { + "name": "SO-TEST", + "partner_id": cls.env.ref("base.res_partner_1").id, + "pricelist_id": cls.env.ref("product.list0").id, + } + ) + cls.so_line_1 = cls.env["sale.order.line"].create( + { + "order_id": cls.sale_order.id, + "product_id": cls.env.ref("product.product_product_6").id, + "product_uom_qty": 10, + "price_unit": 100, + } + ) + cls.so_line_2 = cls.env["sale.order.line"].create( + { + "order_id": cls.sale_order.id, + "product_id": cls.env.ref("product.product_product_7").id, + "product_uom_qty": 7, + "price_unit": 200, + } + ) + + def test_confirmation_with_save_unconfirmed_all(self): + self.env["ir.config_parameter"].set_param( + "sale_order_confirm_partial.save_unconfirmed", True + ) + # Confirm SO partially + self.env["sale.order.confirm.partial"].create( + {"sale_order_id": self.sale_order.id, "mode": "all"} + ).action_confirm() + + # Check SO state + self.assertEqual(self.sale_order.state, "sale", "Sale order is not confirmed") + + # Since we have confirmed all of the lines, there is no need to save + # the unconfirmed part of SO + unconfirmed_so = self.env["sale.order"].search([("name", "like", "SO-TEST-U")]) + self.assertFalse( + unconfirmed_so, + "Unconfirmed part of SO has been created when all of lines were confirmed.", + ) + + # Check original order lines quantities + # (they should stay the same) + self.assertEqual( + self.so_line_1.product_uom_qty, + 10, + "Original order line 1 qty is wrong", + ) + self.assertEqual( + self.so_line_2.product_uom_qty, + 7, + "Original order line 2 qty is wrong", + ) + + def test_confirmation_with_save_unconfirmed_selected(self): + self.env["ir.config_parameter"].set_param( + "sale_order_confirm_partial.save_unconfirmed", True + ) + # Confirm SO partially + self.env["sale.order.confirm.partial"].create( + { + "sale_order_id": self.sale_order.id, + "mode": "selected", + "line_ids": [ + ( + 0, + 0, + { + "so_line_id": self.so_line_1.id, + "confirmed_qty": 4, + }, + ), + ], + } + ).action_confirm() + + # Check SO state + self.assertEqual(self.sale_order.state, "sale", "Sale order is not confirmed") + + # Since we have confirmed selected line, we need to save + # the unconfirmed part of SO + unconfirmed_so = self.env["sale.order"].search([("name", "like", "SO-TEST-U")]) + self.assertTrue( + unconfirmed_so, + "Unconfirmed part of SO hasn't been created when order was confirmed partially.", + ) + # Check unconfirmed SO status + self.assertEqual( + unconfirmed_so.state, "cancel", "Unconfirmed SO is not in cancel state" + ) + # Check unconfirmed order lines quantities + # (they should be equal to statement original_qty - confirmed_qty) + self.assertEqual( + unconfirmed_so.order_line.filtered( + lambda x: x.so_line_id == self.so_line_1 + ).product_uom_qty, + 6, + "Unconfirmed part of order line 1 qty is wrong", + ) + self.assertEqual( + unconfirmed_so.order_line.filtered( + lambda x: x.so_line_id == self.so_line_2 + ).product_uom_qty, + 7, + "Unconfirmed part of order line 2 qty is wrong", + ) + + # Check original order lines quantities + # (they should be reduced accordingly to the confirmed quantities) + self.assertEqual( + self.so_line_1.product_uom_qty, + 4, + "Original order line 1 qty is wrong", + ) + self.assertEqual( + self.so_line_2.product_uom_qty, + 0, + "Original order line 2 qty is wrong", + ) + + def test_confirmation_without_save_unconfirmed_selected(self): + # Confirm SO partially + self.env["sale.order.confirm.partial"].create( + { + "sale_order_id": self.sale_order.id, + "mode": "selected", + "line_ids": [ + ( + 0, + 0, + { + "so_line_id": self.so_line_1.id, + "confirmed_qty": 4, + }, + ), + ], + } + ).action_confirm() + + # Check SO state + self.assertEqual(self.sale_order.state, "sale", "Sale order is not confirmed") + + # Since we have confirmed selected line, we need to save + # the unconfirmed part of SO (but it's disabled) + unconfirmed_so = self.env["sale.order"].search([("name", "like", "SO-TEST-U")]) + self.assertFalse( + unconfirmed_so, + "Unconfirmed part of SO has been created but settings option is disabled.", + ) + + # Check original order lines quantities + # (they should be reduced accordingly to the confirmed quantities) + self.assertEqual( + self.so_line_1.product_uom_qty, + 4, + "Original order line 1 qty is wrong", + ) + self.assertEqual( + self.so_line_2.product_uom_qty, + 0, + "Original order line 2 qty is wrong", + ) + + def test_sale_order_confirm_errors(self): + # Try to create confirmation for quantity less the zero + with self.assertRaises(exceptions.ValidationError): + self.env["sale.order.confirm.partial"].create( + { + "sale_order_id": self.sale_order.id, + "mode": "selected", + "line_ids": [ + ( + 0, + 0, + { + "so_line_id": self.so_line_1.id, + "confirmed_qty": -4, + }, + ), + ], + } + ) + # Try to create confirmation for quantity greater than original quantity + with self.assertRaises(exceptions.ValidationError): + self.env["sale.order.confirm.partial"].create( + { + "sale_order_id": self.sale_order.id, + "mode": "selected", + "line_ids": [ + ( + 0, + 0, + { + "so_line_id": self.so_line_1.id, + "confirmed_qty": 15, + }, + ), + ], + } + ) diff --git a/sale_order_confirm_partial/views/res_config_settings_views.xml b/sale_order_confirm_partial/views/res_config_settings_views.xml new file mode 100644 index 00000000000..4e2799c4c7d --- /dev/null +++ b/sale_order_confirm_partial/views/res_config_settings_views.xml @@ -0,0 +1,53 @@ + + + + + res.config.settings.view.form.inherit.partial.confirm + res.config.settings + + +
+
+
+ +
+
+
+
+
+
+
+ +
diff --git a/sale_order_confirm_partial/wizard/__init__.py b/sale_order_confirm_partial/wizard/__init__.py new file mode 100644 index 00000000000..171a5b3f519 --- /dev/null +++ b/sale_order_confirm_partial/wizard/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import sale_order_confirm_partial_line +from . import sale_order_confirm_partial diff --git a/sale_order_confirm_partial/wizard/sale_order_confirm_partial.py b/sale_order_confirm_partial/wizard/sale_order_confirm_partial.py new file mode 100644 index 00000000000..4a664b34b07 --- /dev/null +++ b/sale_order_confirm_partial/wizard/sale_order_confirm_partial.py @@ -0,0 +1,52 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import Command, api, fields, models + + +class SaleOrderConfirmPartial(models.TransientModel): + _name = "sale.order.confirm.partial" + _description = "Partial Quotation Confirmation" + + sale_order_id = fields.Many2one( + comodel_name="sale.order", + required=True, + ) + mode = fields.Selection( + selection=[ + ("all", "All Items"), + ("selected", "Selected Items"), + ], + string="Confirmation Mode", + default="all", + required=True, + ) + line_ids = fields.One2many( + comodel_name="sale.order.confirm.partial.line", + inverse_name="wizard_id", + string="Lines To Confirm", + ) + + @api.onchange("mode") + def _onchange_mode(self): + """ + If mode has been changed to 'all' then fill + line_ids with lines of related order. + """ + if self.mode == "all": + self.update( + { + "line_ids": [Command.clear()] + + [ + Command.create({"so_line_id": line_id}) + for line_id in self.sale_order_id.order_line.ids + ] + } + ) + + def action_confirm(self): + """ + Update quantities, confirm selected lines and + create unconfirmed quotation if needed. + """ + self.ensure_one() + self.sale_order_id.action_confirm_partial(self.line_ids) diff --git a/sale_order_confirm_partial/wizard/sale_order_confirm_partial_line.py b/sale_order_confirm_partial/wizard/sale_order_confirm_partial_line.py new file mode 100644 index 00000000000..91562a8d99f --- /dev/null +++ b/sale_order_confirm_partial/wizard/sale_order_confirm_partial_line.py @@ -0,0 +1,43 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import _, api, exceptions, fields, models + + +class SaleOrderConfirmPartialLine(models.TransientModel): + _name = "sale.order.confirm.partial.line" + _description = "Partial Quotation Confirmation Line" + + wizard_id = fields.Many2one( + comodel_name="sale.order.confirm.partial", + required=True, + ) + so_line_id = fields.Many2one( + comodel_name="sale.order.line", + string="Order Line", + domain="[('order_id', '=', parent.sale_order_id)]", + required=True, + ) + confirmed_qty = fields.Float( + string="Confirmed Quantity", + compute="_compute_confirmed_qty", + readonly=False, + required=True, + store=True, + ) + + @api.constrains("confirmed_qty", "so_line_id") + def _check_confirmed_qty(self): + for line in self: + if line.confirmed_qty < 0: + raise exceptions.ValidationError( + _("Confirmed quantity cannot be negative.") + ) + if line.confirmed_qty > line.so_line_id.product_uom_qty: + raise exceptions.ValidationError( + _("Confirmed quantity cannot be greater than ordered quantity.") + ) + + @api.depends("so_line_id") + def _compute_confirmed_qty(self): + for line in self: + line.confirmed_qty = line.so_line_id.product_uom_qty diff --git a/sale_order_confirm_partial/wizard/sale_order_confirm_partial_views.xml b/sale_order_confirm_partial/wizard/sale_order_confirm_partial_views.xml new file mode 100644 index 00000000000..224d93bcf8f --- /dev/null +++ b/sale_order_confirm_partial/wizard/sale_order_confirm_partial_views.xml @@ -0,0 +1,56 @@ + + + + + Partial Quotation Confirmation + sale.order.confirm.partial + form + new + + + + sale.order.confirm.partial.view.form + sale.order.confirm.partial + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ +
diff --git a/setup/sale_order_confirm_partial/odoo/addons/sale_order_confirm_partial b/setup/sale_order_confirm_partial/odoo/addons/sale_order_confirm_partial new file mode 120000 index 00000000000..92de55b272c --- /dev/null +++ b/setup/sale_order_confirm_partial/odoo/addons/sale_order_confirm_partial @@ -0,0 +1 @@ +../../../../sale_order_confirm_partial \ No newline at end of file diff --git a/setup/sale_order_confirm_partial/setup.py b/setup/sale_order_confirm_partial/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/sale_order_confirm_partial/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)