fix(pos): enrich quotation/layaway items from inventory and allow final payment on layaway complete
Quotation and layaway endpoints were calling calculate_totals() on raw input items without looking up unit_price/tax_rate from inventory, causing KeyError. Added _enrich_items() helper (with customer price tier support). Also removed non-existent discount_total column from quotations INSERT, and made layaway complete accept a final payment for the remaining balance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,57 @@ from services.audit import log_action
|
||||
pos_bp = Blueprint('pos', __name__, url_prefix='/pos/api')
|
||||
|
||||
|
||||
def _enrich_items(cur, items, customer_id=None):
|
||||
"""Look up inventory data for items that lack unit_price/tax_rate.
|
||||
|
||||
Returns list of dicts with all fields needed by calculate_totals.
|
||||
"""
|
||||
enriched = []
|
||||
for item in items:
|
||||
inv_id = item.get('inventory_id')
|
||||
qty = int(item.get('quantity', 1))
|
||||
if qty <= 0:
|
||||
raise ValueError(f"Invalid quantity for inventory_id {inv_id}")
|
||||
|
||||
cur.execute("""
|
||||
SELECT id, part_number, name, cost, price_1, price_2, price_3,
|
||||
tax_rate, branch_id
|
||||
FROM inventory WHERE id = %s AND is_active = true
|
||||
""", (inv_id,))
|
||||
inv = cur.fetchone()
|
||||
if not inv:
|
||||
raise ValueError(f"Inventory item {inv_id} not found or inactive")
|
||||
|
||||
# Determine price tier from customer if provided
|
||||
price_tier = 1
|
||||
if customer_id:
|
||||
cur.execute("SELECT price_tier FROM customers WHERE id = %s", (customer_id,))
|
||||
cust = cur.fetchone()
|
||||
if cust and cust[0]:
|
||||
price_tier = int(cust[0])
|
||||
|
||||
# price_1=inv[4], price_2=inv[5], price_3=inv[6]
|
||||
tier_prices = {1: inv[4], 2: inv[5], 3: inv[6]}
|
||||
default_price = float(tier_prices.get(price_tier, inv[4]) or inv[4])
|
||||
|
||||
unit_price = float(item.get('unit_price', default_price))
|
||||
discount_pct = float(item.get('discount_pct', 0))
|
||||
tax_rate = float(item.get('tax_rate', inv[7] or 0.16))
|
||||
|
||||
enriched.append({
|
||||
'inventory_id': inv_id,
|
||||
'part_number': inv[1],
|
||||
'name': inv[2],
|
||||
'quantity': qty,
|
||||
'unit_price': unit_price,
|
||||
'unit_cost': float(inv[3]) if inv[3] else 0,
|
||||
'discount_pct': discount_pct,
|
||||
'tax_rate': tax_rate,
|
||||
'branch_id': inv[8],
|
||||
})
|
||||
return enriched
|
||||
|
||||
|
||||
# ─── Sales ───────────────────────────────────────
|
||||
|
||||
@pos_bp.route('/sales', methods=['POST'])
|
||||
@@ -362,8 +413,15 @@ def create_quotation():
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Enrich items with inventory data (price, tax, etc.)
|
||||
try:
|
||||
enriched = _enrich_items(cur, items, data.get('customer_id'))
|
||||
except ValueError as e:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 400
|
||||
|
||||
# Calculate totals
|
||||
totals = calculate_totals(items)
|
||||
totals = calculate_totals(enriched)
|
||||
|
||||
valid_days = int(data.get('valid_days', 7))
|
||||
valid_until = (date.today() + timedelta(days=valid_days)).isoformat()
|
||||
@@ -371,24 +429,21 @@ def create_quotation():
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO quotations
|
||||
(branch_id, customer_id, employee_id, subtotal, discount_total,
|
||||
(branch_id, customer_id, employee_id, subtotal,
|
||||
tax_total, total, status, valid_until, notes)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,'active',%s,%s)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,'active',%s,%s)
|
||||
RETURNING id, created_at
|
||||
""", (
|
||||
g.branch_id, data.get('customer_id'), g.employee_id,
|
||||
totals['subtotal'], totals['discount_total'], totals['tax_total'],
|
||||
totals['subtotal'], totals['tax_total'],
|
||||
totals['total'], valid_until, data.get('notes')
|
||||
))
|
||||
quot_id, created_at = cur.fetchone()
|
||||
|
||||
# Insert quotation items
|
||||
for item in totals['items']:
|
||||
# Look up part_number and name from inventory
|
||||
cur.execute("SELECT part_number, name FROM inventory WHERE id = %s", (item['inventory_id'],))
|
||||
inv = cur.fetchone()
|
||||
part_number = inv[0] if inv else item.get('part_number', '')
|
||||
name = inv[1] if inv else item.get('name', '')
|
||||
part_number = item.get('part_number', '')
|
||||
name = item.get('name', '')
|
||||
|
||||
line_subtotal = round(
|
||||
item['unit_price'] * item['quantity'] * (1 - item['discount_pct'] / 100), 2
|
||||
@@ -670,8 +725,15 @@ def create_layaway():
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Enrich items with inventory data
|
||||
try:
|
||||
enriched = _enrich_items(cur, items, customer_id)
|
||||
except ValueError as e:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': str(e)}), 400
|
||||
|
||||
# Calculate totals
|
||||
totals = calculate_totals(items)
|
||||
totals = calculate_totals(enriched)
|
||||
|
||||
if initial_payment > totals['total']:
|
||||
cur.close(); conn.close()
|
||||
@@ -697,11 +759,9 @@ def create_layaway():
|
||||
# Insert layaway items and reserve stock (table created by migration v1.1_pos_tables.sql)
|
||||
from services.inventory_engine import record_operation
|
||||
for item in totals['items']:
|
||||
cur.execute("SELECT part_number, name, branch_id FROM inventory WHERE id = %s", (item['inventory_id'],))
|
||||
inv = cur.fetchone()
|
||||
part_number = inv[0] if inv else item.get('part_number', '')
|
||||
name = inv[1] if inv else item.get('name', '')
|
||||
item_branch_id = inv[2] if inv else g.branch_id
|
||||
part_number = item.get('part_number', '')
|
||||
name = item.get('name', '')
|
||||
item_branch_id = item.get('branch_id', g.branch_id)
|
||||
|
||||
line_subtotal = round(
|
||||
item['unit_price'] * item['quantity'] * (1 - item['discount_pct'] / 100), 2
|
||||
@@ -997,11 +1057,29 @@ def complete_layaway(layaway_id):
|
||||
if status != 'active':
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': f'Layaway is {status}'}), 400
|
||||
if paid < total:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': f'Layaway not fully paid. Remaining: ${total - paid:.2f}'}), 400
|
||||
|
||||
remaining = round(total - paid, 2)
|
||||
try:
|
||||
# If there's a remaining balance, accept a final payment with the complete call
|
||||
if remaining > 0:
|
||||
final_method = data.get('payment_method', 'efectivo')
|
||||
cur.execute("""
|
||||
INSERT INTO layaway_payments
|
||||
(layaway_id, amount, payment_method, reference, employee_id)
|
||||
VALUES (%s,%s,%s,%s,%s)
|
||||
""", (layaway_id, remaining, final_method,
|
||||
data.get('reference', 'Pago final al completar'), g.employee_id))
|
||||
cur.execute("UPDATE layaways SET amount_paid = total WHERE id = %s", (layaway_id,))
|
||||
paid = total
|
||||
|
||||
# Record cash movement for final payment
|
||||
register_id = data.get('register_id')
|
||||
if register_id and final_method == 'efectivo':
|
||||
cur.execute("""
|
||||
INSERT INTO cash_movements (register_id, type, amount, reason, employee_id)
|
||||
VALUES (%s, 'in', %s, %s, %s)
|
||||
""", (register_id, remaining, f'Apartado #{layaway_id} - pago final', g.employee_id))
|
||||
|
||||
# Get layaway items
|
||||
cur.execute("""
|
||||
SELECT inventory_id, quantity, unit_price, discount_pct, tax_rate
|
||||
|
||||
Reference in New Issue
Block a user