Skip to content

Conversation

@abyo-odoo
Copy link
Contributor

@abyo-odoo abyo-odoo commented Jul 31, 2025

To avoid confusion after confirming an RFQ:

  • New field has been added 'Promised Date' in Purchase Order and Purchase Order Line.
  • 'Promised Date' updates the deadline on a receipt.
  • 'Promised Date' only shows after confirming an RFQ, taking the place of 'Confirmation Date'.
  • Confirmation Date was moved to 'Other information' section.
  • Updating "Expected Arrival" on PO or PO line updates the date scheduled on a receipt and no longer updates the deadline.
  • Updated "Expected Arrival" help message to adapt to the change.
  • Dates in Receipts and POs are linked, changing POL expected arrival should propagate
    to stock move scheduled date (and the other way around), and promised date propagates to deadlines.

In Sales Order:

  • 'Delivery Date' now uses a dynamic placeholder giving it a cleaner look.
  • On Confirmation, if no date is set it will be assigned to the placeholder's value and 'Delivery Date' label is changed to 'Promised Delivery'.
  • Some UI Changes.

Task: 4687135


I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr

@abyo-odoo abyo-odoo self-assigned this Jul 31, 2025
@robodoo
Copy link
Contributor

robodoo commented Jul 31, 2025

Pull request status dashboard

@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 11 times, most recently from 2383031 to 1fcd43d Compare August 7, 2025 12:04
@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 3 times, most recently from 4d9eccc to 32ddbff Compare August 7, 2025 14:28
@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch from 32ddbff to 5d33dd3 Compare August 25, 2025 13:26
@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 4 times, most recently from b62ad05 to a50776b Compare August 28, 2025 12:24
@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 2 times, most recently from 287d4a2 to 7b392a3 Compare August 29, 2025 09:02
@clesgow clesgow marked this pull request as ready for review August 29, 2025 15:03
@C3POdoo C3POdoo requested review from a team and clesgow and removed request for a team August 29, 2025 15:07
@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 2 times, most recently from 5e9690c to bb6768e Compare September 24, 2025 08:34
Copy link
Contributor

@clesgow clesgow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not done, but here's a first part

@@ -33,6 +33,8 @@ def _ondelete_stock_moves(self):
forecasted_issue = fields.Boolean(compute='_compute_forecasted_issue')
is_storable = fields.Boolean(related='product_id.is_storable')
location_final_id = fields.Many2one('stock.location', 'Location from procurement')
date_promised = fields.Datetime('Promised Date', readonly=False, store=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
date_promised = fields.Datetime('Promised Date', readonly=False, store=True,
date_promised = fields.Datetime('Promised Date',

Non-computed fields are by default readonly=False and store=True

new_date = fields.Datetime.to_datetime(values['date_promised'])
for move in self.move_ids:
if move.picking_id.picking_type_code == 'incoming':
self.filtered(lambda line: not line.display_type)._update_move_dates(date_promised=new_date)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_move_dates already iterates on specific moves to update. I don't think you need to repeat that for every move_ids linked to the PO line.

Copy link
Contributor Author

@abyo-odoo abyo-odoo Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I changed this because it caused a traceback randomly while I was testing, There was a use case where move was not just one, If I remember correctly 🤞.
I will double check.

Edit: I will just remove the check of picking type and it will get checked in the method anyways


def write(self, vals):
res = super().write(vals)
if vals.get('date') and vals.get('state') != "done":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the date gets updated but the state isn't changed (thus not in the vals)?

Copy link
Contributor Author

@abyo-odoo abyo-odoo Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None != "done". Therefore, the condition will evaluate to True and date planned will be set accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just don't want the purchase orders dates to be updated when the receipt is validated, to avoid this bug and keep the original dates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None != "done". Therefore, the condition will evaluate to True and date planned will be set accordingly.

But what if the state is already done at that point? 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it goes into _set_date_planned and checks for the state and if its done or cancelled nothing will happen

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed.
However, you're already past super().write(vals) at that point, which means self contains the updated records. I think it's fine to check if date was in the vals, but imho you can just call set_date_planned(), and not bother to check the change in state here.

Comment on lines 134 to 136
if vals.get('date_planned'):
pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'] and p.picking_type_code == "incoming")
pickings.scheduled_date = vals['date_planned']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary?
When you update the date_planned of the PO through the form:

  • It will update the date_planned of the PO lines
  • Which will update the date of the related moves
  • Which will trigger the recompute of _compute_scheduled_date as one of its depends changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch 👌 Its not necessary 🙂

@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 4 times, most recently from b3e2b96 to e39d525 Compare October 2, 2025 12:32
Copy link
Contributor

@clesgow clesgow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting much closer! 👍
Most of the comments are formatting stuff

help="* Red: Late\n"
"* Grey: Pending\n"
"* Blue: Partially Received\n"
"* Green: On time")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"* Green: On time")
"* Green: Fully Received")
Image Like this PO, which was late but is nonetheless green once fully received.

Comment on lines 97 to 99
lines.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
if lines.order_id.state == 'purchase':
lines._set_date_promised()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lines.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
if lines.order_id.state == 'purchase':
lines._set_date_promised()
confirmed_lines = lines.filtered(lambda l: l.order_id.state == 'purchase')
confirmed_lines._create_or_update_picking()
confirmed_lines._set_date_promised()

lines could refer to lines from different POs (nothing forbids it). In that case, you'd have a singleton error in your condition. We can simply re-use the filtered already done above.

Copy link
Contributor Author

@abyo-odoo abyo-odoo Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to check

@@ -17,6 +17,21 @@ class StockMove(models.Model):
'purchase.order.line', 'stock_move_created_purchase_line_rel',
'move_id', 'created_purchase_line_id', 'Created Purchase Order Lines', copy=False)

def _set_date_planned(self, date_planned):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two methods should be defined lower, according to our coding guidelines.

And yup, the file is already a mess, but let's not add to it, shall we? 😅


def write(self, vals):
res = super().write(vals)
if vals.get('date') and vals.get('state') != "done":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed.
However, you're already past super().write(vals) at that point, which means self contains the updated records. I think it's fine to check if date was in the vals, but imho you can just call set_date_planned(), and not bother to check the change in state here.

Comment on lines 23 to 27
move = move.with_context(date_planned_set_ids=already_set_ids)
if move.picking_id.state in ('done', 'cancel') or move.purchase_line_id.id in already_set_ids or move.picking_id.picking_type_code != 'incoming':
continue
already_set_ids.update(move.purchase_line_id.ids)
move.purchase_line_id.date_planned = date_planned
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
move = move.with_context(date_planned_set_ids=already_set_ids)
if move.picking_id.state in ('done', 'cancel') or move.purchase_line_id.id in already_set_ids or move.picking_id.picking_type_code != 'incoming':
continue
already_set_ids.update(move.purchase_line_id.ids)
move.purchase_line_id.date_planned = date_planned
if move.picking_id.state in ('done', 'cancel') or move.purchase_line_id.id in already_set_ids or move.picking_id.picking_type_code != 'incoming':
continue
already_set_ids.update(move.purchase_line_id.ids)
move.with_context(date_planned_set_ids=already_set_ids)
.purchase_line_id.date_planned = date_planned

Nitpick

@@ -210,6 +210,12 @@ def _compute_json_popover(self):
})
order.show_json_popover = bool(late_stock_picking)

def action_confirm(self):
for order in self:
if order.expected_date and not order.commitment_date:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As both fields are defined in sale, I don't think this should be done in sale_stock 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected_date is redefined in sale_stock with a different help message

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which doesn't change its purpose 🙂

@@ -592,7 +592,7 @@ def _default_picking_type_id(self):
PROCUREMENT_PRIORITIES, string='Priority', default='0',
help="Products will be reserved first for the transfers with the highest priorities.")
scheduled_date = fields.Datetime(
'Scheduled Date', compute='_compute_scheduled_date', inverse='_set_scheduled_date', store=True,
'Scheduled Date', compute='_compute_scheduled_date', inverse="_set_scheduled_date", store=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'Scheduled Date', compute='_compute_scheduled_date', inverse="_set_scheduled_date", store=True,
'Scheduled Date', compute='_compute_scheduled_date', inverse='_set_scheduled_date', store=True,

Nope 👀

self.assertEqual(incoming_shipment1.date_deadline, old_deadline1 + timedelta(days=1), 'Deadline should be propagate')
self.assertEqual(incoming_shipment2.date_deadline, old_deadline2, 'Deadline should remain unchanged')
self.assertEqual(incoming_shipment1.date_deadline, old_deadline1, 'Deadline should remain unchanged')
self.assertEqual(purchase.picking_ids.scheduled_date, purchase.date_planned, 'Scheduled Date should propagate')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(purchase.picking_ids.scheduled_date, purchase.date_planned, 'Scheduled Date should propagate')
self.assertEqual(purchase.order_line.move_ids.scheduled_date, purchase.date_planned, 'Scheduled Date should propagate')

I wouldn't rely on this, as it's kind of a side-effect that picking_ids doesn't contain the two other steps. I'd rather target the IN picking directly to avoid issues in the future.

Copy link
Contributor Author

@abyo-odoo abyo-odoo Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't that possibly cause a singleton traceback? given that move_ids is one2many

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would only throw the traceback if there are more than 1 move_id records, and if there are in this test, then you should test the date of each one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would only throw the traceback if there are more than 1 move_id records, and if there are in this test, then you should test the date of each one.

In this test, no its only one move, I'll just add it then but it will be purchase.order_line.move_ids.picking_id.scheduled_date

Comment on lines +283 to +285
self.assertEqual(incoming_shipment2.date_deadline, old_deadline2, 'Deadline should remain unchanged')
self.assertEqual(incoming_shipment1.date_deadline, old_deadline1, 'Deadline should remain unchanged')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems weird to me that we don't have any kind of date propagation now 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

date_planned only updates scheduled dates, deadlines are only updated by date_promised

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but IIRC when I tested, the scheduled dates didn't move either.
And maybe you should now test that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what do you mean by the dates weren't propagated, the next assert right after these 2 checks if date_planned was propagated or not

@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 2 times, most recently from 521da4b to 3a1a6d1 Compare October 6, 2025 09:04
@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch from 3a1a6d1 to 2bdc050 Compare October 15, 2025 11:32
Copy link
Contributor

@ticodoo ticodoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of small nitpicks, will look more/test tomorrow

@@ -89,14 +94,18 @@ def _compute_forecasted_issue(self):
@api.model_create_multi
def create(self, vals_list):
lines = super().create(vals_list)
lines.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
confirmed_lines = lines.filtered(lambda l: l.order_id.state == 'purchase')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this should also filter out display_type != False lines?

def _set_date_planned(self, date_planned):
already_set_ids = self.env.context.get('date_planned_set_ids', set())
for move in self:
if move.picking_id.state in ('done', 'cancel') or move.purchase_line_id.id in already_set_ids or move.picking_id.picking_type_code != 'incoming':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small optimization, take advantage of short-circuiting:

Suggested change
if move.picking_id.state in ('done', 'cancel') or move.purchase_line_id.id in already_set_ids or move.picking_id.picking_type_code != 'incoming':
if move.picking_id.picking_type_code != 'incoming' or move.picking_id.state in ('done', 'cancel') move.purchase_line_id.id in already_set_ids or :

this way the other conditions aren't checked for any moves for incoming pickings

@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 2 times, most recently from 53ccb51 to 4f1d9c5 Compare October 23, 2025 09:44
Copy link
Contributor

@ticodoo ticodoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also update your commit title too to be:

[IMP] purchase, sale, stock: update dates logic/UX

Comment on lines 84 to 86
for order in self:
dates_list = order.order_line.filtered(lambda line: not line.display_type and line.date_promised).mapped('date_promised')
if dates_list and order.state in ('purchase', 'cancel'):
order.date_promised = min(dates_list)
else:
order.date_promised = False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly cleaner/more efficient

Suggested change
for order in self:
dates_list = order.order_line.filtered(lambda line: not line.display_type and line.date_promised).mapped('date_promised')
if dates_list and order.state in ('purchase', 'cancel'):
order.date_promised = min(dates_list)
else:
order.date_promised = False
for order in self:
if order.state not in ('purchase', 'cancel'):
order.date_promised = False
continue
dates_list = order.order_line.filtered(lambda line: not line.display_type and line.date_promised).mapped('date_promised')
order.date_promised = min(dates_list) if dates_list else False

But I also don't understand why you're using the min date rather than the max date, since we would expect the vendor would promise the delivery date for ALL goods, not for the first good to be delivered. Also couldn't see where this is specified in the spec

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't specified in the spec if it should be any different than date_planned, for me I mentioned before that even expected arrival (date_planned) should show the maximum date and arm told me they did this because of some tickets and if a customer needs it to be different they can customize it? but for me It makes sense to have max for both of the dates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description of date_promised should probably be updated to reflect that then because right now it's written in a way where it looks like the date everything should be delivered by. Maybe something like:
Date promised by the vendor for at least 1 or more products to be delivered by

self.assertEqual(incoming_shipment1.date_deadline, old_deadline1 + timedelta(days=1), 'Deadline should be propagate')
self.assertEqual(incoming_shipment2.date_deadline, old_deadline2, 'Deadline should remain unchanged')
self.assertEqual(incoming_shipment1.date_deadline, old_deadline1, 'Deadline should remain unchanged')
self.assertEqual(purchase.picking_ids.scheduled_date, purchase.date_planned, 'Scheduled Date should propagate')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would only throw the traceback if there are more than 1 move_id records, and if there are in this test, then you should test the date of each one.

Green: On time")
], string='Receipt Status', compute='_compute_receipt_status', store=True)
date_promised = fields.Datetime('Promised Date', index=True, copy=False, compute="_compute_date_promised", store=True, readonly=False,
help="Delivery Date promised by the vendor. If the vendor delivers products after this date, their On-Time rate will be negatively impacted.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is inaccurate since the On-Time rate is calculated per POL date_promise. It should be more specific, but what the wording should be depends on my comment in the _compute_date_promised method

@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 5 times, most recently from d4f9d82 to a9fe242 Compare October 24, 2025 15:35
@abyo-odoo abyo-odoo changed the title [IMP] purchase{_stock}, stock: avoid confusion on purchase order [IMP] purchase, sale, stock: update dates logic/UX Oct 27, 2025
Copy link
Contributor

@ticodoo ticodoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also the "Sender Reminder" (email) action that's selectable in POs that should be checked.

image

There also used to be a feature where vendors could update the date_planned that looks like was lost during some portal refactorings, but the code is still there:

https://github.com/odoo/odoo/blob/master/addons/purchase/static/src/interactions/purchase_datetimepicker.js

def portal_my_purchase_order_update_dates(self, order_id=None, access_token=None, **kw):
"""User update scheduled date on purchase order line.
"""
try:
order_sudo = self._document_check_access('purchase.order', order_id, access_token=access_token)
except (AccessError, MissingError):
return request.redirect('/my')
updated_dates = []
for id_str, date_str in kw.items():
try:
line_id = int(id_str)
except ValueError:
return request.redirect(order_sudo.get_portal_url())
line = order_sudo.order_line.filtered(lambda l: l.id == line_id)
if not line:
return request.redirect(order_sudo.get_portal_url())
try:
updated_date = line._convert_to_middle_of_day(datetime.strptime(date_str, '%Y-%m-%d'))
except ValueError:
continue
updated_dates.append((line, updated_date))
if updated_dates:
order_sudo._update_date_planned_for_lines(updated_dates)
return Response(status=204)

If we still want the feature, then we should fix it + check which "date" the vendor should be able to update. If we don't, then we should remove the code since it may not make sense for 3rd party customizations anymore

@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch 3 times, most recently from c6472a7 to a50a876 Compare November 26, 2025 09:21
@ticodoo
Copy link
Contributor

ticodoo commented Nov 26, 2025

@robodoo r+

To avoid confusion after confirming an RFQ:
- New field has been added 'Promised Date' in Purchase Order
and Purchase Order Line.
- 'Promised Date' updates the deadline on a receipt.
- 'Promised Date' only shows after confirming an RFQ,
taking the place of 'Confirmation Date'.
- Confirmation Date was moved to 'Other information' section.
- Updating "Expected Arrival" on PO or PO line updates the date
scheduled on a receipt and no longer updates the deadline.
- Updated "Expected Arrival" help message to adapt to the change.

In Sales Order:
- 'Delivery Date' now uses a dynamic placeholder giving it
 a cleaner look.
- On Confirmation, if no date is set it will be assigned to
 the placeholder's
value and 'Delivery Date' label is changed to 'Promised Delivery'.
- Some UI Changes.

Task: 4687135
@abyo-odoo abyo-odoo force-pushed the master-promised-date-abyo branch from a50a876 to 5862f83 Compare November 26, 2025 12:14
@ticodoo
Copy link
Contributor

ticodoo commented Nov 27, 2025

@robodoo r+

@robodoo
Copy link
Contributor

robodoo commented Nov 27, 2025

@abyo-odoo @ticodoo linked pull request(s) odoo/upgrade#8687 not ready. Linked PRs are not staged until all of them are ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants