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')
|
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 ───────────────────────────────────────
|
# ─── Sales ───────────────────────────────────────
|
||||||
|
|
||||||
@pos_bp.route('/sales', methods=['POST'])
|
@pos_bp.route('/sales', methods=['POST'])
|
||||||
@@ -362,8 +413,15 @@ def create_quotation():
|
|||||||
conn = get_tenant_conn(g.tenant_id)
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
cur = conn.cursor()
|
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
|
# Calculate totals
|
||||||
totals = calculate_totals(items)
|
totals = calculate_totals(enriched)
|
||||||
|
|
||||||
valid_days = int(data.get('valid_days', 7))
|
valid_days = int(data.get('valid_days', 7))
|
||||||
valid_until = (date.today() + timedelta(days=valid_days)).isoformat()
|
valid_until = (date.today() + timedelta(days=valid_days)).isoformat()
|
||||||
@@ -371,24 +429,21 @@ def create_quotation():
|
|||||||
try:
|
try:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO quotations
|
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)
|
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
|
RETURNING id, created_at
|
||||||
""", (
|
""", (
|
||||||
g.branch_id, data.get('customer_id'), g.employee_id,
|
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')
|
totals['total'], valid_until, data.get('notes')
|
||||||
))
|
))
|
||||||
quot_id, created_at = cur.fetchone()
|
quot_id, created_at = cur.fetchone()
|
||||||
|
|
||||||
# Insert quotation items
|
# Insert quotation items
|
||||||
for item in totals['items']:
|
for item in totals['items']:
|
||||||
# Look up part_number and name from inventory
|
part_number = item.get('part_number', '')
|
||||||
cur.execute("SELECT part_number, name FROM inventory WHERE id = %s", (item['inventory_id'],))
|
name = item.get('name', '')
|
||||||
inv = cur.fetchone()
|
|
||||||
part_number = inv[0] if inv else item.get('part_number', '')
|
|
||||||
name = inv[1] if inv else item.get('name', '')
|
|
||||||
|
|
||||||
line_subtotal = round(
|
line_subtotal = round(
|
||||||
item['unit_price'] * item['quantity'] * (1 - item['discount_pct'] / 100), 2
|
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)
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
cur = conn.cursor()
|
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
|
# Calculate totals
|
||||||
totals = calculate_totals(items)
|
totals = calculate_totals(enriched)
|
||||||
|
|
||||||
if initial_payment > totals['total']:
|
if initial_payment > totals['total']:
|
||||||
cur.close(); conn.close()
|
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)
|
# Insert layaway items and reserve stock (table created by migration v1.1_pos_tables.sql)
|
||||||
from services.inventory_engine import record_operation
|
from services.inventory_engine import record_operation
|
||||||
for item in totals['items']:
|
for item in totals['items']:
|
||||||
cur.execute("SELECT part_number, name, branch_id FROM inventory WHERE id = %s", (item['inventory_id'],))
|
part_number = item.get('part_number', '')
|
||||||
inv = cur.fetchone()
|
name = item.get('name', '')
|
||||||
part_number = inv[0] if inv else item.get('part_number', '')
|
item_branch_id = item.get('branch_id', g.branch_id)
|
||||||
name = inv[1] if inv else item.get('name', '')
|
|
||||||
item_branch_id = inv[2] if inv else g.branch_id
|
|
||||||
|
|
||||||
line_subtotal = round(
|
line_subtotal = round(
|
||||||
item['unit_price'] * item['quantity'] * (1 - item['discount_pct'] / 100), 2
|
item['unit_price'] * item['quantity'] * (1 - item['discount_pct'] / 100), 2
|
||||||
@@ -997,11 +1057,29 @@ def complete_layaway(layaway_id):
|
|||||||
if status != 'active':
|
if status != 'active':
|
||||||
cur.close(); conn.close()
|
cur.close(); conn.close()
|
||||||
return jsonify({'error': f'Layaway is {status}'}), 400
|
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:
|
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
|
# Get layaway items
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT inventory_id, quantity, unit_price, discount_pct, tax_rate
|
SELECT inventory_id, quantity, unit_price, discount_pct, tax_rate
|
||||||
|
|||||||
Reference in New Issue
Block a user