-
-
Notifications
You must be signed in to change notification settings - Fork 2
Create order page
- Add a new block class which gets the form field logic and does something with it
- Add a new template which adds the new 'Order Type' section HTML and the RequireJS
<script>
block which adds the JS onchange observers (seeMagento_Sales::order/create/form/account.phtml:21
) - Add a new block directive to the
sales_order_create_load_block_data.xml
andsales_order_create_index.xml
layouts- It should be a child of block name=
data
(\Magento\Sales\Block\Adminhtml\Order\Create\Data
) -
additional_area
is a possible extension point (seeMagento_Sales::order/create/data.phtml:19
)
- It should be a child of block name=
- Extend/mixin
html/vendor/magento/module-sales/view/adminhtml/web/order/create/scripts.js:1048
(or create a new JS AMD module) which will define the callback methods we want to execute when the form fields (the order type dropdown and the order ID text input) are changed (seeAdminOrder.accountFieldsBind
,AdminOrder.accountGroupChange
andAdminOrder.accountFieldChange
) - Add dropdown to MOTO order screen
- It seems the form fields themselves are added in the
\Magento\Sales\Block\Adminhtml\Order\Create\Form\Account::_prepareForm
method
- It seems the form fields themselves are added in the
- Add logic to make hidden 'order number' field appear when certain option selected
-
Add order type and order ref fields to order- Setup Patch class?
- We should take performance considerations into account - i.e. If the sales_orders table is very large, any patch/sql could take a long time to run
- Discuss with Jev/other devs
- Add logic to validate order number
- Add logic to save fields
- Add logic to mark one of these options as selected="selected" (in custom.phtml)
- Add logic to prevent empty reference order number from being saved
- Add dedicated table for saving fields to quote
- Add logic to remove data from order_type_quote table if quote is cancelled
- Add logic to make sure that only one record ever exists for a quote (i.e. Records should be created and then updated, rather than adding a new record for each edit of the field)
- Add dedicated table for saving fields to order
- Add logic to save fields to table with quote_id (i.e. Whilst order is being created)
- Add logic to save fields to table with order_id (i.e. Once the order has been submitted)
- Add logic to remove data from order_type_quote table once order has been created
- Where are these values going to be displayed? Do they need to be displayed anywhere in the admin? In order grids? On manage order pages?
- Add logic to ensure that custom order type is always populated on frontend and MOTO orders
- Presumably by setting 'order' as a default value for the column in MySQL
- Make sure any forms (or data) submitted uses the form key (html/vendor/magento/module-backend/view/adminhtml/templates/admin/formkey.phtml:8)
- I think Magento automatically adds this to the HTML response
The adminhtml checkout experience displays all the checkout sections at the same time. The usual checkout stages (billing, shipping, payment method, etc) are all refreshed by ajax.
The layout for the different sections are defined in html/vendor/magento/module-sales/view/adminhtml/layout/sales_order_create_*
.
Templates are located in Magento_Sales::order/create/*
.
Whenever an ajax request is triggered (by changing any form field), all the blocks & templates of the \Magento\Sales\Block\Adminhtml\Order\Create\Data
are re-processed and the templates rendered again and returned by the ajax. This means, in practice, that it is not possible to only update a specific section of the adminhtml checkout. This is likely by design, as changing one part of the form (e.g. Shipping address) could affect other parts of the form (e.g. Shipping methods), requiring those sections to be updated as well.
On the first page load, the page is rendered using the standard Layout XML system, i.e. The \Magento\Sales\Controller\Adminhtml\Order\Create\Index
controller and the sales_order_create_index
handle are used to render the page and send the response when the http://m23-example-modules.local/admin/sales/order_create/index/key/d4a83114066b28db5289baed58944123e3040d022301fbaa6fb68525c0bd026f/
is loaded for the first time.
On subsequent (ajax) requests, the \Magento\Sales\Controller\Adminhtml\Order\Create\LoadBlock
controller is used. This class adds layout handles corresponding to the parameters of the ajax request, then triggers the Layout XML rendering process by explicitly rendering the content
container element. This element is defined in sales_order_create_load_block_plain
. All the Layout XML handles which are updated by ajax define their blocks under the referenceContainer name="content"
node, which ensures that all the parts of the page that need to be updated by ajax, do so.
The ajax requests all hit the same URL, but the layout
folder in the Magento_Sales
module has dozens of Layout XML files with names like sales_order_create_load_block_billing_address
. So, given that a matching URL for this handle doesn't exist, how are the layout updates within applied?
The answer lies in \Magento\Sales\Controller\Adminhtml\Order\Create\LoadBlock::execute
. The ajax request triggered by the Account Information fields hits the URL Request URL: http://m23-example-modules.local/admin/sales/order_create/loadBlock/key/4715899381c03de2c04369fc740c072835755eb34a656632bbfa71471a608912/block/data?isAjax=true
, which has the parameter /block/data/
.
Ajax requests can return either json
or plain
- this determines the layout handles added to the request, and the controller class return type used.
The controller action method adds a basic layout handle for the page (in this example, sales_order_create_load_block_plain
) and additional handle(s) based on the block
parameter are added to the request in the format sales_order_create_load_block_<block_value>
.
The Account Information form is defined in html/vendor/magento/module-sales/view/adminhtml/layout/sales_order_create_load_block_form_account.xml
.
(there is no such URL which matches that Layout XML handle though - this is not important now, but will become relevant later, when we explain how the page is updated using ajax requests).
The template is defined in Magento_Sales::order/create/form/account.phtml
. The block class for the template is \Magento\Sales\Block\Adminhtml\Order\Create\Form\Account
. Constructing the form is not done using Layout XML, but rather in pure PHP. The form fields in this section are attributes from the customer
entity.
The form is initialised in \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractForm::getForm
. When the block is rendered, \Magento\Framework\Data\FormFactory
creates a form object and the abstract method \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractForm::_prepareForm
is called, which is implemented in \Magento\Sales\Block\Adminhtml\Order\Create\Form\Account
.
The _prepareForm
method of that class gathers all the required system
attributes of the customer
entity, then all the user_defined
attributes of the customer
entity, required or not. If the customer is a guest, the group_id
attribute is skipped and not added to the form.
The method then creates a new fieldset and passes both the fieldset and the array of attributes to \ProcessEightAdminhtmlExamples\AddFieldToCreateOrderPageExample\Block\Adminhtml\Order\Create\Form\Custom::_addAttributesToForm
.
That method is defined in the parent AbstractForm
class. It loops over the array of attributes and adds form fields based on the attribute metadata.
If a field needs custom HTML attributes added, then the \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractForm::_addAdditionalFormElementData
can be overridden, e.g. \Magento\Sales\Block\Adminhtml\Order\Create\Form\Account::_addAdditionalFormElementData
defines extra logic to add validation classes to the email
element and to set it as required.
In order to ensure the form fields just added are processed by the right JS, a prefix, order[account]
is added to the name
attribute of each form field.
Finally, the form fields are populated with their values (which is taken care of by the native \Magento\Sales\Block\Adminhtml\Order\Create\Form\Account::extractValuesFromAttributes
method).
The rendering of the form fields is triggered by calling the getForm()->getHtml()
of the block in the account.phtml
template. getForm()
is defined in the parent class \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractForm::getForm
.
Each element has a renderer assigned to it, based on the attributes' frontend_input
type (e.g. input
, select
, multiselect
, multiline
, date
). Additional input types are defined in the protected method \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractForm::_getAdditionalFormElementTypes
. If a form field needs custom logic, then the renderer is where that can be encapsulated.
Renderers are block classes which implement the \Magento\Framework\Data\Form\Element\Renderer\RendererInterface
and define one method - render
, which accepts one argument, \Magento\Framework\Data\Form\Element\AbstractElement
.
It is the renderers' responsibility to produce the HTML for a form field. This could be by calling toHtml
from it's render
method, or by defining the HTML in the same method.
The default renderer for form fields is \Magento\Backend\Block\Widget\Form\Renderer\Element
, though this has the intriguing comment of @deprecated 100.2.0 in favour of UI component implementation
.
The AbstractForm
class defines two more renderers: \Magento\Backend\Block\Widget\Form\Renderer\Fieldset
and \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element
. The latter is also deprecated for the same reason. The only thing which differentiates these three renderers is that they define element-specific templates. Otherwise their render
methods just call toHtml
to render them.
An interesting example is the region
renderer. The \Magento\Customer\Block\Adminhtml\Edit\Renderer\Region::render
method defines an element which renders a 'Region' element, which dynamically switches between a select dropdown, and a text input field, based on the country selected.
For an element to be rendered using a specific renderer class, \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractForm::_getAdditionalFormElementRenderers
method needs to return an array where the key is the attribute to render and the value is an instance of the specific renderer class. See \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractForm::_getAdditionalFormElementRenderers
for an example.
Additional renderers can be made available to our form by overriding the \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractForm::_getAdditionalFormElementRenderers
method.
The admin create order form is never submitted like a traditional form. Instead, all form fields are submitted using Ajax. Form fields are saved and templates are reloaded using Ajax.
There is an inline RequireJS call which instantiates the Magento_Sales/order/create/form
JS component module. That component module creates and returns a new order, then back in the template, the AdminOrder.accountFieldsBind
method is executed, which binds the onchange events to the callbacks that need to be triggered when the form fields are changed.
AdminOrder.accountFieldsBind
, which is defined in html/vendor/magento/module-sales/view/adminhtml/web/order/create/scripts.js:1035
, finds all the input
, select
and textarea
fields in the bound element (in this case customer_account_fieds
) and then adds an onchange event with one of two callbacks, depending on the form field HTML element type:
-
accountGroupChange
observes the Group dropdown. This method is defined inAdminOrder.accountGroupChange
. -
accountFieldChange
observes the Email text input. This method is defined inAdminOrder.accountFieldChange
.
When an option is selected from the 'Group' dropdown, AdminOrder.accountGroupChange
serialises the form order-form_account
and then passes it to AdminOrder.loadArea
, which submits one of two types of ajax request, depending on the parameters passed into it. AdminOrder.loadArea
allows callbacks to be managed aynschronously using jQuery.Deferred()
. This object is used to handle the responses from the ajax request in an async fashion.
A factory function that returns a chainable utility object with methods to register multiple callbacks into callback queues, invoke callback queues, and relay the success or failure state of any synchronous or asynchronous function. See https://api.jquery.com/jQuery.Deferred/
Other parameters passed to AdminOrder.loadArea
include indicator
, which determines whether the ajaxload loading overlay appears and an array containing one (or presumably more) 'areas'. In this case the area passed is data
. This data
area corresponds to the data
block in sales_order_create_index.xml
, sales_order_create_load_block_data.xml
and the \Magento\Sales\Block\Adminhtml\Order\Create\Data
block class and it's template, Magento_Sales::order/create/data.phtml
.
Other area
s include:
items
: The order items grid, to add items to an order.
search
: Search functionality for the order items grid.
card_validation
: Credit card payment method validation.
message
: Displays messages.
AdminOrder.loadArea
POSTs an ajax request to http://m23-example-modules.local/admin/sales/order_create/loadBlock/key/34dd306dd172bda5bd479269a752f1f7568bc9feb720623bbd4dd6654790f4c3/block/data?isAjax=true
. The area
param is appended to the end of the ajax URL.
If the ajax request is a success, then AdminOrder.loadAreaResponseHandler
is called, where one of several things can happen:
- If the response contains an error message, it is displayed in an
alert
dialog box. - If the response contains a redirect,
setLocation
performs the redirect - If the response does not contain a
message
property, then it is added to theloadingAreas
array (for reasons which will become clear in a moment) - If the response contains a
header
property, it is added to thedata-title
attribute of the.page-actions-inner
HTML element
Finally, for each of the loadingAreas
, if the area id is not message
then the property of the response matching the area id (e.g. response.data
if the area id submitted was data
) is injected into the HTML element indicated by the getAreaId
method, which simply prepends the string order-
onto the existing area id. So data
would become order-data
, meaning div#order-data
in Magento_Sales::order/create/form.phtml
has its contents replaced with the data
from the ajax response.
Another example. For the Account Information section, the Layout XML name attribute is form_account
. The id
HTML attribute is hardcoded as order-form_account
(in Magento_Sales::order/create/data.phtml
). Once the ajax request has succeeded, the AdminOrder.loadAreaResponseHandler
method calls AdminOrder.getAreaId
, which takes the data
parameter passed into AdminOrder.loadArea
, resulting in the area id of order-data
.
If the area id (order-data
in this case) DOM element has a callback, then the callback is called after the DOM is updated.
This request uses the layout XML file html/vendor/magento/module-sales/view/adminhtml/layout/sales_order_create_load_block_data.xml
, in which the block form_account
is defined. The rendering of this block is triggered by getChildHtml
in the data.phtml
template, which renders the whole div#page-create-order
(in Magento_Sales::order/create/data.phtml
). The data.phtml
template is rendered initially by html/vendor/magento/module-sales/view/adminhtml/layout/sales_order_create_index.xml
, when the page is first loaded, and then by html/vendor/magento/module-sales/view/adminhtml/layout/sales_order_create_load_block_data.xml
, whenever a form field which triggers an ajax request is changed (note how the filename is the same as the Layout XML handle for the ajax request).
The AdminOrder.accountFieldChange
method works in a very similar way to the AdminOrder.accountGroupChange
method, with two main diffferences: The ajaxload overlay never appears and an area
is not defined (false
is passed instead). An ajax request still takes place onchange
, but the logic which updates the page is not triggered (AdminOrder.loadAreaResponseHandler
is not called). Any response from this ajax request is totally ignored. The only operation that is executed onSuccess
is deferred.resolve
, which triggers any callbacks registered on the deferred object to be executed.
The first thing that happens is that the entire form is validated using JS using the jQuery Validate plugin (http://docs.jquery.com/Plugins/Validation/validate).
If validation is successful, then processStart
is triggered. This displays the ajax load overlay.
submitOrder
is triggered next. Curiously, this triggers a callback defined in the AdminOrder::initialise
method. This callback triggers the execution of the AdminOrder::realOrder
method, when the onSubmitOrder
event is fired. onRealOrder
also has a callback; It binds the form#edit_form
element to the AdminOrder::_realSubmit
method.
_realSubmit
disables the ajax load overlay if there are any validation errors. Otherwise, it triggers the save
handler.
http://m23-example-modules.local/static/version1601419254/adminhtml/Magento/backend/en_GB/mage/adminhtml/form.js
is eventually called and fires the formSubmit
event of the form#edit_form
. The form is validated again, using http://m23-example-modules.local/static/version1601419254/adminhtml/Magento/backend/en_GB/jquery/jquery.validate.js
. http://m23-example-modules.local/static/version1601419254/adminhtml/Magento/backend/en_GB/mage/backend/validation.js::_submit
fires the submit
event of form#edit_form
.
Control eventually returns to submitOrder
, which allows the submission to continue as normal (i.e. be submitted to the server).
\Magento\Sales\Controller\Adminhtml\Order\Create\Save::execute
is the controller the form submits to. The execute
method calls \Magento\Sales\Controller\Adminhtml\Order\Create::_processActionData
, just like when editing the order (quote). That method dispatches several events, including adminhtml_sales_order_create_process_data
, which is where we subscribe an observer and update the quote_to_order_type
table. The quote is saved immediately after the adminhtml_sales_order_create_process_data
is dispatched.
Back in \Magento\Sales\Controller\Adminhtml\Order\Create\Save::execute
, payment details are updated and the order is created. The $order = $this->quoteManagement->submit($quote, $orderData);
, which actually creates the order, is called in \Magento\Sales\Model\AdminOrder\Create::createOrder
, which is called in the controller execute
method.
We now have our order. Email confirmation is sent, if configured. The checkout_submit_all_after
event is dispatched with the order
and quote
objects. Several modules observe this event and subscribe events to it:
-
Magento\CatalogInventory
: 'Subtract qtys of quote item products after multishipping checkout'- The same observer is disabled in
html/vendor/magento/module-inventory-sales/etc/events.xml:21
with the comment: 'There is no need to register product sale and reindex stock items, as in multi source inventory only reservations are created after order placement'
- The same observer is disabled in
-
Magento\Paypal
: 'Save order into registry to use it in the overloaded controller'. -
Magento_Authorizenet
uses this event to update order increment IDs (This payment method is now deprecated) -
Magento\Signifyd
: 'Creates Signifyd case for single order with online payment method.' (This payment method is also now deprecated)
The session is cleared and the controller redirects to the Manage order page of the admin for the newly created order.