OrderFormTrait
Edit/add form of an order, saved through the API instead of the generic Admin `save()`.
The Admin layer cannot compute catalog prices, VAT or the stock breakdown, so both the AJAX
recalculation preview and the final save go through the single orchestration API action
api/eshop/order/build (OrderPresenter::internalActionBuild(),
the persist flag tells them apart), called directly via $this->apiBridge.
Item, shipping and payment rows carry their own editable price and VAT rate; the catalog only
prefills them (itemsPrefill, shippingRefresh, paymentRefresh force-submits) and saving
never silently reprices. Edit defaults come exclusively from the order snapshot, never from the catalog.
IMPORTANT: this form never changes the order status; only OrderStatusChangeFormTrait does,
via OrderTransitionManager::apply().
Table of Contents
Methods
- hasVatControls() : bool
- Returns whether the form may contain VAT controls at all (`withVat` checkbox, row `vat`/ `priceWithVat`/`totalPriceWithVat`, `shippingVat`, `paymentVat`).
- shouldShowVatFields() : bool
- Returns the initial visibility of the VAT fields (not of the checkbox, see {@see hasVatControls()}).
- createComponentEditForm() : Form
- addOrderBasicFields() : void
- addOrderCustomerContactRefreshButton() : void
- Adds the force-submit button `order-item-calc.js` clicks after the `customerContactId` select changes (`data-refresh-target`, same generic mechanism as `tariffId` -> `shippingRefresh`).
- addOrderCustomerFields() : void
- addOrderCustomerRefreshButton() : void
- Adds the force-submit button picker.js clicks after a customer is picked.
- addOrderGiftsMultiplier() : void
- Adds the gifts multiplier: row = gift picker + quantity.
- addOrderItemsMultiplier() : void
- Adds the items multiplier: row = product set picker + editable price breakdown.
- addOrderItemsPrefillButton() : void
- Adds the shared hidden button picker.js clicks after a product set is picked in any row (`OrderItemPickerTrait::orderItemProductSetPicker()` with `submitButton: 'itemsPrefill'`).
- addOrderPaymentRefreshButton() : void
- Force-submit after `paymentMethodId` changes (JS clears paymentPrice/Vat before the click).
- addOrderPickupPointFields() : void
- Adds the carrier pickup point controls for order edit.
- addOrderPickupRefreshButton() : void
- Adds the force-submit button used after a point is picked in the carrier widget.
- addOrderRecalculateButton() : void
- Adds the "Recalculate" button.
- addOrderSellerFields() : void
- Adds the seller (shop operator) snapshot fields stored on the order.
- addOrderSellerRefreshButton() : void
- Adds the hidden force-submit button that picker.js clicks after a seller is picked.
- addOrderShippingFields() : void
- Adds shipping/payment with an editable price (without VAT) and rate.
- addOrderShippingRefreshButton() : void
- Force-submit after `tariffId` changes (JS clears shippingPrice/Vat before the click).
- addOrderStockCheckFields() : void
- Adds the stock check fieldset below "Shipping and payment" (separate so the override of the strict stock gate is visually apart from shipping/payment, which it does not relate to).
- addOrderVouchersMultiplier() : void
- Limit from the raw HTTP data (`onAnchor`) plus `$maxCopies` as a second defense.
- allowPartialAdminPromptKey() : string
- Returns the effective `4/order.partial.adminAllowed`; missing in DB means "require full stock" (same semantics as the `$value === '1'` fallback in {@see \App\UI\Api\Eshop\Models\Managers\Traits\OrderBuildTrait::resolveAllowPartial()}, where a missing or other value is false).
- applyOrderCustomerUserLink() : void
- Syncs the order's user picker (`userId`, {@see \App\UI\Admin\Eshop\Presenters\Traits\Pickers\OrderCustomerPickerTrait}) and `userTypeId` from the customer record's linked user account (`Customer.userId`).
- buildOrderPayload() : array<string, mixed>
- Builds the raw payload for `api/eshop/order/build`: no catalog logic here, only a transcription of form values (as in {@see \App\UI\Api\Eshop\Presenters\OrderPresenter::internalActionBuild()}). `lines` carries trusted price/vat/name/code/unit; shipping/payment go as nested `shipping`/`payment` (tariffId + price + vat).
- callOrderBuild() : array<string, mixed>
- controlValue() : mixed
- Safely reads a field value from the container (form or multiplier row).
- extractOrderWarnings() : array<int, string>
- Returns the `warnings` of the `api/eshop/order/build` response (currently only the code `paymentMethodUnavailable`, see the API `OrderPresenter::internalActionBuild()` docblock) with a runtime guard to `list<string>`, like {@see orderStringKeyedArray()}.
- fillOrderCustomerDeliveryFromContact() : void
- Copies the contact's delivery address (`CustomerContact.useDelivery`/`dlv*`) into the order's `useDelivery`/`customerDlv*` fields.
- fillOrderCustomerFieldsFromContact() : void
- Overwrites the form's `customer*` fields from the given contact using the same `fieldMap` as the client picker ({@see \App\UI\Admin\Eshop\Presenters\Traits\Pickers\OrderCustomerLookupPickerTrait::orderCustomerLookupFieldMap()}), so the data-key to form-field mapping has a single source of truth.
- formComponent() : IComponent|null
- Calls `getComponent()` through the `Container` type, bypassing the faulty vendor annotation `@method BaseControl getComponent(string $name)` on Contributte `BootstrapContainerTrait` (the real component-model signature has 2 arguments and a nullable return; the annotation on a BootstrapForm-typed variable makes phpstan fail).
- loadOrderCustomerAccountUser() : User|null
- Loads the user account by id, or null when not found.
- onlyPricedLines() : array<int, array<string, mixed>>
- Drops rows without a price from the payload; used only by the shipping/payment refresh, which must not fail on `linePriceRequired` while some items are still unpriced (weight/cartTotal then count only trusted rows). Final save and "Recalculate" send all rows unchanged: a missing price must fail visibly there, not vanish silently.
- orderBuildFailureMessage() : string
- Returns the preview error text from a failed `order/build` response. A technical failure (DB/ORM/transport) has an empty `message` (the detail is only in the server log) and a generic translated error is shown instead of an empty line. A domain validation message passes unchanged.
- orderControlValues() : ArrayHash<string|int, mixed>
- Returns the form values for the AJAX button handlers.
- orderPayloadGifts() : array<int, array{giftId: int|string, amount?: int}>
- orderPayloadId() : int|string|null
- Returns an id for the build payload: canonical becomes int, empty or `0` becomes `null`. Any other value goes on unchanged, so the API (the single authority, {@see \App\UI\Api\Eshop\Models\Helpers\OrderPayloadHelper::findInvalidIdPath()}) rejects it; `intOrNull()` would turn a tampered hidden `'1.9'` into item #1 here.
- orderPayloadLines() : array<int, array{setId: int|string, quantity: int, orderItemId?: int|string, price?: string, vat?: string, name?: string, code?: string, unit?: string, discountPercents?: string}>
- Builds the trusted rows: `price`/`vat` are required on the way to the API (missing: hard fail `linePriceRequired` there, no silent catalog fallback here). The form always carries them from prefill/snapshot, so they are just transcribed.
- orderPayloadPickupManual() : array{code: ?string, name: ?string, street: ?string, city: ?string, zip: ?string, country: ?string}|null
- Returns the manually entered pickup point from the six text fields. `null` when both `code` and `name` are empty (both are required for a valid point); the API then ignores the manual path and decides only from the raw widget payload.
- orderPayloadPickupPoint() : array<string, mixed>|null
- Decodes the hidden `pickupPoint`, which carries the raw JSON object of the widget (in the edit defaults `OrderShipper::$pickupPointData`), so the API gets the same field as the front checkout. Anything that is not a JSON object becomes `null`: the driver would not recognize an array/scalar and the fail-closed guard would turn it into `pickupPointRequired`.
- orderPayloadVoucherCodes() : array<int, string>
- orderPreviewFromOrder() : array<string, mixed>
- Builds a deterministic preview snapshot straight from the saved order, without calling `api/eshop/order/build` (GET only, `OrderPresenter::actionEdit()`).
- orderSellerDefaultsFromSettings() : array<string, scalar|null>
- Returns the default seller from settings for an order that has no own snapshot (add, or a record from before snapshots). The snapshot is built from the live directory because no other source exists: right for a new order, the best available estimate for an old one, which the admin sees and can overwrite before saving.
- orderSellerDefaultsWithNotice() : array<string, scalar|null>
- Returns {@see orderSellerDefaultsFromSettings()} and reports the state to the admin. The flash is part of the contract: the seller is required, so without it the admin would learn of the problem only from a save error, and with empty settings would not know where to look (the shop seller setting).
- orderSettingSellerId() : int|null
- Resolves the seller id from settings: `4/invoicer.sellerId`, falling back to `7/invoice.default.sellerId` (invoice defaults `invoice.default.*`), the same chain the invoice issue uses ({@see \App\UI\Api\Eshop\Models\Generators\OrderInvoiceGenerator::settingIntOrNull()}). An empty string in DB means "does not exist" (`is_numeric('')` is false).
- orderStringKeyedArray() : array<string, mixed>|null
- Runtime guard to `array<string, mixed>`; the `ApiBridge` response is typed `mixed`.
- prefillOrderItemRows() : void
- Picker prefill: for every row with a selected `productSetId` that has no price yet, fetches code/productSetName/unit/vat/price from the catalog (`api/eshop/order/item-prefill`, {@see \App\UI\Api\Eshop\Models\Services\OrderSnapshotBuilder::resolveItemPrefill()}).
- recalculateOrderForm() : void
- Calls `build` (persist=false) with the current form data, including untouched fields.
- refreshOrderCustomer() : void
- Loads the customer with all contacts, fills the address select and prefills the fields from the first contact (same semantics as `refreshOrderSeller()`).
- refreshOrderCustomerContact() : void
- Ownership guard: looks the contact up only among the chosen customer's contacts (`Customer::getContact()`). A foreign or nonexistent `customerContactId` (e.g. the select was filled by AJAX from elsewhere, a race) is silently ignored, no failure.
- refreshOrderPayment() : void
- Same logic as {@see refreshOrderShipping()}, for the payment method.
- refreshOrderPickupPoint() : void
- Handles the pickup widget selection (`pickupRefresh`): calls `build` (persist=false) with the raw widget object in the `pickupPoint` payload and prefills the six text fields from the snapshot in the response (`order.shipper.pickup*`). Same logic as {@see refreshOrderShipping()}, different field set.
- refreshOrderSeller() : void
- Loads the seller snapshot from the Invoicer directory into the form.
- refreshOrderShipping() : void
- Calls `build` (persist=false) after the tariff changes, with the same payload as `recalculateOrderForm` but without incomplete rows (no price).
- resolveOrderCustomerIdForContactSelect() : int|null
- Returns the `customerId` used to build the items of the `customerContactId` select.
- resolveOrderPreviewCurrency() : Currency|null
- Returns the preview currency from `currencyId` in the serialized `order` of the build response, mapped to the entity from `$orderCurrencyEntities` (built in `loadOrderSelectData()`, no extra query).
- resolveOrderSellerUseVat() : bool
- Returns whether the shop operator is a VAT payer, read live from the Invoicer seller.
- rowValue() : mixed
- saveOrderForm() : bool
- setControlValue() : void
- Force-sets a field value in the container (form or multiplier row) if it exists.
- setOrderFormDefaults() : void
- syncPickupControls() : void
- Fills the seven pickup point controls from the shipper snapshot in the `build` response: six text fields, hidden `pickupPoint` with the raw widget object (so saving unchanged keeps exactly what the widget issued) and hidden `pickupPointTariffId` with the tariff the selection belongs to.
- wireOrderMultiplierButtons() : void
- Multiplier add/remove buttons, as in {@see GiftFormTrait::addGiftOfferSetFields()}.
Methods
hasVatControls()
Returns whether the form may contain VAT controls at all (`withVat` checkbox, row `vat`/ `priceWithVat`/`totalPriceWithVat`, `shippingVat`, `paymentVat`).
public
hasVatControls() : bool
On add it depends on the shop seller being a VAT payer (resolveOrderSellerUseVat()); a non-payer gets no controls, since a live toggle would have nothing to switch. On edit always true: the admin must be able to turn VAT on for an order that lacks it, and off for one that has it.
Return values
boolshouldShowVatFields()
Returns the initial visibility of the VAT fields (not of the checkbox, see {@see hasVatControls()}).
public
shouldShowVatFields() : bool
On add it follows the seller's VAT-payer status (same condition as hasVatControls()); on edit only
the saved order.withVat, so an order created with VAT keeps showing VAT fields even if the operator
has since turned VAT off, and a withVat=false order never shows them. Called from edit.latte
($presenter->shouldShowVatFields()); order-item-calc.js toggles it live, this is only the
server-side initial state.
Return values
boolcreateComponentEditForm()
protected
createComponentEditForm() : Form
Return values
FormaddOrderBasicFields()
private
addOrderBasicFields(Form $form) : void
Parameters
- $form : Form
addOrderCustomerContactRefreshButton()
Adds the force-submit button `order-item-calc.js` clicks after the `customerContactId` select changes (`data-refresh-target`, same generic mechanism as `tariffId` -> `shippingRefresh`).
private
addOrderCustomerContactRefreshButton(Form $form) : void
Prefills the fields from the chosen (not necessarily first) address.
Parameters
- $form : Form
addOrderCustomerFields()
private
addOrderCustomerFields(Form $form) : void
Parameters
- $form : Form
addOrderCustomerRefreshButton()
Adds the force-submit button picker.js clicks after a customer is picked.
private
addOrderCustomerRefreshButton(Form $form) : void
The server fills the address select and prefills the customer fields from the first contact
authoritatively; the client fieldMap on the picker only gives an instant response from the same data.
Parameters
- $form : Form
addOrderGiftsMultiplier()
Adds the gifts multiplier: row = gift picker + quantity.
private
addOrderGiftsMultiplier(Form $form) : void
Limit from the raw HTTP data (onAnchor) plus $maxCopies as a second defense.
Parameters
- $form : Form
addOrderItemsMultiplier()
Adds the items multiplier: row = product set picker + editable price breakdown.
private
addOrderItemsMultiplier(Form $form) : void
Modeled on InvoiceFormTrait::addLineControls(): code/quantity/unit/price/vat/
discountPercents are trusted (the admin edits them) plus three read-only client-side computed
fields (order-item-calc.js).
IMPORTANT: a plain Container inside the Multiplier does not decorate form-control
itself, so the class is added by hand on every field.
Parameters
- $form : Form
addOrderItemsPrefillButton()
Adds the shared hidden button picker.js clicks after a product set is picked in any row (`OrderItemPickerTrait::orderItemProductSetPicker()` with `submitButton: 'itemsPrefill'`).
private
addOrderItemsPrefillButton(Form $form) : void
validationScope([]) keeps the force-submit from blocking on required fields; createForm() lets
save through only via update/save, so this never saves.
Parameters
- $form : Form
addOrderPaymentRefreshButton()
Force-submit after `paymentMethodId` changes (JS clears paymentPrice/Vat before the click).
private
addOrderPaymentRefreshButton(Form $form) : void
Parameters
- $form : Form
addOrderPickupPointFields()
Adds the carrier pickup point controls for order edit.
private
addOrderPickupPointFields(Form $form) : void
The admin either picks the point in the carrier widget ("pick on map": JS writes the raw object into
hidden pickupPoint and clicks pickupRefresh) or types it into six text fields (payload
pickupPointManual). Only the server-side driver normalizes either path; JS maps nothing.
IMPORTANT: the controls are always added, regardless of the selected tariff; the template decides
whether to render the block ($presenter->orderPickupInfo()), and hidden pickupPoint must be in the
HTML even outside that condition, otherwise a skipped hidden widget loses its value.
Parameters
- $form : Form
addOrderPickupRefreshButton()
Adds the force-submit button used after a point is picked in the carrier widget.
private
addOrderPickupRefreshButton(Form $form) : void
JS writes the raw widget object into hidden pickupPoint and clicks this button. The server sends the
payload to order/build (persist=false), the driver normalizes it and the six text fields are
prefilled from the response; the driver is the only source of normalization (as with shippingRefresh).
Parameters
- $form : Form
addOrderRecalculateButton()
Adds the "Recalculate" button.
private
addOrderRecalculateButton(Form $form) : void
validationScope([]) so it does not block on required fields while the admin is still composing the
order; it does not save (outside the update/save gate in createForm()), only calls build with
persist=false and redraws the preview panel. It never overwrites the form's row prices: the preview
is read-only totals.
Parameters
- $form : Form
addOrderSellerFields()
Adds the seller (shop operator) snapshot fields stored on the order.
private
addOrderSellerFields(Form $form) : void
The sellerId picker only prefills; the texts stay editable because for an old order that is the
only way to issue a document with the details valid at the time (the directory already holds newer
ones). sellerId is required (mirrors the API guard sellerRequired); setOrderFormDefaults()
prefills it from settings on add and on orders without a snapshot.
sellerContactId/sellerBankAccountId only record which directory contact/account was the source;
the server fills them in refreshOrderSeller(), admins never edit them.
Parameters
- $form : Form
addOrderSellerRefreshButton()
Adds the hidden force-submit button that picker.js clicks after a seller is picked.
private
addOrderSellerRefreshButton(Form $form) : void
The client fills the text fields from fieldMap, but the server overwrites them authoritatively
and also fills sellerContactId/sellerBankAccountId, which the client cannot see.
validationScope([]) skips validation (required seller/items do not block the prefill);
createForm() lets save through only via update/save, so this never saves.
Parameters
- $form : Form
addOrderShippingFields()
Adds shipping/payment with an editable price (without VAT) and rate.
private
addOrderShippingFields(Form $form) : void
Edit defaults come from the snapshot (setOrderFormDefaults); changing the tariff/payment method
select force-submits shippingRefresh/paymentRefresh (JS data-refresh-target) and the server
recomputes the price from the resolver, including free_from, only on prefill, not on save.
Parameters
- $form : Form
addOrderShippingRefreshButton()
Force-submit after `tariffId` changes (JS clears shippingPrice/Vat before the click).
private
addOrderShippingRefreshButton(Form $form) : void
Parameters
- $form : Form
addOrderStockCheckFields()
Adds the stock check fieldset below "Shipping and payment" (separate so the override of the strict stock gate is visually apart from shipping/payment, which it does not relate to).
private
addOrderStockCheckFields(Form $form) : void
Overrides the strict stock gate for this save only.
IMPORTANT: it is not a checkbox. The API applies the override only when allowPartial is explicitly
bool, so a checkbox would always override 4/order.partial.adminAllowed and "leave it to the setting"
would be impossible. A tri-state select with an empty prompt sends null, meaning the key is absent
from the payload and the setting applies. Keys are words, not '1'/'0': PHP casts numeric string keys
to int and 0 is falsy, so "require full stock" would merge with "not chosen".
The prompt is dynamic: it reads 4/order.partial.adminAllowed from the settings context
(getSettingContext()->getSettings()) and picks one of two ready NEON texts (no value interpolated
into the translation, only a key choice), so the admin sees what the setting currently means.
Parameters
- $form : Form
addOrderVouchersMultiplier()
Limit from the raw HTTP data (`onAnchor`) plus `$maxCopies` as a second defense.
private
addOrderVouchersMultiplier(Form $form) : void
Parameters
- $form : Form
allowPartialAdminPromptKey()
Returns the effective `4/order.partial.adminAllowed`; missing in DB means "require full stock" (same semantics as the `$value === '1'` fallback in {@see \App\UI\Api\Eshop\Models\Managers\Traits\OrderBuildTrait::resolveAllowPartial()}, where a missing or other value is false).
private
allowPartialAdminPromptKey() : string
Return values
stringapplyOrderCustomerUserLink()
Syncs the order's user picker (`userId`, {@see \App\UI\Admin\Eshop\Presenters\Traits\Pickers\OrderCustomerPickerTrait}) and `userTypeId` from the customer record's linked user account (`Customer.userId`).
private
applyOrderCustomerUserLink(Form $form, Customer $customer) : void
userTypeId feeds the API-derived buyerType
(OrderBuildTrait::resolveBuyerType()), so it is written directly
after a customer switch, not via setControlValue(), which skips null and would leave it stale.
When userId is null (customer without an account) the field is left alone, so a manual or anonymous
admin choice survives picking a customer record without a link.
The picker label is resolved at render from the current userId value
(Picker::render()); {control picker-userId}
only has to sit inside the snippet the handler redraws (customerFields, see edit.latte).
Parameters
buildOrderPayload()
Builds the raw payload for `api/eshop/order/build`: no catalog logic here, only a transcription of form values (as in {@see \App\UI\Api\Eshop\Presenters\OrderPresenter::internalActionBuild()}). `lines` carries trusted price/vat/name/code/unit; shipping/payment go as nested `shipping`/`payment` (tariffId + price + vat).
private
buildOrderPayload(ArrayHash<string|int, mixed> $values, bool $persist) : array<string, mixed>
Parameters
- $values : ArrayHash<string|int, mixed>
- $persist : bool
Return values
array<string, mixed>callOrderBuild()
private
callOrderBuild(array<string, mixed> $payload) : array<string, mixed>
Parameters
- $payload : array<string, mixed>
Return values
array<string, mixed>controlValue()
Safely reads a field value from the container (form or multiplier row).
private
controlValue(Container $container, string $field) : mixed
Parameters
- $container : Container
- $field : string
extractOrderWarnings()
Returns the `warnings` of the `api/eshop/order/build` response (currently only the code `paymentMethodUnavailable`, see the API `OrderPresenter::internalActionBuild()` docblock) with a runtime guard to `list<string>`, like {@see orderStringKeyedArray()}.
private
extractOrderWarnings(array<string, mixed> $response) : array<int, string>
Parameters
- $response : array<string, mixed>
Return values
array<int, string>fillOrderCustomerDeliveryFromContact()
Copies the contact's delivery address (`CustomerContact.useDelivery`/`dlv*`) into the order's `useDelivery`/`customerDlv*` fields.
private
fillOrderCustomerDeliveryFromContact(Form $form, CustomerContact $contact) : void
Used by both selection paths (picker and address select) via fillOrderCustomerFieldsFromContact().
useDelivery=false on the contact means no separate delivery address, so customerDlv* are always
cleared, not just hidden; otherwise the previous contact's address would linger in the form. ?? ''
instead of a plain (string): with useDelivery=true but an empty (null) dlv* field,
setControlValue() and its is_scalar guard would skip it silently and keep the old value.
Parameters
- $form : Form
- $contact : CustomerContact
fillOrderCustomerFieldsFromContact()
Overwrites the form's `customer*` fields from the given contact using the same `fieldMap` as the client picker ({@see \App\UI\Admin\Eshop\Presenters\Traits\Pickers\OrderCustomerLookupPickerTrait::orderCustomerLookupFieldMap()}), so the data-key to form-field mapping has a single source of truth.
private
fillOrderCustomerFieldsFromContact(Form $form, CustomerContact $contact) : void
Parameters
- $form : Form
- $contact : CustomerContact
formComponent()
Calls `getComponent()` through the `Container` type, bypassing the faulty vendor annotation `@method BaseControl getComponent(string $name)` on Contributte `BootstrapContainerTrait` (the real component-model signature has 2 arguments and a nullable return; the annotation on a BootstrapForm-typed variable makes phpstan fail).
private
formComponent(Container $container, string $name) : IComponent|null
Parameters
- $container : Container
- $name : string
Return values
IComponent|nullloadOrderCustomerAccountUser()
Loads the user account by id, or null when not found.
private
loadOrderCustomerAccountUser(int $id) : User|null
Admin Manager::find() returns a SingleQuery, not an entity, hence findAll()->first() (as in
SellerPickerTrait::loadSeller()).
$this->orderCustomerUserManager is injected by OrderCustomerPickerTrait (same presenter,
properties shared across traits).
Parameters
- $id : int
Return values
User|nullonlyPricedLines()
Drops rows without a price from the payload; used only by the shipping/payment refresh, which must not fail on `linePriceRequired` while some items are still unpriced (weight/cartTotal then count only trusted rows). Final save and "Recalculate" send all rows unchanged: a missing price must fail visibly there, not vanish silently.
private
onlyPricedLines(mixed $lines) : array<int, array<string, mixed>>
Takes mixed (read back from the payload typed array<string, mixed>) and narrows the rows itself,
so callers need no casts.
Parameters
- $lines : mixed
Return values
array<int, array<string, mixed>>orderBuildFailureMessage()
Returns the preview error text from a failed `order/build` response. A technical failure (DB/ORM/transport) has an empty `message` (the detail is only in the server log) and a generic translated error is shown instead of an empty line. A domain validation message passes unchanged.
private
orderBuildFailureMessage(array<string, mixed> $response) : string
Parameters
- $response : array<string, mixed>
Return values
stringorderControlValues()
Returns the form values for the AJAX button handlers.
private
orderControlValues(Form $form) : ArrayHash<string|int, mixed>
IMPORTANT: every AJAX button handler of this form must read values from the controls, never via
$form->getValues(). The helper buttons have setValidationScope([]), so Nette marks controls outside
the scope as omitted and Container::getValues() skips them: getValues()->items returns [}],
the lines payload is empty and the API reports missing items (recalculate and shipping/payment
prefill would always fail with "the order must have at least one item"). Native "Save" validates the
whole form, so nothing is omitted there. getContainers() also forces createCopies() (multiplier
rows from httpData).
Parameters
- $form : Form
Return values
ArrayHash<string|int, mixed> —same shape getValues() would return (for buildOrderPayload(), unchanged)
orderPayloadGifts()
private
orderPayloadGifts(mixed $rows) : array<int, array{giftId: int|string, amount?: int}>
Parameters
- $rows : mixed
Return values
array<int, array{giftId: int|string, amount?: int}>orderPayloadId()
Returns an id for the build payload: canonical becomes int, empty or `0` becomes `null`. Any other value goes on unchanged, so the API (the single authority, {@see \App\UI\Api\Eshop\Models\Helpers\OrderPayloadHelper::findInvalidIdPath()}) rejects it; `intOrNull()` would turn a tampered hidden `'1.9'` into item #1 here.
private
orderPayloadId(mixed $value) : int|string|null
Parameters
- $value : mixed
Return values
int|string|nullorderPayloadLines()
Builds the trusted rows: `price`/`vat` are required on the way to the API (missing: hard fail `linePriceRequired` there, no silent catalog fallback here). The form always carries them from prefill/snapshot, so they are just transcribed.
private
orderPayloadLines(mixed $rows) : array<int, array{setId: int|string, quantity: int, orderItemId?: int|string, price?: string, vat?: string, name?: string, code?: string, unit?: string, discountPercents?: string}>
Parameters
- $rows : mixed
Return values
array<int, array{setId: int|string, quantity: int, orderItemId?: int|string, price?: string, vat?: string, name?: string, code?: string, unit?: string, discountPercents?: string}>orderPayloadPickupManual()
Returns the manually entered pickup point from the six text fields. `null` when both `code` and `name` are empty (both are required for a valid point); the API then ignores the manual path and decides only from the raw widget payload.
private
orderPayloadPickupManual(ArrayHash<string|int, mixed> $values) : array{code: ?string, name: ?string, street: ?string, city: ?string, zip: ?string, country: ?string}|null
Parameters
- $values : ArrayHash<string|int, mixed>
Return values
array{code: ?string, name: ?string, street: ?string, city: ?string, zip: ?string, country: ?string}|nullorderPayloadPickupPoint()
Decodes the hidden `pickupPoint`, which carries the raw JSON object of the widget (in the edit defaults `OrderShipper::$pickupPointData`), so the API gets the same field as the front checkout. Anything that is not a JSON object becomes `null`: the driver would not recognize an array/scalar and the fail-closed guard would turn it into `pickupPointRequired`.
private
orderPayloadPickupPoint(mixed $value) : array<string, mixed>|null
Parameters
- $value : mixed
Return values
array<string, mixed>|nullorderPayloadVoucherCodes()
private
orderPayloadVoucherCodes(mixed $rows) : array<int, string>
Parameters
- $rows : mixed
Return values
array<int, string>orderPreviewFromOrder()
Builds a deterministic preview snapshot straight from the saved order, without calling `api/eshop/order/build` (GET only, `OrderPresenter::actionEdit()`).
private
orderPreviewFromOrder(Order $order) : array<string, mixed>
The shape must be identical to OrderPayloadHelper::serializeOrder():
{snippet orderPreview} reads the same keys whether the preview came from a build (recalculate/
shippingRefresh/paymentRefresh) or from here. No repricing, only reading saved entity snapshots
(items/shipper/paymentMethod/vouchers/gifts/totals), exactly as setOrderFormDefaults() sees them.
Parameters
- $order : Order
Return values
array<string, mixed>orderSellerDefaultsFromSettings()
Returns the default seller from settings for an order that has no own snapshot (add, or a record from before snapshots). The snapshot is built from the live directory because no other source exists: right for a new order, the best available estimate for an old one, which the admin sees and can overwrite before saving.
private
orderSellerDefaultsFromSettings() : array<string, scalar|null>
Return values
array<string, scalar|null> —empty = settings have no seller (or none exists for the configured id), so the form stays empty and the required field asks the admin for one
orderSellerDefaultsWithNotice()
Returns {@see orderSellerDefaultsFromSettings()} and reports the state to the admin. The flash is part of the contract: the seller is required, so without it the admin would learn of the problem only from a save error, and with empty settings would not know where to look (the shop seller setting).
private
orderSellerDefaultsWithNotice(bool $legacyOrder) : array<string, scalar|null>
Parameters
- $legacyOrder : bool
-
true = edit of an order without its own snapshot; false = add, where prefill is expected and nothing is reported
Return values
array<string, scalar|null>orderSettingSellerId()
Resolves the seller id from settings: `4/invoicer.sellerId`, falling back to `7/invoice.default.sellerId` (invoice defaults `invoice.default.*`), the same chain the invoice issue uses ({@see \App\UI\Api\Eshop\Models\Generators\OrderInvoiceGenerator::settingIntOrNull()}). An empty string in DB means "does not exist" (`is_numeric('')` is false).
private
orderSettingSellerId() : int|null
Return values
int|nullorderStringKeyedArray()
Runtime guard to `array<string, mixed>`; the `ApiBridge` response is typed `mixed`.
private
orderStringKeyedArray(mixed $value) : array<string, mixed>|null
Parameters
- $value : mixed
Return values
array<string, mixed>|nullprefillOrderItemRows()
Picker prefill: for every row with a selected `productSetId` that has no price yet, fetches code/productSetName/unit/vat/price from the catalog (`api/eshop/order/item-prefill`, {@see \App\UI\Api\Eshop\Models\Services\OrderSnapshotBuilder::resolveItemPrefill()}).
private
prefillOrderItemRows(Form $form) : void
Rows where the admin already entered a price are skipped. Resolver query only: no order context, no saving. Quantity is left unchanged (it only feeds the price tier).
Parameters
- $form : Form
recalculateOrderForm()
Calls `build` (persist=false) with the current form data, including untouched fields.
private
recalculateOrderForm(Form $form) : void
Parameters
- $form : Form
refreshOrderCustomer()
Loads the customer with all contacts, fills the address select and prefills the fields from the first contact (same semantics as `refreshOrderSeller()`).
private
refreshOrderCustomer(Form $form) : void
A deleted or unselected customer empties the select (prompt only).
Parameters
- $form : Form
refreshOrderCustomerContact()
Ownership guard: looks the contact up only among the chosen customer's contacts (`Customer::getContact()`). A foreign or nonexistent `customerContactId` (e.g. the select was filled by AJAX from elsewhere, a race) is silently ignored, no failure.
private
refreshOrderCustomerContact(Form $form) : void
Parameters
- $form : Form
refreshOrderPayment()
Same logic as {@see refreshOrderShipping()}, for the payment method.
private
refreshOrderPayment(Form $form) : void
Parameters
- $form : Form
refreshOrderPickupPoint()
Handles the pickup widget selection (`pickupRefresh`): calls `build` (persist=false) with the raw widget object in the `pickupPoint` payload and prefills the six text fields from the snapshot in the response (`order.shipper.pickup*`). Same logic as {@see refreshOrderShipping()}, different field set.
private
refreshOrderPickupPoint(Form $form) : void
IMPORTANT: the preview (persist=false) returns no error for a missing or unusable point (contract), so a nonsense widget answer shows only as fields not being prefilled; hence no already filled values are cleared here.
Parameters
- $form : Form
refreshOrderSeller()
Loads the seller snapshot from the Invoicer directory into the form.
private
refreshOrderSeller(Form $form) : void
Uses the first contact and first bank account (same semantics as
SellerPickerTrait::sellerPrefillData()). Values are always overwritten: the admin picked the
seller again, so the current details are wanted.
Parameters
- $form : Form
refreshOrderShipping()
Calls `build` (persist=false) after the tariff changes, with the same payload as `recalculateOrderForm` but without incomplete rows (no price).
private
refreshOrderShipping(Form $form) : void
JS clears shippingPrice/shippingVat beforehand. Without dropping those rows the strict
linePriceRequired would block even the shipping prefill. Empty shippingPrice/Vat in the payload
makes buildShipperFromRow() compute fresh through ShippingPriceResolver (including free_from),
only here, not on final save.
Parameters
- $form : Form
resolveOrderCustomerIdForContactSelect()
Returns the `customerId` used to build the items of the `customerContactId` select.
private
resolveOrderCustomerIdForContactSelect() : int|null
Evaluated at form construction, not in onValidate:
- POST/AJAX (save or any force-submit): takes the submitted value from httpData
(
Nette\Http\Request::getPost()), not from the control, which has not read httpData yet at this point. OtherwiseSelectBoxdrops the submitted address because items were built from an emptycustomerId(classic dynamic-select trap, seeDependentSelectTrait). - GET edit: the saved order's
customerId(items must contain the savedcustomerContactIdthatsetOrderFormDefaults()sets). - GET add: no customer yet,
null, the select stays empty with a prompt.
Return values
int|nullresolveOrderPreviewCurrency()
Returns the preview currency from `currencyId` in the serialized `order` of the build response, mapped to the entity from `$orderCurrencyEntities` (built in `loadOrderSelectData()`, no extra query).
private
resolveOrderPreviewCurrency(array<string, mixed>|null $orderData) : Currency|null
null (missing or unknown id) makes the priceFormat filter leave numbers raw, see
OrderPresenter::orderCurrencyById().
Parameters
- $orderData : array<string, mixed>|null
Return values
Currency|nullresolveOrderSellerUseVat()
Returns whether the shop operator is a VAT payer, read live from the Invoicer seller.
private
resolveOrderSellerUseVat() : bool
IMPORTANT: do not read the settings copy 4/useVat; nothing synchronizes it with the seller, so after
the company is edited in Invoicer (VAT registered or canceled) the form would offer VAT controls that
contradict how the document will be issued. The seller is the authority.
The seller is resolved by the same chain as the invoice generator
(OrderInvoiceGenerator::buildFromOrder()): the order
snapshot order.sellerId, then 4/invoicer.sellerId, then 7/invoice.default.sellerId;
orderSettingSellerId() handles the settings half (including skipping non-numeric or <= 0
values). The seller's tax status as of today is taken from its history (Seller::resolveUseVatOn()):
a payer gets VAT controls, an identified person or non-payer does not. The per-document mode at the
tax point stays with the composer/generator.
A missing or unconfigured seller yields false (no VAT controls) and is logged; without the log VAT would vanish from the whole screen with no trace of why.
NOTE: not the seller's display flag show_prices_with_vat, which only controls how prices are shown
on the front end (the price list is always net).
Return values
boolrowValue()
private
rowValue(mixed $row, string $key) : mixed
Parameters
- $row : mixed
- $key : string
saveOrderForm()
private
saveOrderForm(ArrayHash<string|int, mixed> $values) : bool
Parameters
- $values : ArrayHash<string|int, mixed>
Return values
boolsetControlValue()
Force-sets a field value in the container (form or multiplier row) if it exists.
private
setControlValue(Container $container, string $field, mixed $value) : void
Parameters
- $container : Container
- $field : string
- $value : mixed
setOrderFormDefaults()
private
setOrderFormDefaults(Form $form) : void
Parameters
- $form : Form
syncPickupControls()
Fills the seven pickup point controls from the shipper snapshot in the `build` response: six text fields, hidden `pickupPoint` with the raw widget object (so saving unchanged keeps exactly what the widget issued) and hidden `pickupPointTariffId` with the tariff the selection belongs to.
private
syncPickupControls(Form $form, array<string, mixed>|null $shipper, int|null $tariffId) : void
IMPORTANT: without a pickup code in the snapshot (the tariff needs none, or build cleared it after a
tariff change) all seven are emptied; otherwise the previous carrier's point would stay in the fields
and the next submit would send it back.
Parameters
- $form : Form
- $shipper : array<string, mixed>|null
-
shipping snapshot from
order.shipperof the response - $tariffId : int|null
-
tariff just submitted by the form (not in the
buildresponse)
wireOrderMultiplierButtons()
Multiplier add/remove buttons, as in {@see GiftFormTrait::addGiftOfferSetFields()}.
private
wireOrderMultiplierButtons(Multiplier $multiplier, string $snippet) : void
Parameters
- $multiplier : Multiplier
- $snippet : string