"""Quotation stock reservation engine. Uses inventory_operations with operation types: QUOTE_RESERVE — negative quantity, reserves stock when quote is created QUOTE_RELEASE — positive quantity, restores stock when quote is cancelled/expired QUOTE_CONVERT — neutral (just a marker), actual sale uses SALE operation The trigger update_stock_summary() recalculates inventory_stock_summary by summing ALL operations, so reservations automatically affect visible stock. """ from services.inventory_engine import record_operation def reserve_for_quotation(conn, quotation_id, items, employee_id=None): """Reserve stock for each item in a new quotation. Args: conn: tenant DB connection (not committed by this function). quotation_id: the quotations.id. items: list of dicts with inventory_id, quantity, branch_id (optional). employee_id: optional, passed explicitly when g.employee_id is unavailable. Returns: list of operation IDs. """ op_ids = [] for item in items: inv_id = item.get('inventory_id') qty = item.get('quantity', 0) branch_id = item.get('branch_id') if not inv_id or qty <= 0: continue op_id = record_operation( conn, inv_id, branch_id, 'QUOTE_RESERVE', quantity=-qty, reference_id=quotation_id, reference_type='quotation', notes=f'Reserva cotizacion #{quotation_id}' ) op_ids.append(op_id) return op_ids def release_quotation_reservation(conn, quotation_id, items, employee_id=None): """Release previously reserved stock (cancel, expire, or convert). Args: conn: tenant DB connection. quotation_id: the quotations.id. items: list of dicts with inventory_id, quantity, branch_id. employee_id: optional. Returns: list of operation IDs. """ op_ids = [] for item in items: inv_id = item.get('inventory_id') qty = item.get('quantity', 0) branch_id = item.get('branch_id') if not inv_id or qty <= 0: continue op_id = record_operation( conn, inv_id, branch_id, 'QUOTE_RELEASE', quantity=qty, reference_id=quotation_id, reference_type='quotation', notes=f'Liberacion cotizacion #{quotation_id}' ) op_ids.append(op_id) return op_ids def convert_quotation_reservation(conn, quotation_id, items, sale_id=None, employee_id=None): """Convert reservation to actual sale. Flow: 1. Release the reservation (QUOTE_RELEASE +qty) 2. Record the actual sale (SALE -qty) Args: conn: tenant DB connection. quotation_id: the quotations.id. items: list of dicts with inventory_id, quantity, branch_id. sale_id: the resulting sales.id (for reference). employee_id: optional. Returns: list of operation IDs. """ op_ids = release_quotation_reservation(conn, quotation_id, items, employee_id) for item in items: inv_id = item.get('inventory_id') qty = item.get('quantity', 0) branch_id = item.get('branch_id') if not inv_id or qty <= 0: continue op_id = record_operation( conn, inv_id, branch_id, 'SALE', quantity=-qty, reference_id=sale_id or quotation_id, reference_type='sale' if sale_id else 'quotation', notes=f'Venta convertida de cotizacion #{quotation_id}' ) op_ids.append(op_id) return op_ids def get_quotation_items_for_reservation(conn, quotation_id): """Fetch items from a quotation joined with inventory to get branch_id. Returns list of dicts: {inventory_id, quantity, branch_id} """ cur = conn.cursor() cur.execute(""" SELECT qi.inventory_id, qi.quantity, i.branch_id FROM quotation_items qi JOIN inventory i ON i.id = qi.inventory_id WHERE qi.quotation_id = %s """, (quotation_id,)) rows = cur.fetchall() cur.close() return [ {'inventory_id': r[0], 'quantity': r[1], 'branch_id': r[2]} for r in rows ]