From 432cd6fe7072ca74d763c355f60359cc343d56f3 Mon Sep 17 00:00:00 2001 From: Pierrick Brun Date: Wed, 1 Aug 2018 11:46:37 +0200 Subject: [PATCH 01/29] [ADD] sale_delivery_state module --- sale_delivery_state/__init__.py | 3 +++ sale_delivery_state/__manifest__.py | 19 ++++++++++++++ sale_delivery_state/models/__init__.py | 4 +++ sale_delivery_state/models/sale_order.py | 19 ++++++++++++++ .../views/sale_order_views.xml | 26 +++++++++++++++++++ 5 files changed, 71 insertions(+) create mode 100644 sale_delivery_state/__init__.py create mode 100644 sale_delivery_state/__manifest__.py create mode 100644 sale_delivery_state/models/__init__.py create mode 100644 sale_delivery_state/models/sale_order.py create mode 100644 sale_delivery_state/views/sale_order_views.xml diff --git a/sale_delivery_state/__init__.py b/sale_delivery_state/__init__.py new file mode 100644 index 00000000000..cde864bae21 --- /dev/null +++ b/sale_delivery_state/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/sale_delivery_state/__manifest__.py b/sale_delivery_state/__manifest__.py new file mode 100644 index 00000000000..e7320c846d6 --- /dev/null +++ b/sale_delivery_state/__manifest__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# 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": "10.0.1.0.0", + "category": "Product", + "website": "www.akretion.com", + "author": " Akretion", + "license": "AGPL-3", + "depends": ["sale_stock"], + "data": [ + "views/sale_order_views.xml", + ], + "installable": True, +} diff --git a/sale_delivery_state/models/__init__.py b/sale_delivery_state/models/__init__.py new file mode 100644 index 00000000000..a6d09a9d541 --- /dev/null +++ b/sale_delivery_state/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import sale_order +from . import stock_picking diff --git a/sale_delivery_state/models/sale_order.py b/sale_delivery_state/models/sale_order.py new file mode 100644 index 00000000000..e747ef16d7d --- /dev/null +++ b/sale_delivery_state/models/sale_order.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Pierrick BRUN +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + delivery_state = fields.Selection([ + ('unprocessed', 'Unprocessed'), + ('partially', 'Partially processed'), + ('done', 'Done')], + string='Picking state', + copy=False, + default='unprocessed' + ) 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..7d6280ad371 --- /dev/null +++ b/sale_delivery_state/views/sale_order_views.xml @@ -0,0 +1,26 @@ + + + + + sale.order.form.sale.stock + sale.order + + + + + + + + + + sale.order.tree + + sale.order + + + + + + + + From 8df91fb833d51fe51050bd374bdabbae91f39918 Mon Sep 17 00:00:00 2001 From: Benoit Date: Sun, 30 Sep 2018 20:36:08 +0200 Subject: [PATCH 02/29] [IMP] refactor to use a computed field with a close behaviour as invoice_status --- sale_delivery_state/__manifest__.py | 5 ++- sale_delivery_state/demo/sale_demo.xml | 34 +++++++++++++++++++ sale_delivery_state/models/__init__.py | 1 - sale_delivery_state/models/sale_order.py | 23 ++++++++++--- sale_delivery_state/tests/__init__.py | 2 ++ .../tests/test_delivery_state.py | 31 +++++++++++++++++ .../views/sale_order_views.xml | 4 +-- 7 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 sale_delivery_state/demo/sale_demo.xml create mode 100644 sale_delivery_state/tests/__init__.py create mode 100644 sale_delivery_state/tests/test_delivery_state.py diff --git a/sale_delivery_state/__manifest__.py b/sale_delivery_state/__manifest__.py index e7320c846d6..be812a0b8ca 100644 --- a/sale_delivery_state/__manifest__.py +++ b/sale_delivery_state/__manifest__.py @@ -11,9 +11,12 @@ "website": "www.akretion.com", "author": " Akretion", "license": "AGPL-3", - "depends": ["sale_stock"], + "depends": ["sale"], "data": [ "views/sale_order_views.xml", ], + "demo": [ + "demo/sale_demo.xml", + ], "installable": True, } diff --git a/sale_delivery_state/demo/sale_demo.xml b/sale_delivery_state/demo/sale_demo.xml new file mode 100644 index 00000000000..ecca227e37b --- /dev/null +++ b/sale_delivery_state/demo/sale_demo.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + Laptop E5023 + + 3 + + 2950.00 + + + + + Pen drive, 16GB + + 5 + + 145.00 + + + + diff --git a/sale_delivery_state/models/__init__.py b/sale_delivery_state/models/__init__.py index a6d09a9d541..6064afee178 100644 --- a/sale_delivery_state/models/__init__.py +++ b/sale_delivery_state/models/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- from . import sale_order -from . import stock_picking diff --git a/sale_delivery_state/models/sale_order.py b/sale_delivery_state/models/sale_order.py index e747ef16d7d..faa15238275 100644 --- a/sale_delivery_state/models/sale_order.py +++ b/sale_delivery_state/models/sale_order.py @@ -3,17 +3,32 @@ # @author Pierrick BRUN # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models, fields +from odoo import models, fields, api +from odoo.tools import float_is_zero, float_compare class SaleOrder(models.Model): _inherit = 'sale.order' delivery_state = fields.Selection([ + ('no', 'No delivery'), ('unprocessed', 'Unprocessed'), ('partially', 'Partially processed'), ('done', 'Done')], - string='Picking state', - copy=False, - default='unprocessed' + string='Delivery state', + compute='compute_delivery_state', + store=True ) + + @api.depends('order_line', 'order_line.qty_delivered', 'state') + def compute_delivery_state(self): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + for order in self: + if order.state in ('draft', 'cancel'): + order.delivery_state = 'no' + elif all((float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) >= 0 for line in order.order_line)): + order.delivery_state = 'done' + elif any((not float_is_zero(line.qty_delivered, precision_digits=precision) for line in order.order_line)): + order.delivery_state = 'partially' + else: + order.delivery_state = 'unprocessed' diff --git a/sale_delivery_state/tests/__init__.py b/sale_delivery_state/tests/__init__.py new file mode 100644 index 00000000000..7675548063b --- /dev/null +++ b/sale_delivery_state/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +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..5c5244eea7e --- /dev/null +++ b/sale_delivery_state/tests/test_delivery_state.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Benoît GUILLOT +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestDeliveryState(TransactionCase): + + def setUp(self): + super(TestDeliveryState, self).setUp() + self.order = self.env.ref('sale_delivery_state.sale_order_1') + + def test_no_delivery(self): + self.assertEqual(self.order.delivery_state, 'no') + + def test_unprocessed_delivery(self): + self.order.action_confirm() + self.assertEqual(self.order.delivery_state, 'unprocessed') + + def test_partially(self): + self.order.action_confirm() + self.order.order_line[0].qty_delivered = 2 + self.assertEqual(self.order.delivery_state, 'partially') + + 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_state, 'done') diff --git a/sale_delivery_state/views/sale_order_views.xml b/sale_delivery_state/views/sale_order_views.xml index 7d6280ad371..b683f0ed2bc 100644 --- a/sale_delivery_state/views/sale_order_views.xml +++ b/sale_delivery_state/views/sale_order_views.xml @@ -7,7 +7,7 @@ - + @@ -18,7 +18,7 @@ sale.order - + From 4222067b97cca23d5c949c38ac6df69117a69a35 Mon Sep 17 00:00:00 2001 From: Yannick Vaucher Date: Wed, 12 Dec 2018 10:44:09 +0100 Subject: [PATCH 03/29] Filter delivery costs that are not considered as delivered Make it optional to work with module `delivery` without adding it as a dependency --- sale_delivery_state/models/sale_order.py | 76 +++++++++++++++++-- .../tests/test_delivery_state.py | 70 +++++++++++++++++ 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/sale_delivery_state/models/sale_order.py b/sale_delivery_state/models/sale_order.py index faa15238275..ffac2486579 100644 --- a/sale_delivery_state/models/sale_order.py +++ b/sale_delivery_state/models/sale_order.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # 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 models, fields, api @@ -16,19 +17,84 @@ class SaleOrder(models.Model): ('partially', 'Partially processed'), ('done', 'Done')], string='Delivery state', - compute='compute_delivery_state', + compute='_compute_delivery_state', store=True ) + 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 + Carrier = self.env.get('delivery.carrier') + if Carrier: + sale_lines = sale_lines.filtered( + lambda rec: not rec.is_delivery_cost()) + 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 + Carrier = self.env.get('delivery.carrier') + if Carrier: + sale_lines = sale_lines.filtered( + lambda rec: not rec.is_delivery_cost()) + 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 self.order_line + ) + @api.depends('order_line', 'order_line.qty_delivered', 'state') - def compute_delivery_state(self): - precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + def _compute_delivery_state(self): for order in self: if order.state in ('draft', 'cancel'): order.delivery_state = 'no' - elif all((float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) >= 0 for line in order.order_line)): + elif order._all_qty_delivered(): order.delivery_state = 'done' - elif any((not float_is_zero(line.qty_delivered, precision_digits=precision) for line in order.order_line)): + elif order._partially_delivered(): order.delivery_state = 'partially' else: order.delivery_state = 'unprocessed' + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + def is_delivery_cost(self): + """ + Returns if a sale line has a delivery products + check that a line is a delivery costs + + :returns boolean: + """ + self.ensure_one() + Carrier = self.env.get('delivery.carrier') + # If you call this without `delivery` module installed + # you are probably doing it wrong because it will always + # returns False + if not Carrier or not self.product_id: + return False + search_domain = [('product_id', '=', self.product_id.id)] + return bool(Carrier.search_count(search_domain)) diff --git a/sale_delivery_state/tests/test_delivery_state.py b/sale_delivery_state/tests/test_delivery_state.py index 5c5244eea7e..96fbe29da8a 100644 --- a/sale_delivery_state/tests/test_delivery_state.py +++ b/sale_delivery_state/tests/test_delivery_state.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- # 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 mock import Mock +from functools import partial from odoo.tests.common import TransactionCase @@ -29,3 +32,70 @@ def test_delivery_done(self): for line in self.order.order_line: line.qty_delivered = line.product_uom_qty self.assertEqual(self.order.delivery_state, 'done') + + def mock_delivery(self): + """ + Mock `delivery.carrier` model to make tests even if + `delivery` module is not installed + + Warning this messes with create, makes sure you + don't use create afterwards. + """ + carrier_mock = Mock() + + def search_count(cost_id, domain, *args, **kwargs): + """ + Mock search count for delivery.carrier + as we want to make the tests even if `delivery` + module is not installed + """ + if domain[0][2] == cost_id: + return 1 + return 0 + + cost_id = self.env.ref('product.service_delivery').id + carrier_mock._browse.return_value.search_count = partial( + search_count, cost_id) + self.env.registry['delivery.carrier'] = carrier_mock + + def add_delivery_cost_line(self): + # let's assume for this test that service_delivery is assigned + # to a carrier + delivery_cost = self.env.ref('product.service_delivery') + self.env['sale.order.line'].create({ + 'order_id': self.order.id, + 'name': 'Delivery cost', + 'product_id': delivery_cost.id, + 'product_uom_qty': 1, + 'product_uom': self.env.ref('product.product_uom_unit').id, + 'price_unit': 10.0, + }) + + def test_no_delivery_delivery_cost(self): + self.add_delivery_cost_line() + self.mock_delivery() + self.assertEqual(self.order.delivery_state, 'no') + + def test_unprocessed_delivery_delivery_cost(self): + self.add_delivery_cost_line() + self.mock_delivery() + self.order.action_confirm() + self.assertEqual(self.order.delivery_state, 'unprocessed') + + def test_partially_delivery_cost(self): + self.add_delivery_cost_line() + self.mock_delivery() + self.order.action_confirm() + self.order.order_line[0].qty_delivered = 2 + self.assertEqual(self.order.delivery_state, 'partially') + + def test_delivery_done_delivery_cost(self): + self.add_delivery_cost_line() + self.mock_delivery() + self.order.action_confirm() + delivery_cost = self.env.ref('product.service_delivery') + for line in self.order.order_line: + if line.product_id == delivery_cost: + continue + line.qty_delivered = line.product_uom_qty + self.assertEqual(self.order.delivery_state, 'done') From c43b88a03a05c3fdd39c260acf153575b72153bd Mon Sep 17 00:00:00 2001 From: Yannick Vaucher Date: Wed, 12 Dec 2018 11:33:58 +0100 Subject: [PATCH 04/29] Add a button to force the delivery state --- sale_delivery_state/models/sale_order.py | 18 ++++++++++++++++-- .../tests/test_delivery_state.py | 14 ++++++++++++++ sale_delivery_state/views/sale_order_views.xml | 8 ++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/sale_delivery_state/models/sale_order.py b/sale_delivery_state/models/sale_order.py index ffac2486579..573c4b10e9e 100644 --- a/sale_delivery_state/models/sale_order.py +++ b/sale_delivery_state/models/sale_order.py @@ -21,6 +21,12 @@ class SaleOrder(models.Model): store=True ) + force_delivery_state = fields.Boolean( + string='Force delivery state', + 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 @@ -66,18 +72,26 @@ def _partially_delivered(self): for line in self.order_line ) - @api.depends('order_line', 'order_line.qty_delivered', 'state') + @api.depends('order_line', 'order_line.qty_delivered', + 'state', 'force_delivery_state') def _compute_delivery_state(self): for order in self: if order.state in ('draft', 'cancel'): order.delivery_state = 'no' - elif order._all_qty_delivered(): + elif (order.force_delivery_state or + order._all_qty_delivered()): order.delivery_state = 'done' elif order._partially_delivered(): order.delivery_state = 'partially' else: order.delivery_state = 'unprocessed' + def action_force_delivery_state(self): + self.write({'force_delivery_state': True}) + + def action_unforce_delivery_state(self): + self.write({'force_delivery_state': False}) + class SaleOrderLine(models.Model): _inherit = 'sale.order.line' diff --git a/sale_delivery_state/tests/test_delivery_state.py b/sale_delivery_state/tests/test_delivery_state.py index 96fbe29da8a..c2f7ad5b222 100644 --- a/sale_delivery_state/tests/test_delivery_state.py +++ b/sale_delivery_state/tests/test_delivery_state.py @@ -27,6 +27,12 @@ def test_partially(self): self.order.order_line[0].qty_delivered = 2 self.assertEqual(self.order.delivery_state, 'partially') + def test_forced_delivery_cost(self): + self.order.action_confirm() + self.order.order_line[0].qty_delivered = 2 + self.order.force_delivery_state = True + self.assertEqual(self.order.delivery_state, 'done') + def test_delivery_done(self): self.order.action_confirm() for line in self.order.order_line: @@ -89,6 +95,14 @@ def test_partially_delivery_cost(self): self.order.order_line[0].qty_delivered = 2 self.assertEqual(self.order.delivery_state, 'partially') + def test_forced_delivery_cost(self): + self.add_delivery_cost_line() + 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_state, 'done') + def test_delivery_done_delivery_cost(self): self.add_delivery_cost_line() self.mock_delivery() diff --git a/sale_delivery_state/views/sale_order_views.xml b/sale_delivery_state/views/sale_order_views.xml index b683f0ed2bc..b969f5468b9 100644 --- a/sale_delivery_state/views/sale_order_views.xml +++ b/sale_delivery_state/views/sale_order_views.xml @@ -6,8 +6,16 @@ sale.order + + +