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.
Data model
Section titled “Data model”Inventory splits the product catalog from per-store stock rather than keeping one flat “products” table:
NewStock(tablenew_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(tablestore_stocks), one row per (store, stock): the per-branch pricing override and nominal quantity. Thequantitycolumn is not the source of truth for on-hand stock. See the caution below.StockEvent(tablestock_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 polymorphicreference(toDebtHistory,SupplierHistory,TransferHistory,InstallmentSaleItem, …).GrnHeader/GrnLine, a Goods Received Note: header hasgrn_number,supplier_id,total_amount,status(onlypostedexists today, perGrnStatus),import_batch_id; lines carrystock_id,quantity,buying_price,selling_price,serial_number_array, and an optionallpo_line_idlinking back to a Purchasing LPO line.ProductVariant, a sellable unit-of-measure variant of aNewStock(e.g. “carton of 24”), with its ownmultiply_factor,price/normal_price/wholesale_price.StoreVariantPriceis the per-store override of a variant’s price, mirroringStoreStockat 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; seeStockTypebelow.StoreStockSerials(tablenew_stock_serial_numbers), one row per physical serialized unit, within_stock, and nullable links to thesale,supplier,debt_history,sale_item, orreserved_by_installment_sale_idthat 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(PriceChangeSourceenum),changed_by_user_id,changes(JSON diff), polymorphicreference.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 viastock_modifier_groups.Tag/StockTag, free-form tags many-to-many withNewStockvia thestock_tagspivot (modeled explicitly asStockTag extends Pivotsince it’s also queried directly).Unit, a unit of measure (name,abbreviation) referenced byGroupedItemand product variants.
Item types and bundles
Section titled “Item types and bundles”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.
Receiving stock (GRNs)
Section titled “Receiving stock (GRNs)”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.
Bulk serial receiving
Section titled “Bulk serial receiving”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.
Stock taking
Section titled “Stock taking”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.
Pricing
Section titled “Pricing”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).
Duplicate detection
Section titled “Duplicate detection”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).
Storefront-facing endpoints
Section titled “Storefront-facing endpoints”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).
Import/export
Section titled “Import/export”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.
Route surface
Section titled “Route surface”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 |
Related
Section titled “Related”- Sales: deducts stock through
App\Services\StockService, which ultimately writesStockEventrows - Etims: reads
NewStock/category data for fiscal item mapping - StockTransfer
- Tax