Add allow_negative_stock toggle for sales, remissions, quotes, layaways and service orders
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

This commit is contained in:
2026-07-15 18:02:43 +00:00
parent 116d4452da
commit c90012ca37
7 changed files with 76 additions and 44 deletions

View File

@@ -1133,17 +1133,17 @@ def update_counter_remission_config():
@config_bp.route('/sales-settings', methods=['GET'])
@require_auth('pos.view')
def get_sales_settings():
"""Get sales-related settings (zero-price sales, etc.)."""
"""Get sales-related settings (zero-price sales, negative stock)."""
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
cur.execute("SELECT value FROM tenant_config WHERE key = 'allow_zero_price_sales'")
row = cur.fetchone()
cur.execute("SELECT key, value FROM tenant_config WHERE key IN ('allow_zero_price_sales', 'allow_negative_stock')")
rows = {k: v for k, v in cur.fetchall()}
cur.close(); conn.close()
# Default to true to avoid breaking existing tenants that sell at $0.
allow = True
if row:
allow = str(row[0]).lower() in ('true', '1', 'yes')
return jsonify({'allow_zero_price_sales': allow})
# Default allow_zero_price_sales to true to avoid breaking existing tenants.
allow = str(rows.get('allow_zero_price_sales', 'true')).lower() in ('true', '1', 'yes')
# Default allow_negative_stock to false (safer).
neg = str(rows.get('allow_negative_stock', 'false')).lower() in ('true', '1', 'yes')
return jsonify({'allow_zero_price_sales': allow, 'allow_negative_stock': neg})
@config_bp.route('/sales-settings', methods=['PUT'])
@@ -1152,15 +1152,20 @@ def update_sales_settings():
"""Update sales-related settings."""
data = request.get_json() or {}
allow = 'true' if data.get('allow_zero_price_sales') else 'false'
neg = 'true' if data.get('allow_negative_stock') else 'false'
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
cur.execute("""
INSERT INTO tenant_config (key, value) VALUES (%s, %s)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
""", ('allow_zero_price_sales', allow))
cur.execute("""
INSERT INTO tenant_config (key, value) VALUES (%s, %s)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
""", ('allow_negative_stock', neg))
conn.commit()
cur.close(); conn.close()
return jsonify({'allow_zero_price_sales': allow == 'true'})
return jsonify({'allow_zero_price_sales': allow == 'true', 'allow_negative_stock': neg == 'true'})
@config_bp.route('/onboarding-status', methods=['GET'])

View File

@@ -55,6 +55,21 @@ def _validate_zero_price(conn, items):
raise ValueError(f"No está permitido vender artículos en $0 ({name})")
def _validate_stock_availability(conn, items, branch_id):
"""Raise ValueError if stock is insufficient and the tenant forbids negative stock."""
if _tenant_allows_negative_stock(conn):
return
for item in items:
inv_id = item.get('inventory_id')
qty = int(item.get('quantity', 1) or 1)
if not inv_id:
continue
available = get_stock(conn, inv_id, branch_id)
if available < qty:
name = item.get('name') or item.get('part_number') or inv_id
raise ValueError(f'Sin stock suficiente para {name}. Disponible: {available}, solicitado: {qty}')
def _enrich_items(cur, items, customer_id=None):
"""Look up inventory data for items that lack unit_price/tax_rate.
@@ -136,22 +151,10 @@ def create_sale():
data = request.get_json() or {}
conn = get_tenant_conn(g.tenant_id)
# Verify stock availability per item for the active branch
# (skipped for tenants configured to allow negative stock / oversell)
branch_id = data.get('branch_id', g.branch_id)
if not _tenant_allows_negative_stock(conn):
for item in data.get('items', []):
inv_id = item.get('inventory_id')
qty = int(item.get('quantity', 1))
if inv_id:
available = get_stock(conn, inv_id, branch_id)
if available < qty:
conn.close()
return jsonify({
'error': f'Insufficient stock for item {inv_id}. Available: {available}, requested: {qty}'
}), 400
try:
_validate_stock_availability(conn, data.get('items', []), branch_id)
_validate_zero_price(conn, data.get('items', []))
sale = process_sale(conn, data)
conn.commit()
@@ -175,19 +178,9 @@ def create_remission():
conn = get_tenant_conn(g.tenant_id)
branch_id = data.get('branch_id', g.branch_id)
if not _tenant_allows_negative_stock(conn):
for item in data.get('items', []):
inv_id = item.get('inventory_id')
qty = int(item.get('quantity', 1))
if inv_id:
available = get_stock(conn, inv_id, branch_id)
if available < qty:
conn.close()
return jsonify({
'error': f'Insufficient stock for item {inv_id}. Available: {available}, requested: {qty}'
}), 400
try:
_validate_stock_availability(conn, data.get('items', []), branch_id)
_validate_zero_price(conn, data.get('items', []))
sale = create_remission_note(conn, {
'tenant_id': g.tenant_id,
@@ -753,6 +746,7 @@ def create_quotation():
# Enrich items with inventory data (price, tax, etc.)
try:
enriched = _enrich_items(cur, items, data.get('customer_id'))
_validate_stock_availability(conn, enriched, g.branch_id)
_validate_zero_price(conn, enriched)
except ValueError as e:
cur.close(); conn.close()
@@ -1142,6 +1136,7 @@ def update_quotation(quot_id):
try:
enriched = _enrich_items(cur, items, data.get('customer_id'))
_validate_stock_availability(conn, enriched, g.branch_id)
_validate_zero_price(conn, enriched)
except ValueError as e:
cur.close(); conn.close()
@@ -1629,6 +1624,7 @@ def convert_quotation(quot_id):
}
try:
_validate_stock_availability(conn, items, g.branch_id)
_validate_zero_price(conn, items)
sale = process_sale(conn, sale_data)
@@ -1788,6 +1784,7 @@ def create_layaway():
# Enrich items with inventory data
try:
enriched = _enrich_items(cur, items, customer_id)
_validate_stock_availability(conn, enriched, g.branch_id)
_validate_zero_price(conn, enriched)
except ValueError as e:
cur.close(); conn.close()