StockOperationTrait uses trait:short
Stock operations (receipt and transfer between warehouses) inside the variant modal, without leaving product editing.
All writes go through the existing bridge endpoints api/store/stock-document/receive and
transfer (the same path as StockDocumentPresenter),
called through the injected $this->apiBridge.
The forms open in a sub-modal (getModalWindow() plus a rendered partial) layered over the
variant modal, see PriceSnapshotTrait::showPriceSnapshotModal() and
ModalAwareTrait. After a successful
Store action the handler must not call $presenter->redirect(), which would close the variant
modal; it calls reloadEditedSet() (the constructor-time editedSet is stale) and queues
formNotices[] instead.
The stock item card is always created automatically: a new variant gets it right after saving (FormTrait::saveProductVariantForm()), a legacy variant without a card gets it at its first stock action (ensureStockItemCard()).
Table of Contents
Constants
- PRIVILEGE_STOCK_CARD : mixed = ':Admin:Store:StockItem:add'
- Creating the missing stock item card of a variant writes to the warehouse (`:Api:Eshop:ProductVariant:foundStockItemCard`).
- PRIVILEGE_STOCK_MOVE : mixed = ':Admin:Store:StockDocument:add'
- Stock actions of the variant editor (receipt, transfer) write a stock document, so they check the same admin privilege as the Store module. The Api enforces `:Api:Store:StockDocument:receive` / `transfer` independently; this is its admin counterpart, not a replacement.
Methods
- canMoveStock() : bool
- Returns whether the logged-in admin may move stock from the variant editor.
- handleOpenReceiveModal() : void
- Opens the receipt sub-modal.
- handleOpenTransferModal() : void
- Opens the transfer sub-modal.
- onStockReceiveSuccess() : void
- onStockTransferSuccess() : void
- createComponentStockReceiveForm() : Form
- createComponentStockTransferForm() : Form
- isTargetAllowed() : bool
- Returns whether the logged-in user may open the given admin target. For visibility (tiles, autocomplete, buttons); it does not throw.
- requirePrivilege() : void
- Throws 403 when the logged-in user lacks the given admin privilege.
- resolvePageTarget() : string|null
- Returns the RBAC key of the target page: `<presenter>:<action>`, e.g. `:Admin:System:User:show`.
- addInitialStockFields() : void
- Adds the per-warehouse initial stocking fields (quantity, price) and the shared currency.
- buildStockReceiveModalContent() : string
- buildStockTransferModalContent() : string
- collectInitialStockRows() : array<int, array{quantity: float, unitPrice: float|null}>
- Collects the initial stocking rows to be sent as receipts.
- currentStockItemId() : int|null
- Stock item id of the edited variant; null when it has no card yet ({@see ensureStockItemCard()} creates it at the first stock action).
- currentUserId() : int|null
- Returns the logged-in admin id, stored as `userId` of the stock document.
- ensureStockItemCard() : void
- Creates the stock item card of a legacy variant at its first stock action (receipt or transfer); no-op when the card already exists.
- loadStockLevels() : array<int, array{warehouseName: string, quantity: string, reserved: string, available: string}>
- Returns the read-only rows of the per-warehouse stock table.
- loadWarehouseOptions() : array<int, NotTranslate>
- Returns the active warehouses for a select (id => "code — name"), only `activated=true`.
- requireStockMovePrivilege() : void
- Enforces the stock privileges, one set for opening the sub-modal and for its submit.
- showReceiveModal() : void
- showTransferModal() : void
- warehouseLabel() : string
- Returns `code — name` (name in the current language, falling back to the first non-empty one).
Constants
PRIVILEGE_STOCK_CARD
Creating the missing stock item card of a variant writes to the warehouse (`:Api:Eshop:ProductVariant:foundStockItemCard`).
private
mixed
PRIVILEGE_STOCK_CARD
= ':Admin:Store:StockItem:add'
PRIVILEGE_STOCK_MOVE
Stock actions of the variant editor (receipt, transfer) write a stock document, so they check the same admin privilege as the Store module. The Api enforces `:Api:Store:StockDocument:receive` / `transfer` independently; this is its admin counterpart, not a replacement.
private
mixed
PRIVILEGE_STOCK_MOVE
= ':Admin:Store:StockDocument:add'
Methods
canMoveStock()
Returns whether the logged-in admin may move stock from the variant editor.
public
canMoveStock() : bool
IMPORTANT: one policy for all three places (button visibility, the server-side signal gate, the wizard initial stocking). Without it the UI offers receipt and transfer to anyone who may edit the variant, a click tries to create the stock item card, and only the Api then rejects the operation with a generic error. Public because the template calls it.
Return values
boolhandleOpenReceiveModal()
Opens the receipt sub-modal.
public
handleOpenReceiveModal() : void
A legacy variant without a card gets it here (ensureStockItemCard()) before the
noStockItem guard is evaluated. The AJAX modal never redirects, so a failed guard just
stays in place with a message.
handleOpenTransferModal()
Opens the transfer sub-modal.
public
handleOpenTransferModal() : void
Creates a missing card like handleOpenReceiveModal(). Guards: the stock item card (nothing to transfer without it) and at least 2 active warehouses (nowhere to transfer to).
onStockReceiveSuccess()
public
onStockReceiveSuccess(Form $form, ArrayHash<string|int, mixed> $values) : void
Parameters
- $form : Form
- $values : ArrayHash<string|int, mixed>
onStockTransferSuccess()
public
onStockTransferSuccess(Form $form, ArrayHash<string|int, mixed> $values) : void
Parameters
- $form : Form
- $values : ArrayHash<string|int, mixed>
createComponentStockReceiveForm()
protected
createComponentStockReceiveForm() : Form
Return values
FormcreateComponentStockTransferForm()
protected
createComponentStockTransferForm() : Form
Return values
FormisTargetAllowed()
Returns whether the logged-in user may open the given admin target. For visibility (tiles, autocomplete, buttons); it does not throw.
protected
isTargetAllowed(string|null $key) : bool
IMPORTANT: fail-closed like requirePrivilege(): an empty key and a presenter
without isAllowed() both mean no. A component that skips this question lists the
target to everyone.
Parameters
- $key : string|null
Return values
boolrequirePrivilege()
Throws 403 when the logged-in user lacks the given admin privilege.
protected
requirePrivilege(string $key) : void
Call it as the first statement of a mutating signal, before loading data, so the check cannot be bypassed by something being written first.
Parameters
- $key : string
resolvePageTarget()
Returns the RBAC key of the target page: `<presenter>:<action>`, e.g. `:Admin:System:User:show`.
protected
static resolvePageTarget(object|null $page) : string|null
Same interpretation as Menu::filterByPrivileges(), BasePresenter::authorizeListing() and PrivilegeCheckTrait::checkCurrentPrivilege(). The key is derived from the page target only, never from a file or class name.
Returns null when the page is missing or has no presenter, so there is nothing to allow.
Parameters
- $page : object|null
-
AdminPageentity (hydrated, hence justobject)
Return values
string|nulladdInitialStockFields()
Adds the per-warehouse initial stocking fields (quantity, price) and the shared currency.
private
addInitialStockFields(Form $form) : void
Wizard only (new variant, the isWizard() block of FormTrait::createComponentEditForm()).
The fields sit directly on the edit form, without a multiplier, because warehouses are a
fixed set. All fields are optional: zero or empty means no receipt for that warehouse, see
collectInitialStockRows().
Parameters
- $form : Form
buildStockReceiveModalContent()
private
buildStockReceiveModalContent() : string
Return values
stringbuildStockTransferModalContent()
private
buildStockTransferModalContent() : string
Return values
stringcollectInitialStockRows()
Collects the initial stocking rows to be sent as receipts.
private
collectInitialStockRows(ArrayHash<string|int, mixed> $values) : array<int, array{quantity: float, unitPrice: float|null}>
Only warehouses with a quantity above zero are included; zero or empty means the admin does not stock that warehouse and no receipt is created.
Parameters
- $values : ArrayHash<string|int, mixed>
Return values
array<int, array{quantity: float, unitPrice: float|null}> —row per warehouse id
currentStockItemId()
Stock item id of the edited variant; null when it has no card yet ({@see ensureStockItemCard()} creates it at the first stock action).
private
currentStockItemId() : int|null
Return values
int|nullcurrentUserId()
Returns the logged-in admin id, stored as `userId` of the stock document.
private
currentUserId() : int|null
Return values
int|nullensureStockItemCard()
Creates the stock item card of a legacy variant at its first stock action (receipt or transfer); no-op when the card already exists.
private
ensureStockItemCard() : void
The admin has no picker or checkbox to create the card by hand. $this->variantId is
always a saved variant here, because the sub-modals open only from editing; in the wizard
the card is created only by FormTrait::saveProductVariantForm(). The edited set is
reloaded before currentStockItemId() is read again, since the constructor-time
editedSet is stale. Best effort: on failure currentStockItemId() stays null and the
caller shows the noStockItem message.
loadStockLevels()
Returns the read-only rows of the per-warehouse stock table.
private
loadStockLevels(int $stockItemId) : array<int, array{warehouseName: string, quantity: string, reserved: string, available: string}>
Never cache: stock levels must come from a live query, same as loadAvailability().
Parameters
- $stockItemId : int
Return values
array<int, array{warehouseName: string, quantity: string, reserved: string, available: string}>loadWarehouseOptions()
Returns the active warehouses for a select (id => "code — name"), only `activated=true`.
private
loadWarehouseOptions() : array<int, NotTranslate>
The name is already translated DB data, not a key, hence NotTranslate; otherwise the form
domain translator looks it up as a key and Tracy reports a missing translation.
warehouseLabel() returns a plain string because it is also used outside selects.
Return values
array<int, NotTranslate>requireStockMovePrivilege()
Enforces the stock privileges, one set for opening the sub-modal and for its submit.
private
requireStockMovePrivilege() : void
IMPORTANT: the submit of stockReceiveForm / stockTransferForm is a separate routable
POST; without the check there a role with Product:edit but no Store privileges could
trigger a receipt or transfer by a hand-built POST. Call it as the first statement, before
ensureStockItemCard() and before calling the Api. The visibility counterpart is
canMoveStock() (same pair of keys).
showReceiveModal()
private
showReceiveModal() : void
showTransferModal()
private
showTransferModal() : void
warehouseLabel()
Returns `code — name` (name in the current language, falling back to the first non-empty one).
private
warehouseLabel(Warehouse $warehouse) : string
Parameters
- $warehouse : Warehouse