Overview
Torn implements a Kardex-based inventory system with immutable stock movements for complete traceability. Every inventory change is recorded as a StockMovement with timestamp, user, and reason.
Architecture
Product Stock Control
Products have two key inventory fields:
class Product :
controla_stock: bool = False # Enable/disable stock tracking
stock_actual: Decimal = 0 # Current available quantity
stock_minimo: Decimal = 0 # Reorder point threshold
Source: Referenced in /app/models/product.py
Only products with controla_stock = true will have stock validation during sales. Non-tracked products (services, digital goods) can be sold without quantity limits.
Stock Movement Model
Every inventory transaction creates an immutable audit record:
class StockMovement :
product_id: int # Product affected
user_id: int # Who made the change
tipo: str # "ENTRADA" or "SALIDA"
motivo: str # "VENTA", "COMPRA", "AJUSTE", "DEVOLUCION", "INICIAL"
cantidad: Decimal # Absolute quantity
fecha: datetime # Timestamp
balance_after: Decimal # Stock snapshot after movement
description: str # Free-text explanation
sale_id: int # Link to sale if applicable
Source: /app/models/inventory.py:10-54
Checking Inventory Levels
Retrieve current stock for all active products:
curl -X GET https://api.torn.cl/inventory/ \
-H "Authorization: Bearer YOUR_TOKEN"
Response:
[
{
"id" : 15 ,
"codigo_interno" : "MART-001" ,
"nombre" : "Martillo Carpintero" ,
"precio_neto" : 8500.00 ,
"controla_stock" : true ,
"stock_actual" : 47.00 ,
"stock_minimo" : 10.00 ,
"unidad_medida" : "unidad" ,
"is_active" : true
},
{
"id" : 42 ,
"codigo_interno" : "TAL-042" ,
"nombre" : "Taladro Eléctrico" ,
"precio_neto" : 12411.76 ,
"controla_stock" : true ,
"stock_actual" : 8.00 ,
"stock_minimo" : 5.00 ,
"unidad_medida" : "unidad" ,
"is_active" : true
},
{
"id" : 73 ,
"codigo_interno" : "SERV-001" ,
"nombre" : "Instalación" ,
"precio_neto" : 25000.00 ,
"controla_stock" : false ,
"stock_actual" : 0 ,
"unidad_medida" : "servicio" ,
"is_active" : true
}
]
Source: /app/routers/inventory.py:15-32
Implement low-stock alerts by filtering products where stock_actual <= stock_minimo.
Automatic Stock Management
During Sales (SALIDA)
When a sale is created, inventory is automatically decremented:
if product.controla_stock:
# 1. Validate sufficient stock
if product.stock_actual < item.cantidad:
raise HTTPException( status_code = 409 , detail = "Stock insuficiente" )
# 2. Deduct stock
product.stock_actual -= item.cantidad
# 3. Create movement record
movement = StockMovement(
product_id = product.id,
user_id = seller_id,
tipo = "SALIDA" ,
motivo = "VENTA" ,
cantidad = item.cantidad,
description = f "Venta en proceso"
)
stock_movements.append(movement)
Source: /app/routers/sales.py:162-184
Stock deduction happens before payment processing to prevent overselling in concurrent transactions.
During Returns (ENTRADA)
When products are returned, stock is replenished:
if product.controla_stock:
# 1. Add stock back
product.stock_actual += item.cantidad
# 2. Create return movement
movement = StockMovement(
product_id = product.id,
user_id = user_id,
tipo = "ENTRADA" ,
motivo = "DEVOLUCION" ,
cantidad = item.cantidad,
description = f "Devolución venta f. { original_sale.folio } : { return_reason } "
)
stock_movements.append(movement)
Source: /app/routers/sales.py:370-381
Manual Stock Adjustments
For inventory corrections, purchases, or initial stock loading, use the stock movement endpoints:
Recording a Stock Entry (Purchase/Adjustment)
Create a Purchase Record
If receiving stock from a supplier, first create a purchase: curl -X POST https://api.torn.cl/purchases/ \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"provider_id": 5,
"tipo_documento": "FACTURA",
"numero_documento": "F-12345",
"fecha_emision": "2026-03-08",
"items": [
{
"product_id": 15,
"cantidad": 100,
"precio_unitario": 6500.00
},
{
"product_id": 42,
"cantidad": 25,
"precio_unitario": 10000.00
}
]
}'
This automatically:
Increments stock_actual for each product
Creates ENTRADA movements with motivo = "COMPRA"
Links movements to the purchase record
Direct Stock Adjustment (Inventory Count)
For corrections without a purchase document: # Note: This endpoint would need to be implemented
# Current implementation handles adjustments through purchases
curl -X POST https://api.torn.cl/inventory/adjust \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": 15,
"tipo": "ENTRADA",
"cantidad": 10,
"motivo": "AJUSTE",
"description": "Inventario físico - encontradas 10 unidades no registradas"
}'
The current Torn implementation handles inventory entries primarily through the purchases module. Direct adjustment endpoints should be added for full inventory management capabilities.
Tracking Stock History (Kardex)
View all movements for a specific product to trace its inventory history:
# Query using product ID filter (implementation may vary)
curl -X GET "https://api.torn.cl/inventory/movements?product_id=15&limit=50" \
-H "Authorization: Bearer YOUR_TOKEN"
Expected Response:
[
{
"id" : 1523 ,
"product_id" : 15 ,
"user_id" : 8 ,
"tipo" : "SALIDA" ,
"motivo" : "VENTA" ,
"cantidad" : 2.00 ,
"fecha" : "2026-03-08T14:23:15Z" ,
"balance_after" : 45.00 ,
"description" : "Venta en proceso" ,
"sale_id" : 1523 ,
"user" : {
"full_name" : "Juan Pérez" ,
"email" : "[email protected] "
}
},
{
"id" : 1489 ,
"product_id" : 15 ,
"user_id" : 3 ,
"tipo" : "ENTRADA" ,
"motivo" : "COMPRA" ,
"cantidad" : 100.00 ,
"fecha" : "2026-03-05T09:15:30Z" ,
"balance_after" : 147.00 ,
"description" : "Compra OC-2024-234" ,
"sale_id" : null
},
{
"id" : 1452 ,
"product_id" : 15 ,
"user_id" : 1 ,
"tipo" : "ENTRADA" ,
"motivo" : "INICIAL" ,
"cantidad" : 47.00 ,
"fecha" : "2026-03-01T08:00:00Z" ,
"balance_after" : 47.00 ,
"description" : "Stock inicial al migrar sistema"
}
]
Movement Types and Reasons
Movement Types (tipo)
Type Description ENTRADAStock increase (purchase, return, adjustment up) SALIDAStock decrease (sale, damage, adjustment down)
Movement Reasons (motivo)
Reason When Used Typical Type VENTAProduct sold to customer SALIDA COMPRAProduct received from supplier ENTRADA DEVOLUCIONProduct returned by customer ENTRADA AJUSTEManual inventory correction Either INICIALInitial stock when setting up system ENTRADA MERMAShrinkage, damage, theft SALIDA TRASLADOTransfer between warehouses Either
Source: /app/models/inventory.py:35
Consistent use of motivo codes enables powerful inventory analytics: sales velocity, return rates, shrinkage tracking, etc.
Low Stock Alerts
Implement client-side alerts by comparing stock levels:
// Example: Filter products needing reorder
const lowStockProducts = inventory . filter (
product => product . controla_stock &&
product . stock_actual <= product . stock_minimo
);
// Alert critical items (out of stock)
const outOfStock = inventory . filter (
product => product . controla_stock &&
product . stock_actual <= 0
);
Reorder Point Strategy Set stock_minimo based on:
Average daily sales velocity
Supplier lead time
Safety stock buffer
Criticality of product
Formula: stock_minimo = (daily_sales × lead_time_days) + safety_stock
Inventory Valuation
Calculate total inventory value:
# Query with product details
total_value = sum (
product.stock_actual * product.precio_costo
for product in inventory
if product.controla_stock
)
Torn tracks selling price (precio_neto) by default. For accurate valuation, ensure you also track precio_costo (cost price) in your product model.
Stock Transfer Between Locations
For multi-location warehouses, implement transfers:
Create SALIDA at Origin
movement_out = StockMovement(
product_id = product_id,
user_id = current_user_id,
tipo = "SALIDA" ,
motivo = "TRASLADO" ,
cantidad = quantity,
description = f "Transferencia a { destination_warehouse } "
)
Create ENTRADA at Destination
movement_in = StockMovement(
product_id = product_id,
user_id = current_user_id,
tipo = "ENTRADA" ,
motivo = "TRASLADO" ,
cantidad = quantity,
description = f "Recepción desde { origin_warehouse } "
)
Link Movements
Store a reference to correlate the transfer: transfer_id = str (uuid.uuid4())
movement_out.description += f " [Transfer: { transfer_id } ]"
movement_in.description += f " [Transfer: { transfer_id } ]"
The current Torn schema assumes single-location inventory. Multi-warehouse support requires extending the products and stock_movements tables with location fields.
Physical Inventory Counts
Reconcile physical counts with system records:
Perform Physical Count
Count actual quantities on shelves/warehouse.
Compare with System
curl -X GET https://api.torn.cl/inventory/ \
-H "Authorization: Bearer YOUR_TOKEN" > current_stock.json
Create Adjustment Movements
For each discrepancy: discrepancy = physical_count - system_stock
if discrepancy > 0 :
tipo = "ENTRADA"
description = f "Ajuste inventario físico: sobrante de { discrepancy } "
else :
tipo = "SALIDA"
description = f "Ajuste inventario físico: faltante de { abs (discrepancy) } "
movement = StockMovement(
product_id = product_id,
tipo = tipo,
motivo = "AJUSTE" ,
cantidad = abs (discrepancy),
description = description
)
Perform physical counts regularly (monthly/quarterly) to maintain inventory accuracy and identify shrinkage patterns.
Integration with Sales
The sales process automatically handles inventory:
Validation : Checks stock_actual >= cantidad for all items
Reservation : Decrements stock before payment confirmation
Audit : Creates linked StockMovement records with sale_id
Atomicity : Rolls back inventory changes if payment/DTE fails
See the Sales Process Guide for complete flow.
Integration with Returns
Returns automatically restore inventory:
Stock Return : Increments stock_actual by returned quantity
Movement : Creates ENTRADA with motivo = "DEVOLUCION"
Traceability : Links to original sale via related_sale_id
See the Returns & Refunds Guide for details.
Reporting and Analytics
Stock Value Report
SELECT
p . codigo_interno ,
p . nombre ,
p . stock_actual ,
p . precio_costo ,
( p . stock_actual * p . precio_costo ) AS valor_total
FROM products p
WHERE p . controla_stock = true
AND p . is_active = true
ORDER BY valor_total DESC ;
Sales Velocity (Last 30 Days)
SELECT
p . codigo_interno ,
p . nombre ,
p . stock_actual ,
SUM ( sd . cantidad ) AS unidades_vendidas,
AVG ( sd . cantidad ) AS promedio_por_venta
FROM products p
JOIN sale_details sd ON sd . product_id = p . id
JOIN sales s ON s . id = sd . sale_id
WHERE s . fecha_emision >= NOW () - INTERVAL '30 days'
AND p . controla_stock = true
GROUP BY p . id
ORDER BY unidades_vendidas DESC ;
Shrinkage Analysis
SELECT
p . codigo_interno ,
p . nombre ,
SUM ( sm . cantidad ) AS total_merma,
COUNT ( * ) AS eventos_merma
FROM stock_movements sm
JOIN products p ON p . id = sm . product_id
WHERE sm . tipo = 'SALIDA'
AND sm . motivo = 'MERMA'
AND sm . fecha >= NOW () - INTERVAL '90 days'
GROUP BY p . id
ORDER BY total_merma DESC ;
Best Practices
Enable Stock Control Selectively Only enable controla_stock for physical products. Keep it false for:
Services (installation, consulting)
Digital products (licenses, downloads)
Non-inventoried items (custom orders)
Immutable Kardex Never delete or modify StockMovement records. If you need to correct an error, create a new offsetting movement with motivo = "AJUSTE".
Descriptive Movement Notes Always provide clear description values:
Reference external documents (“OC-2024-456”, “Guía 789”)
Explain adjustments (“Inventario físico trim Q1”)
Link to sales (“Venta folio 1234”)
Regular Audits Schedule periodic physical inventory counts and reconcile with system records. Document all discrepancies.