feat: enhance inventory management features

- Update stock movement model with improved functionality
- Enhance inventory routes and API endpoints
- Improve inventory templates for movements, reports, and stock items
- Add better history tracking and valuation reporting
This commit is contained in:
Dries Peeters
2026-01-03 20:27:54 +01:00
parent 7c169fb401
commit eb0cd0005e
7 changed files with 19 additions and 4 deletions

View File

@@ -13,7 +13,7 @@ class StockMovement(db.Model):
id = db.Column(db.Integer, primary_key=True)
movement_type = db.Column(
db.String(20), nullable=False, index=True
) # 'adjustment', 'transfer', 'sale', 'purchase', 'return', 'waste'
) # 'adjustment', 'transfer', 'sale', 'rent', 'purchase', 'return', 'waste'
stock_item_id = db.Column(db.Integer, db.ForeignKey("stock_items.id"), nullable=False, index=True)
warehouse_id = db.Column(db.Integer, db.ForeignKey("warehouses.id"), nullable=False, index=True)
quantity = db.Column(db.Numeric(10, 2), nullable=False) # Positive for additions, negative for removals
@@ -307,6 +307,12 @@ class StockMovement(db.Model):
return
# Outbound: consume FIFO (oldest first). Prefer a specific lot if provided (used after devaluation).
# Special case: "rent" movements keep value in stock (don't consume from lots) for accounting purposes
if qty < 0 and movement.movement_type == "rent":
# For rent, we don't consume from lots - this keeps the value in inventory
# while removing physical quantity from warehouse
return
qty_to_consume = abs(qty)
allow_negative = movement.movement_type == "adjustment"

View File

@@ -1906,7 +1906,12 @@ def reports_valuation():
stock_item_id=item_detail["item_id"], warehouse_id=item_detail["warehouse_id"]
).first()
if stock:
items_with_value.append({"stock": stock, "value": item_detail["value"]})
items_with_value.append({
"stock": stock,
"value": item_detail["value"],
"quantity": item_detail.get("quantity"),
"cost": item_detail.get("cost")
})
return render_template(
"inventory/reports/valuation.html",

View File

@@ -25,6 +25,7 @@
<option value="adjustment">{{ _('Adjustment') }}</option>
<option value="transfer">{{ _('Transfer') }}</option>
<option value="sale">{{ _('Sale') }}</option>
<option value="rent">{{ _('Rent') }}</option>
<option value="purchase">{{ _('Purchase') }}</option>
<option value="return">{{ _('Return') }}</option>
<option value="waste">{{ _('Waste') }}</option>

View File

@@ -24,6 +24,7 @@
<option value="adjustment" {% if movement_type == 'adjustment' %}selected{% endif %}>{{ _('Adjustment') }}</option>
<option value="transfer" {% if movement_type == 'transfer' %}selected{% endif %}>{{ _('Transfer') }}</option>
<option value="sale" {% if movement_type == 'sale' %}selected{% endif %}>{{ _('Sale') }}</option>
<option value="rent" {% if movement_type == 'rent' %}selected{% endif %}>{{ _('Rent') }}</option>
<option value="purchase" {% if movement_type == 'purchase' %}selected{% endif %}>{{ _('Purchase') }}</option>
<option value="return" {% if movement_type == 'return' %}selected{% endif %}>{{ _('Return') }}</option>
<option value="waste" {% if movement_type == 'waste' %}selected{% endif %}>{{ _('Waste') }}</option>

View File

@@ -42,6 +42,7 @@
<option value="adjustment" {% if selected_movement_type == 'adjustment' %}selected{% endif %}>{{ _('Adjustment') }}</option>
<option value="transfer" {% if selected_movement_type == 'transfer' %}selected{% endif %}>{{ _('Transfer') }}</option>
<option value="sale" {% if selected_movement_type == 'sale' %}selected{% endif %}>{{ _('Sale') }}</option>
<option value="rent" {% if selected_movement_type == 'rent' %}selected{% endif %}>{{ _('Rent') }}</option>
<option value="purchase" {% if selected_movement_type == 'purchase' %}selected{% endif %}>{{ _('Purchase') }}</option>
<option value="return" {% if selected_movement_type == 'return' %}selected{% endif %}>{{ _('Return') }}</option>
<option value="waste" {% if selected_movement_type == 'waste' %}selected{% endif %}>{{ _('Waste') }}</option>

View File

@@ -78,8 +78,8 @@
</td>
<td class="p-4 font-mono text-sm">{{ item_data.stock.stock_item.sku }}</td>
<td class="p-4">{{ item_data.stock.stock_item.category or '—' }}</td>
<td class="p-4">{{ item_data.stock.quantity_on_hand }}</td>
<td class="p-4">{{ "%.2f"|format(item_data.stock.stock_item.default_cost or 0) }} EUR</td>
<td class="p-4">{{ "%.2f"|format(item_data.quantity) if item_data.quantity is not none else item_data.stock.quantity_on_hand }}</td>
<td class="p-4">{{ "%.2f"|format(item_data.cost) if item_data.cost is not none else (item_data.stock.stock_item.default_cost or 0) }} EUR</td>
<td class="p-4 font-semibold">{{ "%.2f"|format(item_data.value) }} EUR</td>
</tr>
{% else %}

View File

@@ -34,6 +34,7 @@
<option value="adjustment" {% if selected_movement_type == 'adjustment' %}selected{% endif %}>{{ _('Adjustment') }}</option>
<option value="transfer" {% if selected_movement_type == 'transfer' %}selected{% endif %}>{{ _('Transfer') }}</option>
<option value="sale" {% if selected_movement_type == 'sale' %}selected{% endif %}>{{ _('Sale') }}</option>
<option value="rent" {% if selected_movement_type == 'rent' %}selected{% endif %}>{{ _('Rent') }}</option>
<option value="purchase" {% if selected_movement_type == 'purchase' %}selected{% endif %}>{{ _('Purchase') }}</option>
<option value="return" {% if selected_movement_type == 'return' %}selected{% endif %}>{{ _('Return') }}</option>
<option value="waste" {% if selected_movement_type == 'waste' %}selected{% endif %}>{{ _('Waste') }}</option>