Skip to content

Book 2 · Chapter 7 — Item Master & Pricing

Bizwiz Module Guide — Inventory & Stock. The "Maintain Item" area of the inventory navigation.


1. Purpose

Every physical thing Bizwiz can buy, count, break down, price, promote, or sell has to exist first as a catalogue record. This chapter covers the part of Bizwiz that creates and maintains that catalogue — the item master — and everything that controls how much an item costs the business and how much it sells for.

In plain terms, this area lets a distributor:

  • Create and describe products — give each item a stock code, name, category, subcategory, pack size, tax setting, and picture; define the units it's sold in (e.g. a piece, a dozen, a carton) and how those units convert into each other.
  • Set and change prices — the cost the business paid, the selling price, and the profit margin — either one item at a time, in bulk ("batch price change"), or through reusable price lists attached to sales routes, branches, or specific customers.
  • Control the money-sensitive bits carefully — because prices directly affect profit, most changes route through an approval workflow. Someone requests a change; a manager approves or rejects it; and every change is recorded in a price history.
  • Reserve, block, promote, and retire items — freeze stock for a specific purpose (reserved stocks), stop an item from being sold or repriced (blocked items), run promotions and hampers (gift bundles), and retire items that are no longer stocked.
  • Track true cost — the weighted average cost of an item rises and falls as new stock arrives at different prices; this area records that history.

The recurring theme is governance: creating an item, editing its critical fields, changing its price, blocking it, retiring it at a branch, or changing its stock levels almost always creates a request that a privileged user must approve before it takes effect. This chapter maps each of those request→approval loops.

A note on model naming. Almost every table and model in this area is prefixed Wa / wa_ (for example WaInventoryItem, wa_inventory_items). "WA" stands for Weighted Average — the current-generation inventory schema. Older, non-Wa tables still exist in places and are flagged as legacy where found.

---

2. Users & roles

Access is gated by permission strings of the form module___action (three underscores). The nav parent "Maintain Item" opens when a user holds maintain-items___view or is the superadmin role (config('app.allowed_role'), role_id 1). Below are the permissions that gate each function in this chapter.

Function Permission string(s) Source
View item list / "Manage Items" maintain-items___view sidebar:255, controller index:142
Add new item maintain-items___add InventoryItemController:1409
Edit item maintain-items___edit InventoryItemController:2592
View QOH across branches maintain-items___view-per-branch InventoryItemController:166
Hide QOH from a user maintain-items___restrict-qoh-visibility InventoryItemController:154
Item approval (new/edit/rejected/history/stock-status) maintain-items___item-approval sidebar:455; controller:5410
Approve branch retire maintain-items___approve-branch-retire ItemBranchStatusController:135,175
Request branch retire maintain-items___request-branch-retire ItemBranchStatusController:29
Item block approval maintain-items___item-block-approval sidebar:496
View blocked items blocked-items___view-blocked-items sidebar:313
Manage standard cost maintain-items___manage-standard-cost PriceChangeController; sidebar:280
Change cost price maintain-items___change-cost-price sidebar:281
Change selling price maintain-items___change-selling-price sidebar:282
Reserved stocks reserved-items___view / ___create / ___invoice / ___destroy / ___release / ___revert Permissions/Inventory.php
Subbin items item-subbins___view / ___update sidebar:270 (gated by activate-subbins setting)
Weighted averages weighted-average-history___view sidebar:449
Promotions promotions___view / ___add / ___edit sidebar:345
Hampers hampers___view / ___create / ___edit sidebar (Promotions submenu)
Sales basket basket___view / ___prompt-create-basket / ___create-basket / ___populate-basket / ___share-basket / ___retire-basket Permissions/Inventory.php
Price lists price-lists___view, price-lists___attach_route_branch sidebar:339; PriceListController
Additional cost price lists additional-cost-price-lists view (via can('view', ...)) sidebar:307
Transport sell price requests maintain-items___manage-standard-cost InventoryTransportPriceChangeRequestController

Typical human roles (inferred from the permission split):

  • Storekeeper / branch inventory clerk — views items and QOH for their branch, may request retire/stock-status/price changes, populates sales baskets.
  • Category / pricing manager — creates items, changes prices, builds price lists and promotions.
  • Approver / supervisor — holds item-approval, approve-branch-retire, item-block-approval; sits on the receiving end of the request queues.
  • Superadmin (role_id 1) — bypasses all permission gates and can override price-below-minimum guards.

---

3. Processes

3.1 Item master — create / edit with approval

Trigger: A user opens Inventory → Maintain Item → Manage Items (maintain-items.index) and clicks Create, or edits an existing item.

The item list is built by InventoryItemController@index. QOH ("qty on hand") is computed live by summing wa_stock_moves.qauntity [sic] for the item, optionally scoped to the user's branch unless they hold maintain-items___view-per-branch; qty-on-order sums approved, un-GRN'd purchase-order lines (InventoryItemController.php:161–189). Whether the form is a 2-step or 3-step wizard depends on the INVENTORY_SETUP administration setting: value UOM BASED enables a third UOM step (InventoryItemController.php:149).

The critical branch is the approval gate, controlled by administration setting REQUIRE_APPROVAL_FOR_NEW_OR_EDITED_INVENTORY_ITEMS (defaults to true when unset — InventoryItemController.php:1820, 3108):

stateDiagram-v2
    [*] --> Draft: user fills form
    Draft --> PendingNewApproval: store() & approval required
    Draft --> Approved: store() & approval NOT required (direct insert)
    PendingNewApproval --> Approved: item_new_approval_approve()
    PendingNewApproval --> Rejected: item_new_approval_reject()

    Approved --> PendingEditApproval: update() changes a critical field & approval required
    Approved --> Approved: update() non-critical change (applied directly)
    PendingEditApproval --> Approved: update_approval() (no blocking changes)
    PendingEditApproval --> Rejected: reject
  • Create (new item): If approval is required, no live item row is created yet. Instead a row goes into wa_inventory_item_approval_statuses with status = "Pending New Approval" and the entire proposed item captured as JSON in new_data. A Task Center task (inventory_item_new) is registered and approvers (users with maintain-items + item-approval) are notified. On approval (item_new_approval_approve, controller:2335) the real wa_inventory_items row is created, stock_id_code is generated (auto-numeric or manual), suppliers / conversion units / integration codes / branch settings are written, and a SyncApprovedItemToVscu job is dispatched (VSCU = KRA fiscal device sync). Rejection sets status = "Rejected" and cancels the task.
  • Edit (existing item): update() runs getOptimizedFieldChanges() to compute a delta. "Critical" fields (margin %, actual margin, selling price, standard cost, status, category, subcategory, tax, pack size, weights, HS code, block/allow-sell-at-loss, suppliers, integration codes) trigger approval_status = "Pending Edit Approval", store the delta as JSON in changes, and register an inventory_item_edit task. Non-critical edits apply immediately.

Screens/routes: maintain-items.index/create/store/edit/update/show/destroy (Route::resource('maintain-items', ...) at web.php:3456); item-new-approval (web.php:3421); item-approval/{status} dispatcher (web.php:3420); admin.show.approval (web.php:3427).

Outcome: A governed, audited item record; a live catalogue entry after approval.


3.2 UOM variants, categories & aliases

An item belongs to a category (wa_inventory_categories, via wa_inventory_category_id) and optionally a subcategory (wa_item_sub_categories, via item_sub_category_id). Its units of measure are rows in inventory_item_conversion_units: one base unit (is_base_unit, conversion_factor = 1), plus optional second/third units each with their own conversion_factor, selling_price, standard_cost, and additional_profit. Exactly one unit is is_default_unit. Per-branch UOM preference lives in wa_inventory_location_uom.

Aliases (alternate names/codes for an item and for a UOM) are managed inline via maintain-items.add-alias / update-alias / delete-alias and the add-uom-alias variants (web.php:2187–2192).


3.3 Selling price / cost / QOH / reorder levels & stock-status change requests

Core pricing columns on wa_inventory_items: standard_cost, selling_price, percentage_margin, actual_margin, margin_type (1 = percentage, 0 = value), last_grn_cost, weighted_average_cost, price_list_cost. QOH is not stored on the item — it is derived from wa_stock_moves. Per-branch stock policy (max stock, reorder level) lives in WaInventoryLocationStockStatus.

Changing a branch's max stock / reorder level goes through a request queue:

flowchart LR
    A[User proposes new max_stock / re_order_level] --> B[(wa_stock_status_change: status=pending)]
    B --> C{Approver acts}
    C -->|bulkApprove| D[Update WaInventoryLocationStockStatus<br/>status=approved]
    C -->|bulkReject| E[status=rejected + reason]

wa_stock_status_change stores previous vs. proposed max_stock and re_order_level, initiated_by, reviewed_by, reviewed_at, rejection_reason. Approval uses pessimistic locking and completes an inventory_stock_status task. Routes: stock-status-change-requests.index/bulk-approve/bulk-reject (web.php:3440–3442). Gated by maintain-items___item-approval (StockStatusChangeController:29).


3.4 Batch price change (the primary price-change flow)

Trigger: Inventory → Maintain Item → Price Change → Price Change (price-change.batch-requests, web.php:5455) → PriceChangeController@showBatchChangePage.

The user searches items (getItems), picks new cost/selling prices (per-item, percentage, or fixed value), and submits (processBatchPriceChange, PriceChangeController.php:283).

flowchart TD
    A[Select items + new prices] --> B{Permission?}
    B -->|manage-standard-cost / change-cost-price / change-selling-price| C[Validate parent↔child margin constraints]
    B -->|none| X[Reject]
    C --> D[Apply to wa_inventory_items directly]
    D --> E[(Write wa_inventory_item_price_history: status=Approved)]
    D --> F[(Write price_changes_timelines / PriceTimeline)]
    D --> G{create_demand_on_price_change?}
    G -->|yes, cost dropped| H[Create demand record]
    D --> I{BLOCK_SALES_ON_COST_CHANGE?}
    I -->|yes| J[Set block_this=1 on item]

Key behaviours:

  • Permission-gated at controller level — one of manage-standard-cost, change-cost-price, change-selling-price is required or the request is rejected (PriceChangeController:287–291).
  • Parent/child margin guard — child (broken-down) items must keep a growing margin over the mother's converted cost. Governed by settings MOTHER_TO_CHILD_PRICE_CHANGE_GROWING_MARGIN (default 5) and ENFORCE_CHILD_SELLING_PRICE_ON_ATTACH (default true).
  • Settings that alter behaviour: create_demand_on_price_change, SHOW_MARGIN_IN_PRICE_CHANGE, BLOCK_SALES_ON_COST_CHANGE, SEND_EMAIL_TO_CUSTOMER.

The batch flow updates the item directly (auto-approved). A separate, older request-queue path exists via wa_inventory_item_price_history rows with status = "Pending" that a user verifies through maintain-items.item_price_pending_verify (web.php:3415) — see §6 on the two coexisting mechanisms.

Outcome: Live prices updated; a history row and a price timeline row recorded; optional demand/block side effects.


3.5 Price change history

Price Change → Price Change History (maintain-items.item_price_history_list, web.php:3414). Lists non-pending history from wa_inventory_item_price_history filtered by item / supplier / branch (location_id) / date, exportable to Excel. Columns include old vs. new standard_cost, selling_price, price_list_cost, weighted_cost, last_grn_cost, initiated_by, approved_by, status, and source_action (e.g. "Price Change"). Item Centre has a parallel per-item view (item-centre.price-change-history, web.php:3452).


3.6 Transport sell price requests

Price Change → Transport sell requests (inventory-transport-price-change-requests.index, web.php:5456) → InventoryTransportPriceChangeRequestController. These are proposed selling-price changes originating from a GRN / receive-purchase-order context (a "transport sell" scenario). Stored in wa_inventory_transport_price_change_requests (columns include wa_inventory_item_id, grn_number, current_selling_price, proposed_selling_price, status, initiated_by, approved_by, reviewed_at).

stateDiagram-v2
    [*] --> pending: request created from GRN context
    pending --> approved: approve() (service applies price)
    pending --> rejected: reject()

Setting TRANSPORT_SELL_CHANGE_AUTO_APPROVE_WITHIN (default 5) implies a scheduled job auto-approves changes within N days. Gated by maintain-items___manage-standard-cost.


3.7 Cost price lists (additional cost) — setting-gated

Price Change → Cost Price Lists (additional-cost-price-lists.index, web.php-region). Only visible when setting enable-additional-cost-price-list-module is on (default 0/off; sidebar:306). Lets you layer an additional cost (delivery/handling surcharge) on top of base prices per item, as either a value or percentage, with optional rounding, and attach the list to routes / branches / customers. Tables: additional_cost_price_lists, additional_cost_price_list_items (with cost_type, rounding_applied), _routes, _branches, _customers, _audit_log. Controller AdditionalCostPriceListController.


3.8 Blocked items

Price Change → Blocked Items (blocked-items, web.php:5558). An item is "blocked" (excluded from sale/repricing) via the boolean column wa_inventory_items.block_this. Blocks can be set manually or automatically when BLOCK_SALES_ON_COST_CHANGE is enabled and a cost changes. The page (PriceChangeController@showBlockedItemsPage) lists items where block_this = 1; unblockItem sets it back to 0. Gated by any of the three price permissions; the nav link additionally checks blocked-items___view-blocked-items.

Distinct from item-block approval (§3.13). block_this on the item is the simple flag used here; the location-level, approval-driven blocking lives in a separate blocked_inventory_items table.


3.9 Price lists (route/branch/customer pricing)

Maintain Item → Price Lists (price-lists.index, web.php:5466) → PriceListController. The nav link only shows when the identify-sales-orders-from-normal-orders setting is on (sidebar:333–343). A price list is a named set of item prices that can be attached to sales routes, branches, and/or customers, so different channels see different prices.

Data model:

Table Role
price_lists header: name, status, flags (is_distribution_price_list, is_category_a_list), formula fields (has_pricing_formula, formula_base_price, formula_minimum_margin, formula_additional_profit)
price_list_items per (list, item, uom) price; unique on that triple
price_list_routes / _branches / _customers attachment pivots; also denormalised onto routes.price_list_id, wa_location_and_stores.price_list_id, wa_route_customers.price_list_id
price_list_audit_log every price change: old/new price, change_type, change_value, rounding, changed_by/at

Formula-based pricing: a list (or item) can compute price from a base cost — formula_base_price picks one of standard_cost / price_list_cost / last_grn_cost / weighted_cost — plus a percentage_markup and UOM additional_profit, giving a computed minimum price. Setting a price below that minimum triggers the price-list approval path (§3.10). Bulk category formula application is gated by UPDATE_PRICE_FORMULA_PER_CATEGORY_UPDATES_PRICELIST.

flowchart LR
    A[Create price list] --> B[Add items<br/>by filter / all / manual]
    B --> C[Set prices or formula]
    C --> D{Price ≥ formula minimum?}
    D -->|yes| E[Apply to price_list_items<br/>+ audit log]
    D -->|no, superadmin| F[Explicit confirm → apply]
    D -->|no, non-admin| G[(price_change_requests: pending)]
    E --> H[Attach to routes / branches / customers]

3.10 Price-list change approval

When a non-admin sets a price below the formula minimum, a row is created in price_change_requests (status = pending, with reason, minimum_allowed_price, loss_amount). It is reviewed via ApprovePriceListChangeController (maintain-items.approve-price-list-change / -confirm, web.php:2246–2247). Superadmins can confirm-through inline. Statuses: pending / approved / rejected / cancelled.


3.11 Reserved stocks

Maintain Item → Reserved Stocks (maintain-items.reserved-items.index, web.php:2206) → InventoryReservedItemController. Freezes a quantity of stock at a store for a purpose (e.g. a production order), so it is subtracted from "available" QOH.

Available-stock formula (store method): available = max(0, sum(wa_stock_moves.qauntity for item@store) − already-reserved-active-qty).

stateDiagram-v2
    [*] --> pending: create()
    pending --> active: store()
    active --> partially_fulfilled: partial fulfill
    active --> fulfilled: fulfill
    partially_fulfilled --> fulfilled: fulfill remainder
    active --> released: release()
    partially_fulfilled --> released: release()
    active --> cancelled: cancel()
    released --> active: revert()
    cancelled --> active: revert()
    active --> expired: reserved_until passes
    expired --> destroyed: destroy (purge)
    cancelled --> destroyed: destroy

Tables: reserved_stocks (header — reference_number RES-YYYYMMDD-NNNN, store, from/until dates, status, actor + timestamp per state, reservable_type/reservable_id), reserved_stock_items (lines with item, uom, quantity, and captured cost snapshots: selling_price, standard_cost, weighted_avg_cost, last_grn_cost, price_list_cost), and reserved_stock_penalties / employee_penalty (penalties for expired reservations). Permissions: reserved-items___view/create/invoice/destroy/release/revert.


3.12 Subbin items — setting-gated

Maintain Item → Subbin Items (item-subbins.index, web.php:2320). Only appears when the activate-subbins setting's description = '1' (sidebar:250–275). A subbin is a sub-storage location inside a bin. SubbinItemsController lists/exports/deletes item→subbin mappings via cascading branch → bin → subbin filters. Tables: item_subbin_relations (inventory_id, subbin_id), sub_bins (name, bin_id), where a "bin" is a wa_unit_of_measures row tied to a wa_location_and_stores. Permissions: item-subbins___view/update.


3.13 Item block approval (location-level)

Item Approval → Item Block Approval (item-block-approval, web.php:2235) → ItemBlockController. A richer, per-location, approval-driven block distinct from the simple block_this flag (§3.8).

stateDiagram-v2
    [*] --> pending_block: updateBlockStatus / blockAllLocations
    pending_block --> blocked: approveBlockApproval (is_approved=true)
    pending_block --> rejected: rejectBlockApproval
    blocked --> pending_unblock: unblockAllLocations (pending_unblock=true)
    pending_unblock --> unblocked: approve → soft-delete record
    pending_unblock --> blocked: reject unblock (reset flag)

Table blocked_inventory_items (item_id, location_id, blocked_by/at, is_approved, approved_by/at, pending_unblock, unblock_requested_by/at, soft-deletes; unique per item+location). Uses Task Center workflows inventory_item_block / inventory_item_unblock. It syncs the item-level block_this flag via syncItemBlockThisFlag. Permissions: maintain-items___item-block-approval, maintain-items___view-item-block.


3.14 Branch retire / restore approval

Item Approval → Pending Retire Approval (item-pending-retire-approval, web.php:3438) → ItemBranchStatusController. A user with request-branch-retire requests retiring an item at specific stores (requires QOH = 0 there); rows go into branch_retire_item_requests (location_id, item_id, action retire/restore, initiated_by, status). An approver with approve-branch-retire acts:

  • Approve retire: create WaInventoryLocationRetiredItem; if retired at all stores, set item status = 0 (global retire); cascade via ItemRetirementCascadeService::cascadeBranchRetire().
  • Approve restore: delete the retired-item row; if previously global-retired, set status = 1; cascade restore; resolve any transfers held in PENDING_REACTIVATION.
  • Reject: status = rejected; cancel task.

Task workflow: inventory_branch_retire.


3.15 Item history / log & rejected requests

Item Approval → Item History (admin.show.item.log, web.php:3428) — a DataTable-backed log (showItemLog/showItemLogDatatable/showItemLogView). Backed by wa_inventory_item_approval_statuses (each approve/reject is a row; changes JSON holds field deltas, new_data holds full new-item JSON) and update_new_item_inventory_utility_logs (wa_inventory_item_id, initiated_by, approved_by). Rejected Request (item-approval with status rejected-approval, web.php:3420) lists rejected new/edit requests.


3.16 Promotions, hampers & sales basket

Promotions (Maintain Item → Promotions → Promotions, bulk-promotions.listings, PromotionsController). Five promotion types (buy-X-get-Y-free; price discount; group buy → free; group buy → discount; group buy → redeemable discount). Header promotions table (type, supplier, branch_ids, route/POS flags, start/end dates, demand_type/demand_value); qualifiers promotion_qualifying_groups + promotion_qualifying_items; rewards promotion_group_reward_items; a denormalised POS lookup promotion_group_item_index; and legacy single-item item_promotions. Supports CSV bulk upload. States: active / deactivated (deactivated_on) / expired (ends_at).

Hampers (promotions.hampers.index, HamperController) — a gift-bundle item assembled from child items. Lifecycle: Pending (hamper inventory item created) → Approved (categories/contents defined) → Processed (stock broken down / "fed"). Tables: hampers, hamper_categories, hamper_category_items, using wa_inventory_assigned_items + wa_stock_breaking(_item) for the split.

Sales basket (sales-basket.index, SalesBasketController) — a branch stock-verification/distribution workflow: HQ prompts a branch to build a basket by a deadline → items added → sent to storekeeper for QOH verification → verified quantities distributed to route mini-baskets. Tables: sales_baskets, sales_basket_items, basket_creation_prompts, basket_bin_stock_verification, mini_basket_items, basket_log. (This is a large sub-module in its own right; only its role in the Maintain-Item nav is documented here.)


3.17 Weighted averages

Maintain Item → Weighted Averages (weighted-average-history.index, WeightedAverageHistoryController). Shows a historical log (default last 7 days) comparing an item's opening_standard_cost vs grn_standard_cost to flag cost variances caused by goods receipt. Backed by the legacy weighted_average_history table (status, date, cost columns). The live weighted-average cost sits on wa_inventory_items.weighted_average_cost and is recalculated as GRNs arrive (see §5, Interactions). Gated by weighted-average-history___view.

---

4. Tables touched & key data

Process Primary tables Key columns
Item master CRUD wa_inventory_items stock_id_code, title, slug, wa_inventory_category_id, item_sub_category_id, pack_size_id, standard_cost, selling_price, percentage_margin, actual_margin, margin_type, block_this, allow_sell_at_loss, approval_status, status, last_grn_cost, weighted_average_cost, price_list_cost, item_type, min/max_order_quantity
New/edit approval wa_inventory_item_approval_statuses wa_inventory_items_id, approval_by, status (Pending New/Edit, Approved, Rejected), changes (JSON), new_data (JSON), approval_date
Categories / UOM wa_inventory_categories, wa_item_sub_categories, inventory_item_conversion_units, wa_inventory_location_uom conversion_factor, is_base_unit, is_default_unit, per-UOM selling_price/standard_cost/additional_profit
QOH / stock policy wa_stock_moves (QOH derived), WaInventoryLocationStockStatus, wa_stock_status_change qauntity [sic]; max_stock, re_order_level; proposed vs previous, status, reviewed_by
Batch price change wa_inventory_items, wa_inventory_item_price_history, price_changes_timelines (PriceTimeline) old/new cost & price, initiated_by, approved_by, status, source_action, location_id
Price change requests (formula/legacy) price_change_requests, wa_inventory_item_price_history status, requested_price, minimum_allowed_price, loss_amount, change_type, reason
Transport sell requests wa_inventory_transport_price_change_requests grn_number, current/proposed_selling_price, status, initiated_by, approved_by
Price lists price_lists, price_list_items, price_list_routes/_branches/_customers, price_list_audit_log formula fields, per-(list,item,uom) price, attachment pivots
Additional cost price lists additional_cost_price_lists(+_items/_routes/_branches/_customers/_audit_log) additional_cost, cost_type, rounding_applied
Blocked items (flag) wa_inventory_items block_this
Item block approval (location) blocked_inventory_items item_id, location_id, is_approved, pending_unblock, soft-deletes
Branch retire branch_retire_item_requests, WaInventoryLocationRetiredItem action, status, location_id
Reserved stocks reserved_stocks, reserved_stock_items, reserved_stock_penalties reference_number, status lifecycle, cost snapshots
Subbins item_subbin_relations, sub_bins inventory_id, subbin_id, bin_id
Promotions promotions, promotion_qualifying_groups/_items, promotion_group_reward_items, promotion_group_item_index, item_promotions type, dates, reward config, POS index
Hampers hampers, hamper_categories, hamper_category_items pcs_in_hamper, conversion_factor, status
Sales basket sales_baskets, sales_basket_items, basket_creation_prompts, mini_basket_items, basket_log verification & distribution fields
Item history/log wa_inventory_item_approval_statuses, update_new_item_inventory_utility_logs changes, new_data, initiated_by, approved_by
Weighted averages weighted_average_history; wa_inventory_items.weighted_average_cost opening_standard_cost, grn_standard_cost, date, status

---

5. Interactions with other modules

  • GRN / Receiving (Book 2): Goods receipt is the main driver of cost. Each GRN feeds last_grn_cost and recomputes weighted_average_cost on the item; the weighted_average_history log records opening vs GRN cost. GRN context is also the origin of transport sell price requests (wa_inventory_transport_price_change_requests carries grn_number and links to receive-purchase-order records). QOH-on-order in the item list joins purchase orders/GRNs (InventoryItemController.php:180–188).
  • Procurement / price approvals: Formula-below-minimum price-list edits become price_change_requests approved via ApprovePriceListChangeController. Supplier data on items links to wa_inventory_item_suppliers / WaSupplier.
  • Sales & stock (Book 3/POS): Selling prices and price lists attached to routes/branches/customers determine what POS charges; promotion_group_item_index is the denormalised table POS reads for promotions. block_this and location blocks stop items from being sold. Reserved stocks reduce sellable QOH.
  • Stock breaking (Chapter in this book): Hampers and parent→child items rely on wa_stock_breaking / wa_inventory_assigned_items; the parent↔child margin guard in batch price change enforces pricing coherence across split levels.
  • Task Center / notifications: Every approval loop (new item, edit, stock-status, branch-retire, item-block/unblock) registers/completes/cancels a Task Center task and notifies approvers.
  • KRA fiscalisation (VSCU): Approving a new item dispatches SyncApprovedItemToVscu.
  • Mobile apps (retail_pay_utils): The Flutter apps cache inventory items locally via ObjectBox (retail_pay_utils/lib/objectbox.g.dart, object_box_relations_test.dart). Item master + pricing data from this module is what the mobile side consumes; details of the sync are out of scope for this chapter (see the mobile/sync chapter). [INFERENCE — confirmed only that an ObjectBox item cache exists, not the exact sync contract.]

---

6. Alternatives & variants

  • Approval on/off: REQUIRE_APPROVAL_FOR_NEW_OR_EDITED_INVENTORY_ITEMS (default true) toggles whether create/edit route through the pending queue or write live immediately. Tenants that disable it get direct edits.
  • UOM-based vs simple setup: INVENTORY_SETUP = 'UOM BASED' turns on the third form step and multi-unit conversion; otherwise items are single-unit.
  • Two price-change mechanisms coexist: (a) the batch price change page applies changes directly to the item and records Approved history/timeline rows; (b) an older pending price change queue (wa_inventory_item_price_history rows with status = "Pending", verified via item_price_pending_verify) still exists. Which one a tenant uses appears to depend on settings/permissions — see §7.
  • Two "blocking" concepts: the simple block_this flag (blocked-items page) vs the location-level, approval-driven blocked_inventory_items (item-block approval). They are synced one-way via syncItemBlockThisFlag.
  • Setting-gated nav items: Price Lists + Stock Move Report show only under identify-sales-orders-from-normal-orders; Cost Price Lists only under enable-additional-cost-price-list-module (default off); Subbin Items only under activate-subbins.
  • Transport auto-approve: TRANSPORT_SELL_CHANGE_AUTO_APPROVE_WITHIN (default 5) implies scheduled auto-approval of transport sell price requests within N days.
  • Legacy/dead code: PriceChangeController contains a large commented-out child-item cascade block (child cost/price auto-update) — parent→child cascading price changes are currently disabled there. Sidebar "Manual Cost Change" links (maintain-items.manual-cost-change, ManualPriceChangeController) are commented out in the nav (sidebar:324–329) though routes still exist. The weighted_average_history table has no recent migration and is queried directly (legacy).
  • QOH visibility: maintain-items___restrict-qoh-visibility hides QOH/QOO columns; maintain-items___view-per-branch widens QOH from own-branch to all branches.

---

7. Open questions to confirm

CODE-PROVEN (verified by reading files): - Item master model is App\Model\WaInventoryItem, table wa_inventory_items; QOH is derived from wa_stock_moves, not stored. (InventoryItemController.php:161–189) - New/edit approval uses wa_inventory_item_approval_statuses with statuses Pending New Approval / Pending Edit Approval / Approved / Rejected, JSON changes and new_data. (controller:1820, 2335, 3108; ApprovalStatus enum) - Approval gate driven by REQUIRE_APPROVAL_FOR_NEW_OR_EDITED_INVENTORY_ITEMS (default true). (controller:1820, 3108) - Batch price change applies directly + writes wa_inventory_item_price_history and price_changes_timelines. (PriceChangeController) - Price lists gated by identify-sales-orders-from-normal-orders; cost price lists by enable-additional-cost-price-list-module; subbins by activate-subbins. (sidebar:333, 306, 250) - Reserved-stock state machine and tables as in §3.11. (InventoryReservedItemController) - Item-block approval uses blocked_inventory_items + Task Center; simple flag is wa_inventory_items.block_this. (ItemBlockController, PriceChangeController)

INFERENCES / TO CONFIRM: 1. Which price-change path is authoritative per tenant — the batch (direct) path vs the legacy Pending history-verify queue. Both exist; the selection rule (setting? permission? flavor?) was not pinned down. 2. Weighted-average recompute location — the exact code that recalculates weighted_average_cost on GRN (likely in the GRN/receiving service) was not read in this chapter; confirm formula and trigger. 3. reserved-items route duplication — several route names are declared twice (e.g. reserved-items.release, .destroy at web.php:2215/2221 and 2218/2225) with different verbs/handlers; confirm which resolves and whether one is dead. 4. Transport sell auto-approve job — TRANSPORT_SELL_CHANGE_AUTO_APPROVE_WITHIN implies a scheduled command; the command/schedule entry was not located. 5. Sales basket full lifecycle and its interaction with routes/POS is only sketched here (it is arguably its own chapter). 6. Mobile sync contract — confirmed an ObjectBox item cache exists in retail_pay_utils; the sync endpoint/payload for item master + pricing was not traced. 7. Item Centre (item-centre.*) appears to be a newer per-item hub overlapping "maintain-items"; its relationship to the legacy Maintain-Item screens needs clarification. 8. item_type FK migration (2026_05_29) changed item_type from value to FK — confirm the item_types lookup table and whether both old/new forms coexist.

---

8. Source references

Navigation - bizwiz/resources/views/admin/includes/sidebar_includes/inventory.blade.php:212–372 — Maintain Item menu tree, permission gates, setting checks (activate-subbins:250, price change submenu:278–332, promotions:345–370, item approval:453–507).

Routes (bizwiz/routes/web.php) - maintain-items resource + CRUD helpers: :3456, :2185–2200, :3407–3454 - Reserved items group: :2205–2227 - Item block: :2231–2238 - Manual price change (legacy): :2242–2244 - Approve price-list change: :2246–2247 - Subbins: :2320–2324 - Price change pending/history/verify: :3413–3415 - Item approval (new/edit/rejected/history): :3420–3430 - Branch retire + stock-status change: :3436–3442 - Weighted average history resource: :3775 - Batch price change / transport / blocked items: :5455–5561 - Price lists: :5466–5514 - Sales basket: :510–543 - Promotions/hampers: bizwiz/routes/modules/inventory.php:29–54

Controllers (bizwiz/app/Http/Controllers/Admin/) - InventoryItemController.php — index:140, create:1406, store:1776 (approval gate:1820), edit:2588, update:3049 (approval gate:3108), item_new_approval:2257/2316/2335/5419, item_price_pending_list:4874, item_price_history_list:4916, item_price_pending_verify:5023, showItemLog:3428-region - PriceChangeController.php — showBatchChangePage:60, getItems:171, processBatchPriceChange:283, showBlockedItemsPage:145, getBlockedItems:213, unblockItem:256; commented child-cascade:~521–605 - PriceListController.php — formula calc:~1724–1746, savePriceChanges/below-minimum request:~1462–1564, attach:~1983–2296, getAuditLog:~2299 - InventoryTransportPriceChangeRequestController.php — index:20, approve:133, reject:149 - ApprovePriceListChangeController.php — index, confirmPriceChange - AdditionalCostPriceListController.php — index/store:24–35 - InventoryReservedItemController.php — index:46, create:85, store:297 (available calc:407), release:815, revert:1442 - SubbinItemsController.php — index:32, datatable:75, delete:125 - WeightedAverageHistoryController.php — index:26 - ItemBlockController.php — updateBlockStatus:36, blockAllLocations:137, pendingApproval:268, approveBlockApproval:371, rejectBlockApproval:389 - ItemBranchStatusController.php — request:25, pendingRequests:133, approve:173 - StockStatusChangeController.php — index:24, bulkApprove:91, bulkReject:178 - PromotionsController.php — promotionsListings:92, processPromotions:273; HamperController.php — index:38, store:104, storeHamperCategories:261, feedStocks:575; SalesBasketController.php — showBasketsPage:35, createBasket:343, addItemToBasket:465

Models - bizwiz/app/Model/WaInventoryItem.php:34 (relations from :309) - bizwiz/app/Models/WaInventoryItemApprovalStatus.php, bizwiz/app/Enums/Status/ApprovalStatus.php - bizwiz/app/Model/PriceChangeRequest.php, bizwiz/app/Models/PriceTimeline.php, bizwiz/app/Models/WaInventoryTransportPriceChangeRequest.php - bizwiz/app/Model/PriceListItem.php, PriceListAuditLog.php, AdditionalCostPriceList.php, AdditionalCostPriceListItem.php - bizwiz/app/Models/InventoryItemConversionUnit.php, bizwiz/app/Model/WaInventoryLocationUom.php, WaInventoryLocationStockStatus.php, WaStockStatusChange.php, BranchRetireItemRequest.php, BlockedInventoryItem.php

Migrations (bizwiz/database/migrations/) - 2023_09_08_134414_create_wa_inventory_items_table.php - 2024_04_18_100057_*_approval_statuses, 2024_04_30_110257_*_new_data - 2026_01_18_120000_create_price_change_requests_table.php - 2025_12_31_160602_create_price_lists_table.php (+ _items, _routes, _branches, _customers, _audit_log sibling migrations) - 2026_01_06_150146_add_formula_fields_to_wa_inventory_items_table.php, 2026_07_16_112014_add_percentage_markup... - 2026_02_18_100000_create_additional_cost_price_lists_table.php (+ siblings), 2026_02_19_100000_add_setting_to_enable_additional_cost_price_list_module.php - 2026_04_22_120000_create_wa_inventory_transport_price_change_requests_table.php - 2026_04_30_084719_create_reserved_stocks_table.php - 2025_03_10_085708_create_item_subbin_relations_table.php - 2026_05_22_103044_create_blocked_inventory_items_table.php - 2026_06_02_120000_*_wa_stock_status_change - 2025_05_21_124730_create_hampers_table.php

Permissions - bizwiz/app/Permissions/Inventory.php — maintain-items___* (~:340–394), reserved-items___*, item-subbins___*, promotions___*, hampers___*, basket___*, weighted-average-history___view.

Mobile - retail_pay_utils/lib/objectbox.g.dart, retail_pay_utils/lib/test/object_box_relations_test.dart — local ObjectBox item cache.