diff --git a/sale_automatic_workflow/tests/test_automatic_workflow.py b/sale_automatic_workflow/tests/test_automatic_workflow.py index 3f8a7e48912..8a729c92bc0 100644 --- a/sale_automatic_workflow/tests/test_automatic_workflow.py +++ b/sale_automatic_workflow/tests/test_automatic_workflow.py @@ -91,8 +91,10 @@ def test_create_invoice_from_sale_order(self): mocked.assert_called() self.assertEqual(line.qty_delivered, 1.0) sale.action_confirm() - self.assertTrue(sale.delivery_status, "pending") - self.assertFalse(sale.all_qty_delivered) + # Force the state to "full" + # note : this is not needed if you have the module sale_delivery_state + # installed but sale_automatic_workflow do not depend on it + # so we just force it so we can check the sale.all_qty_delivered sale.delivery_status = "full" sale._compute_all_qty_delivered() self.assertTrue(sale.all_qty_delivered) diff --git a/sale_delivery_state/README.rst b/sale_delivery_state/README.rst new file mode 100644 index 00000000000..1fbbe4b0c42 --- /dev/null +++ b/sale_delivery_state/README.rst @@ -0,0 +1,99 @@ +=================== +Sale delivery State +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5e6de03092efc72436a466046560f3139702a1be2ce615fe519211325e7dd488 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/16.0/sale_delivery_state + :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_delivery_state + :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 odoo module add delivery state on the sale order. + +Delivery state is computed based on `qty_delivered` field on sale order lines. + +This is usefull for other modules to provide the state of delivery. +The state of the sale order can be forced to fully delivered in case +some quantities were cancelled by the customer and you consider you have +nothing more to deliver. + +Sale order lines can have products or services, as long as the field `qty_delivered` +is set, it will trigger the computation of delivery state. + +This module also works with delivery.carrier fees that are added as a +sale order line. Thoses line are special as they will never be considered delivered. +Delivery fees lines are ignored in the computation of the delivery state. + +When the 'sale_stock' module is installed, the glue module 'sale_stock_delivery_state' +should also be installed; this module is designed to override the compute method +of the delivery status field from 'sale_stock'. + +**Table of contents** + +.. contents:: + :local: + +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 +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Pierrick BRUN +* Benoît Guillot +* Yannick Vaucher +* Daniel Reis , + `Open Source Integrators `_ +* Carlos Lopez + +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_delivery_state/__init__.py b/sale_delivery_state/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_delivery_state/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_delivery_state/__manifest__.py b/sale_delivery_state/__manifest__.py new file mode 100644 index 00000000000..b76ad9cce54 --- /dev/null +++ b/sale_delivery_state/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Pierrick BRUN +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Sale delivery State", + "summary": "Show the delivery state on the sale order", + "version": "16.0.1.0.0", + "category": "Product", + "website": "https://github.com/OCA/sale-workflow", + "author": "Akretion, Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": ["sale"], + "data": [ + "views/sale_order_views.xml", + ], + "demo": [ + "demo/sale_demo.xml", + ], +} diff --git a/sale_delivery_state/demo/sale_demo.xml b/sale_delivery_state/demo/sale_demo.xml new file mode 100644 index 00000000000..46f2eddadc5 --- /dev/null +++ b/sale_delivery_state/demo/sale_demo.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + Laptop E5023 + + 3 + + 2950.00 + + + + + Pen drive, 16GB + + 5 + + 145.00 + + + diff --git a/sale_delivery_state/i18n/sale_delivery_state.pot b/sale_delivery_state/i18n/sale_delivery_state.pot new file mode 100644 index 00000000000..bb879167183 --- /dev/null +++ b/sale_delivery_state/i18n/sale_delivery_state.pot @@ -0,0 +1,66 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_delivery_state +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_delivery_state +#: model:ir.model.fields,help:sale_delivery_state.field_sale_order__force_delivery_state +msgid "" +"Allow to enforce done state of delivery, for instance if some quantities " +"were cancelled" +msgstr "" + +#. module: sale_delivery_state +#: model:ir.model.fields,field_description:sale_delivery_state.field_sale_order__delivery_state +msgid "Delivery State" +msgstr "" + +#. module: sale_delivery_state +#: model:ir.model.fields.selection,name:sale_delivery_state.selection__sale_order__delivery_state__done +msgid "Done" +msgstr "" + +#. module: sale_delivery_state +#: model:ir.model.fields,field_description:sale_delivery_state.field_sale_order__force_delivery_state +msgid "Force Delivery State" +msgstr "" + +#. module: sale_delivery_state +#: model_terms:ir.ui.view,arch_db:sale_delivery_state.view_order_form_inherit_delivery_state +msgid "Force delivery done" +msgstr "" + +#. module: sale_delivery_state +#: model:ir.model.fields.selection,name:sale_delivery_state.selection__sale_order__delivery_state__no +msgid "No delivery" +msgstr "" + +#. module: sale_delivery_state +#: model:ir.model.fields.selection,name:sale_delivery_state.selection__sale_order__delivery_state__partially +msgid "Partially processed" +msgstr "" + +#. module: sale_delivery_state +#: model:ir.model,name:sale_delivery_state.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_delivery_state +#: model_terms:ir.ui.view,arch_db:sale_delivery_state.view_order_form_inherit_delivery_state +msgid "Unforce delivery done" +msgstr "" + +#. module: sale_delivery_state +#: model:ir.model.fields.selection,name:sale_delivery_state.selection__sale_order__delivery_state__unprocessed +msgid "Unprocessed" +msgstr "" diff --git a/sale_delivery_state/models/__init__.py b/sale_delivery_state/models/__init__.py new file mode 100644 index 00000000000..6aacb753131 --- /dev/null +++ b/sale_delivery_state/models/__init__.py @@ -0,0 +1 @@ +from . import sale_order diff --git a/sale_delivery_state/models/sale_order.py b/sale_delivery_state/models/sale_order.py new file mode 100644 index 00000000000..c4149002338 --- /dev/null +++ b/sale_delivery_state/models/sale_order.py @@ -0,0 +1,87 @@ +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Pierrick BRUN +# Copyright 2018 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.tools import float_compare, float_is_zero + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + delivery_status = fields.Selection( + [ + ("pending", "Not Delivered"), + ("partial", "Partially Delivered"), + ("full", "Fully Delivered"), + ], + # Compute method have a different name then the field because + # the method _compute_delivery_status already exist in odoo sale_stock + compute="_compute_oca_delivery_status", + store=True, + ) + + force_delivery_state = fields.Boolean( + help=( + "Allow to enforce done state of delivery, for instance if some" + " quantities were cancelled" + ), + ) + + def _all_qty_delivered(self): + """ + Returns True if all line have qty_delivered >= to ordered quantities + + If `delivery` module is installed, ignores the lines with delivery costs + + :returns: boolean + """ + self.ensure_one() + # Skip delivery costs lines + sale_lines = self.order_line.filtered(lambda rec: not rec._is_delivery()) + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + return all( + float_compare( + line.qty_delivered, line.product_uom_qty, precision_digits=precision + ) + >= 0 + for line in sale_lines + ) + + def _partially_delivered(self): + """ + Returns True if at least one line is delivered + + :returns: boolean + """ + self.ensure_one() + # Skip delivery costs lines + sale_lines = self.order_line.filtered(lambda rec: not rec._is_delivery()) + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + return any( + not float_is_zero(line.qty_delivered, precision_digits=precision) + for line in sale_lines + ) + + @api.depends("order_line.qty_delivered", "state", "force_delivery_state") + def _compute_oca_delivery_status(self): + for order in self: + if order.state in ("draft", "cancel"): + order.delivery_status = None + elif order.force_delivery_state or order._all_qty_delivered(): + order.delivery_status = "full" + elif order._partially_delivered(): + order.delivery_status = "partial" + else: + order.delivery_status = "pending" + + def action_force_delivery_state(self): + self.write({"force_delivery_state": True}) + + def action_unforce_delivery_state(self): + self.write({"force_delivery_state": False}) diff --git a/sale_delivery_state/readme/CONTRIBUTORS.rst b/sale_delivery_state/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..1415fe3a026 --- /dev/null +++ b/sale_delivery_state/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* Pierrick BRUN +* Benoît Guillot +* Yannick Vaucher +* Daniel Reis , + `Open Source Integrators `_ +* Carlos Lopez diff --git a/sale_delivery_state/readme/DESCRIPTION.rst b/sale_delivery_state/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..e0a7e3cd514 --- /dev/null +++ b/sale_delivery_state/readme/DESCRIPTION.rst @@ -0,0 +1,19 @@ +This odoo module add delivery state on the sale order. + +Delivery state is computed based on `qty_delivered` field on sale order lines. + +This is usefull for other modules to provide the state of delivery. +The state of the sale order can be forced to fully delivered in case +some quantities were cancelled by the customer and you consider you have +nothing more to deliver. + +Sale order lines can have products or services, as long as the field `qty_delivered` +is set, it will trigger the computation of delivery state. + +This module also works with delivery.carrier fees that are added as a +sale order line. Thoses line are special as they will never be considered delivered. +Delivery fees lines are ignored in the computation of the delivery state. + +When the 'sale_stock' module is installed, the glue module 'sale_stock_delivery_state' +should also be installed; this module is designed to override the compute method +of the delivery status field from 'sale_stock'. diff --git a/sale_delivery_state/static/description/icon.png b/sale_delivery_state/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/sale_delivery_state/static/description/icon.png differ diff --git a/sale_delivery_state/static/description/index.html b/sale_delivery_state/static/description/index.html new file mode 100644 index 00000000000..5d6155b4dc8 --- /dev/null +++ b/sale_delivery_state/static/description/index.html @@ -0,0 +1,439 @@ + + + + + + +Sale delivery State + + + +
+

Sale delivery State

+ + +

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

+

This odoo module add delivery state on the sale order.

+

Delivery state is computed based on qty_delivered field on sale order lines.

+

This is usefull for other modules to provide the state of delivery. +The state of the sale order can be forced to fully delivered in case +some quantities were cancelled by the customer and you consider you have +nothing more to deliver.

+

Sale order lines can have products or services, as long as the field qty_delivered +is set, it will trigger the computation of delivery state.

+

This module also works with delivery.carrier fees that are added as a +sale order line. Thoses line are special as they will never be considered delivered. +Delivery fees lines are ignored in the computation of the delivery state.

+

When the ‘sale_stock’ module is installed, the glue module ‘sale_stock_delivery_state’ +should also be installed; this module is designed to override the compute method +of the delivery status field from ‘sale_stock’.

+

Table of contents

+ +
+

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

+
    +
  • Akretion
  • +
+
+ +
+

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_delivery_state/tests/__init__.py b/sale_delivery_state/tests/__init__.py new file mode 100644 index 00000000000..0a44b89d8dd --- /dev/null +++ b/sale_delivery_state/tests/__init__.py @@ -0,0 +1 @@ +from . import test_delivery_state diff --git a/sale_delivery_state/tests/test_delivery_state.py b/sale_delivery_state/tests/test_delivery_state.py new file mode 100644 index 00000000000..25e813145be --- /dev/null +++ b/sale_delivery_state/tests/test_delivery_state.py @@ -0,0 +1,93 @@ +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Benoît GUILLOT +# Copyright 2018 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from unittest import mock + +from odoo.tests import TransactionCase + + +class TestDeliveryState(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.order = cls.env.ref("sale_delivery_state.sale_order_1") + cls.delivery_cost = cls.env["product.product"].create( + {"name": "delivery", "type": "service"} + ) + + def _mock_delivery(self, delivery_prod=None): + delivery_prod = delivery_prod or self.delivery_cost + return mock.patch.object( + type(self.env["sale.order.line"]), + "_is_delivery", + lambda self: self.product_id == delivery_prod, + ) + + def _add_delivery_cost_line(self): + self.env["sale.order.line"].create( + { + "order_id": self.order.id, + "name": "Delivery cost", + "product_id": self.delivery_cost.id, + "product_uom_qty": 1, + "product_uom": self.env.ref("uom.product_uom_unit").id, + "price_unit": 10.0, + } + ) + + def test_no_delivery(self): + self.assertFalse(self.order.delivery_status) + + def test_unprocessed_delivery(self): + self.order.action_confirm() + self.assertEqual(self.order.delivery_status, "pending") + + def test_partially(self): + self.order.action_confirm() + self.order.order_line[0].qty_delivered = 2 + self.assertEqual(self.order.delivery_status, "partial") + + def test_delivery_done(self): + self.order.action_confirm() + for line in self.order.order_line: + line.qty_delivered = line.product_uom_qty + self.assertEqual(self.order.delivery_status, "full") + + def test_no_delivery_delivery_cost(self): + self._add_delivery_cost_line() + with self._mock_delivery(): + self.assertFalse(self.order.delivery_status) + + def test_unprocessed_delivery_delivery_cost(self): + self._add_delivery_cost_line() + with self._mock_delivery(): + self.order.action_confirm() + self.assertEqual(self.order.delivery_status, "pending") + + def test_partially_delivery_cost(self): + self._add_delivery_cost_line() + with self._mock_delivery(): + self.order.action_confirm() + self.order.order_line[0].qty_delivered = 2 + self.assertEqual(self.order.delivery_status, "partial") + + def test_forced_delivery_cost(self): + self._add_delivery_cost_line() + with self._mock_delivery(): + self.order.action_confirm() + self.order.order_line[0].qty_delivered = 2 + self.order.force_delivery_state = True + self.assertEqual(self.order.delivery_status, "full") + + def test_delivery_done_delivery_cost(self): + self._add_delivery_cost_line() + with self._mock_delivery(): + self.order.action_confirm() + for line in self.order.order_line: + if line._is_delivery(): + continue + line.qty_delivered = line.product_uom_qty + self.assertEqual(self.order.delivery_status, "full") diff --git a/sale_delivery_state/views/sale_order_views.xml b/sale_delivery_state/views/sale_order_views.xml new file mode 100644 index 00000000000..205e46ad921 --- /dev/null +++ b/sale_delivery_state/views/sale_order_views.xml @@ -0,0 +1,53 @@ + + + + sale.order.form.sale.stock + sale.order + + + + +