CoolCMS API

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
bool

handleOpenReceiveModal()

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>

isTargetAllowed()

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
bool

requirePrivilege()

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

AdminPage entity (hydrated, hence just object)

Return values
string|null

addInitialStockFields()

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
string

buildStockTransferModalContent()

private buildStockTransferModalContent() : string
Return values
string

collectInitialStockRows()

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|null

currentUserId()

Returns the logged-in admin id, stored as `userId` of the stock document.

private currentUserId() : int|null
Return values
int|null

ensureStockItemCard()

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).

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
Return values
string
On this page

Search results