Add allow_zero_price_sales toggle: config UI, POS enforcement and backend validation
This commit is contained in:
@@ -1130,6 +1130,39 @@ def update_counter_remission_config():
|
||||
return jsonify({'enabled': enabled == 'true'})
|
||||
|
||||
|
||||
@config_bp.route('/sales-settings', methods=['GET'])
|
||||
@require_auth('pos.view')
|
||||
def get_sales_settings():
|
||||
"""Get sales-related settings (zero-price sales, etc.)."""
|
||||
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.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})
|
||||
|
||||
|
||||
@config_bp.route('/sales-settings', methods=['PUT'])
|
||||
@require_auth('config.edit')
|
||||
def update_sales_settings():
|
||||
"""Update sales-related settings."""
|
||||
data = request.get_json() or {}
|
||||
allow = 'true' if data.get('allow_zero_price_sales') 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))
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'allow_zero_price_sales': allow == 'true'})
|
||||
|
||||
|
||||
@config_bp.route('/onboarding-status', methods=['GET'])
|
||||
@require_auth('pos.view')
|
||||
def get_onboarding_status():
|
||||
|
||||
@@ -32,6 +32,29 @@ def _tenant_allows_negative_stock(conn):
|
||||
return row is not None and str(row[0]).lower() in ('true', '1', 'yes')
|
||||
|
||||
|
||||
def _tenant_allows_zero_price(conn):
|
||||
"""Return True if the tenant allows selling items at $0. Defaults to True."""
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT value FROM tenant_config WHERE key = 'allow_zero_price_sales'")
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return str(row[0]).lower() in ('true', '1', 'yes') if row else True
|
||||
|
||||
|
||||
def _validate_zero_price(conn, items):
|
||||
"""Raise ValueError if any item line would be <= $0 and the tenant forbids it."""
|
||||
if _tenant_allows_zero_price(conn):
|
||||
return
|
||||
for item in items:
|
||||
unit_price = float(item.get('unit_price', 0) or 0)
|
||||
quantity = float(item.get('quantity', 1) or 1)
|
||||
discount_pct = float(item.get('discount_pct', 0) or 0)
|
||||
line_total = unit_price * quantity * (1 - discount_pct / 100)
|
||||
if line_total <= 0:
|
||||
name = item.get('name') or item.get('part_number') or item.get('inventory_id')
|
||||
raise ValueError(f"No está permitido vender artículos en $0 ({name})")
|
||||
|
||||
|
||||
def _enrich_items(cur, items, customer_id=None):
|
||||
"""Look up inventory data for items that lack unit_price/tax_rate.
|
||||
|
||||
@@ -129,6 +152,7 @@ def create_sale():
|
||||
}), 400
|
||||
|
||||
try:
|
||||
_validate_zero_price(conn, data.get('items', []))
|
||||
sale = process_sale(conn, data)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -164,6 +188,7 @@ def create_remission():
|
||||
}), 400
|
||||
|
||||
try:
|
||||
_validate_zero_price(conn, data.get('items', []))
|
||||
sale = create_remission_note(conn, {
|
||||
'tenant_id': g.tenant_id,
|
||||
'branch_id': branch_id,
|
||||
@@ -728,6 +753,7 @@ def create_quotation():
|
||||
# Enrich items with inventory data (price, tax, etc.)
|
||||
try:
|
||||
enriched = _enrich_items(cur, items, data.get('customer_id'))
|
||||
_validate_zero_price(conn, enriched)
|
||||
except ValueError as e:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 400
|
||||
@@ -1116,6 +1142,7 @@ def update_quotation(quot_id):
|
||||
|
||||
try:
|
||||
enriched = _enrich_items(cur, items, data.get('customer_id'))
|
||||
_validate_zero_price(conn, enriched)
|
||||
except ValueError as e:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 400
|
||||
@@ -1602,6 +1629,7 @@ def convert_quotation(quot_id):
|
||||
}
|
||||
|
||||
try:
|
||||
_validate_zero_price(conn, items)
|
||||
sale = process_sale(conn, sale_data)
|
||||
|
||||
# Mark quotation as converted
|
||||
@@ -1760,6 +1788,7 @@ def create_layaway():
|
||||
# Enrich items with inventory data
|
||||
try:
|
||||
enriched = _enrich_items(cur, items, customer_id)
|
||||
_validate_zero_price(conn, enriched)
|
||||
except ValueError as e:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 400
|
||||
|
||||
@@ -38,6 +38,29 @@ from blueprints.config_bp import _get_workshop_permissions, _DEFAULT_WORKSHOP_PE
|
||||
service_order_bp = Blueprint('service_orders', __name__, url_prefix='/pos/api/service-orders')
|
||||
|
||||
|
||||
def _tenant_allows_zero_price(conn):
|
||||
"""Return True if the tenant allows selling items at $0. Defaults to True."""
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT value FROM tenant_config WHERE key = 'allow_zero_price_sales'")
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return str(row[0]).lower() in ('true', '1', 'yes') if row else True
|
||||
|
||||
|
||||
def _validate_zero_price(conn, items):
|
||||
"""Raise ValueError if any item line would be <= $0 and the tenant forbids it."""
|
||||
if _tenant_allows_zero_price(conn):
|
||||
return
|
||||
for item in items:
|
||||
unit_price = float(item.get('unit_price', 0) or 0)
|
||||
quantity = float(item.get('quantity', 1) or 1)
|
||||
discount_pct = float(item.get('discount_pct', 0) or 0)
|
||||
line_total = unit_price * quantity * (1 - discount_pct / 100)
|
||||
if line_total <= 0:
|
||||
name = item.get('name') or item.get('part_number') or item.get('inventory_id')
|
||||
raise ValueError(f"No está permitido vender artículos en $0 ({name})")
|
||||
|
||||
|
||||
# Roles allowed to access the workshop module at all.
|
||||
_WORKSHOP_VIEW_ROLES = {'owner', 'admin', 'counter', 'cashier', 'workshop', 'mechanic'}
|
||||
|
||||
@@ -174,6 +197,7 @@ def create_order_from_pos():
|
||||
)
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
_validate_zero_price(conn, items)
|
||||
result = create_service_order(conn, {
|
||||
'tenant_id': g.tenant_id,
|
||||
'branch_id': data.get('branch_id', g.branch_id),
|
||||
|
||||
Reference in New Issue
Block a user