diff --git a/scripts_sql/fix_inventory_flow_complete.sql b/scripts_sql/fix_inventory_flow_complete.sql new file mode 100644 index 0000000..c25c20b --- /dev/null +++ b/scripts_sql/fix_inventory_flow_complete.sql @@ -0,0 +1,609 @@ +-- ============================================================ +-- FIX COMPLETO: Flujo de inventario sin duplicacion +-- Fecha: 2026-06-09 +-- Autor: Analisis automatico del sistema +-- ============================================================ +-- PROBLEMA IDENTIFICADO: +-- El stock se duplicaba al pagar un expense porque: +-- 1. newexpensev2 y updateexpensev4 creaban movimientos de +-- stock al crear/editar un expense (movimientos fantasma). +-- 2. purchaseentry tambien creaba movimientos y actualizaba +-- el stock al recibir fisicamente. +-- 3. setpaymentstatusv2 sumaba de nuevo al stock al pagar. +-- +-- ADEMAS: +-- La unica forma de cancelar un expense es rechazando el pago +-- (status = 3), pero setpaymentstatusv2 no revertia el stock +-- al cancelar. +-- +-- SOLUCION APLICADA: +-- 1. newexpensev2 y updateexpensev4 ya NO tocan stock ni +-- stock_movements. Solo manejan expenses y purchase_detail. +-- 2. purchaseentry es la UNICA funcion que ingresa stock al +-- recibir mercancia fisicamente. +-- 3. setpaymentstatusv2(status = 1) solo paga, no toca stock. +-- 4. setpaymentstatusv2(status = 3) revierte el stock si ya +-- se habia recibido fisicamente (purchaseentry), creando +-- un movimiento de tipo "Cancellation". +-- 5. Se crea el tipo de movimiento "Cancellation" en +-- movement_type si no existe. +-- +-- NOTA: Este cambio es reversible. Si algo falla, ejecutar +-- rollback_inventory_flow_complete.sql +-- ============================================================ + +-- ============================================================ +-- 1. CREAR TIPO DE MOVIMIENTO "Cancellation" SI NO EXISTE +-- ============================================================ +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM movement_type WHERE name_mov_type = 'Cancellation' + ) THEN + INSERT INTO movement_type(name_mov_type) VALUES ('Cancellation'); + END IF; +END $$; + +-- ============================================================ +-- 2. FIX: setpaymentstatusv2 +-- ============================================================ +-- Pagar (status = 1): solo actualiza payments. +-- Cancelar/Rechazar pago (status = 3): revierte stock si hubo +-- recepcion fisica previa (purchaseentry). +-- ============================================================ +CREATE OR REPLACE FUNCTION public.setpaymentstatusv2( + expense integer, + status integer) + RETURNS integer + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE +AS $BODY$ +DECLARE + rows_updated INT := 0; + details RECORD; + approval INT := 0; + stock_anterior BIGINT; + stock_actual BIGINT; + v_id_stock_mov INT; + cancellation_type_id INT; +BEGIN + SELECT id_approval_stat INTO approval FROM expenses WHERE id_expense = expense; + + -- Solo procesar si el gasto esta aprobado + IF approval = 1 THEN + + -- ============================================================ + -- PAGAR: status = 1 + -- ============================================================ + IF status = 1 THEN + UPDATE payments + SET id_payment_status = status, + payment_date = CURRENT_DATE + WHERE id_expense = expense; + + GET DIAGNOSTICS rows_updated = ROW_COUNT; + + IF rows_updated = 0 THEN + RETURN -1; + END IF; + + -- NO se toca stock. purchaseentry ya lo hizo correctamente. + RETURN status; + + -- ============================================================ + -- CANCELAR/RECHAZAR PAGO: status = 3 + -- ============================================================ + ELSIF status = 3 THEN + UPDATE payments + SET id_payment_status = status, + payment_date = CURRENT_DATE + WHERE id_expense = expense; + + GET DIAGNOSTICS rows_updated = ROW_COUNT; + + IF rows_updated = 0 THEN + RETURN -1; + END IF; + + -- Obtener id del tipo de movimiento Cancellation + SELECT id_mov_type INTO cancellation_type_id + FROM movement_type + WHERE name_mov_type = 'Cancellation'; + + -- Revertir stock si ya se habia recibido fisicamente + IF EXISTS ( + SELECT 1 + FROM purchase_detail pd + WHERE pd.id_expense = expense + AND pd.delivered > 0 + ) THEN + FOR details IN + SELECT pd.id_product, pd.delivered + FROM purchase_detail pd + WHERE pd.id_expense = expense + AND pd.delivered > 0 + LOOP + SELECT stock INTO stock_anterior + FROM stock + WHERE id_product = details.id_product + FOR UPDATE; + + stock_actual := stock_anterior - details.delivered; + + UPDATE stock + SET stock = stock_actual + WHERE id_product = details.id_product; + + INSERT INTO stock_movements( + id_product, + id_mov_type, + date_st_mov, + id_expense, + quantity, + stock_before, + stock_current + ) + VALUES ( + details.id_product, + cancellation_type_id, + CURRENT_DATE, + expense, + details.delivered * (-1), + stock_anterior, + stock_actual + ) + RETURNING id_sto_mov INTO v_id_stock_mov; + END LOOP; + END IF; + + RETURN status; + + ELSE + -- Status invalido + RETURN -2; + END IF; + + ELSE + -- No esta aprobado, no se puede actualizar el pago + RETURN -3; + END IF; +END; +$BODY$; + +-- ============================================================ +-- 3. FIX: newexpensev2 +-- ============================================================ +-- Ya NO crea stock_movements. Solo crea expenses y +-- purchase_detail. +-- ============================================================ +CREATE OR REPLACE FUNCTION public.newexpensev2( + new_description character varying, + suppliers_id integer, + new_request_date date, + new_payment_deadline date, + request_by integer, + area integer, + expense_cat integer, + currency_id integer, + needtoapprove boolean, + products jsonb DEFAULT NULL::jsonb, + new_subtotal numeric DEFAULT NULL::numeric, + new_iva numeric DEFAULT NULL::numeric, + new_ieps numeric DEFAULT NULL::numeric, + new_total numeric DEFAULT NULL::numeric) + RETURNS integer + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE +AS $BODY$ +DECLARE + new_expense INT; + product JSONB; + + approval_status INT := 3; + approval_datevar DATE := NULL; + + monedacambio NUMERIC := 1; + fecha_busqueda DATE; + + productsubtotal NUMERIC := 0; + producttotal NUMERIC := 0; + productsiva NUMERIC := 0; + + subtotalcambiomoneda NUMERIC := 0; + totalcambiomoneda NUMERIC := 0; + ivacambiomoneda NUMERIC := 0; + iepscambiomoneda NUMERIC := 0; +BEGIN + + IF needtoapprove THEN + approval_status := 3; + approval_datevar := NULL; + ELSE + approval_status := 1; + approval_datevar := CURRENT_DATE; + END IF; + + -- Si la moneda es dolares (2), buscar tipo de cambio + IF (currency_id = 2) THEN + IF (new_payment_deadline < CURRENT_DATE) THEN + fecha_busqueda := new_payment_deadline; + ELSE + fecha_busqueda := CURRENT_DATE; + END IF; + + LOOP + SELECT onegetchange(fecha_busqueda) INTO monedacambio; + EXIT WHEN monedacambio IS NOT NULL; + fecha_busqueda := fecha_busqueda - INTERVAL '1 day'; + + IF fecha_busqueda < CURRENT_DATE - INTERVAL '30 day' THEN + RAISE NOTICE 'No se encontro tipo de cambio valido en los ultimos 30 dias desde %', fecha_busqueda; + monedacambio := 1; + EXIT; + END IF; + END LOOP; + END IF; + + -- Calcular totales de productos + IF jsonb_array_length(products) > 0 THEN + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC) + INTO productsubtotal + FROM jsonb_array_elements(products) AS prod; + + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * t.number) + INTO productsiva + FROM jsonb_array_elements(products) AS prod + LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT; + + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * (1 + t.number)) + INTO producttotal + FROM jsonb_array_elements(products) AS prod + LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT; + + producttotal := producttotal + COALESCE(new_ieps, 0); + ELSE + productsubtotal := new_subtotal; + producttotal := new_total; + productsiva := new_iva; + END IF; + + -- Aplicar cambio de moneda + IF (currency_id = 2) THEN + subtotalcambiomoneda := productsubtotal * monedacambio; + ivacambiomoneda := productsiva * monedacambio; + iepscambiomoneda := new_ieps * monedacambio; + totalcambiomoneda := producttotal * monedacambio; + ELSE + subtotalcambiomoneda := productsubtotal; + totalcambiomoneda := producttotal; + ivacambiomoneda := productsiva; + iepscambiomoneda := new_ieps; + END IF; + + INSERT INTO expenses( + id_properties, id_expense_type, id_recurrence, + description, id_suppliers, request_date, + payment_deadline, approval_date, id_request_by, + id_approval_by, id_area, id_expense_cat, + id_approval_stat, id_currency, + subtotal, iva, ieps, total + ) + VALUES ( + 1, 2, 3, + new_description, suppliers_id, + new_request_date, new_payment_deadline, + approval_datevar, request_by, + null, area, expense_cat, + approval_status, currency_id, + COALESCE(subtotalcambiomoneda, 0), + COALESCE(ivacambiomoneda, 0), + COALESCE(iepscambiomoneda, 0), + COALESCE(totalcambiomoneda, 0) + ) + RETURNING id_expense INTO new_expense; + + INSERT INTO payments(id_expense, id_payment_status, payment_date) + VALUES (new_expense, 2, NULL); + + -- Crear purchase_detail. NO se crea stock_movements aqui. + IF products IS NOT NULL AND jsonb_array_length(products) > 0 THEN + FOR product IN SELECT jsonb_array_elements(products) + LOOP + INSERT INTO purchase_detail ( + id_expense, id_product, quantity, id_tax, + unit_cost, total, id_stock_mov + ) + VALUES ( + new_expense, + (product->>'id_product')::INT, + (product->>'quantity')::INT, + (product->>'id_tax')::INT, + ((product->>'unit_cost')::NUMERIC * monedacambio), + COALESCE( + (SELECT + ((product->>'quantity')::NUMERIC * + (product->>'unit_cost')::NUMERIC * + (1 + COALESCE(t.number, 0))) * monedacambio + FROM taxes t + WHERE t.id_tax = (product->>'id_tax')::INT), + 0 + ), + NULL -- id_stock_mov se asigna en purchaseentry + ); + END LOOP; + END IF; + + RETURN new_expense; +END; +$BODY$; + +-- ============================================================ +-- 4. FIX: updateexpensev4 +-- ============================================================ +-- Ya NO crea ni elimina stock_movements. Solo actualiza +-- expenses y purchase_detail. +-- ============================================================ +CREATE OR REPLACE FUNCTION public.updateexpensev4( + expense_id integer, + up_description character varying, + suppliers_id integer, + up_req_date date, + up_deadline date, + up_currency integer, + up_request_by integer, + up_area integer, + up_category integer, + needtoapprove boolean, + up_subtotal numeric DEFAULT NULL::numeric, + up_iva numeric DEFAULT NULL::numeric, + up_ieps numeric DEFAULT NULL::numeric, + up_total numeric DEFAULT NULL::numeric, + products jsonb DEFAULT NULL::jsonb) + RETURNS integer + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE +AS $BODY$ +DECLARE + product JSONB; + approval_status INT := 3; + approval_datevar date := NULL; + + monedacambio NUMERIC := 1; + fecha_busqueda DATE; + + productsubtotal NUMERIC := 0; + producttotal NUMERIC := 0; + productsiva NUMERIC := 0; + + subtotalcambiomoneda NUMERIC := 0; + totalcambiomoneda NUMERIC := 0; + ivacambiomoneda NUMERIC := 0; + iepscambiomoneda NUMERIC := 0; +BEGIN + + IF needtoapprove THEN + approval_status := 3; + approval_datevar := NULL; + ELSE + approval_status := 1; + approval_datevar := CURRENT_DATE; + END IF; + + -- Si la moneda es dolares (2), buscar tipo de cambio + IF (up_currency = 2) THEN + IF (up_deadline < CURRENT_DATE) THEN + fecha_busqueda := up_deadline; + ELSE + fecha_busqueda := CURRENT_DATE; + END IF; + + LOOP + SELECT onegetchange(fecha_busqueda) INTO monedacambio; + EXIT WHEN monedacambio IS NOT NULL; + fecha_busqueda := fecha_busqueda - INTERVAL '1 day'; + + IF fecha_busqueda < CURRENT_DATE - INTERVAL '30 day' THEN + RAISE NOTICE 'No se encontro tipo de cambio valido en los ultimos 30 dias desde %', fecha_busqueda; + monedacambio := 1; + EXIT; + END IF; + END LOOP; + END IF; + + -- Calcular totales de productos + IF jsonb_array_length(products) > 0 THEN + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC) + INTO productsubtotal + FROM jsonb_array_elements(products) AS prod; + + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * t.number) + INTO productsiva + FROM jsonb_array_elements(products) AS prod + LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT; + + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * (1 + t.number)) + INTO producttotal + FROM jsonb_array_elements(products) AS prod + LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT; + + producttotal := producttotal + COALESCE(up_ieps, 0); + ELSE + productsubtotal := up_subtotal; + producttotal := up_total; + productsiva := up_iva; + END IF; + + -- Aplicar cambio de moneda + IF (up_currency = 2) THEN + subtotalcambiomoneda := productsubtotal * monedacambio; + ivacambiomoneda := productsiva * monedacambio; + iepscambiomoneda := up_ieps * monedacambio; + totalcambiomoneda := producttotal * monedacambio; + ELSE + subtotalcambiomoneda := productsubtotal; + totalcambiomoneda := producttotal; + ivacambiomoneda := productsiva; + iepscambiomoneda := up_ieps; + END IF; + + -- Actualizar la cabecera del gasto + UPDATE expenses + SET + description = up_description, + id_suppliers = suppliers_id, + request_date = up_req_date, + payment_deadline = up_deadline, + id_currency = up_currency, + id_request_by = up_request_by, + id_area = up_area, + id_expense_cat = up_category, + approval_date = approval_datevar, + id_approval_stat = approval_status, + subtotal = COALESCE(subtotalcambiomoneda, 0), + iva = COALESCE(ivacambiomoneda, 0), + ieps = COALESCE(iepscambiomoneda, 0), + total = COALESCE(totalcambiomoneda, 0) + WHERE id_expense = expense_id; + + -- Manejar detalle de productos + IF products IS NOT NULL THEN + -- Eliminar purchase_detail que ya no vienen en el JSON + DELETE FROM purchase_detail pd + WHERE pd.id_expense = expense_id + AND pd.id_product NOT IN ( + SELECT (p->>'id_product')::INT + FROM jsonb_array_elements(products) AS p + ); + + -- Actualizar o insertar cada producto del JSON + FOR product IN SELECT jsonb_array_elements(products) + LOOP + UPDATE purchase_detail + SET + quantity = (product->>'quantity')::INT, + id_tax = (product->>'id_tax')::INT, + unit_cost = ((product->>'unit_cost')::NUMERIC * monedacambio), + total = COALESCE( + (SELECT ((product->>'quantity')::NUMERIC * + (product->>'unit_cost')::NUMERIC * + (1 + COALESCE(t.number, 0))) * monedacambio + FROM taxes t + WHERE t.id_tax = (product->>'id_tax')::INT), + 0 + ) + WHERE id_expense = expense_id + AND id_product = (product->>'id_product')::INT; + + -- Si no actualizo nada (no existia), insertar + IF NOT FOUND THEN + INSERT INTO purchase_detail ( + id_expense, id_product, quantity, id_tax, + unit_cost, total, id_stock_mov + ) + VALUES ( + expense_id, + (product->>'id_product')::INT, + (product->>'quantity')::INT, + (product->>'id_tax')::INT, + ((product->>'unit_cost')::NUMERIC * monedacambio), + COALESCE( + (SELECT ((product->>'quantity')::NUMERIC * + (product->>'unit_cost')::NUMERIC * + (1 + COALESCE(t.number, 0))) * monedacambio + FROM taxes t + WHERE t.id_tax = (product->>'id_tax')::INT), + 0 + ), + NULL + ); + END IF; + END LOOP; + END IF; + + RETURN 1; +END; +$BODY$; + +-- ============================================================ +-- 5. FIX: purchaseentry +-- ============================================================ +-- Actualiza purchase_detail.id_stock_mov con el movimiento +-- recien creado, para mantener integridad referencial. +-- ============================================================ +CREATE OR REPLACE FUNCTION public.purchaseentry( + purchase_id integer, + checking integer) + RETURNS integer + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE +AS $BODY$ +DECLARE + v_id_product INT; + v_id_expense INT; + v_quantity INT; + v_unit_cost NUMERIC; + v_delivered INT; + stock_anterior BIGINT; + stock_actual BIGINT; + v_id_stock_mov INT; + v_quantity_received INT; +BEGIN + -- Recuperamos la informacion de purchase_detail + SELECT id_product, id_expense, quantity, unit_cost, delivered + INTO v_id_product, v_id_expense, v_quantity, v_unit_cost, v_delivered + FROM purchase_detail + WHERE id_purchase_dt = purchase_id; + + -- Cantidad efectivamente recibida en este movimiento + v_quantity_received := LEAST(checking, v_quantity - COALESCE(v_delivered, 0)); + + IF v_quantity_received <= 0 THEN + RETURN 0; -- Ya se recibio todo o cantidad invalida + END IF; + + UPDATE purchase_detail + SET delivered = LEAST(COALESCE(delivered, 0) + checking, quantity) + WHERE id_purchase_dt = purchase_id; + + SELECT stock INTO stock_anterior + FROM stock WHERE id_product = v_id_product FOR UPDATE; + + stock_actual := stock_anterior + v_quantity_received; + + INSERT INTO stock_movements( + id_product, id_mov_type, date_st_mov, id_purchase_dt, + id_expense, quantity, stock_before, stock_current + ) + VALUES ( + v_id_product, 1, CURRENT_DATE, purchase_id, + v_id_expense, v_quantity_received, stock_anterior, stock_actual + ) + RETURNING id_sto_mov INTO v_id_stock_mov; + + UPDATE stock + SET + average_cost = CASE + WHEN average_cost IS NULL THEN v_unit_cost + ELSE ( + ((average_cost * stock) + (v_quantity_received * v_unit_cost)) + / (stock + v_quantity_received) + ) + END, + stock = stock_actual + WHERE id_product = v_id_product; + + -- Asignar el id del movimiento al purchase_detail + UPDATE purchase_detail + SET id_stock_mov = v_id_stock_mov + WHERE id_purchase_dt = purchase_id; + + RETURN 1; +END; +$BODY$; + +-- Confirmacion +SELECT 'Fix completo de flujo de inventario aplicado correctamente' AS resultado; diff --git a/scripts_sql/rollback_inventory_flow_complete.sql b/scripts_sql/rollback_inventory_flow_complete.sql new file mode 100644 index 0000000..1040f52 --- /dev/null +++ b/scripts_sql/rollback_inventory_flow_complete.sql @@ -0,0 +1,506 @@ +-- ============================================================ +-- ROLLBACK COMPLETO: Restaurar flujo de inventario original +-- Fecha: 2026-06-09 +-- ============================================================ +-- Usar este script SOLO si el fix completo causa problemas +-- y se necesita restaurar el comportamiento anterior. +-- ============================================================ + +-- ============================================================ +-- 1. RESTAURAR setpaymentstatusv2 +-- ============================================================ +CREATE OR REPLACE FUNCTION public.setpaymentstatusv2( + expense integer, + status integer) + RETURNS integer + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE +AS $BODY$ +DECLARE + rows_updated INT := 0; + details RECORD; + approval int :=0; +BEGIN + SELECT id_approval_stat INTO approval FROM expenses where id_expense = expense; + IF(approval = 1 ) --si el gasto esta aprobado se procede al pago + THEN + -- Validar estados permitidos + IF status = 1 THEN + UPDATE payments + SET id_payment_status = status, + payment_date = CURRENT_DATE + WHERE id_expense = expense; + GET DIAGNOSTICS rows_updated = ROW_COUNT; + + IF rows_updated = 0 THEN + RETURN -1; + END IF; + + -- Procesar solo si existen movimientos de stock + IF EXISTS ( + SELECT 1 + FROM stock_movements st + INNER JOIN purchase_detail pd ON st.id_expense = pd.id_expense + WHERE pd.id_expense = expense + ) THEN + + -- Actualizar movimiento + UPDATE stock_movements + SET date_st_mov = CURRENT_DATE + WHERE id_expense = expense; + + -- Actualizar ajuste + UPDATE stock_adjusments + SET date_adjusment = CURRENT_DATE + WHERE id_expense = expense; + + -- Actualizar stock + FOR details IN + SELECT id_product, quantity + FROM purchase_detail + WHERE id_expense = expense + LOOP + UPDATE stock + SET stock = stock + details.quantity + WHERE id_product = details.id_product; + END LOOP; + END IF; + RETURN status; + + ELSIF status = 3 THEN + -- Actualizar pago + UPDATE payments + SET id_payment_status = status, + payment_date = CURRENT_DATE + WHERE id_expense = expense; + GET DIAGNOSTICS rows_updated = ROW_COUNT; + + IF rows_updated = 0 THEN + RETURN -1; + END IF; + + RETURN status; + + ELSE + -- Status invalido + RETURN -2; + END IF; + ELSE + return -3; --si no esta aprobado o rechazado no se puede actualizar el pago + END IF; +END; +$BODY$; + +-- ============================================================ +-- 2. RESTAURAR newexpensev2 +-- ============================================================ +CREATE OR REPLACE FUNCTION public.newexpensev2( + new_description character varying, + suppliers_id integer, + new_request_date date, + new_payment_deadline date, + request_by integer, + area integer, + expense_cat integer, + currency_id integer, + needtoapprove boolean, + products jsonb DEFAULT NULL::jsonb, + new_subtotal numeric DEFAULT NULL::numeric, + new_iva numeric DEFAULT NULL::numeric, + new_ieps numeric DEFAULT NULL::numeric, + new_total numeric DEFAULT NULL::numeric) + RETURNS integer + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE +AS $BODY$ +DECLARE + new_expense INT; + product JSONB; + + approval_status INT := 3; + approval_datevar DATE := NULL; + + monedacambio NUMERIC := 1; + fecha_busqueda DATE; + mov_id INT; + + productsubtotal NUMERIC := 0; + producttotal NUMERIC := 0; + productsiva NUMERIC := 0; + + subtotalcambiomoneda NUMERIC := 0; + totalcambiomoneda NUMERIC := 0; + ivacambiomoneda NUMERIC := 0; + iepscambiomoneda NUMERIC := 0; + +BEGIN + + IF needtoapprove THEN + approval_status := 3; + approval_datevar := NULL; + ELSE + approval_status := 1; + approval_datevar := CURRENT_DATE; + END IF; + + IF (currency_id = 2) THEN + IF (new_payment_deadline < CURRENT_DATE) THEN + fecha_busqueda := new_payment_deadline; + ELSE + fecha_busqueda := CURRENT_DATE; + END IF; + + LOOP + SELECT onegetchange(fecha_busqueda) INTO monedacambio; + EXIT WHEN monedacambio IS NOT NULL; + fecha_busqueda := fecha_busqueda - INTERVAL '1 day'; + + IF fecha_busqueda < CURRENT_DATE - INTERVAL '30 day' THEN + RAISE NOTICE 'No se encontro tipo de cambio valido en los ultimos 30 dias desde %', fecha_busqueda; + monedacambio := 1; + EXIT; + END IF; + END LOOP; + END IF; + + IF jsonb_array_length(products) > 0 THEN + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC) + INTO productsubtotal + FROM jsonb_array_elements(products) AS prod; + + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * t.number) + INTO productsiva + FROM jsonb_array_elements(products) AS prod + LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT; + + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * (1 + t.number)) + INTO producttotal + FROM jsonb_array_elements(products) AS prod + LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT; + + producttotal := producttotal + COALESCE(new_ieps, 0); + ELSE + productsubtotal := new_subtotal; + producttotal := new_total; + productsiva := new_iva; + END IF; + + IF (currency_id = 2) THEN + subtotalcambiomoneda := productsubtotal * monedacambio; + ivacambiomoneda := productsiva * monedacambio; + iepscambiomoneda := new_ieps * monedacambio; + totalcambiomoneda := producttotal * monedacambio; + ELSE + subtotalcambiomoneda := productsubtotal; + totalcambiomoneda := producttotal; + ivacambiomoneda := productsiva; + iepscambiomoneda := new_ieps; + END IF; + + INSERT INTO expenses( + id_properties, id_expense_type, id_recurrence, + description, id_suppliers, request_date, + payment_deadline, approval_date, id_request_by, + id_approval_by, id_area, id_expense_cat, + id_approval_stat, id_currency, + subtotal, iva, ieps, total + ) + VALUES ( + 1, 2, 3, + new_description, suppliers_id, + new_request_date, new_payment_deadline, + approval_datevar, request_by, + null, area, expense_cat, + approval_status, currency_id, + COALESCE(subtotalcambiomoneda, 0), + COALESCE(ivacambiomoneda, 0), + COALESCE(iepscambiomoneda, 0), + COALESCE(totalcambiomoneda, 0) + ) + RETURNING id_expense INTO new_expense; + + INSERT INTO payments(id_expense, id_payment_status, payment_date) + VALUES (new_expense, 2, NULL); + + IF products IS NOT NULL AND jsonb_array_length(products) > 0 THEN + FOR product IN SELECT jsonb_array_elements(products) + LOOP + INSERT INTO stock_movements(id_product,id_mov_type,date_st_mov,id_expense) + VALUES((product->>'id_product')::INT,1,new_request_date,new_expense) + RETURNING id_sto_mov INTO mov_id; + + INSERT INTO purchase_detail (id_expense, id_product, quantity, id_tax, unit_cost, total,id_stock_mov) + VALUES ( + new_expense, + (product->>'id_product')::INT, + (product->>'quantity')::INT, + (product->>'id_tax')::INT, + ((product->>'unit_cost')::NUMERIC * monedacambio), + COALESCE( + (SELECT + ((product->>'quantity')::NUMERIC * + (product->>'unit_cost')::NUMERIC * + (1 + COALESCE(t.number, 0))) * monedacambio + FROM taxes t + WHERE t.id_tax = (product->>'id_tax')::INT), + 0 + ),mov_id + ); + END LOOP; + END IF; + + RETURN new_expense; +END; +$BODY$; + +-- ============================================================ +-- 3. RESTAURAR updateexpensev4 +-- ============================================================ +CREATE OR REPLACE FUNCTION public.updateexpensev4( + expense_id integer, + up_description character varying, + suppliers_id integer, + up_req_date date, + up_deadline date, + up_currency integer, + up_request_by integer, + up_area integer, + up_category integer, + needtoapprove boolean, + up_subtotal numeric DEFAULT NULL::numeric, + up_iva numeric DEFAULT NULL::numeric, + up_ieps numeric DEFAULT NULL::numeric, + up_total numeric DEFAULT NULL::numeric, + products jsonb DEFAULT NULL::jsonb) + RETURNS integer + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE +AS $BODY$ +DECLARE + product JSONB; + approval_status INT := 3; + approval_datevar date := NULL; + + monedacambio NUMERIC := 1; + fecha_busqueda DATE; + mov_id int; + + productsubtotal NUMERIC := 0; + producttotal NUMERIC := 0; + productsiva NUMERIC := 0; + + subtotalcambiomoneda NUMERIC := 0; + totalcambiomoneda NUMERIC := 0; + ivacambiomoneda NUMERIC := 0; + iepscambiomoneda NUMERIC := 0; +BEGIN + + IF needtoapprove THEN + approval_status := 3; + approval_datevar := NULL; + ELSE + approval_status := 1; + approval_datevar := CURRENT_DATE; + END IF; + + IF (up_currency = 2) THEN + IF (up_deadline < CURRENT_DATE) THEN + fecha_busqueda := up_deadline; + ELSE + fecha_busqueda := CURRENT_DATE; + END IF; + + LOOP + SELECT onegetchange(fecha_busqueda) INTO monedacambio; + EXIT WHEN monedacambio IS NOT NULL; + fecha_busqueda := fecha_busqueda - INTERVAL '1 day'; + + IF fecha_busqueda < CURRENT_DATE - INTERVAL '30 day' THEN + RAISE NOTICE 'No se encontro tipo de cambio valido en los ultimos 30 dias desde %', fecha_busqueda; + monedacambio := 1; + EXIT; + END IF; + END LOOP; + END IF; + + IF jsonb_array_length(products) > 0 THEN + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC) + INTO productsubtotal + FROM jsonb_array_elements(products) AS prod; + + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * t.number) + INTO productsiva + FROM jsonb_array_elements(products) AS prod + LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT; + + SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * (1 + t.number)) + INTO producttotal + FROM jsonb_array_elements(products) AS prod + LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT; + + producttotal := producttotal + COALESCE(up_ieps, 0); + ELSE + productsubtotal := up_subtotal; + producttotal := up_total; + productsiva := up_iva; + END IF; + + IF (up_currency = 2) THEN + subtotalcambiomoneda := productsubtotal * monedacambio; + ivacambiomoneda := productsiva * monedacambio; + iepscambiomoneda := up_ieps * monedacambio; + totalcambiomoneda := producttotal * monedacambio; + ELSE + subtotalcambiomoneda := productsubtotal; + totalcambiomoneda := producttotal; + ivacambiomoneda := productsiva; + iepscambiomoneda := up_ieps; + END IF; + + UPDATE expenses + SET + description = up_description, + id_suppliers = suppliers_id, + request_date = up_req_date, + payment_deadline = up_deadline, + id_currency = up_currency, + id_request_by = up_request_by, + id_area = up_area, + id_expense_cat = up_category, + approval_date = approval_datevar, + id_approval_stat = approval_status, + subtotal = COALESCE(subtotalcambiomoneda, 0), + iva = COALESCE(ivacambiomoneda, 0), + ieps = COALESCE(iepscambiomoneda, 0), + total = COALESCE(totalcambiomoneda, 0) + WHERE id_expense = expense_id; + + IF products IS NOT NULL THEN + DELETE FROM purchase_detail pd + WHERE pd.id_expense = expense_id + AND pd.id_product NOT IN ( + SELECT (p->>'id_product')::INT + FROM jsonb_array_elements(products) AS p + ); + + DELETE FROM stock_movements sm + WHERE sm.id_expense = expense_id + AND sm.id_sto_mov NOT IN ( + SELECT pd.id_stock_mov + FROM purchase_detail pd + WHERE pd.id_expense = expense_id); + + FOR product IN SELECT jsonb_array_elements(products) + LOOP + UPDATE stock_movements + SET date_st_mov = up_req_date + WHERE id_expense = expense_id + AND id_product = (product->>'id_product')::INT; + + IF NOT FOUND THEN + INSERT INTO stock_movements(id_product,id_mov_type,date_st_mov,id_expense) + VALUES((product->>'id_product')::INT,1,up_req_date,expense_id) + RETURNING id_sto_mov INTO mov_id; + END IF; + + UPDATE purchase_detail + SET + quantity = (product->>'quantity')::INT, + id_tax = (product->>'id_tax')::INT, + unit_cost = ((product->>'unit_cost')::NUMERIC * monedacambio), + total = COALESCE( + (SELECT ((product->>'quantity')::NUMERIC * + (product->>'unit_cost')::NUMERIC * + (1 + COALESCE(t.number, 0))) * monedacambio + FROM taxes t + WHERE t.id_tax = (product->>'id_tax')::INT), + 0 + ) + WHERE id_expense = expense_id + AND id_product = (product->>'id_product')::INT; + + IF NOT FOUND THEN + INSERT INTO purchase_detail ( + id_expense, id_product, quantity, id_tax, unit_cost, total, id_stock_mov + ) + VALUES ( + expense_id, + (product->>'id_product')::INT, + (product->>'quantity')::INT, + (product->>'id_tax')::INT, + ((product->>'unit_cost')::NUMERIC * monedacambio), + COALESCE( + (SELECT ((product->>'quantity')::NUMERIC * + (product->>'unit_cost')::NUMERIC * + (1 + COALESCE(t.number, 0))) * monedacambio + FROM taxes t + WHERE t.id_tax = (product->>'id_tax')::INT), + 0 + ),mov_id + ); + END IF; + END LOOP; + END IF; + + RETURN 1; +END; +$BODY$; + +-- ============================================================ +-- 4. RESTAURAR purchaseentry +-- ============================================================ +CREATE OR REPLACE FUNCTION public.purchaseentry( + purchase_id integer, + checking integer) + RETURNS integer + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE +AS $BODY$ +DECLARE + v_id_product INT; + v_id_expense INT; + v_quantity INT; + v_unit_cost NUMERIC; + v_delivered INT; + stock_anterior BIGINT; + stock_actual BIGINT; +BEGIN + SELECT id_product, id_expense, quantity, unit_cost, delivered + INTO v_id_product, v_id_expense, v_quantity, v_unit_cost, v_delivered + FROM purchase_detail + WHERE id_purchase_dt = purchase_id; + + UPDATE purchase_detail + SET delivered = LEAST(COALESCE(delivered, 0) + checking, quantity) + WHERE id_purchase_dt = purchase_id; + + SELECT stock INTO stock_anterior + FROM stock WHERE id_product = v_id_product FOR UPDATE; + + stock_actual := stock_anterior + LEAST(checking, v_quantity - COALESCE(v_delivered, 0)); + + INSERT INTO stock_movements(id_product, id_mov_type, date_st_mov, id_purchase_dt, id_expense, quantity, stock_before, stock_current) + VALUES (v_id_product, 1, CURRENT_DATE, purchase_id, v_id_expense, LEAST(checking, v_quantity - COALESCE(v_delivered, 0)), stock_anterior, stock_actual); + + UPDATE stock + SET + average_cost = CASE + WHEN average_cost IS NULL THEN v_unit_cost + ELSE ( + ((average_cost * stock) + (LEAST(checking, v_quantity - COALESCE(v_delivered, 0)) * v_unit_cost)) + / (stock + LEAST(checking, v_quantity - COALESCE(v_delivered, 0))) + ) + END, + stock = stock_actual + WHERE id_product = v_id_product; + + RETURN 1; +END; +$BODY$; + +-- Confirmacion +SELECT 'Rollback completo de flujo de inventario aplicado correctamente' AS resultado;