From b34f26406f1ff955715d69708eb641bb22365b5c Mon Sep 17 00:00:00 2001 From: Jaykishan Solanki Date: Mon, 30 Apr 2018 16:38:58 +0200 Subject: [PATCH] [IMP] sale_order_dates, sale_stock: change dates computation In order to make the module more userfriendly: - Some labels have been changed/added - The Commitement date is now called the Expected date - The Requested date becomes the Commitement date - The Effective date is now related to the picking `date_done` Task #34343 Closes #18325 --- addons/sale_order_dates/__manifest__.py | 11 +-- addons/sale_order_dates/models/sale_order.py | 73 ++++++++--------- addons/sale_order_dates/tests/__init__.py | 3 +- ...uested_date.py => test_commitment_date.py} | 12 +-- .../tests/test_expected_date.py | 82 +++++++++++++++++++ .../views/sale_order_views.xml | 8 +- addons/sale_stock/models/sale_order.py | 4 +- .../views/res_config_settings_views.xml | 4 +- addons/stock/models/stock_picking.py | 2 +- 9 files changed, 136 insertions(+), 63 deletions(-) rename addons/sale_order_dates/tests/{test_requested_date.py => test_commitment_date.py} (56%) create mode 100644 addons/sale_order_dates/tests/test_expected_date.py diff --git a/addons/sale_order_dates/__manifest__.py b/addons/sale_order_dates/__manifest__.py index 2ee5c999d25..f2704dca85b 100644 --- a/addons/sale_order_dates/__manifest__.py +++ b/addons/sale_order_dates/__manifest__.py @@ -3,18 +3,13 @@ { - 'name': 'Dates on Sales Order', + 'name': 'Delivery Dates on Sales Order', 'version': '1.1', 'category': 'Sales', 'description': """ -Add additional date information to the sales order. +Manage delivery dates from sales orders. =================================================== - -You can add the following additional dates to a sales order: ------------------------------------------------------------- - * Requested Date (will be used as the expected date on pickings) - * Commitment Date - * Effective Date + This option introduces extra fields in the sales order to easily schedule product deliveries on your own: expected date, commitment date, effective date. """, 'website': 'https://www.odoo.com/page/crm', 'depends': ['sale_stock'], diff --git a/addons/sale_order_dates/models/sale_order.py b/addons/sale_order_dates/models/sale_order.py index 0a01683040d..b38b6a507c1 100644 --- a/addons/sale_order_dates/models/sale_order.py +++ b/addons/sale_order_dates/models/sale_order.py @@ -9,54 +9,47 @@ class SaleOrder(models.Model): """Add several date fields to Sales Orders, computed or user-entered""" _inherit = 'sale.order' - commitment_date = fields.Datetime(compute='_compute_commitment_date', string='Commitment Date', store=True, - help="Date by which the products are sure to be delivered. This is " - "a date that you can promise to the customer, based on the " - "Product Lead Times.") - requested_date = fields.Datetime('Requested Date', readonly=True, states={'draft': [('readonly', False)], - 'sent': [('readonly', False)]}, copy=False, - help="Date by which the customer has requested the items to be " - "delivered.\n" - "When this Order gets confirmed, the Delivery Order's " - "expected date will be computed based on this date and the " - "Company's Security Delay.\n" - "Leave this field empty if you want the Delivery Order to be " - "processed as soon as possible. In that case the expected " - "date will be computed using the default method: based on " - "the Product Lead Times and the Company's Security Delay.") - effective_date = fields.Date(compute='_compute_picking_ids', string='Effective Date', store=True, - help="Date on which the first Delivery Order was created.") + expected_date = fields.Datetime(compute='_compute_expected_date', + string='Expected Date', store=False, oldname='commitment_date', + help="Delivery date you can promise to the customer, computed from " + "product lead times and from the shipping policy of the order.") + commitment_date = fields.Datetime('Commitment Date', + states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, + copy=False, oldname='requested_date', readonly=True, + help="This is the delivery date promised to the customer. If set, the delivery order " + "will be scheduled based on this date rather than product lead times.") + effective_date = fields.Date(compute='_compute_effective_date', + string='Effective Date', store=True, + help="Completion date of the first delivery order.") - @api.depends('date_order', 'order_line.customer_lead') - def _compute_commitment_date(self): - """Compute the commitment date""" + @api.depends('order_line.customer_lead', 'confirmation_date', 'picking_policy', 'order_line.state') + def _compute_expected_date(self): + """Compute the expected date""" for order in self: dates_list = [] - order_datetime = fields.Datetime.from_string(order.date_order) + confirm_date = fields.Datetime.from_string(order.confirmation_date if order.state == 'sale' else fields.Datetime.now()) for line in order.order_line.filtered(lambda x: x.state != 'cancel'): - dt = order_datetime + timedelta(days=line.customer_lead or 0.0) + dt = confirm_date + timedelta(days=line.customer_lead or 0.0) dates_list.append(dt) if dates_list: - commit_date = min(dates_list) if order.picking_policy == 'direct' else max(dates_list) - order.commitment_date = fields.Datetime.to_string(commit_date) + expected_date = min(dates_list) if order.picking_policy == 'direct' else max(dates_list) + order.expected_date = fields.Datetime.to_string(expected_date) - def _compute_picking_ids(self): - super(SaleOrder, self)._compute_picking_ids() + @api.depends('picking_ids.date_done') + def _compute_effective_date(self): for order in self: - dates_list = [] - for pick in order.picking_ids: - dates_list.append(fields.Datetime.from_string(pick.date)) - if dates_list: - order.effective_date = fields.Datetime.to_string(min(dates_list)) + pickings = order.picking_ids.filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer') + dates_list = pickings.mapped('date_done') + order.effective_date = dates_list and min(dates_list) - @api.onchange('requested_date') - def onchange_requested_date(self): - """Warn if the requested dates is sooner than the commitment date""" - if (self.requested_date and self.commitment_date and self.requested_date < self.commitment_date): + @api.onchange('commitment_date') + def onchange_commitment_date(self): + """Warn if the commitment dates is sooner than the expected date""" + if (self.commitment_date and self.expected_date and self.commitment_date < self.expected_date): return {'warning': { - 'title': _('Requested date is too soon!'), - 'message': _("The date requested by the customer is " - "sooner than the commitment date. You may be " + 'title': _('Commitment date is too soon!'), + 'message': _("The date committed by the customer is " + "sooner than the expected date. You may be " "unable to honor the customer's request.") } } @@ -68,8 +61,8 @@ class SaleOrderLine(models.Model): @api.multi def _prepare_procurement_values(self, group_id): vals = super(SaleOrderLine, self)._prepare_procurement_values(group_id=group_id) - for line in self.filtered("order_id.requested_date"): - date_planned = fields.Datetime.from_string(line.order_id.requested_date) - timedelta(days=line.order_id.company_id.security_lead) + for line in self.filtered("order_id.commitment_date"): + date_planned = fields.Datetime.from_string(line.order_id.commitment_date) - timedelta(days=line.order_id.company_id.security_lead) vals.update({ 'date_planned': fields.Datetime.to_string(date_planned), }) diff --git a/addons/sale_order_dates/tests/__init__.py b/addons/sale_order_dates/tests/__init__.py index 9df19b529fd..49db8141e9d 100644 --- a/addons/sale_order_dates/tests/__init__.py +++ b/addons/sale_order_dates/tests/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import test_requested_date +from . import test_expected_date +from . import test_commitment_date \ No newline at end of file diff --git a/addons/sale_order_dates/tests/test_requested_date.py b/addons/sale_order_dates/tests/test_commitment_date.py similarity index 56% rename from addons/sale_order_dates/tests/test_requested_date.py rename to addons/sale_order_dates/tests/test_commitment_date.py index 7ed58f42f27..3b9494702ed 100644 --- a/addons/sale_order_dates/tests/test_requested_date.py +++ b/addons/sale_order_dates/tests/test_commitment_date.py @@ -7,16 +7,16 @@ from odoo import fields class TestSaleOrderDates(common.TransactionCase): - def test_sale_order_requested_date(self): + def test_sale_order_commitment_date(self): - # In order to test the Requested Date feature in Sales Orders in Odoo, - # I copy a demo Sales Order with Requested Date on 2010-07-12 - new_order = self.env.ref('sale.sale_order_6').copy({'requested_date': '2010-07-12'}) + # In order to test the Commitment Date feature in Sales Orders in Odoo, + # I copy a demo Sales Order with committed Date on 2010-07-12 + new_order = self.env.ref('sale.sale_order_6').copy({'commitment_date': '2010-07-12'}) # I confirm the Sales Order. new_order.action_confirm() # I verify that the Procurements and Stock Moves have been generated with the correct date security_delay = timedelta(days=new_order.company_id.security_lead) - requested_date = fields.Datetime.from_string(new_order.requested_date) - right_date = fields.Datetime.to_string(requested_date - security_delay) + commitment_date = fields.Datetime.from_string(new_order.commitment_date) + right_date = fields.Datetime.to_string(commitment_date - security_delay) for line in new_order.order_line: self.assertEqual(line.move_ids[0].date_expected, right_date, "The expected date for the Stock Move is wrong") diff --git a/addons/sale_order_dates/tests/test_expected_date.py b/addons/sale_order_dates/tests/test_expected_date.py new file mode 100644 index 00000000000..a565621cb4c --- /dev/null +++ b/addons/sale_order_dates/tests/test_expected_date.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from odoo import fields +from odoo.tests import common + + +class TestSaleExpectedDate(common.TransactionCase): + + def test_sale_order_expected_date(self): + """ Test expected date and effective date of Sales Orders """ + Product = self.env['product.product'] + + product_A = Product.create({ + 'name': 'Product A', + 'type': 'product', + 'sale_delay': 5, + 'uom_id': 1, + }) + product_B = Product.create({ + 'name': 'Product B', + 'type': 'product', + 'sale_delay': 10, + 'uom_id': 1, + }) + product_C = Product.create({ + 'name': 'Product C', + 'type': 'product', + 'sale_delay': 15, + 'uom_id': 1, + }) + + self.env['stock.quant']._update_available_quantity(product_A, self.env.ref('stock.stock_location_stock'), 10) + self.env['stock.quant']._update_available_quantity(product_B, self.env.ref('stock.stock_location_stock'), 10) + self.env['stock.quant']._update_available_quantity(product_C, self.env.ref('stock.stock_location_stock'), 10) + sale_order = self.env['sale.order'].create({ + 'partner_id': self.ref('base.res_partner_3'), + 'picking_policy': 'direct', + 'order_line': [ + (0, 0, {'name': product_A.name, 'product_id': product_A.id, 'customer_lead': product_A.sale_delay, 'product_uom_qty': 5}), + (0, 0, {'name': product_B.name, 'product_id': product_B.id, 'customer_lead': product_B.sale_delay, 'product_uom_qty': 5}), + (0, 0, {'name': product_C.name, 'product_id': product_C.id, 'customer_lead': product_C.sale_delay, 'product_uom_qty': 5}) + ], + }) + + # if Shipping Policy is set to `direct`(when SO is in draft state) then expected date should be + # current date + shortest lead time from all of it's order lines + expected_date = fields.Datetime.to_string(fields.Datetime.from_string(fields.Datetime.now()) + timedelta(days=5)) + self.assertEquals(expected_date, sale_order.expected_date, "Wrong expected date on sale order!") + + # if Shipping Policy is set to `one`(when SO is in draft state) then expected date should be + # current date + longest lead time from all of it's order lines + sale_order.write({'picking_policy': 'one'}) + expected_date = fields.Datetime.to_string(fields.Datetime.from_string(fields.Datetime.now()) + timedelta(days=15)) + self.assertEquals(expected_date, sale_order.expected_date, "Wrong expected date on sale order!") + + sale_order.action_confirm() + + # Setting confirmation date of SO to 5 days from today so that the expected/effective date could be checked + # against real confirmation date + confirm_date = fields.Datetime.from_string(fields.Datetime.now()) + timedelta(days=5) + sale_order.write({'confirmation_date': confirm_date}) + + # if Shipping Policy is set to `one`(when SO is confirmed) then expected date should be + # SO confirmation date + longest lead time from all of it's order lines + expected_date = fields.Datetime.to_string(confirm_date + timedelta(days=15)) + self.assertEquals(expected_date, sale_order.expected_date, "Wrong expected date on sale order!") + + # if Shipping Policy is set to `direct`(when SO is confirmed) then expected date should be + # SO confirmation date + shortest lead time from all of it's order lines + sale_order.write({'picking_policy': 'direct'}) + expected_date = fields.Datetime.to_string(confirm_date + timedelta(days=5)) + self.assertEquals(expected_date, sale_order.expected_date, "Wrong expected date on sale order!") + + # Check effective date, it should be date on which the first shipment successfully delivered to customer + picking = sale_order.picking_ids[0] + for ml in picking.move_line_ids: + ml.qty_done = ml.product_uom_qty + picking.action_done() + self.assertEquals(picking.state, 'done', "Picking not processed correctly!") + self.assertEquals(fields.Date.today(), sale_order.effective_date, "Wrong effective date on sale order!") diff --git a/addons/sale_order_dates/views/sale_order_views.xml b/addons/sale_order_dates/views/sale_order_views.xml index e2038b72a3b..352a962ba20 100644 --- a/addons/sale_order_dates/views/sale_order_views.xml +++ b/addons/sale_order_dates/views/sale_order_views.xml @@ -6,7 +6,7 @@ - + @@ -17,20 +17,20 @@ - + - + sale.order - + diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 21f75b5cf0c..4a8a85ee606 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -24,7 +24,9 @@ class SaleOrder(models.Model): ('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')], string='Shipping Policy', required=True, readonly=True, default='direct', - states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}) + states={'draft': [('readonly', False)], 'sent': [('readonly', False)]} + ,help="If you deliver all products at once, the delivery order will be scheduled based on the greatest " + "product lead time. Otherwise, it will be based on the shortest.") warehouse_id = fields.Many2one( 'stock.warehouse', string='Warehouse', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, diff --git a/addons/sale_stock/views/res_config_settings_views.xml b/addons/sale_stock/views/res_config_settings_views.xml index cf734b0dd66..532b26f4ecd 100644 --- a/addons/sale_stock/views/res_config_settings_views.xml +++ b/addons/sale_stock/views/res_config_settings_views.xml @@ -7,14 +7,14 @@ -
+
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index a928857ab9e..47422bb080d 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -633,7 +633,7 @@ class Picking(models.Model): # # Link existing moves or add moves when no one is related for ops in pick.move_line_ids.filtered(lambda x: not x.move_id): # Search move with this product - moves = pick.move_lines.filtered(lambda x: x.product_id == ops.product_id) + moves = pick.move_lines.filtered(lambda x: x.product_id == ops.product_id) if moves: #could search move that needs it the most (that has some quantities left) ops.move_id = moves[0].id else: