Skip to content

Inventory

Location: dg_smart_pos_api/modules/Inventory. The largest module in the codebase by model and controller count, and the one Sales deducts against. It owns the product catalog (NewStock), per-store on-hand quantity and pricing (StoreStock), goods receiving (GRNs), serials, stock taking (physical counts), variants/SKUs/units of measure, grouped items (bundles), modifier groups (restaurant add-ons), tags, and item properties. Inventory is one of the modules under the core architecture change freeze: bug fixes continue, new features are paused.

Inventory is enabled in config/modules.php and is a tenant route module: its routes are required directly from routes/tenant.php (require base_path('modules/Inventory/Routes/stock.php')), not auto-loaded through the generic api-prefixed loader in ModulesServiceProvider::boot(). config/modules.php records Inventory’s own dependency graph as bidirectional with Sales and Ecommerce (“the graph contains real cycles”), which is why the registry treats module dependencies as descriptive metadata rather than a boot order.

Inventory splits the product catalog from per-store stock rather than keeping one flat “products” table:

  • NewStock (table new_stocks), the catalog/master item: name, SKU, global (catalog) selling/minimum/buying price, category, brand, type (see below), track_stock, require_serial, specifications, images. Not store-scoped; every store either inherits its global price or overrides it.
  • StoreStock (table store_stocks), one row per (store, stock): the per-branch pricing override and nominal quantity. The quantity column is not the source of truth for on-hand stock. See the caution below.
  • StockEvent (table stock_events), an append-only ledger of every quantity change: event_type (purchase, sale, adjustment, wastage, transfer_in/transfer_out, sale_return, stock_taking, stock_taking_reversal, opening_balance, production_consume/output, repair_*, installment_reserve/release, job_*, etc.), quantity_before/quantity_after, unit_cost, and a polymorphic reference (to DebtHistory, SupplierHistory, TransferHistory, InstallmentSaleItem, …).
  • GrnHeader / GrnLine, a Goods Received Note: header has grn_number, supplier_id, total_amount, status (only posted exists today, per GrnStatus), import_batch_id; lines carry stock_id, quantity, buying_price, selling_price, serial_number_array, and an optional lpo_line_id linking back to a Purchasing LPO line.
  • ProductVariant, a sellable unit-of-measure variant of a NewStock (e.g. “carton of 24”), with its own multiply_factor, price/normal_price/wholesale_price. StoreVariantPrice is the per-store override of a variant’s price, mirroring StoreStock at the variant level.
  • Sku, a barcode/SKU string attached to a stock (a stock can have several).
  • GroupedItem, a bundle’s component line: parent_stock_id, component_stock_id, quantity_in_base_unit, unit_id. Selling the parent deducts the components, never the parent’s own (non-existent) stock; see StockType below.
  • StoreStockSerials (table new_stock_serial_numbers), one row per physical serialized unit, with in_stock, and nullable links to the sale, supplier, debt_history, sale_item, or reserved_by_installment_sale_id that currently holds it.
  • StockTakingSession / StockTakingEntry / StockTakingEntryIdempotency, a per-store physical count: a session (in_progress, completed, discarded, recount_in_progress) containing per-stock entries (counted_quantity, system_quantity_snapshot, applied_delta, plus a full second-count/recount trail: second_counted_quantity, recount_status, recount_requested_by). Entries are idempotency-keyed so a retried POST from a flaky POS connection doesn’t double-count.
  • StorePriceHistory, an audit row per price change: source (PriceChangeSource enum), changed_by_user_id, changes (JSON diff), polymorphic reference.
  • StockDuplicateSuggestion, output of a background duplicate-name-detection job; group_key, reason, items (JSON), resolved/dismissed by a user.
  • ModifierGroup / ModifierOption, restaurant-mode add-on groups (e.g. “Size”, required/min/max selections) and their priced options, attached to stock many-to-many via stock_modifier_groups.
  • Tag / StockTag, free-form tags many-to-many with NewStock via the stock_tags pivot (modeled explicitly as StockTag extends Pivot since it’s also queried directly).
  • Unit, a unit of measure (name, abbreviation) referenced by GroupedItem and product variants.

NewStock.type is a StockType enum: stock, service, bundle, menu_item. Only stock “holds inventory” (StockType::holdsInventory()). Services and menu items are never stocked and never checked for availability. A bundle holds no stock of its own either: StockType::isBundle() routes sales/returns to deduct or restock its GroupedItem components instead of the bundle row itself.

Catalog and per-store stock (InventoryStoreStockController)

Section titled “Catalog and per-store stock (InventoryStoreStockController)”

index lists store stock (or, with for_stock_taking=1, a stock-taking-shaped projection); show/update/destroy operate on one (store, stock) pair. update (PUT/PATCH stock/{stock}) is a quantity adjustment (feeds InventoryStockService, which writes a StockEvent), not a generic field editor, per the route file’s own comment. storePresence/addToStores/removeFromStores/addStocksToStore manage which stores carry a given product at all. stockLocations returns a paginated stock-by-store matrix with event-derived quantities and prices. moveQuantities moves quantity between stores directly.

InventoryGrnController::store (CreateGrnRequest: supplier_id, received_date, notes, lines[] each with stock_id, quantity, buying_price, optional selling_price/minimum_price/serial_numbers/lpo_line_id) calls GrnService::createAndPost inside a DB transaction, and, per line, if lpo_line_id is set, calls LpoService::applyGrnFulfillment in the same transaction so a Purchasing LPO’s fulfillment commits or rolls back atomically with the GRN. Cashiers can be blocked from receiving stock per store via the cashier_can_add_from_supplier_warehouse/_shop system config keys, checked before the transaction even starts.

There’s also a lighter-weight, non-GRN path: InventoryStockInventoryController::store (stock/{stock}/inventory, InventoryAddStockInventoryRequest) adds inventory for a single stock/supplier without going through the GRN header/line structure, and enforces “serials required” only when both the store’s serial_stock_required config and the stock’s own require_serial are true (skipped entirely for warehouses). InventoryStockInventoryController::create (stock/create) creates a brand-new catalog item by name only (InventoryCreateStockRequest: stock_name unique, category, optional require_serial/type/track_stock); quantity is added afterward via one of the above.

BulkSerialReceivingController (grns/bulk-serial-receive/*) lets a supplier’s already-serialized units be imported as one GRN from a template/CSV: template → validate (async, returns a validationId) → poll validationStatus → optionally download invalidRowsCsv → commit. Gated behind both the enable_bulk_serial_receiving system config (off by default) and the inventory:bulk-receive permission, since a single commit can write thousands of stock/serial rows.

InventoryStockTakingController runs one active session per store at a time. start locks and checks-then-creates inside a transaction so two concurrent requests can’t create two in-progress sessions. saveEntry/removeEntry record counts against the session (idempotency-keyed via StockTakingEntryRequest); complete closes counting. Adjustments are not applied automatically: applyAllAdjustments and reverseAllAdjustments are separate, permission-gated steps (inventory:apply-stock-taking-adjustments) from performing the count itself (inventory:perform-stock-taking), and each adjustment application is tagged with an apply_batch_id so it can be reversed as one unit. A completed session can go through a second round: requestRecount → saveRecountEntry → completeRecount, tracked on the same StockTakingEntry rows via the second_* / recount_* columns. discard abandons the session outright.

InventoryStockPricingController separates global (catalog) price on NewStock from a per-store override on StoreStock/StoreVariantPrice: showGlobal/updateGlobal read/write the catalog price directly; update (PUT stock/{stock}/pricing) writes a store-specific override; resetToGlobal deletes a store’s override so it goes back to inheriting the catalog price live; branchPricing returns the global price plus every visible branch’s resolved price side by side, for the branch-pricing table. syncSharedPrices (behind share_buying_prices_across_stores) prefills stores that have no price row yet from the shared buying/selling price, rather than overwriting existing overrides.

Serials, SKUs, units, variants, tags, modifier groups, item properties

Section titled “Serials, SKUs, units, variants, tags, modifier groups, item properties”

Each of these is a fairly self-contained CRUD surface: InventorySerialNumberController (list/show/delete by serial, per-stock listing, and a “short-unused” prune workflow for serials that were reserved but never sold), InventorySkuController/InventoryStockSkuController (standalone SKU CRUD vs. per-stock attach), InventoryUnitController, InventoryProductVariantController (apiResource, per stock), InventoryTagController/InventoryStockTagController (standalone tag CRUD vs. per-stock sync/attach/detach), InventoryModifierGroupController (group + option CRUD, per-stock assignment, and bulk assign-by-category), ItemPropertyDefinitionController/StockPropertyController (defines what properties a business tracks per category, and records the actual values per item), InventoryGroupedItemController (bundle component CRUD, apiResource-style under stock/{stock}/components).

StockDuplicateSuggestionController exposes a background job’s output: index (polled continuously by every role for a toolbar badge, hence its own lighter inventory:view-duplicates permission separate from inventory:manage-duplicates), destroy (dismiss a suggestion), triggerCheck (force a re-run).

StockStorefrontController and CategoryStorefrontController manage the Ecommerce-facing tab of a stock/category (description, images, specifications) scoped to the current store’s own storefront. CategoryStorefrontController and the top of stock.php also import Modules\Ecommerce\Http\Controllers\StorefrontSettingsController directly for the website-wide purchase-channel settings, one of the concrete places Inventory and Ecommerce reference each other (the cycle config/modules.php documents).

InventoryCompatibilityController covers CSV/JSON catalog export, CSV catalog import, a separate “initial stock” import/template pair, a separate pricing import/export/template pair, and a “manage stock” CSV import that runs as a tracked async job (getManageStockImportStatus polls by importId) rather than synchronously, per docs/’s note on the Redis inventory-import queue.

All under auth:sanctum, prefix inventory/stores/{store}, registered via modules/Inventory/Routes/stock.php (required once from routes/tenant.php, not duplicated elsewhere the way Sales’ routes are):

Method Path Controller method
GET warehouses InventoryWarehousesController@index
POST warehouses InventoryWarehousesController@store
GET stock-locations InventoryStoreStockController@stockLocations
GET stock/{stock}/store-presence InventoryStoreStockController@storePresence
POST stock/{stock}/store-presence InventoryStoreStockController@addToStores
DELETE stock/{stock}/store-presence InventoryStoreStockController@removeFromStores
POST store-presence/bulk InventoryStoreStockController@addStocksToStore
GET stock InventoryStoreStockController@index
GET stock/{stock} InventoryStoreStockController@show
PUT/PATCH stock/{stock} InventoryStoreStockController@update (quantity adjustment)
DELETE stock/{stock} InventoryStoreStockController@destroy
PUT stock/{stock}/base-unit InventoryStoreStockController@updateBaseUnit
PATCH stock/{stock}/production-settings InventoryStoreStockController@updateProductionSettings
POST stock/{stock}/images StockImageController@store
DELETE stock/{stock}/images/{position} StockImageController@destroy
GET stock/{stock}/storefront StockStorefrontController@show
PUT stock/{stock}/storefront StockStorefrontController@update
PUT stock/{stock}/storefront/specifications StockStorefrontController@updateSpecifications
POST stock/{stock}/storefront/description-image StockStorefrontController@uploadDescriptionImage
GET storefront-settings StorefrontSettingsController@show (Ecommerce module)
PUT storefront-settings StorefrontSettingsController@update (Ecommerce module)
GET duplicates StockDuplicateSuggestionController@index
DELETE duplicates/{id} StockDuplicateSuggestionController@destroy
POST duplicates/trigger-check StockDuplicateSuggestionController@triggerCheck
GET stock-taking/session InventoryStockTakingController@show
POST stock-taking/start InventoryStockTakingController@start
POST stock-taking/session/entries InventoryStockTakingController@saveEntry
DELETE stock-taking/session/entries/{stock} InventoryStockTakingController@removeEntry
POST stock-taking/complete InventoryStockTakingController@complete
POST stock-taking/apply-all-adjustments InventoryStockTakingController@applyAllAdjustments
POST stock-taking/reverse-all-adjustments InventoryStockTakingController@reverseAllAdjustments
GET stock-taking/sessions InventoryStockTakingController@history
GET stock-taking/sessions/{session} InventoryStockTakingController@report
POST stock-taking/sessions/{session}/recount/request InventoryStockTakingController@requestRecount
POST stock-taking/sessions/{session}/recount/entries InventoryStockTakingController@saveRecountEntry
POST stock-taking/sessions/{session}/recount/complete InventoryStockTakingController@completeRecount
POST stock-taking/discard InventoryStockTakingController@discard
POST stock/{stock}/move-quantities InventoryStoreStockController@moveQuantities
GET pricing InventoryStockPricingController@index
POST pricing/sync-shared-prices InventoryStockPricingController@syncSharedPrices
PUT stock/{stock}/pricing InventoryStockPricingController@update
GET stock/{stock}/price-history InventoryPriceHistoryController@index
GET stock/{stock}/global-price InventoryStockPricingController@showGlobal
PUT stock/{stock}/global-price InventoryStockPricingController@updateGlobal
POST stock/{stock}/reset-to-global InventoryStockPricingController@resetToGlobal
GET stock/{stock}/branch-pricing InventoryStockPricingController@branchPricing
PUT stock/{stock}/prices InventoryStockPricingController@update (deprecated alias)
GET grns InventoryGrnController@index
POST grns InventoryGrnController@store
GET grns/export InventoryGrnController@export
GET grns/{grn} InventoryGrnController@show
GET grns/bulk-serial-receive/template BulkSerialReceivingController@template
POST grns/bulk-serial-receive/validate BulkSerialReceivingController@validate
GET grns/bulk-serial-receive/validate/{validationId} BulkSerialReceivingController@validationStatus
GET grns/bulk-serial-receive/validate/{validationId}/errors.csv BulkSerialReceivingController@invalidRowsCsv
POST grns/bulk-serial-receive/commit BulkSerialReceivingController@commit
POST stock/{stock}/inventory InventoryStockInventoryController@store
POST stock/create InventoryStockInventoryController@create
POST stock/transfers InventoryStockTransferController@store
POST stock/transfers/receive InventoryStockTransferController@receive
GET stock/categories InventoryStockCategoryController@index
GET stock/categories/{category} InventoryStockCategoryController@getStockByCategory
PUT stock/{stock}/categories InventoryStockCategoryController@update
PUT categories/{category}/serial-requirement InventoryStockCategoryController@updateSerialRequirement
GET categories/{category}/storefront CategoryStorefrontController@show
PUT categories/{category}/storefront CategoryStorefrontController@update
POST categories/{category}/storefront/image CategoryStorefrontController@uploadImage
DELETE categories/{category}/storefront/image CategoryStorefrontController@deleteImage
GET stock/{stock}/history InventoryStockHistoryController@index
GET event-history InventoryStockHistoryController@storeIndex
POST customers/{customer}/stock/returns InventoryStockReturnController@store
GET serials InventorySerialNumberController@index
GET serials/short-unused InventorySerialNumberController@shortUnusedPreview
DELETE serials/short-unused InventorySerialNumberController@pruneShortUnused
GET serials/{serial} InventorySerialNumberController@show
DELETE serials/{serial} InventorySerialNumberController@destroy
GET stock/{stock}/serials InventorySerialNumberController@getByStock
GET/POST/PUT/DELETE units (apiResource) InventoryUnitController
GET/POST/PUT/DELETE stock/{stock}/product-variants (apiResource) InventoryProductVariantController
GET/POST/PUT/DELETE stock/{stock}/components (apiResource) InventoryGroupedItemController
GET/POST/PUT/DELETE skus (apiResource) InventorySkuController
GET stock/{stock}/skus InventoryStockSkuController@index
POST stock/{stock}/skus InventoryStockSkuController@store
DELETE stock/{stock}/skus/{sku} InventoryStockSkuController@destroy
GET/POST/PUT/DELETE modifier-groups, modifier-groups/{g}, options InventoryModifierGroupController
GET stock/{stock}/modifier-groups InventoryModifierGroupController@forStock
POST stock/{stock}/modifier-groups/sync InventoryModifierGroupController@syncForStock
POST modifier-groups/{g}/assign-category InventoryModifierGroupController@assignToCategory
GET/POST/PUT/DELETE tags (apiResource) InventoryTagController
GET stock-tags InventoryStockTagController@index
GET stock/{stock}/tags InventoryStockTagController@byStock
POST stock/{stock}/tags/sync InventoryStockTagController@sync
POST stock/{stock}/tags/{tag} InventoryStockTagController@attach
DELETE stock/{stock}/tags/{tag} InventoryStockTagController@detach
GET/POST/PUT/DELETE item-properties, item-properties/{d}, category attach/detach ItemPropertyDefinitionController
GET stock/{stock}/properties StockPropertyController@show
PUT stock/{stock}/properties StockPropertyController@update
GET compatibility/export/csv | /json InventoryCompatibilityController@exportCsv | exportJson
POST compatibility/import/csv InventoryCompatibilityController@importCsv
GET compatibility/import/template InventoryCompatibilityController@getImportTemplate
GET/POST compatibility/initial-stock/* InventoryCompatibilityController@getInitialStockTemplate | importInitialStockCsv
GET/POST compatibility/pricing/* InventoryCompatibilityController@getPricingTemplate | exportPricingCsv | importPricingCsv
GET/POST compatibility/manage-stock/* InventoryCompatibilityController@getManageStockTemplate | getManageStockImportStatus | importManageStockCsv
  • Sales: deducts stock through App\Services\StockService, which ultimately writes StockEvent rows
  • Etims: reads NewStock/category data for fiscal item mapping
  • StockTransfer
  • Tax