[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
This commit is contained in:
Jaykishan Solanki
2018-04-30 17:41:53 +02:00
committed by RomainLibert
parent 2ebf703420
commit b34f26406f
9 changed files with 136 additions and 63 deletions
+3 -8
View File
@@ -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'],
+33 -40
View File
@@ -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),
})
+2 -1
View File
@@ -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
@@ -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")
@@ -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!")
@@ -6,7 +6,7 @@
<field name="inherit_id" ref="sale_stock.view_order_form_inherit_sale_stock"/>
<field name="arch" type="xml">
<group name="sale_shipping" position="inside">
<field name="requested_date"/>
<field name="expected_date"/>
<field name="commitment_date"/>
<field name="effective_date"/>
</group>
@@ -17,20 +17,20 @@
<field name="inherit_id" ref="sale.view_order_tree"/>
<field name="arch" type="xml">
<field name="confirmation_date" position="after">
<field name="requested_date"/>
<field name="commitment_date"/>
<field name="expected_date"/>
</field>
</field>
</record>
<!-- Add requested_date and commitment_date on quotation view -->
<!-- Add commitment_date and expected_date on quotation view -->
<record id="view_quotation_tree_date" model="ir.ui.view">
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_quotation_tree"/>
<field name="arch" type="xml">
<field name="date_order" position="after">
<field name="requested_date"/>
<field name="commitment_date"/>
<field name="expected_date"/>
</field>
</field>
</record>
+3 -1
View File
@@ -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)]},
@@ -7,14 +7,14 @@
<field name="inherit_id" ref="sale.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='delivery']" position="after">
<div class="col-xs-12 col-md-6 o_setting_box" title="By default Odoo computes the expected delivery date this way: order date + highest customer lead time of products. With this option, you can set a specific delivery date on orders rather than setting lead times on products.">
<div class="col-xs-12 col-md-6 o_setting_box" title="This option introduces extra fields in the sales order to easily schedule product deliveries on your own: expected date, commitment date, effective date.">
<div class="o_setting_left_pane">
<field name="module_sale_order_dates"/>
</div>
<div class="o_setting_right_pane">
<label for="module_sale_order_dates"/>
<div class="text-muted">
Set a delivery date on orders
Manage delivery dates from sales orders
</div>
</div>
</div>
+1 -1
View File
@@ -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: