In the course of working on our specific business case, as well as integrating with the latest changes in Tax, it appears we need the ability to allow order refresh to occur on non-draft orders, as well. Currently Order module will abort automatic refresh in the default configuration if the order is not a draft. However, it is possible to manually set the refresh state or call the refresh manager directly.
The reasons for doing this vary, but all have to do with responding to changes to the order after it is placed but before it's considered "complete." In our case, this means adjustments during fulfillment (e.g., changes to the actual quantity we can ship) but mglaman points out this relates to backorders and other types of order validation workflow. The goal of refreshing the order is essentially to call order processors, which could for instance re-calculate tax.
The logic in OrderRefresh::refresh() performs other tasks, however, that are destructive to non-drafts. Order item unit prices are re-set (which would appear to be generally undesirable) and the order item's order ID relationship is removed, which is obviously not good. There is a @todo note to "evaluate which order items have changed," which would be more or less what we need to do.
In the short term I have manually triggered tax re-calculation and other processor calls in pre-save hooks, but that isn't a sustainable solution for Commerce Order. What do the maintainers think about this, particularly since it touches some common use-cases like backorder?
One example of an edge case we will need to address: The availability order processor does not pass through the order status to the various availability plugins, only a global Context object. This is rational because there may not actually be an order, yet, but if a product resolves as available only because it's not yet in a placed order, it will be removed during the order refresh because we lack context to decide otherwise.
Issue fork commerce-2875804
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
agoradesign commentedTouching non-draft orders sounds dangerous to me. So this should definitely not be done by default. Maybe introduce a config setting?
Comment #3
mglamanAgreed. I don't know the solution. But we do need a way for people using Fulfilment / Validation workflows to reconcile the order if quantities are adjusted.
I don't know if this involves adding sort some kind of "re-open" transition? I'd like to see how other systems handle this issue first. Research is key here.
Comment #4
bojanz commentedOrder refresh will definitely stay limited to draft orders, cause as Brad pointed out, it does a number of things that are not safe to do to a placed order.
We need a different/parallel process for recalculating placed orders. Question is whether that process should still invoke order processors or not.
Comment #5
bradjones1My vote would be to re-use as much of the existing Commerce API as possible, including order processors. I am essentially doing this through some custom hooks (as described in the summary) and so far, most of the default order processing is compatible with this workflow. The only exception so far appears to be product availability, but only if you are implementing logic that limits the availability of products after order placement. I'm not sure where commerce_stock is on this since I'm not using it, but I just un-tagged my availability plugin and am running it directly inside a new order processor, so as to have access to the $order's state.
Re: mglaman's thought about a "re-open" state, it seems like that might be more than we need out of the box? It seems to me that the use case presented here kind of assumes the order isn't complete, yet, and we have a "complete" state out of the box already. This pertains to the middle-ground between placement and completion, where the order is POST-processed. There's not much if anything in Commerce 2.x yet about protecting completed orders, anyway, so data integrity in those various steps is just another feature we can add on as the project grows. But at the moment there's still a lot of "don't touch" you must enforce manually as part of your business process, so tackling that huge question just for the sake of order adjustments post-placement seems like a lot to bite off.
Thanks everyone for considering this.
Comment #6
andrewbelcher commentedAnother side of this is orders that 'stay' open. In my use case, we have bookings for an event that are potentially created 12 months ahead of time. Over the 12 month period, there are a few pricing windows for tickets and customers can come in any time during that period and add additional tickets or make additional payments. Once a ticket is paid in full, the price is locked.
The refresh process seems ideal for our needs, as refreshing when the order is loaded is ideal for ensuring the information is always up to date, and we can trigger a refresh of all items when a pricing window passes in the background. However, this would suggest that we have to leave our orders in the draft state.
Perhaps an ability to configure what states should be recalculated would be a good way to make this more flexible, draft by default, but order types can specify both the interval and what states should be recalculated. I suppose an alternative would be to be able to have a 'draft' flag on a state, but I wonder if that's more complex for no real benefit...
Comment #7
bojanz commentedMarked #3095678: Allow placed orders to be refreshed as a duplicate.
Quoting it:
Lisa also had a good point that this process is needed for placed-but-not-yet-complete orders:
Comment #9
dercheffeSorry didn't want make a commit to this issue fork just a normal comment :-/ Just delete my commit please.
I have another point that might have a relation to this issue:
Use case:
perhaps a simple bool option or a bool field makes sense if the order processor's may fire or not?
Comment #10
Robert S commentedOK, I understand this is a tricky issue with many edge cases.
However, as it stands, it appears the commerce_tax module does not work well with editing the order -- that is with editing anything that will affect the order total, and thus the tax. In my case, we often need to adjust quantity based on availability (selling fabric by the meter, it is not easy to measure precisely in advance). I suppose many people have this issue, after all, that is the purpose of "Fulfilment / Validation workflow".
So, is there some recommended workaround? Adding a button to manually do order refresh?
Comment #11
kaszarobertWe also encountered this issue numerous times. Order came in, the customer later called the shop to add something else to the order or change the shipping method, etc. The result was always a frustrating talk about why the prices and taxes are now wrong on the order and on the sent invoice. For non technical people this is very confusing. The worse is that on the order's form you don't even have a chance to at least fix the wrong or missing adjustments at the order item level (such as promotion discounts and taxes). The order item's adjustments field is not editable on the UI.
Other webshops solve this by showing a recalculate button on the order edit forms. So I also support that manual order refresh-alike idea.
If that's not a viable option, implement some guard or validation that blocks editing everything that changes the price for non draft orders. So it would be impossible to break things with invalid adjustment values. That way at least everyone would understand that if I want to edit an order, I must cancel it and make a new one instead.
Comment #12
jsacksick commentedWondering if we should focus on fixing the admin order edit first? Perhaps add a separate submit button that allows explicitly refresh the order? (if the order is not a draft).
If the order is a draft, it'll be refreshed on presave anyway. If it's not a draft, we could allow triggering the refresh via a separate button, though we probably should skip refreshing prices, or better, allow opting out price refresh?
There are still several problems around refreshing a placed order:
Comment #13
rhovlandI've been working at this issue for our store for a couple months now and after debating a lot of ways of approaching this I think I've figured out what is a sane way of addressing this.
After an order has been placed any edits to an order are in the admin domain. A trained person is doing operations on the order. Each store is going to have their own process.
The very first thing commerce needs for administrators is controls, not automatic processes. If a store wants something to automatically happen they could call the function the manual control triggers in an event hook or whatever is appropriate. But for everyone else they would have their own processes to follow which could include clicking a button to run a certain type of order processor.
Promotions:
Some stores may wish to honor promotions even after the order has changed and no longer qualifies for a promotion. Other stores want to recalculate promotions when the order is edited. We need both a button to recalculate all order promotions, and a way to do it on a per item basis. We need to be able to choose if it recalculates using the promotions that were active at the time the order was placed, or the current promotions.
Taxes:
Tax calculation is often whole order dependent (taxing authority rules) so providing a form with a button to recalculate taxes on the order makes sense. Other tax modules could extend that form to take other actions such as submitting an adjustment to the transaction to a tax recording service.
The tax recalculation form should also document that taxes were calculated and why. This would be written to the order log and the reason could also be used by tax modules for record keeping purposes.
There's a few other parts of the order refresh I haven't touched on yet but this is what I've considered so far.
Comment #14
xmacinfoYes, we need some way to refresh the calculation instead of deleting an order and recreating it. It could be a button available only to a user with a specific role.
In my use case, I created an order and forgot to add the billing information. I placed the order too quickly before checking if everything was fine.
I edited the order to add the correct billing information, but the taxes won't refresh.
Comment #15
rhovland@xmacinfo See the attached child issue. I added a form to refresh taxes
https://www.drupal.org/project/commerce/issues/3333475
Comment #16
m_z commentedI came across this issue and want to report how you can manually trigger an order refresh if you have an order type with shipping.
On the /admin/commerce/orders/ORDER_ID/shipments tab you can "edit" an existing shipment for your order.
You can active the checkboxes for all shipment items and then click the "Recalculate shipping" button.
Don't forget to click the "Save" button at the bottom.
Afterwards you should see the correct tax values in the order summary on /admin/commerce/orders/ORDER_ID tab.
This workaround-like solution should work for a shop administrator with proper permissions.
------
(For developers you can use devel_php or a custom module to execute the following code for the $order entity that should be refreshed:
But this solution isn't suitable for a typical shop administrator, so I want to share my shipping refresh solution above.)
------
EDIT / UPDATE:
My explanation above isn't really right. The order refresh will only be triggered if the new line items and the new order total lead to another shipping option. Then the whole order will be refreshed (like stated above).
Moreover I found out that a complete order refresh does a lot of things (e.g. see https://www.susubi.io/sa/editing-placed-orders) and especially the price and / or quantity changes are highly unwanted in my use case. I only want to make it possible for the shop administrator to re-calculate the taxes for the order total.
For my use case I implemented a custom module based on the solution and the patch of comment 6 in https://www.drupal.org/project/commerce/issues/3333475#comment-14980510 and this works very well. It only re-calculates the taxes without doing the other "order refresh" stuff...
Comment #17
introfini commentedRelated contrib module: Commerce Order Amend
I've published a contrib module that addresses the practical need for editing placed orders while working around the limitations described in this issue:
Commerce Order Amend (1.0.0-beta1)
It provides an "Amend Order" tab on placed orders with operations for swapping item variations, adding/removing items, and managing coupons.
To deal with the destructive nature of
OrderRefreshon placed orders, the module:overridden_unit_priceon every order item, preserving the original checkout pricingIt uses
$order->setRefreshState(OrderInterface::REFRESH_ON_SAVE)to force recalculation of promotions and taxes, but the price locking prevents the unit price re-resolution that makes blanket refresh unsafe for placed orders.Key implementation details
Before calling
refreshAndSave(), the module locks every order item:The diff compares adjustments by type (not label) to avoid false positives from translated labels (e.g., "Envio" vs "Shipping" both map to the
shippingtype).This is a workaround rather than a core solution. If a selective refresh API lands in core (as discussed in this issue), the module will adopt it for more granular control over which processors run during amendments.