[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:
committed by
RomainLibert
parent
2ebf703420
commit
b34f26406f
@@ -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'],
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
@@ -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
|
||||
+6
-6
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user