@fayz-ai/plugin-inventory 0.12.9 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/InventoryGeneralSettings.d.ts +15 -1
- package/dist/components/InventoryGeneralSettings.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +10 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.js +4016 -100
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +14 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/store.d.ts +8 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InventoryPage.d.ts +1 -1
- package/dist/views/InventoryPage.d.ts.map +1 -1
- package/dist/views/UsageAuditView.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/locales/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6drC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6dvC,CAAA"}
|
|
@@ -4,6 +4,20 @@ export declare const MIGRATION_002_A_AUDITORIA_FECHA_EM_LOTE = "-- 002_a_auditor
|
|
|
4
4
|
export declare const MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER = "-- 003_stock_count_closes_through_ledger.sql\n--\n-- A physical count is a reconciliation, not a second stock writer. The old\n-- function inserted values into GENERATED columns, recorded the counted total\n-- instead of the variance and then updated the position a second time outside\n-- the ledger trigger. This replacement emits one canonical `adjust` movement\n-- per divergent line and lets the existing movement trigger update the balance.\n--\n-- Safety gates: every line must be counted (zero is valid); any stock movement\n-- after the opening snapshot aborts the close; lot stock requires a future\n-- batch-aware count instead of guessing which lot changed.\n\nCREATE OR REPLACE FUNCTION public.inventory_close_count_session(p_session_id uuid, p_reason text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := app.inventory_require_tenant();\n v_session public.plg_inventory_count_sessions%ROWTYPE;\n v_item public.plg_inventory_count_items%ROWTYPE;\n v_op record;\n v_delta numeric;\n v_current numeric;\n v_current_cost numeric;\n v_has_lots boolean;\n v_movement_id uuid;\n v_reason text;\n v_key text := 'count:' || p_session_id::text || ':close:v1';\n v_line_no int := 0;\n v_uncounted int;\n v_conflicts int;\n v_emitted int := 0;\n v_movements jsonb := '[]'::jsonb;\n v_positions jsonb := '[]'::jsonb;\n v_result jsonb;\nBEGIN\n SELECT * INTO v_session FROM public.plg_inventory_count_sessions\n WHERE id = p_session_id AND tenant_id = v_tenant FOR UPDATE;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('ok', false, 'error', 'unknown count session');\n END IF;\n PERFORM app.inventory_authorize_location(v_tenant, v_session.stock_location_id, 'inventory.edit');\n\n IF v_session.status = 'closed' THEN\n RETURN jsonb_build_object('ok', true, 'session_id', p_session_id,\n 'status', 'closed', 'already_closed', true, 'adjustments_created', 0);\n END IF;\n IF v_session.status = 'cancelled' THEN\n RETURN jsonb_build_object('ok', false, 'error', 'this count was cancelled');\n END IF;\n\n v_reason := COALESCE(NULLIF(btrim(COALESCE(p_reason, '')), ''), 'Stock count');\n\n SELECT count(*) INTO v_uncounted\n FROM public.plg_inventory_count_items i\n WHERE i.session_id = p_session_id AND i.counted_quantity IS NULL;\n IF v_uncounted > 0 THEN\n RAISE EXCEPTION 'inventory: count session has % uncounted line(s)', v_uncounted\n USING ERRCODE = '22023', HINT = 'Count every line, including physical zero, before closing.';\n END IF;\n\n PERFORM p.id\n FROM public.plg_inventory_stock_positions p\n JOIN public.plg_inventory_count_items i\n ON i.tenant_id = p.tenant_id AND i.product_id = p.product_id\n WHERE i.session_id = p_session_id\n AND p.stock_location_id = v_session.stock_location_id\n ORDER BY p.id\n FOR UPDATE OF p;\n\n SELECT count(DISTINCT i.id) INTO v_conflicts\n FROM public.plg_inventory_count_items i\n JOIN public.plg_inventory_stock_movements m\n ON m.tenant_id = i.tenant_id\n AND m.product_id = i.product_id\n AND (m.source_location_id = v_session.stock_location_id\n OR m.destination_location_id = v_session.stock_location_id)\n WHERE i.session_id = p_session_id\n AND m.created_at > v_session.opened_at\n AND (i.movement_id IS NULL OR m.id <> i.movement_id);\n IF v_conflicts > 0 THEN\n RAISE EXCEPTION 'inventory: stock changed after this count opened (% line(s)); review and recount', v_conflicts\n USING ERRCODE = '40001';\n END IF;\n\n SELECT * INTO v_op FROM app.inventory_begin_operation(\n v_tenant, 'adjust', v_key,\n jsonb_build_object('count_session_id', p_session_id, 'reason', v_reason));\n IF v_op.existing IS NOT NULL THEN\n UPDATE public.plg_inventory_count_sessions\n SET status = 'closed', closed_at = coalesce(closed_at, now()),\n closed_by = coalesce(closed_by, auth.uid()), updated_at = now()\n WHERE id = p_session_id;\n RETURN v_op.existing || jsonb_build_object('already_closed', true, 'adjustments_created', 0);\n END IF;\n\n FOR v_item IN\n SELECT * FROM public.plg_inventory_count_items\n WHERE session_id = p_session_id AND movement_id IS NULL\n ORDER BY id\n FOR UPDATE\n LOOP\n SELECT coalesce(sum(p.quantity), 0),\n coalesce(max(p.unit_cost), v_item.unit_cost, 0),\n coalesce(bool_or((p.batch_number IS NOT NULL OR p.expiration_date IS NOT NULL) AND p.quantity <> 0), false)\n INTO v_current, v_current_cost, v_has_lots\n FROM public.plg_inventory_stock_positions p\n WHERE p.tenant_id = v_tenant\n AND p.product_id = v_item.product_id\n AND p.stock_location_id = v_session.stock_location_id;\n\n v_delta := v_item.counted_quantity - v_current;\n IF v_delta = 0 THEN CONTINUE; END IF;\n IF v_has_lots THEN\n RAISE EXCEPTION 'inventory: product % has batch stock; close it with a batch-aware count', v_item.product_id\n USING ERRCODE = '22023';\n END IF;\n\n v_line_no := v_line_no + 1;\n INSERT INTO public.plg_inventory_stock_movements (\n tenant_id, product_id, kind, quantity, unit_cost,\n source_location_id, document_type, reason,\n idempotency_key, line_no, operation_id,\n source_item_type, source_item_id, metadata\n ) VALUES (\n v_tenant, v_item.product_id, 'adjust', v_delta, v_current_cost,\n v_session.stock_location_id, 'stock_count', v_reason,\n v_key, v_line_no, v_op.op_id,\n 'stock_count_item', v_item.id,\n jsonb_build_object(\n 'countSessionId', p_session_id,\n 'countItemId', v_item.id,\n 'systemQuantity', v_item.system_quantity,\n 'currentQuantity', v_current,\n 'countedQuantity', v_item.counted_quantity,\n 'variance', v_delta\n )\n )\n RETURNING id INTO v_movement_id;\n\n UPDATE public.plg_inventory_count_items\n SET movement_id = v_movement_id, updated_at = now()\n WHERE id = v_item.id;\n v_movements := v_movements || app.inventory_movement_json(v_movement_id);\n v_positions := v_positions || app.inventory_position_json(\n v_tenant, v_item.product_id, v_session.stock_location_id, NULL, NULL);\n v_emitted := v_emitted + 1;\n END LOOP;\n\n UPDATE public.plg_inventory_count_sessions\n SET status = 'closed', closed_at = now(), closed_by = auth.uid(), updated_at = now()\n WHERE id = p_session_id;\n\n v_result := jsonb_build_object(\n 'ok', true, 'session_id', p_session_id, 'status', 'closed',\n 'already_closed', false, 'adjustments_created', v_emitted,\n 'operation_id', v_op.op_id, 'idempotency_key', v_key,\n 'movements', v_movements, 'positions', v_positions);\n RETURN app.inventory_finish_operation(\n v_tenant, v_op.op_id, 'count.close', v_key, v_result,\n jsonb_build_object('count_session_id', p_session_id, 'adjustments_created', v_emitted));\nEND;\n$$;\n";
|
|
5
5
|
export declare const MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA = "-- A posi\u00E7\u00E3o de estoque atravessa.\n--\n-- `stock_positions` \u2014 o saldo por produto \u00D7 local \u00D7 lote \u2014 estava no plano de\n-- leitura e sem destino: 1.861 linhas do tenant Pertinho do C\u00E9u ficaram\n-- `pendentes` em toda rodada, nunca ofertadas a writer nenhum. A nota no\n-- extrator dizia \"no V2 a posi\u00E7\u00E3o \u00E9 derivada do movimento\", e essa frase \u00E9\n-- verdadeira e insuficiente.\n--\n-- Derivar o saldo do movimento s\u00F3 d\u00E1 o mesmo n\u00FAmero quando o movimento\n-- atravessa INTEIRO. Neste tenant 2.208 dos 2.697 movimentos estavam em\n-- quarentena, ent\u00E3o o saldo derivado seria o saldo de 18% do hist\u00F3rico \u2014\n-- apresentado com a mesma confian\u00E7a do saldo certo. E lote e validade, que a\n-- posi\u00E7\u00E3o carrega e o movimento n\u00E3o, n\u00E3o se derivam de soma nenhuma.\n--\n-- Ent\u00E3o a posi\u00E7\u00E3o vem do V1 como fato, e n\u00E3o como conta.\n--\n-- \u2500\u2500 O que o destino exige e a origem n\u00E3o d\u00E1 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `plg_inventory_stock_positions` tem tr\u00EAs colunas NOT NULL que precisam de\n-- tradu\u00E7\u00E3o:\n--\n-- product_id \u2192 resolvido pelo ledger de `products`\n-- stock_location_id \u2192 pelo ledger de `stock_locations`\n-- unit_id \u2192 a unidade da EMPRESA (app.units), e o V1 guarda duas\n-- coisas com nome parecido: `unit_id` (a unidade de\n-- MEDIDA do item) e `company_unit_id` (a filial). \u00C9 a\n-- segunda. Confundi-las poria a posi\u00E7\u00E3o na unidade\n-- errada \u2014 e num tenant de 63 filiais isso \u00E9 saldo no\n-- lugar de outro.\n--\n-- Quando a filial n\u00E3o vem na linha, ela vem do LOCAL: um local de estoque\n-- pertence a uma unidade, e o saldo que est\u00E1 nele est\u00E1 nela.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. O writer\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_inv_position(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_product uuid := (p_row ->> 'product_id')::uuid;\n v_location uuid := (p_row ->> 'stock_location_id')::uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_qty numeric := round(coalesce((p_row ->> 'quantity')::numeric, 0), 4);\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'uma posi\u00E7\u00E3o precisa de id');\n END IF;\n IF v_product IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'posi\u00E7\u00E3o de saldo cujo produto ainda n\u00E3o atravessou');\n END IF;\n IF v_location IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'posi\u00E7\u00E3o de saldo sem local \u2014 saldo sem lugar n\u00E3o \u00E9 saldo');\n END IF;\n\n -- A filial pelo LOCAL quando a linha n\u00E3o a traz: o local pertence a uma\n -- unidade, e o saldo que est\u00E1 nele est\u00E1 nela.\n IF v_unit IS NULL THEN\n SELECT l.unit_id INTO v_unit\n FROM public.plg_inventory_stock_locations l\n WHERE l.id = v_location AND l.tenant_id = p_tenant;\n END IF;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'posi\u00E7\u00E3o sem unidade, e o local dela tamb\u00E9m n\u00E3o diz de qual \u00E9');\n END IF;\n\n -- `quantity >= 0` \u00E9 CHECK da tabela. O V1 admite saldo negativo (sa\u00EDda que\n -- n\u00E3o conferiu com a entrada); recusar aqui, com a frase, \u00E9 melhor que o erro\n -- cru do banco no meio do lote \u2014 e melhor que gravar zero, que apagaria a\n -- evid\u00EAncia de que a contagem daquele item est\u00E1 furada.\n IF v_qty < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('saldo negativo (%s) \u2014 o V1 admite, o raz\u00E3o de estoque do V2 n\u00E3o', v_qty));\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_inventory_stock_positions WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_inventory_stock_positions\n (id, tenant_id, unit_id, product_id, stock_location_id, quantity, unit_cost,\n batch_number, expiration_date, unit_type)\n VALUES (p_id, p_tenant, v_unit, v_product, v_location, v_qty,\n round(coalesce((p_row ->> 'unit_cost')::numeric, 0), 4),\n p_row ->> 'batch_number', (p_row ->> 'expiration_date')::date,\n coalesce(nullif(btrim(p_row ->> 'unit_type'), ''), 'base'));\n v_created := true;\n ELSE\n UPDATE public.plg_inventory_stock_positions SET\n unit_id = coalesce(v_unit, unit_id),\n product_id = coalesce(v_product, product_id),\n stock_location_id = coalesce(v_location, stock_location_id),\n quantity = CASE WHEN p_row ? 'quantity' THEN v_qty ELSE quantity END,\n unit_cost = CASE WHEN p_row ? 'unit_cost' THEN round(coalesce((p_row ->> 'unit_cost')::numeric, 0), 4) ELSE unit_cost END,\n batch_number = CASE WHEN p_row ? 'batch_number' THEN p_row ->> 'batch_number' ELSE batch_number END,\n expiration_date = CASE WHEN p_row ? 'expiration_date' THEN (p_row ->> 'expiration_date')::date ELSE expiration_date END,\n unit_type = CASE WHEN p_row ? 'unit_type' THEN coalesce(nullif(btrim(p_row ->> 'unit_type'), ''), 'base') ELSE unit_type END,\n updated_at = now()\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_inventory_stock_positions', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. O vocabul\u00E1rio de writers aceita o nome novo\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'inv_position') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''inv_movement''::text',\n '''inv_movement''::text, ''inv_position''::text');\nEND $$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 3. O de-para\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `unit_id` do V1 \u00E9 a unidade de MEDIDA e n\u00E3o a filial \u2014 por isso ela vai para\n-- metadata e quem responde pela filial \u00E9 `company_unit_id`.\n\n-- A allowlist \u00E9 CONGELADA (`trg_allowlist_frozen`): mudan\u00E7a de core-A \u00E9\n-- arquivo de migration, n\u00E3o escrita solta \u2014 e esta \u00E9 um arquivo. A escotilha\n-- \u00E9 transacional (`true` no terceiro argumento), ent\u00E3o n\u00E3o fica porta aberta.\nSELECT set_config('migration.allowlist_unlock', 'on', true);\n\nSELECT migration._allow(\n 'stock_positions', 'public.plg_inventory_stock_positions', 'inventory',\n 'inv_position', '{}'::jsonb, ARRAY['quantity', 'unit_cost'],\n jsonb_build_object(\n 'uuid', 'id',\n 'product_id', 'product_id@products',\n 'stock_location_id', 'stock_location_id@stock_locations',\n 'company_unit_id', 'unit_id@companies:app.units',\n 'quantity', 'quantity',\n 'unit_cost', 'unit_cost',\n 'batch_number', 'batch_number',\n 'expiration_date', 'expiration_date',\n 'unit_type', 'unit_type',\n 'location', 'jsonb:v1.location_label',\n 'unit_id', 'jsonb:v1.measurement_unit_ref'\n ),\n 'O SALDO por produto \u00D7 local \u00D7 lote. Vem da origem como FATO e n\u00E3o derivado '\n 'do movimento: o saldo derivado s\u00F3 bate quando o movimento atravessa inteiro, '\n 'e lote e validade a posi\u00E7\u00E3o carrega e o movimento n\u00E3o.'\n);\n\nSELECT set_config('migration.allowlist_unlock', 'off', true);\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_inv_position(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_inv_position(uuid, uuid, jsonb, jsonb) TO service_role;\n";
|
|
6
6
|
export declare const MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO = "-- A posi\u00E7\u00E3o \u00E9 consequ\u00EAncia do movimento.\n--\n-- Desfaz a migration 004, que estava errada \u2014 e o registro do porqu\u00EA vale mais\n-- que o c\u00F3digo que sai.\n--\n-- A 004 criou um writer para trazer `stock_positions` do V1 como FATO, com o\n-- argumento de que derivar o saldo do movimento s\u00F3 d\u00E1 o mesmo n\u00FAmero quando o\n-- movimento atravessa inteiro. O argumento era bom e a premissa tinha deixado\n-- de valer no mesmo dia: os 2.697 movimentos passaram a atravessar inteiros\n-- assim que a entrada por DANFE ganhou o local (a ponte no extrator).\n--\n-- E o destino j\u00E1 dizia isso, em voz alta:\n--\n-- plg_inventory_stock_positions is written only through the inventory RPCs\n-- (inventory_receive / _transfer \u2026)\n--\n-- 339 das 1.861 linhas bateram nesse gatilho. O gatilho est\u00E1 certo: no V2 a\n-- posi\u00E7\u00E3o n\u00E3o \u00E9 algo que se escreve, \u00E9 o que sobra depois que o movimento\n-- passou. Escrever por fora seria manter duas verdades sobre o mesmo saldo e\n-- deixar a diverg\u00EAncia para depois.\n--\n-- \u2500\u2500 A prova \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Medido em 19/09/2026 no tenant Pertinho do C\u00E9u, depois de os movimentos\n-- entrarem:\n--\n-- V1 stock_positions 1.861 linhas 251.561,2484\n-- V2 plg_inventory_stock_positions 990 linhas 251.561,2484\n--\n-- O SALDO \u00E9 o mesmo at\u00E9 o quarto decimal. A contagem de linhas difere porque o\n-- V1 guarda uma posi\u00E7\u00E3o por lote/validade e o V2 consolida por produto \u00D7 local\n-- quando n\u00E3o h\u00E1 lote \u2014 duas formas de escrever o mesmo estoque, e \u00E9 o estoque\n-- que se confere.\n--\n-- \u2500\u2500 O que sai e o que fica \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Sai a linha de `stock_positions` da allowlist. Fica `migration.excluded_tables`\n-- com o motivo medido, para que a pr\u00F3xima pessoa que vir 1.861 linhas paradas\n-- no dashboard leia a resposta em vez de refazer a investiga\u00E7\u00E3o.\n--\n-- O writer `upsert_inv_position` FICA. Ele n\u00E3o faz mal parado, e um pool que\n-- porte posi\u00E7\u00E3o sem portar movimento vai precisar dele \u2014 o que muda \u00E9 que este\n-- n\u00E3o \u00E9 esse caso.\n--\n-- Idempotente.\n\n-- A linha est\u00E1 CONGELADA, e o gatilho recusa DELETE nela pelo mesmo motivo que\n-- recusa UPDATE: mudan\u00E7a de core-A \u00E9 arquivo de migration. Este \u00E9 o arquivo, e\n-- a escotilha \u00E9 transacional \u2014 n\u00E3o fica porta aberta depois dele.\nSELECT set_config('migration.allowlist_unlock', 'on', true);\nDELETE FROM migration.allowlist WHERE source_table = 'stock_positions';\nSELECT set_config('migration.allowlist_unlock', 'off', true);\n\n-- `WHERE tenant_id IS NULL` no \u00E1rbitro: a 073 da espinha trocou a PK de\n-- `migration.excluded_tables` por dois \u00EDndices \u00FAnicos PARCIAIS \u2014 um global e\n-- um por inquilino \u2014, porque \"n\u00E3o trazer esta tabela\" passou a ser decis\u00E3o de\n-- cliente e n\u00E3o do cluster. \u00CDndice parcial s\u00F3 serve de \u00E1rbitro de ON CONFLICT\n-- quando o comando repete o mesmo recorte. Esta decis\u00E3o \u00E9 global.\nINSERT INTO migration.excluded_tables (source_table, reason)\nVALUES ('stock_positions',\n 'Consequ\u00EAncia, n\u00E3o fato: no V2 a posi\u00E7\u00E3o s\u00F3 se escreve pelas RPCs de '\n 'invent\u00E1rio (o gatilho recusa INSERT direto), e ela se forma sozinha '\n 'quando o movimento atravessa. Conferido em 19/09/2026 no Pertinho do '\n 'C\u00E9u \u2014 V1 1.861 linhas / 251.561,2484 contra V2 990 linhas / '\n '251.561,2484: mesmo saldo, formas diferentes de agrupar (o V1 guarda '\n 'por lote, o V2 consolida sem lote).')\n-- `WHERE tenant_id IS NULL` porque a PK de `excluded_tables` deixou de existir:\n-- o spine 070 a trocou por dois \u00EDndices \u00FAnicos PARCIAIS \u2014 um por tabela global,\n-- outro por tabela e tenant. `ON CONFLICT (source_table)` sozinho n\u00E3o casa com\n-- \u00EDndice parcial, e a cadeia do zero parou aqui com \"there is no unique or\n-- exclusion constraint matching the ON CONFLICT specification\".\n--\n-- Esta linha \u00E9 global (sem tenant), ent\u00E3o \u00E9 o \u00EDndice global que ela quer.\nON CONFLICT (source_table) WHERE tenant_id IS NULL DO UPDATE SET reason = EXCLUDED.reason;\n";
|
|
7
|
+
export declare const MIGRATION_006_A_PRODUCAO_TEM_ORDEM_E_ESTACAO = "-- A produ\u00E7\u00E3o tem ordem e esta\u00E7\u00E3o.\n--\n-- O Pertinho do C\u00E9u produz: pega insumo, passa por etapas, sai produto. Seis\n-- tabelas do V1 descrevem isso e nenhuma tinha destino.\n--\n-- production_orders 3 a ordem: o que produzir, quanto, quando\n-- production_order_inputs 27 o insumo consumido\n-- production_order_outputs 2 o que saiu\n-- production_order_steps 7 a etapa, na esta\u00E7\u00E3o, com tempo\n-- production_plan_items 2 o plano do dia, antes de virar ordem\n-- production_stations 4 onde a etapa acontece\n--\n-- O volume \u00E9 rid\u00EDculo \u2014 45 linhas \u2014 e \u00E9 exatamente por isso que vale trazer: a\n-- casa est\u00E1 COME\u00C7ANDO a usar produ\u00E7\u00E3o, e o dia em que ela usar de verdade o\n-- hist\u00F3rico j\u00E1 vai estar do lado certo. Reconstituir depois \u00E9 imposs\u00EDvel: uma\n-- ordem de produ\u00E7\u00E3o fechada diz quanto de insumo virou quanto de produto, e\n-- esse n\u00FAmero n\u00E3o se recalcula.\n--\n-- \u2500\u2500 A receita j\u00E1 existe, a ordem n\u00E3o existia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `plg_inventory_recipes`, `_recipe_ingredients` e `_recipe_groups` j\u00E1 est\u00E3o no\n-- V2: \u00E9 a FICHA, o que a coisa leva. O que faltava \u00E9 a EXECU\u00C7\u00C3O da ficha \u2014 a\n-- ordem que a aplica num dia, com o que realmente entrou e o que realmente\n-- saiu. `recipe_snapshot` guarda a ficha como ela era no dia, porque mudar a\n-- receita amanh\u00E3 n\u00E3o pode reescrever a produ\u00E7\u00E3o de ontem.\n--\n-- \u2500\u2500 A esta\u00E7\u00E3o \u00E9 do KDS tamb\u00E9m \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `production_stations` do V1 carrega sete colunas de KDS (`kds_enabled`,\n-- `kds_role`, `kds_sla_minutes`\u2026). O V2 j\u00E1 tem `plg_kds_stations` para isso, e\n-- duplicar a configura\u00E7\u00E3o do monitor aqui daria duas respostas para \"qual a SLA\n-- desta esta\u00E7\u00E3o\". Ent\u00E3o elas v\u00E3o para `metadata.kds`, de onde o dia da migra\u00E7\u00E3o\n-- do KDS as l\u00EA \u2014 e a esta\u00E7\u00E3o de PRODU\u00C7\u00C3O fica com o que \u00E9 dela: nome, unidade,\n-- se est\u00E1 ativa.\n--\n-- Idempotente.\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_production_stations (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n name text NOT NULL,\n description text,\n is_active boolean DEFAULT true NOT NULL,\n sort_order integer DEFAULT 0 NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_inventory_production_stations IS\n 'Onde a etapa de produ\u00E7\u00E3o acontece. V1: production_stations \u2014 a configura\u00E7\u00E3o de KDS dela fica em metadata.kds, porque plg_kds_stations \u00E9 quem responde por monitor.';\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_production_orders (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n order_number text,\n status text DEFAULT 'draft'::text NOT NULL,\n product_id uuid,\n recipe_id uuid,\n -- A ficha como ela era no dia. Mudar a receita amanh\u00E3 n\u00E3o reescreve a\n -- produ\u00E7\u00E3o de ontem.\n recipe_snapshot jsonb,\n planned_quantity numeric(18,4),\n measurement_unit_id uuid,\n planned_start timestamp with time zone,\n planned_end timestamp with time zone,\n actual_start timestamp with time zone,\n actual_end timestamp with time zone,\n source_type text,\n priority integer DEFAULT 0 NOT NULL,\n notes text,\n owner_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_inventory_production_orders_window_check\n CHECK (actual_start IS NULL OR actual_end IS NULL OR actual_end >= actual_start)\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_production_steps (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n production_order_id uuid NOT NULL,\n station_id uuid,\n sequence integer DEFAULT 0 NOT NULL,\n name text,\n instructions text,\n estimated_minutes integer,\n status text DEFAULT 'pending'::text NOT NULL,\n started_at timestamp with time zone,\n completed_at timestamp with time zone,\n -- O V1 guarda o nome do operador em texto, sem FK. Fica texto: inventar uma\n -- pessoa a partir de um nome digitado \u00E9 como se cria cadastro duplicado.\n operator_name text,\n notes text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_production_inputs (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n production_order_id uuid NOT NULL,\n step_id uuid,\n product_id uuid NOT NULL,\n -- Planejado contra real: \u00E9 a diferen\u00E7a entre os dois que diz se a ficha\n -- est\u00E1 certa. Guardar s\u00F3 o real perderia a pergunta.\n planned_quantity numeric(18,4),\n actual_quantity numeric(18,4),\n measurement_unit_id uuid,\n stock_position_id uuid,\n unit_cost numeric(18,6),\n consumed_at timestamp with time zone,\n operator_name text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_production_outputs (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n production_order_id uuid NOT NULL,\n product_id uuid NOT NULL,\n -- Principal, subproduto ou perda. \u00C9 a palavra que separa rendimento de\n -- desperd\u00EDcio, e sem ela a conta de custo n\u00E3o fecha.\n output_type text DEFAULT 'main'::text NOT NULL,\n planned_quantity numeric(18,4),\n actual_quantity numeric(18,4),\n measurement_unit_id uuid,\n batch_number text,\n expiration_date date,\n stock_position_id uuid,\n unit_cost numeric(18,6),\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_production_plan_items (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n plan_date date NOT NULL,\n product_id uuid NOT NULL,\n planned_quantity numeric(18,4),\n measurement_unit_id uuid,\n status text DEFAULT 'planned'::text NOT NULL,\n -- A ordem que o plano virou, quando virou. Nulo \u00E9 plano ainda n\u00E3o liberado.\n production_order_id uuid,\n notes text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_inventory_production_plan_items IS\n 'O que se pretende produzir naquele dia, antes de virar ordem. V1: production_plan_items.';\n\n-- \u2500\u2500 chaves \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_inventory_production_stations', 'plg_inventory_production_orders',\n 'plg_inventory_production_steps', 'plg_inventory_production_inputs',\n 'plg_inventory_production_outputs', 'plg_inventory_production_plan_items'] LOOP\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = ('public.' || t)::regclass AND contype = 'p') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I PRIMARY KEY (id)', t, t || '_pkey');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_id_id_key') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I UNIQUE (tenant_id, id)', t, t || '_tenant_id_id_key');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_fkey') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE', t, t || '_tenant_fkey');\n END IF;\n END LOOP;\nEND $$;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_stations_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_stations ADD CONSTRAINT plg_inventory_production_stations_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_orders_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_orders ADD CONSTRAINT plg_inventory_production_orders_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_orders_product_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_orders ADD CONSTRAINT plg_inventory_production_orders_product_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_orders_recipe_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_orders ADD CONSTRAINT plg_inventory_production_orders_recipe_fkey\n FOREIGN KEY (recipe_id) REFERENCES public.plg_inventory_recipes(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_orders_uom_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_orders ADD CONSTRAINT plg_inventory_production_orders_uom_fkey\n FOREIGN KEY (measurement_unit_id) REFERENCES public.plg_inventory_measurement_units(id) ON DELETE SET NULL;\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_steps_order_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_steps ADD CONSTRAINT plg_inventory_production_steps_order_fkey\n FOREIGN KEY (tenant_id, production_order_id) REFERENCES public.plg_inventory_production_orders(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_steps_station_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_steps ADD CONSTRAINT plg_inventory_production_steps_station_fkey\n FOREIGN KEY (station_id) REFERENCES public.plg_inventory_production_stations(id) ON DELETE SET NULL;\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_inputs_order_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_inputs ADD CONSTRAINT plg_inventory_production_inputs_order_fkey\n FOREIGN KEY (tenant_id, production_order_id) REFERENCES public.plg_inventory_production_orders(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_inputs_step_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_inputs ADD CONSTRAINT plg_inventory_production_inputs_step_fkey\n FOREIGN KEY (step_id) REFERENCES public.plg_inventory_production_steps(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_inputs_product_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_inputs ADD CONSTRAINT plg_inventory_production_inputs_product_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_inputs_uom_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_inputs ADD CONSTRAINT plg_inventory_production_inputs_uom_fkey\n FOREIGN KEY (measurement_unit_id) REFERENCES public.plg_inventory_measurement_units(id) ON DELETE SET NULL;\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_outputs_order_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_outputs ADD CONSTRAINT plg_inventory_production_outputs_order_fkey\n FOREIGN KEY (tenant_id, production_order_id) REFERENCES public.plg_inventory_production_orders(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_outputs_product_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_outputs ADD CONSTRAINT plg_inventory_production_outputs_product_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_outputs_uom_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_outputs ADD CONSTRAINT plg_inventory_production_outputs_uom_fkey\n FOREIGN KEY (measurement_unit_id) REFERENCES public.plg_inventory_measurement_units(id) ON DELETE SET NULL;\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_plan_items_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_plan_items ADD CONSTRAINT plg_inventory_production_plan_items_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_plan_items_product_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_plan_items ADD CONSTRAINT plg_inventory_production_plan_items_product_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_production_plan_items_order_fkey') THEN\n ALTER TABLE ONLY public.plg_inventory_production_plan_items ADD CONSTRAINT plg_inventory_production_plan_items_order_fkey\n FOREIGN KEY (production_order_id) REFERENCES public.plg_inventory_production_orders(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCREATE UNIQUE INDEX IF NOT EXISTS plg_inventory_production_orders_number_uidx\n ON public.plg_inventory_production_orders USING btree (tenant_id, order_number)\n WHERE order_number IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_inventory_production_orders_open_idx\n ON public.plg_inventory_production_orders USING btree (tenant_id, unit_id, status, planned_start);\nCREATE INDEX IF NOT EXISTS plg_inventory_production_steps_order_idx\n ON public.plg_inventory_production_steps USING btree (tenant_id, production_order_id, sequence);\nCREATE INDEX IF NOT EXISTS plg_inventory_production_inputs_order_idx\n ON public.plg_inventory_production_inputs USING btree (tenant_id, production_order_id);\nCREATE INDEX IF NOT EXISTS plg_inventory_production_inputs_product_idx\n ON public.plg_inventory_production_inputs USING btree (tenant_id, product_id, consumed_at DESC);\nCREATE INDEX IF NOT EXISTS plg_inventory_production_outputs_order_idx\n ON public.plg_inventory_production_outputs USING btree (tenant_id, production_order_id);\nCREATE INDEX IF NOT EXISTS plg_inventory_production_plan_items_day_idx\n ON public.plg_inventory_production_plan_items USING btree (tenant_id, unit_id, plan_date);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_inventory_production_stations_name_uidx\n ON public.plg_inventory_production_stations USING btree (tenant_id, unit_id, lower(name))\n WHERE unit_id IS NOT NULL;\n\n-- RLS, uma a uma e por extenso. O la\u00E7o adiante j\u00E1 liga tudo isto \u2014 mas ele liga\n-- por `EXECUTE format(...)`, e o port\u00E3o que revisa plugin l\u00EA o ARQUIVO, n\u00E3o o\n-- banco: para ele, tabela com tenant_id e sem um ENABLE literal \u00E9 tabela\n-- aberta. Uma garantia que o revisor n\u00E3o consegue ler \u00E9 uma garantia que\n-- ningu\u00E9m confere.\nALTER TABLE public.plg_inventory_production_stations ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_inventory_production_orders ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_inventory_production_steps ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_inventory_production_inputs ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_inventory_production_outputs ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_inventory_production_plan_items ENABLE ROW LEVEL SECURITY;\n\nDO $$\nDECLARE t text;\nDECLARE tem_unidade boolean;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_inventory_production_stations', 'plg_inventory_production_orders',\n 'plg_inventory_production_steps', 'plg_inventory_production_inputs',\n 'plg_inventory_production_outputs', 'plg_inventory_production_plan_items'] LOOP\n tem_unidade := t IN ('plg_inventory_production_stations', 'plg_inventory_production_orders',\n 'plg_inventory_production_plan_items');\n EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', t || '_updated_at', t);\n EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', t || '_updated_at', t);\n EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', t);\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);\n EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.%I TO authenticated', t);\n EXECUTE format('GRANT ALL ON TABLE public.%I TO service_role', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_select', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (%s)\n AND (SELECT app.has_permission('inventory.read', %s)))$f$,\n t || '_authz_select', t,\n CASE WHEN tem_unidade THEN format('unit_id IS NULL OR (SELECT app.has_unit(%I.unit_id))', t) ELSE 'true' END,\n CASE WHEN tem_unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_write', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR ALL TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('inventory.edit', %s)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('inventory.edit', %s)))$f$,\n t || '_authz_write', t,\n CASE WHEN tem_unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END,\n CASE WHEN tem_unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n END LOOP;\nEND $$;\n\nINSERT INTO public.scoped_resources (resource_table, unit_column, owner_column, shareable, default_visibility, label)\nVALUES ('plg_inventory_production_orders', 'unit_id', 'owner_id', false, 'unit', 'Ordens de produ\u00E7\u00E3o')\nON CONFLICT (resource_table) DO UPDATE\n SET unit_column = EXCLUDED.unit_column, default_visibility = EXCLUDED.default_visibility, label = EXCLUDED.label;\n";
|
|
8
|
+
export declare const MIGRATION_007_A_PRODUCAO_ATRAVESSA = "-- A produ\u00E7\u00E3o atravessa.\n--\n-- A 006 fez as seis tabelas. Esta escreve o de-para com `by_shape` (spine 123).\n-- A ordem importa: esta\u00E7\u00E3o antes da etapa, ordem antes de tudo que pendura\n-- nela, e o plano por \u00FAltimo porque ele aponta para a ordem que gerou.\n--\n-- \u2500\u2500 A configura\u00E7\u00E3o de KDS n\u00E3o vem para c\u00E1 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `production_stations` carrega sete colunas de KDS (`kds_enabled`, `kds_role`,\n-- `kds_sla_minutes`\u2026). Elas v\u00E3o para `metadata.kds` e n\u00E3o para coluna: o V2 tem\n-- `plg_kds_stations`, e duplicar a configura\u00E7\u00E3o do monitor aqui daria duas\n-- respostas para \"qual a SLA desta esta\u00E7\u00E3o\". Quando o KDS portar, ele l\u00EA o\n-- metadata e a esta\u00E7\u00E3o de produ\u00E7\u00E3o fica com o que \u00E9 dela.\n--\n-- \u2500\u2500 O movimento de estoque fica como refer\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `stock_movement_uuid` no insumo e no produto aponta para a baixa/entrada que\n-- a produ\u00E7\u00E3o gerou. Vem como refer\u00EAncia em metadata, n\u00E3o como FK: o movimento\n-- atravessa por outro caminho (`stock_movements`) e amarrar os dois aqui\n-- exigiria que a ordem de produ\u00E7\u00E3o esperasse o raz\u00E3o \u2014 uma depend\u00EAncia que o\n-- volume (45 linhas) n\u00E3o justifica.\n--\n-- Idempotente.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'production_stations', 'public.plg_inventory_production_stations', 'inventory', 'by_shape',\n jsonb_build_object('target', 'public.plg_inventory_production_stations'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'name', 'name',\n 'description', 'description',\n 'active', 'is_active',\n 'kds_display_order', 'sort_order',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'unit_id', '-',\n 'kds_enabled', 'jsonb:kds.enabled',\n 'kds_role', 'jsonb:kds.role',\n 'is_kds_fallback', 'jsonb:kds.is_fallback',\n 'kds_sla_minutes', 'jsonb:kds.sla_minutes',\n 'kds_catalog_mode', 'jsonb:kds.catalog_mode',\n 'kds_config_version', 'jsonb:kds.config_version',\n 'id', 'jsonb:inventory.v1_ref'),\n 'Onde a etapa acontece. `unit_id` da origem \u00E9 uuid de companies e n\u00E3o o id '\n 'num\u00E9rico que o ledger resolve \u2014 a unidade vem pelo augment do extrator. A '\n 'configura\u00E7\u00E3o de KDS fica em metadata.kds porque plg_kds_stations \u00E9 quem '\n 'responde por monitor.'\n);\n\nSELECT migration._allow(\n 'production_orders', 'public.plg_inventory_production_orders', 'inventory', 'by_shape',\n jsonb_build_object('target', 'public.plg_inventory_production_orders'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'unit_id', 'unit_id@companies',\n 'order_number', 'order_number',\n 'status', 'status',\n 'product_id', 'product_id@products',\n 'recipe_id', 'recipe_id@recipes',\n 'recipe_snapshot', 'recipe_snapshot',\n 'planned_qty', 'planned_quantity',\n 'unit_id_measure', 'measurement_unit_id@measurement_units',\n 'planned_start', 'planned_start',\n 'planned_end', 'planned_end',\n 'actual_start', 'actual_start',\n 'actual_end', 'actual_end',\n 'source_type', 'source_type',\n 'priority', 'priority',\n 'notes', 'notes',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'created_by', '-',\n 'released_by', '-',\n 'finished_by', '-',\n 'id', 'jsonb:inventory.v1_ref'),\n 'A ordem de produ\u00E7\u00E3o. `recipe_snapshot` \u00E9 a ficha como ela era no dia \u2014 '\n 'mudar a receita amanh\u00E3 n\u00E3o pode reescrever a produ\u00E7\u00E3o de ontem.'\n);\n\nSELECT migration._allow(\n 'production_order_steps', 'public.plg_inventory_production_steps', 'inventory', 'by_shape',\n jsonb_build_object('target', 'public.plg_inventory_production_steps'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'order_id', 'production_order_id@production_orders',\n 'production_station_id', 'station_id@production_stations',\n 'sequence', 'sequence',\n 'name', 'name',\n 'instructions', 'instructions',\n 'estimated_minutes', 'estimated_minutes',\n 'status', 'status',\n 'started_at', 'started_at',\n 'completed_at', 'completed_at',\n 'operator_name', 'operator_name',\n 'notes', 'notes',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:inventory.v1_ref'),\n 'A etapa, na esta\u00E7\u00E3o, com tempo. `operator_name` continua texto: inventar uma '\n 'pessoa a partir de um nome digitado \u00E9 como se cria cadastro duplicado.'\n);\n\nSELECT migration._allow(\n 'production_order_inputs', 'public.plg_inventory_production_inputs', 'inventory', 'by_shape',\n jsonb_build_object('target', 'public.plg_inventory_production_inputs'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'order_id', 'production_order_id@production_orders',\n 'step_id', 'step_id@production_order_steps',\n 'product_id', 'product_id@products',\n 'planned_qty', 'planned_quantity',\n 'actual_qty', 'actual_quantity',\n 'unit_id', 'measurement_unit_id@measurement_units',\n 'stock_position_id', 'stock_position_id@stock_positions',\n 'unit_cost', 'unit_cost',\n 'consumed_at', 'consumed_at',\n 'operator_name', 'operator_name',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'product_uuid', '-',\n 'unit_label_id', '-',\n 'stock_movement_uuid', 'jsonb:inventory.v1_stock_movement_ref',\n 'id', 'jsonb:inventory.v1_ref'),\n 'O insumo consumido. Planejado E real: \u00E9 a diferen\u00E7a entre os dois que diz se '\n 'a ficha est\u00E1 certa. `unit_id` aqui \u00E9 UNIDADE DE MEDIDA, n\u00E3o filial \u2014 o nome '\n 'da coluna do V1 engana, e por isso o destino se chama measurement_unit_id.'\n);\n\nSELECT migration._allow(\n 'production_order_outputs', 'public.plg_inventory_production_outputs', 'inventory', 'by_shape',\n jsonb_build_object('target', 'public.plg_inventory_production_outputs'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'order_id', 'production_order_id@production_orders',\n 'product_id', 'product_id@products',\n 'output_type', 'output_type',\n 'planned_qty', 'planned_quantity',\n 'actual_qty', 'actual_quantity',\n 'unit_id', 'measurement_unit_id@measurement_units',\n 'batch_number', 'batch_number',\n 'expiration_date', 'expiration_date',\n 'stock_position_id', 'stock_position_id@stock_positions',\n 'unit_cost', 'unit_cost',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'product_uuid', '-',\n 'stock_movement_uuid', 'jsonb:inventory.v1_stock_movement_ref',\n 'id', 'jsonb:inventory.v1_ref'),\n 'O que saiu: principal, subproduto ou perda. \u00C9 essa palavra que separa '\n 'rendimento de desperd\u00EDcio, e sem ela a conta de custo n\u00E3o fecha.'\n);\n\nSELECT migration._allow(\n 'production_plan_items', 'public.plg_inventory_production_plan_items', 'inventory', 'by_shape',\n jsonb_build_object('target', 'public.plg_inventory_production_plan_items'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'unit_id', 'unit_id@companies',\n 'plan_date', 'plan_date',\n 'product_id', 'product_id@products',\n 'planned_qty', 'planned_quantity',\n 'unit_id_measure', 'measurement_unit_id@measurement_units',\n 'status', 'status',\n 'production_order_id', 'production_order_id@production_orders',\n 'notes', 'notes',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:inventory.v1_ref'),\n 'O que se pretende produzir naquele dia, antes de virar ordem. Vem por \u00FAltimo '\n 'porque aponta para a ordem que ele gerou.'\n);\n\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('production_stations', 'production_orders', 'production_order_steps',\n 'production_order_inputs', 'production_order_outputs', 'production_plan_items');\n\nRESET migration.allowlist_unlock;\n";
|
|
9
|
+
export declare const MIGRATION_008_O_GRUPO_DA_RECEITA_TAMBEM_E_DE_UMA_CASA = "-- O grupo da receita tamb\u00E9m \u00E9 de uma casa.\n--\n-- `plg_inventory_recipes` e `plg_inventory_recipe_ingredients` j\u00E1 carregam\n-- `unit_id`. `plg_inventory_recipe_groups` \u2014 que \u00E9 irm\u00E3o dos dois e agrupa\n-- ingredientes alternativos dentro da mesma ficha \u2014 ficou sem.\n--\n-- S\u00E3o 172 grupos no Pertinho do C\u00E9u. Sem a coluna, o grupo de uma cozinha\n-- aparece na ficha da outra, e a substitui\u00E7\u00E3o de ingrediente sugere um produto\n-- que aquela casa n\u00E3o tem em estoque.\n--\n-- Idempotente.\n\nALTER TABLE public.plg_inventory_recipe_groups ADD COLUMN IF NOT EXISTS unit_id uuid;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_inventory_recipe_groups_unit_fkey') THEN\n ALTER TABLE public.plg_inventory_recipe_groups ADD CONSTRAINT plg_inventory_recipe_groups_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCOMMENT ON COLUMN public.plg_inventory_recipe_groups.unit_id IS\n 'A cozinha a que o grupo pertence \u2014 as irm\u00E3s plg_inventory_recipes e _recipe_ingredients j\u00E1 a tinham (008).';\n\nCREATE INDEX IF NOT EXISTS plg_inventory_recipe_groups_unit_idx\n ON public.plg_inventory_recipe_groups USING btree (tenant_id, unit_id);\n";
|
|
10
|
+
export declare const MIGRATION_009_A_DIRECAO_DO_MOVIMENTO_NAO_ESTA_NO_NUMERO = "-- A dire\u00E7\u00E3o do movimento n\u00E3o est\u00E1 no n\u00FAmero \u2014 e o V2 guarda ela l\u00E1.\n--\n-- `plg_inventory_stock_movements` assina a quantidade. O CHECK diz isso em voz\n-- alta desde o baseline:\n--\n-- plg_inventory_stock_movements_sign_check\n-- (kind = 'in' AND quantity > 0)\n-- OR (kind = 'out' AND quantity < 0)\n-- OR (kind = 'transfer' AND quantity < 0)\n-- OR kind IN ('adjust', 'reverse')\n--\n-- E n\u00E3o \u00E9 decora\u00E7\u00E3o: `app.inventory_movements_after_insert` soma `NEW.quantity`\n-- na posi\u00E7\u00E3o de origem e `-NEW.quantity` na de destino. O sinal \u00C9 o efeito no\n-- saldo. Uma sa\u00EDda positiva n\u00E3o seria recusada por capricho \u2014 ela AUMENTARIA o\n-- estoque.\n--\n-- O V1 guarda a dire\u00E7\u00E3o em outro lugar: no `movement_type`. O n\u00FAmero \u00E9 sempre a\n-- magnitude. Medido em 21/09/2026 sobre os 2.997 movimentos do banco inteiro,\n-- todos os inquilinos:\n--\n-- entrada 2.871 menor 0,001 maior 10.000 negativos: 0\n-- saida 119 menor 0,2 maior 26 negativos: 0\n-- ajuste 6 menor 1 maior 1 negativos: 0\n-- transferencia 1 menor 1 maior 1 negativos: 0\n--\n-- `migration.upsert_inv_movement` traduzia o R\u00D3TULO (`saida` \u2192 `out`) e passava\n-- o n\u00FAmero adiante intacto. Ou seja: toda sa\u00EDda e toda transfer\u00EAncia do V1 era\n-- uma viola\u00E7\u00E3o de constraint garantida. Isso ficou dois inquilinos sem\n-- aparecer porque o Pertinho do C\u00E9u s\u00F3 tem entrada \u2014 2.707 de 2.707. O Espa\u00E7o\n-- Facial \u00E9 o primeiro com sa\u00EDda, e as tr\u00EAs sa\u00EDdas dele que tinham local\n-- bateram na constraint:\n--\n-- state: 23514: ... violates check constraint\n-- \"plg_inventory_stock_movements_sign_check\" \u00D73\n--\n-- \u2500\u2500 O ajuste \u00E9 o caso que n\u00E3o se resolve pelo r\u00F3tulo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `entrada`, `saida` e `transferencia` dizem a dire\u00E7\u00E3o no nome. `ajuste` n\u00E3o:\n-- ele tanto acha quanto perde, e o V1 escreve 1 nos dois casos. Quem sabe \u00E9 a\n-- contagem que o gerou \u2014 `stock_count_items.counted_qty - system_qty_at_review`\n-- \u00E9 a diferen\u00E7a apurada, com sinal. Conferido nos 4 ajustes de invent\u00E1rio do\n-- Espa\u00E7o Facial: os dois \"falta apurada\" d\u00E3o -1, os dois \"sobra apurada\" d\u00E3o\n-- +1. O extrator passou a mandar isso como `adjust_delta` (ver\n-- fayz-etl/fayz_etl/sources/beautysoft_v1.py, passo `stock_movements`), e o\n-- de-para abaixo abre a porta para a coluna.\n--\n-- Quando o ajuste n\u00E3o vem de contagem, `adjust_delta` chega nulo \u2014 e a\u00ED a\n-- linha vai para a quarentena em vez de entrar com o sinal chutado. S\u00E3o 2\n-- linhas hoje (\"Corre\u00E7\u00E3o de auditoria (estorno)\"), e as duas j\u00E1 estavam\n-- paradas por falta de local. Somar onde era para subtrair \u00E9 o erro que\n-- ningu\u00E9m v\u00EA: ele n\u00E3o quebra nada, s\u00F3 deixa o saldo errado.\n--\n-- \u2500\u2500 O que N\u00C3O muda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A guarda de entrada continua exigindo `quantity > 0`: o writer recebe do V1,\n-- e no V1 a dire\u00E7\u00E3o \u00E9 o tipo, n\u00E3o o sinal. O que muda \u00E9 a SA\u00CDDA dele \u2014 a\n-- tradu\u00E7\u00E3o para a conven\u00E7\u00E3o da tabela de destino, que \u00E9 onde ela sempre\n-- deveria ter acontecido. As 2.208 + 47 entradas j\u00E1 migradas n\u00E3o se movem: para\n-- `in`, assinado e magnitude s\u00E3o o mesmo n\u00FAmero, e a tabela \u00E9 append-only.\n--\n-- Idempotente: CREATE OR REPLACE e um UPDATE de jsonb por concatena\u00E7\u00E3o.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. O writer assina o n\u00FAmero\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_inv_movement(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'kind', ''))), '');\n v_kind text := CASE v_kind_raw\n WHEN 'in' THEN 'in' WHEN 'out' THEN 'out'\n WHEN 'transfer' THEN 'transfer' WHEN 'adjust' THEN 'adjust' WHEN 'reverse' THEN 'reverse'\n -- the SDK's legacy labels, and V1's pt-BR\n WHEN 'entry' THEN 'in' WHEN 'entrada' THEN 'in'\n WHEN 'exit' THEN 'out' WHEN 'saida' THEN 'out' WHEN 'sa\u00EDda' THEN 'out'\n WHEN 'loss' THEN 'out' WHEN 'perda' THEN 'out'\n WHEN 'adjustment' THEN 'adjust' WHEN 'ajuste' THEN 'adjust'\n WHEN 'transferencia' THEN 'transfer' WHEN 'transfer\u00EAncia' THEN 'transfer'\n END;\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n -- A magnitude, como a origem a manda, e o n\u00FAmero ASSINADO que a tabela pede.\n -- Os dois existem porque s\u00E3o contratos de lados diferentes da fronteira.\n v_qty numeric := coalesce((p_row ->> 'quantity')::numeric, 0);\n v_delta numeric := (p_row ->> 'adjust_delta')::numeric;\n v_signed numeric;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR (p_row ->> 'product_id') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', 'a stock movement needs an id and its product');\n END IF;\n IF v_kind IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown movement type %L (expected in|out|transfer|adjust|reverse, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));\n END IF;\n IF v_qty <= 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a stock movement needs a quantity > 0 \u2014 direction is the kind, not the sign');\n END IF;\n IF v_kind = 'transfer' AND (p_row ->> 'destination_location_id') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a transfer needs its destination location');\n END IF;\n\n -- A tradu\u00E7\u00E3o. O r\u00F3tulo diz a dire\u00E7\u00E3o em tr\u00EAs dos cinco tipos; no ajuste ela\n -- vem da contagem, e sem ela n\u00E3o se inventa.\n IF v_kind = 'adjust' THEN\n IF v_delta IS NULL OR v_delta = 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', 'an adjustment does not say which way it goes \u2014 the V1 writes the size and leaves the sign to the count it came from (adjust_delta)');\n END IF;\n v_signed := sign(v_delta) * v_qty;\n ELSIF v_kind IN ('out', 'transfer') THEN\n v_signed := -v_qty;\n ELSE\n -- 'in' e 'reverse'. O estorno mant\u00E9m a magnitude: o sign_check o dispensa,\n -- e quem decide o sentido dele \u00E9 o movimento que ele desfaz.\n v_signed := v_qty;\n END IF;\n\n SELECT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements WHERE id = p_id) INTO v_exists;\n IF v_exists THEN\n -- the ledger is append-only: a second offer of the same movement is a replay,\n -- not an update. Nothing to do, and saying \"migrated\" again is honest \u2014\n -- the destination already holds exactly this row.\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'already in the ledger (append-only)');\n END IF;\n\n -- 005's single-writer gate refuses any write \"however privileged\" unless this\n -- is set. The migration IS one of the internal writers \u2014 definer-only, fenced\n -- and audited \u2014 so it declares itself rather than being exempted, and the\n -- after-insert trigger derives the position from what it writes.\n PERFORM set_config('fayz.inventory_internal', 'on', true);\n INSERT INTO public.plg_inventory_stock_movements\n (id, tenant_id, product_id, kind, quantity, unit_cost, total_cost,\n source_location_id, destination_location_id, batch_number, expiration_date,\n document_number, reason, notes, movement_date, metadata)\n VALUES (p_id, p_tenant, (p_row ->> 'product_id')::uuid, v_kind,\n v_signed,\n (p_row ->> 'unit_cost')::numeric, (p_row ->> 'total_cost')::numeric,\n (p_row ->> 'source_location_id')::uuid, (p_row ->> 'destination_location_id')::uuid,\n p_row ->> 'batch_number', (p_row ->> 'expiration_date')::date,\n p_row ->> 'document_number', p_row ->> 'reason', p_row ->> 'notes',\n coalesce((p_row ->> 'movement_date')::timestamptz, now()), v_meta);\n PERFORM set_config('fayz.inventory_internal', 'off', true);\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_inventory_stock_movements', p_id, jsonb_build_object('reason', 'created')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');\nEND $function$;\n\nCOMMENT ON FUNCTION migration.upsert_inv_movement(uuid, uuid, jsonb, jsonb) IS\n 'Movimento de estoque do V1 para o V2. Recebe magnitude + tipo (a conven\u00E7\u00E3o do V1) e grava quantidade ASSINADA (a conven\u00E7\u00E3o da tabela). A dire\u00E7\u00E3o do ajuste vem de adjust_delta \u2014 ver 009.';\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. O de-para abre a porta para a dire\u00E7\u00E3o do ajuste\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `adjust_delta` n\u00E3o \u00E9 coluna do V1: \u00E9 derivada pelo plano de leitura, a partir\n-- de `stock_count_items`. Entra no mapa como campo simples porque \u00E9 isso que\n-- ela \u00E9 no `p_row` \u2014 o writer l\u00EA e n\u00E3o grava, ent\u00E3o ela n\u00E3o precisa (nem pode)\n-- casar com coluna do destino.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nUPDATE migration.allowlist\n SET columns = columns || jsonb_build_object('adjust_delta', 'adjust_delta')\n WHERE source_table = 'stock_movements';\n\nRESET migration.allowlist_unlock;\n";
|
|
11
|
+
export declare const MIGRATION_010_A_ENTRADA_DE_NOTAS_ATRAVESSA = "-- A entrada de notas atravessa.\n--\n-- A importa\u00E7\u00E3o de DANFE do V1 \u2014 607 notas (527 no Pertinho do C\u00E9u, 80 no\n-- Espa\u00E7o Facial), 3.008 linhas, 68 lotes \u2014 n\u00E3o tinha nenhuma linha na\n-- `migration.allowlist`. Este arquivo escreve a dela e a das linhas; a parcela,\n-- que \u00E9 do financeiro, mora em plugin-financial/084 (o financeiro roda ANTES do\n-- invent\u00E1rio na corrente, ent\u00E3o o de-para dela n\u00E3o podia vir por aqui).\n--\n-- \u2500\u2500 O achado que mudou o desenho: a linha da nota J\u00C1 \u00E9 o movimento \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O caminho \u00F3bvio seria `danfe_import_items` \u2192 `plg_inventory_stock_movements`.\n-- Medido em 21/09/2026 no V1 de produ\u00E7\u00E3o, ele DOBRARIA o estoque:\n--\n-- tenant 8: 2.707 dos 2.860 itens (94,7%) j\u00E1 t\u00EAm um `stock_movements` no V1,\n-- e a tabela `stock_movements` do tenant 8 \u00E9 INTEIRA de DANFE \u2014\n-- 2.707 de 2.707 linhas com `danfe_import_item_id`.\n-- ledger: 2.757 movimentos j\u00E1 atravessaram (`status = 'migrated'`).\n--\n-- Quantidade e custo batem linha a linha (o movimento \u00E9 gerado pela concilia\u00E7\u00E3o\n-- da nota). Criar um segundo movimento a partir do item somaria a mesma entrada\n-- duas vezes \u2014 e, pior, o gatilho `plg_inventory_stock_movements_after_insert`\n-- deriva a POSI\u00C7\u00C3O do movimento, ent\u00E3o o saldo dobraria junto, sem ningu\u00E9m\n-- acusar.\n--\n-- Ent\u00E3o a linha da nota n\u00E3o cria movimento: ela ADOTA o que j\u00E1 atravessou.\n-- `movement_uuid` resolve pelo ledger de `stock_movements`, o writer devolve\n-- aquele id como `target_id`, e o conte\u00FAdo da linha \u2014 o que o movimento n\u00E3o\n-- carrega: NCM, CFOP, tributo, fator de convers\u00E3o, produto sugerido pela IA \u2014\n-- vai para o `result` da opera\u00E7\u00E3o, que \u00E9 onde o V2 j\u00E1 guarda o que uma opera\u00E7\u00E3o\n-- de invent\u00E1rio produziu (`app.inventory_movement_json`, `inventory_receive`).\n--\n-- Item sem movimento no V1 (153 no tenant 8, 114 no tenant 1) \u00E9 linha que nunca\n-- virou estoque: entra no `result` e fica `skipped`. O porte n\u00E3o inventa\n-- entrada que a origem n\u00E3o fez.\n--\n-- \u2500\u2500 E o conte\u00FAdo FISCAL da nota tamb\u00E9m j\u00E1 atravessa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- 607 de 607 notas t\u00EAm a contraparte em `dfe_inbox` pela chave de acesso, e as\n-- 607 t\u00EAm o MESMO n\u00FAmero de linhas em `dfe_inbox_items` (149 = 149 no tenant 1,\n-- 2.859 = 2.859 no tenant 8). O XML, o tributo por coluna e a duplicata j\u00E1 v\u00E3o\n-- para `plg_fiscalbr_inbox_*` por fiscal_br/003. Copiar tudo de novo aqui daria\n-- duas respostas para \"qual o ICMS desta linha\".\n--\n-- O que N\u00C3O atravessava \u00E9 a DECIS\u00C3O DE ENTRADA feita sobre a nota: a qual\n-- produto a linha foi ligada, em que unidade de estoque, com que fator de\n-- convers\u00E3o, em que conta cont\u00E1bil, e o plano de pagamento que o operador\n-- montou. \u00C9 isso que este arquivo traz.\n--\n-- \u2500\u2500 A nota \u00E9 uma opera\u00E7\u00E3o de recebimento \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `plg_inventory_operations` \u00E9 o raz\u00E3o de opera\u00E7\u00F5es do invent\u00E1rio, com\n-- vocabul\u00E1rio fechado (`receive | transfer | adjust | reserve | confirm |\n-- reverse`). Uma importa\u00E7\u00E3o de DANFE \u00E9 um `receive`, e a `idempotency_key` dela\n-- \u00E9 a CHAVE DE ACESSO: 607 de 607 notas t\u00EAm chave, e ela \u00E9 \u00FAnica dentro do\n-- tenant nas duas casas. Isso faz o `op.idempotency_key = doc.access_key`\n-- ligar a opera\u00E7\u00E3o ao documento fiscal sem coluna nova e sem FK entre plugins\n-- (o fiscal_br roda DEPOIS do invent\u00E1rio).\n--\n-- \u2500\u2500 A coluna que eu tive de criar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `plg_inventory_operations` n\u00E3o tinha `metadata`. Sem ela o `by_shape` joga\n-- fora EM SIL\u00CANCIO tudo que n\u00E3o casa com uma coluna do destino \u2014 que aqui s\u00E3o\n-- os 20 campos da nota \u2014 e joga fora tamb\u00E9m a proveni\u00EAncia que\n-- `_upsert_row_impl` carimba em toda linha portada (`migration.source_table`,\n-- `source_numeric_id`, `batch_id`). Seriam 607 notas no V2 sem como dizer de\n-- onde vieram. \u00C9 a \u00FAnica coluna nova deste arquivo.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. A opera\u00E7\u00E3o ganha onde guardar a nota\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nALTER TABLE public.plg_inventory_operations\n ADD COLUMN IF NOT EXISTS metadata jsonb NOT NULL DEFAULT '{}'::jsonb;\n\nCOMMENT ON COLUMN public.plg_inventory_operations.metadata IS\n 'O que a opera\u00E7\u00E3o carrega e nenhuma coluna nomeia. No porte \u00E9 a nota de '\n 'entrada inteira (metadata.danfe) e a proveni\u00EAncia da linha (metadata.migration). '\n 'A identidade da nota fica em idempotency_key, que \u00E9 a chave de acesso.';\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. O writer gen\u00E9rico com a porteira do invent\u00E1rio aberta\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `migration.upsert_by_shape` (spine 123/124) resolve o upsert desta tabela\n-- sozinho: as colunas do destino j\u00E1 t\u00EAm o nome que o de-para produz. O que ele\n-- N\u00C3O faz \u00E9 abrir a porteira `fayz.inventory_internal`, e as tr\u00EAs tabelas de\n-- invent\u00E1rio recusam qualquer escrita sem ela \u2014 inclusive a do porte, que \u00E9\n-- definer e cercada:\n--\n-- inventory: plg_inventory_operations is written only through the inventory RPCs\n--\n-- Foi essa frase que p\u00F4s 339 posi\u00E7\u00F5es em quarentena na 004 e que a 005 acabou\n-- dando por certa. Ela continua certa para a POSI\u00C7\u00C3O \u2014 a 005 tirou\n-- `stock_positions` da allowlist porque o saldo \u00E9 consequ\u00EAncia do movimento \u2014\n-- e n\u00E3o diz nada contra a OPERA\u00C7\u00C3O, que \u00E9 um fato do V1 e n\u00E3o um derivado de\n-- nada. O que ela exige \u00E9 que quem escreve se declare.\n--\n-- Em vez de copiar o by_shape inteiro s\u00F3 para acrescentar duas linhas, este\n-- writer abre a porteira, delega, e fecha. A porteira \u00E9 transacional\n-- (`set_config(..., true)`): ela morre com a transa\u00E7\u00E3o e n\u00E3o vaza para a linha\n-- seguinte do lote.\n\nCREATE OR REPLACE FUNCTION migration.upsert_inv_shape(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_prev text := coalesce(current_setting('fayz.inventory_internal', true), 'off');\n v_res jsonb;\nBEGIN\n PERFORM set_config('fayz.inventory_internal', 'on', true);\n v_res := migration.upsert_by_shape(p_tenant, p_id, p_row, p_args);\n PERFORM set_config('fayz.inventory_internal', v_prev, true);\n RETURN v_res;\nEND $function$;\n\nCOMMENT ON FUNCTION migration.upsert_inv_shape(uuid, uuid, jsonb, jsonb) IS\n 'upsert_by_shape com fayz.inventory_internal ligado. Para a tabela de invent\u00E1rio '\n 'que j\u00E1 tem a forma certa e cuja porteira de escrita \u00FAnica recusaria o porte.';\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 3. O writer da linha da nota \u2014 adota, nunca duplica\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Serve as duas partes da nota, escolhidas por `writer_args.part`:\n--\n-- 'item' a linha da nota \u2192 o MOVIMENTO que ela gerou no V1\n-- 'batch' o lote da linha \u2192 a POSI\u00C7\u00C3O que aquele movimento derivou\n--\n-- Nenhuma das duas insere: as duas devolvem o id do que j\u00E1 est\u00E1 l\u00E1. \u00C9 a \u00FAnica\n-- forma de a linha atravessar sem somar estoque duas vezes, porque\n-- `plg_inventory_stock_movements` \u00E9 append-only (o gatilho recusa at\u00E9 um UPDATE\n-- que s\u00F3 acrescente coluna nula) e a posi\u00E7\u00E3o \u00E9 consequ\u00EAncia do movimento.\n--\n-- O conte\u00FAdo da linha vai para `plg_inventory_operations.result`, indexado pelo\n-- id da linha \u2014 objeto e n\u00E3o lista, para a segunda oferta da mesma linha\n-- substituir a primeira em vez de empilhar.\n\nCREATE OR REPLACE FUNCTION migration.upsert_inv_danfe_line(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_part text := coalesce(p_args ->> 'part', 'item');\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_operation uuid := (p_row ->> 'operation_id')::uuid;\n v_movement uuid := (p_row ->> 'movement_id')::uuid;\n v_product uuid := (p_row ->> 'product_id')::uuid;\n v_location uuid := (p_row ->> 'stock_location_id')::uuid;\n v_target uuid;\n v_linha jsonb;\n v_prev text;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma linha de nota precisa de id');\n END IF;\n IF v_operation IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'linha de nota cuja importa\u00E7\u00E3o ainda n\u00E3o atravessou');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_inventory_operations o\n WHERE o.id = v_operation AND o.tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a opera\u00E7\u00E3o %s n\u00E3o existe neste tenant', v_operation));\n END IF;\n\n -- O que atravessa da linha: tudo que o movimento n\u00E3o nomeia. Fica no `result`\n -- da opera\u00E7\u00E3o, que \u00E9 onde o V2 guarda o que uma opera\u00E7\u00E3o produziu.\n -- `coalesce` porque `NULL || jsonb` \u00E9 NULL, e uma nota sem nenhum campo de\n -- linha mapeado apagaria o registro inteiro em sil\u00EAncio.\n v_linha := jsonb_strip_nulls(\n coalesce(v_meta -> 'danfe' -> CASE WHEN v_part = 'batch' THEN 'batch' ELSE 'line' END, '{}'::jsonb)\n || jsonb_build_object(\n 'movement_id', to_jsonb(v_movement),\n 'product_id', to_jsonb(v_product),\n 'line_no', p_row -> 'line_no',\n 'quantity', p_row -> 'quantity',\n 'batch_number', p_row -> 'batch_number',\n 'expiration_date', p_row -> 'expiration_date'));\n\n IF v_part = 'batch' THEN\n -- O lote n\u00E3o cria posi\u00E7\u00E3o: ele aponta para a que o movimento derivou. A\n -- chave da posi\u00E7\u00E3o \u00E9 (tenant, produto, local, lote, validade) \u2014 a mesma que\n -- `app.inventory_apply_to_position` usa para decidir se soma ou cria.\n IF v_movement IS NOT NULL AND v_product IS NOT NULL AND v_location IS NOT NULL THEN\n SELECT sp.id INTO v_target\n FROM public.plg_inventory_stock_positions sp\n WHERE sp.tenant_id = p_tenant\n AND sp.product_id = v_product\n AND sp.stock_location_id = v_location\n AND sp.batch_number IS NOT DISTINCT FROM nullif(btrim(p_row ->> 'batch_number'), '')\n AND sp.expiration_date IS NOT DISTINCT FROM (p_row ->> 'expiration_date')::date\n LIMIT 1;\n END IF;\n ELSE\n v_target := v_movement;\n END IF;\n\n -- A porteira do invent\u00E1rio: a opera\u00E7\u00E3o recusa UPDATE sem ela, do porte\n -- inclusive. Transacional, para n\u00E3o vazar para a linha seguinte.\n v_prev := coalesce(current_setting('fayz.inventory_internal', true), 'off');\n PERFORM set_config('fayz.inventory_internal', 'on', true);\n UPDATE public.plg_inventory_operations o\n SET result = coalesce(o.result, '{}'::jsonb) || jsonb_build_object(\n CASE WHEN v_part = 'batch' THEN 'batches' ELSE 'lines' END,\n coalesce(o.result -> CASE WHEN v_part = 'batch' THEN 'batches' ELSE 'lines' END, '{}'::jsonb)\n || jsonb_build_object(p_id::text, v_linha))\n WHERE o.id = v_operation AND o.tenant_id = p_tenant;\n PERFORM set_config('fayz.inventory_internal', v_prev, true);\n\n IF v_target IS NULL THEN\n -- Linha declarada e nunca conciliada: no V1 ela n\u00E3o virou estoque, e o\n -- porte n\u00E3o inventa a entrada que a origem n\u00E3o fez. O conte\u00FAdo dela j\u00E1 foi\n -- gravado no `result` acima \u2014 o que falta \u00E9 s\u00F3 o v\u00EDnculo com o raz\u00E3o.\n RETURN jsonb_build_object('status', 'skipped',\n 'reason', CASE WHEN v_part = 'batch'\n THEN 'lote sem posi\u00E7\u00E3o no V2 \u2014 o movimento dele n\u00E3o atravessou'\n ELSE 'linha sem movimento no V1 \u2014 nunca virou estoque' END);\n END IF;\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant,\n CASE WHEN v_part = 'batch' THEN 'public.plg_inventory_stock_positions'\n ELSE 'public.plg_inventory_stock_movements' END,\n v_target, jsonb_build_object('reason', 'adopted'));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_target,\n 'reason', CASE WHEN v_part = 'batch'\n THEN 'a posi\u00E7\u00E3o deste lote j\u00E1 \u00E9 consequ\u00EAncia do movimento'\n ELSE 'a linha \u00E9 o movimento que j\u00E1 atravessou' END);\nEND $function$;\n\nCOMMENT ON FUNCTION migration.upsert_inv_danfe_line(uuid, uuid, jsonb, jsonb) IS\n 'A linha (ou o lote) da nota de entrada. Adota o movimento/posi\u00E7\u00E3o que o V1 j\u00E1 '\n 'gerou em vez de criar um segundo \u2014 94,7% das linhas do tenant 8 t\u00EAm movimento, '\n 'e duplic\u00E1-lo dobraria o estoque \u2014 e guarda o conte\u00FAdo da linha em operations.result.';\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 4. O vocabul\u00E1rio de writers aceita os nomes novos\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Lendo e costurando o CHECK que est\u00E1 l\u00E1, nunca reescrevendo com lista literal:\n-- a lista literal apaga as palavras que os outros passos da corrente\n-- acrescentaram, e o erro s\u00F3 aparece na onda seguinte.\n\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'inv_shape') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text', '''person''::text, ''inv_shape''::text');\nEND $$;\n\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'inv_danfe_line') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text', '''person''::text, ''inv_danfe_line''::text');\nEND $$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 5. O de-para\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A ordem \u00E9 a da depend\u00EAncia: a nota antes das linhas, as linhas antes dos\n-- lotes. As tr\u00EAs resolvem `import_id@danfe_imports` pelo ledger, e o ledger s\u00F3\n-- responde depois que a nota atravessou.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'danfe_imports', 'public.plg_inventory_operations', 'inventory', 'inv_shape',\n jsonb_build_object('target', 'public.plg_inventory_operations'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n -- `kind` e `op_status` v\u00EAm do augment do extrator: o V1 n\u00E3o guarda a\n -- palavra \"receive\" porque a tabela INTEIRA \u00E9 de entrada, e o status dele\n -- tem cinco palavras (pending | partial | stocked | conciliated |\n -- asset_registered) onde o V2 tem duas. Medido: 71 pendentes e 536\n -- conclu\u00EDdas nos dois tenants.\n 'kind', 'kind',\n 'op_status', 'status',\n 'access_key', 'idempotency_key',\n 'unit_id', 'unit_id@companies',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'status', 'jsonb:danfe.v1_status',\n 'nfe_number', 'jsonb:danfe.nfe_number',\n 'nfe_series', 'jsonb:danfe.nfe_series',\n 'issue_date', 'jsonb:danfe.issue_date',\n 'nature_operation', 'jsonb:danfe.nature_operation',\n 'supplier_name', 'jsonb:danfe.supplier.name',\n 'supplier_cnpj', 'jsonb:danfe.supplier.cnpj',\n 'supplier_id', 'jsonb:danfe.supplier.v1_ref',\n 'total_value', 'jsonb:danfe.totals.total',\n 'discount_value', 'jsonb:danfe.totals.discount',\n 'freight_value', 'jsonb:danfe.totals.freight',\n 'insurance_value', 'jsonb:danfe.totals.insurance',\n 'other_expenses', 'jsonb:danfe.totals.other',\n 'total_items', 'jsonb:danfe.total_items',\n 'conciliated_items', 'jsonb:danfe.conciliated_items',\n 'imported_by', 'jsonb:danfe.imported_by',\n 'payment_summary', 'jsonb:danfe.payment_summary',\n 'default_chart_account_id', 'jsonb:danfe.v1_default_chart_account_ref',\n 'id', 'jsonb:danfe.v1_ref'),\n 'A importa\u00E7\u00E3o de DANFE \u00E9 uma opera\u00E7\u00E3o de RECEBIMENTO, e a idempotency_key '\n 'dela \u00E9 a chave de acesso \u2014 607 de 607 notas t\u00EAm chave e ela \u00E9 \u00FAnica no '\n 'tenant, ent\u00E3o op.idempotency_key = doc.access_key liga a opera\u00E7\u00E3o ao '\n 'documento fiscal sem FK entre plugins. O conte\u00FAdo fiscal n\u00E3o \u00E9 copiado: as '\n '607 notas j\u00E1 est\u00E3o em plg_fiscalbr_inbox_documents, com o mesmo n\u00FAmero de '\n 'linhas em 607 de 607.'\n);\n\nSELECT migration._allow(\n 'danfe_import_items', 'public.plg_inventory_stock_movements', 'inventory', 'inv_danfe_line',\n jsonb_build_object('part', 'item'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'import_id', 'operation_id@danfe_imports',\n -- O movimento que ESTA linha gerou no V1. Resolve pelo ledger de\n -- `stock_movements`; nulo quando a linha nunca foi conciliada, e\n -- irresolvido (quarentena `fk`) quando o movimento existe e n\u00E3o atravessou.\n 'movement_uuid', 'movement_id@stock_movements',\n 'linked_product_id', 'product_id@products',\n 'item_number', 'line_no',\n 'created_at', 'created_at',\n 'tenant_id', '-',\n 'description', 'jsonb:danfe.line.description',\n 'product_code_supplier', 'jsonb:danfe.line.supplier_code',\n 'ean_barcode', 'jsonb:danfe.line.ean',\n 'ncm', 'jsonb:danfe.line.ncm',\n 'cfop', 'jsonb:danfe.line.cfop',\n 'quantity', 'jsonb:danfe.line.quantity',\n 'unit_abbreviation', 'jsonb:danfe.line.unit',\n 'unit_price', 'jsonb:danfe.line.unit_price',\n 'total_price', 'jsonb:danfe.line.total_price',\n 'icms_value', 'jsonb:danfe.line.icms_value',\n 'ipi_value', 'jsonb:danfe.line.ipi_value',\n 'pis_value', 'jsonb:danfe.line.pis_value',\n 'cofins_value', 'jsonb:danfe.line.cofins_value',\n 'entry_type', 'jsonb:danfe.line.entry_type',\n 'cst_entry', 'jsonb:danfe.line.cst_entry',\n 'chart_account_id', 'jsonb:danfe.line.v1_chart_account_ref',\n 'stock_unit_id', 'jsonb:danfe.line.v1_stock_unit_ref',\n 'stock_quantity', 'jsonb:danfe.line.stock_quantity',\n 'conversion_factor', 'jsonb:danfe.line.conversion_factor',\n 'cost_mode', 'jsonb:danfe.line.cost_mode',\n 'reconciled', 'jsonb:danfe.line.reconciled',\n 'xml_batches', 'jsonb:danfe.line.xml_batches',\n 'ai_suggested', 'jsonb:danfe.line.ai.suggested',\n 'ai_create_product', 'jsonb:danfe.line.ai.create_product',\n 'ai_product_name', 'jsonb:danfe.line.ai.product_name',\n 'ai_product_barcode', 'jsonb:danfe.line.ai.product_barcode',\n 'ai_product_cost_price', 'jsonb:danfe.line.ai.product_cost_price',\n 'ai_product_unit_id', 'jsonb:danfe.line.ai.v1_product_unit_ref',\n 'id', 'jsonb:danfe.line.v1_ref'),\n 'A linha da nota. N\u00C3O cria movimento: 2.707 dos 2.860 itens do tenant 8 j\u00E1 '\n 't\u00EAm o movimento que o V1 gerou, e ele j\u00E1 atravessou \u2014 um segundo dobraria a '\n 'entrada e, pelo gatilho after_insert, o saldo junto. A linha adota aquele '\n 'movimento e guarda em operations.result o que ele n\u00E3o nomeia: fator de '\n 'convers\u00E3o, conta cont\u00E1bil, CST de entrada e o produto sugerido pela IA.'\n);\n\nSELECT migration._allow(\n 'danfe_import_item_batches', 'public.plg_inventory_stock_positions', 'inventory', 'inv_danfe_line',\n jsonb_build_object('part', 'batch'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'import_id', 'operation_id@danfe_imports',\n 'product_ref', 'product_id@products',\n 'stock_location_id', 'stock_location_id@stock_locations',\n 'stock_movement_uuid', 'movement_id@stock_movements',\n 'quantity', 'quantity',\n 'batch_number', 'batch_number',\n 'expiration_date', 'expiration_date',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'item_id', 'jsonb:danfe.batch.v1_item_ref',\n 'received_at', 'jsonb:danfe.batch.received_at',\n 'received_by', 'jsonb:danfe.batch.v1_received_by',\n 'source', 'jsonb:danfe.batch.source',\n 'id', 'jsonb:danfe.batch.v1_ref'),\n 'O lote declarado na entrada \u2014 66 no tenant 1 (todos do XML) e 2 no tenant 8 '\n '(manuais). N\u00E3o desfaz a 005: a posi\u00E7\u00E3o continua sendo consequ\u00EAncia do '\n 'movimento e este de-para n\u00E3o grava nenhuma. O gatilho after_insert j\u00E1 criou '\n 'a posi\u00E7\u00E3o com este lote e esta validade, e a linha do lote aponta para ela. '\n '32 dos 68 lotes t\u00EAm movimento; os outros 36 ficam skipped, porque lote sem '\n 'movimento nunca foi saldo.'\n);\n\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('danfe_imports', 'danfe_import_items', 'danfe_import_item_batches');\n\nRESET migration.allowlist_unlock;\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_inv_shape(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_inv_shape(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_inv_danfe_line(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_inv_danfe_line(uuid, uuid, jsonb, jsonb) TO service_role;\n";
|
|
12
|
+
export declare const MIGRATION_011_O_CONSUMO_TEM_DUAS_ORIGENS = "-- 011_o_consumo_tem_duas_origens.sql\n--\n-- A AUDITORIA DE USO ESTAVA VAZIA, E A CULPA N\u00C3O ERA DELA.\n--\n-- A 001 leu o consumo no \u00FAnico lugar onde ele existia quando foi escrita: o\n-- raz\u00E3o, `plg_inventory_stock_movements` com `source_item_type='order_item'`.\n-- Isso pressup\u00F5e que consumir DEU BAIXA. \u00C9 verdade para o consumo que nasce no\n-- V2 \u2014 `order.completed` consome a receita no mesmo instante \u2014 e \u00E9 falso para\n-- tudo o que veio do V1.\n--\n-- O N\u00DAMERO QUE OBRIGA ESTA MIGRATION. No V1, tenant 1:\n--\n-- execution_products ......................... 117.024 linhas\n-- stock_deducted = true ...................... 0\n-- stock_movement_id preenchido ............... 0\n--\n-- Zero. Em nenhum momento da hist\u00F3ria daquele banco o consumo declarado virou\n-- movimento de estoque \u2014 \u00E9 exatamente o que os dois FLOW.md descrevem: no V1 a\n-- auditoria \u00E9 um PORT\u00C3O PR\u00C9-BAIXA, e a fila nunca foi drenada. Migrado, esse\n-- hist\u00F3rico chegou ao V2 como `plg_clinical_session_products` (116.911 linhas\n-- no Espa\u00E7o Facial) e n\u00E3o produziu nenhum movimento, porque n\u00E3o havia nenhum\n-- para produzir.\n--\n-- Resultado medido no cluster, pelo papel `authenticated` com as claims do\n-- tenant: `v_inventory_consumption_audit` devolvia **0 linhas**. A tela do V2 \u00E9 um\n-- porte fiel da do V1 \u2014 produto, quantidade com o \u00ABPadr\u00E3o: N\u00BB s\u00F3 quando\n-- diverge, \u00ABSem posi\u00E7\u00E3o\u00BB, data, profissional, cliente \u2014 e estava passando\n-- fome.\n--\n-- O QUE MUDA AQUI: o consumo passa a ter DUAS ORIGENS.\n--\n-- `ledger` \u2014 a baixa que realmente saiu do estoque (a 001, intacta).\n-- `clinical` \u2014 a declara\u00E7\u00E3o do atendimento que nunca virou baixa.\n--\n-- A PERGUNTA DA TELA \u00C9 A MESMA NAS DUAS. \u00ABO que o profissional disse ter usado\n-- bate com o padr\u00E3o?\u00BB n\u00E3o depende de o saldo ter mudado. O que muda \u00E9 o que\n-- \u00ABcorrigir\u00BB significa, e \u00E9 por isso que a vista ganha `stock_posted`: na\n-- origem do raz\u00E3o corrigir DEVOLVE ao saldo; na origem cl\u00EDnica n\u00E3o h\u00E1 o que\n-- devolver, e corrigir \u00E9 o mesmo ato do V1 \u2014 conserta a quantidade declarada e\n-- aprova.\n--\n-- POR QUE N\u00C3O UMA FILA NOVA. Pelo mesmo motivo da 001: seria um segundo lugar\n-- guardando a mesma quantidade. `plg_clinical_session_products` J\u00C1 \u00E9 o registro\n-- do consumo declarado, com `default_quantity` (o \u00ABPadr\u00E3o: N\u00BB do V1),\n-- `used_quantity`, `is_audited`, `audited_at` e `audit_notes`. A auditoria l\u00EA\n-- de onde o dado mora.\n--\n-- FORMA DA VISTA: as 29 colunas existentes continuam nas mesmas posi\u00E7\u00F5es e com\n-- os MESMOS tipos \u2014 `variance` \u00E9 `numeric` sem typmod, e um `numeric(14,4)` ali\n-- faria o `CREATE OR REPLACE` recusar. As duas colunas novas entram no fim, que\n-- \u00E9 o \u00FAnico lugar onde o REPLACE aceita coluna nova. Nada de DROP: consultado o\n-- `pg_depend`, nada depende desta vista hoje, e mesmo assim um DROP CASCADE\n-- aqui seria o quarto acidente da casa.\n--\n-- `security_invoker` PRESERVADO. Sem ele a vista leria com os direitos de quem\n-- a criou e furaria a RLS das duas origens.\n--\n-- CONSEQU\u00CANCIA CONHECIDA, e ela \u00E9 de prop\u00F3sito: com `security_invoker`, a\n-- origem cl\u00EDnica \u00E9 filtrada pela policy de `plg_clinical_session_products`, que\n-- exige `clinical.read`. Um estoquista com `inventory.read` e sem\n-- `clinical.read` continua vendo a auditoria vazia. Alargar isso \u00E9 decis\u00E3o da\n-- policy do plugin cl\u00EDnico \u2014 dono da tabela \u2014, n\u00E3o de uma migration do estoque,\n-- e escond\u00EA-la atr\u00E1s de um `security_definer` seria trocar uma tela vazia por\n-- um vazamento.\n\n-- \u2500\u2500 replay-safety \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- DROP before CREATE, and the reason is the rule this file exists under: the\n-- chain bench re-applies every migration on a populated database, and\n-- `CREATE OR REPLACE VIEW` may only APPEND columns \u2014 it refuses a narrower\n-- column list with \"cannot drop columns from view\". Files 012 through 018\n-- each widen this view, so on the second pass this statement would meet the\n-- final, wider shape and fail.\n--\n-- Dropping is free here and nowhere else: this view is born in this file. Its\n-- predecessor `v_inventory_usage_audit` (001, applied 2026-09-06) keeps its own\n-- name and its own narrow contract precisely so that it never has to be\n-- touched \u2014 an applied file with a changed checksum is a hard stop for\n-- `fayz db apply` on every cluster that already ran it.\nDROP VIEW IF EXISTS public.v_inventory_consumption_audit CASCADE;\n\nCREATE OR REPLACE VIEW public.v_inventory_consumption_audit\nWITH (security_invoker = 'true') AS\nWITH consumed AS (\n SELECT m.tenant_id,\n m.unit_id,\n m.source_item_id AS item_id,\n m.product_id,\n -- o raz\u00E3o guarda sa\u00EDda como negativo; quem confere pensa em positivo\n sum(- m.quantity) AS posted_quantity,\n sum(m.total_cost) AS total_cost,\n min(m.created_at) AS used_at,\n min(m.movement_date) AS movement_date,\n (array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,\n count(DISTINCT m.source_location_id)::int AS location_count,\n (array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,\n (array_agg(m.metadata ->> 'recipe_id' ORDER BY m.created_at, m.id))[1] AS recipe_id\n FROM public.plg_inventory_stock_movements m\n WHERE m.source_item_type = 'order_item'\n AND m.source_item_id IS NOT NULL\n AND m.kind = 'out'\n -- estornado n\u00E3o \u00E9 consumo: devolver ao estoque apaga a linha da fila\n AND NOT EXISTS (\n SELECT 1 FROM public.plg_inventory_stock_movements r\n WHERE r.reverses_movement_id = m.id\n )\n GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id\n), corrected AS (\n SELECT c.tenant_id,\n c.source_item_id AS item_id,\n c.product_id,\n -- devolver ao estoque \u00E9 ajuste positivo, e reduz o consumido\n sum(- c.quantity) AS correction_delta,\n -- o raz\u00E3o guarda custo sempre positivo; o sinal vem da quantidade\n sum(- sign(c.quantity) * c.total_cost) AS correction_cost,\n max(c.created_at) AS corrected_at,\n (array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason\n FROM public.plg_inventory_stock_movements c\n WHERE c.source_item_type = 'usage_audit'\n AND c.source_item_id IS NOT NULL\n GROUP BY c.tenant_id, c.source_item_id, c.product_id\n),\n\n-- \u2500\u2500 origem 1: o consumo que saiu do estoque (a 001, sem uma v\u00EDrgula a menos) \u2500\nledger AS (\n SELECT c.tenant_id,\n c.unit_id,\n c.item_id,\n c.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n c.used_at,\n c.movement_date,\n c.posted_quantity,\n (c.posted_quantity + coalesce(k.correction_delta, 0)) AS net_quantity,\n std.standard_quantity,\n CASE WHEN std.standard_quantity IS NULL THEN NULL\n ELSE ((c.posted_quantity + coalesce(k.correction_delta, 0))\n - std.standard_quantity)\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n -- o custo acompanha a conclus\u00E3o da auditoria, n\u00E3o o que foi lan\u00E7ado:\n -- devolver um frasco devolve o custo dele junto\n (c.total_cost + coalesce(k.correction_cost, 0)) AS total_cost,\n c.stock_location_id,\n sl.name AS stock_location_name,\n -- mais de uma posi\u00E7\u00E3o quer dizer que o FIFO atravessou lotes\n c.location_count,\n r.id AS recipe_id,\n r.name AS recipe_name,\n i.order_id,\n i.name AS item_name,\n o.reference_number AS order_reference,\n i.assignee_id AS performed_by_id,\n perf.name AS performed_by_name,\n o.party_id AS customer_id,\n cust.name AS customer_name,\n k.corrected_at,\n k.correction_reason,\n (k.item_id IS NOT NULL) AS is_corrected,\n 'ledger'::text AS source,\n true AS stock_posted\n FROM consumed c\n LEFT JOIN corrected k\n ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id\n LEFT JOIN public.products p ON p.id = c.product_id\n LEFT JOIN public.plg_inventory_stock_locations sl ON sl.id = c.stock_location_id\n LEFT JOIN public.plg_inventory_measurement_units u ON u.id = c.measurement_unit_id\n -- A linha vendida e seu pedido chegam por LEFT JOIN de prop\u00F3sito: quem l\u00EA o\n -- estoque pode n\u00E3o ler a venda, e nesse caso a linha aparece sem o nome do\n -- profissional em vez de desaparecer da confer\u00EAncia.\n LEFT JOIN public.items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id\n LEFT JOIN public.orders o ON o.id = i.order_id\n LEFT JOIN public.people perf ON perf.id = i.assignee_id\n LEFT JOIN public.people cust ON cust.id = o.party_id\n LEFT JOIN public.plg_inventory_recipes r\n ON r.id = nullif(c.recipe_id, '')::uuid AND r.tenant_id = c.tenant_id\n LEFT JOIN LATERAL (\n SELECT CASE\n WHEN r.id IS NOT NULL AND coalesce(r.yield_quantity, 0) > 0 THEN (\n SELECT round(sum(ri.quantity) * coalesce(i.quantity, 1) / r.yield_quantity, 4)\n FROM public.plg_inventory_recipe_ingredients ri\n WHERE ri.tenant_id = c.tenant_id\n AND ri.recipe_id = r.id\n AND ri.product_id = c.product_id\n )\n -- sem receita o produto vendido \u00E9 o pr\u00F3prio consumido: o padr\u00E3o \u00E9\n -- a quantidade da linha, e divergir a\u00ED seria erro de sistema\n WHEN r.id IS NULL THEN i.quantity\n ELSE NULL\n END AS standard_quantity\n ) std ON true\n),\n\n-- \u2500\u2500 origem 2: a declara\u00E7\u00E3o do atendimento que nunca virou baixa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- IDENTIDADE. A vista \u00E9 chaveada por (item_id, produto), e 116.144 das 116.911\n-- linhas cl\u00EDnicas n\u00E3o t\u00EAm `order_item_id` \u2014 o consumo do V1 pendurava na\n-- execu\u00E7\u00E3o, n\u00E3o na linha vendida. Ent\u00E3o `item_id` aqui \u00E9 o `id` da PR\u00D3PRIA\n-- linha cl\u00EDnica. Ela \u00E9 \u00FAnica, \u00E9 est\u00E1vel, \u00E9 o que a tela usa como chave de linha\n-- e \u00E9 por ela que a corre\u00E7\u00E3o encontra o registro de volta.\n--\n-- O PADR\u00C3O VEM DO V1, N\u00C3O DA RECEITA. `default_quantity` \u00E9 literalmente o\n-- \u00ABPadr\u00E3o: N\u00BB que a tela do V1 mostrava \u2014 o n\u00FAmero que estava cadastrado no\n-- servi\u00E7o no dia do atendimento. Recalcular pela receita vigente do V2 seria\n-- comparar o passado com um padr\u00E3o que ele n\u00E3o teve.\n--\n-- `nullif(default_quantity, 0)` N\u00C3O \u00C9 COSM\u00C9TICO: 377 linhas vieram com padr\u00E3o\n-- zero, que no V1 quer dizer \u00ABsem padr\u00E3o cadastrado\u00BB (a tela testa\n-- `default_quantity > 0` antes de mostrar o selo). Sem o nullif, cada uma delas\n-- viraria uma diverg\u00EAncia do valor inteiro e o filtro \u00ABs\u00F3 divergentes\u00BB passaria\n-- de 172 achados reais para 421 \u2014 afogar o achado \u00E9 o mesmo que n\u00E3o ter achado.\nclinical AS (\n SELECT cs.tenant_id,\n cs.unit_id,\n cs.id AS item_id,\n cs.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n coalesce(cs.executed_on::timestamptz, cs.created_at) AS used_at,\n coalesce(cs.executed_on, cs.created_at::date) AS movement_date,\n -- N\u00E3o houve postagem. \u00ABposted\u00BB aqui \u00E9 o DECLARADO, e quem diz a verdade\n -- sobre o raz\u00E3o \u00E9 `stock_posted = false`, logo abaixo. Igual ao V1,\n -- corrigir reescreve `used_quantity` no lugar, ent\u00E3o declarado e\n -- l\u00EDquido s\u00E3o sempre o mesmo n\u00FAmero nesta origem.\n coalesce(cs.used_quantity, 0) AS posted_quantity,\n coalesce(cs.used_quantity, 0) AS net_quantity,\n nullif(cs.default_quantity, 0) AS standard_quantity,\n CASE WHEN nullif(cs.default_quantity, 0) IS NULL THEN NULL\n ELSE coalesce(cs.used_quantity, 0) - cs.default_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n -- custo zero n\u00E3o \u00E9 \u00ABn\u00E3o sei\u00BB: \u00E9 \u00ABnada saiu do estoque, logo nada\n -- custou ao estoque\u00BB. Inventar aqui o pre\u00E7o de cat\u00E1logo daria \u00E0 tela\n -- um valor que nenhum raz\u00E3o jamais escreveu.\n 0::numeric AS total_cost,\n NULL::uuid AS stock_location_id,\n NULL::text AS stock_location_name,\n -- o V1 mostrava \u00ABSem posi\u00E7\u00E3o\u00BB em 117.024 de 117.024 linhas; a aus\u00EAncia\n -- de local aqui \u00E9 o porte fiel daquele selo, n\u00E3o uma perda de dado\n 1 AS location_count,\n NULL::uuid AS recipe_id,\n NULL::text AS recipe_name,\n i.order_id,\n -- o V1 mostrava o SERVI\u00C7O na linha; no V2 o servi\u00E7o \u00E9 um `products`\n sv.name AS item_name,\n o.reference_number AS order_reference,\n coalesce(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,\n perf.name AS performed_by_name,\n s.party_id AS customer_id,\n cust.name AS customer_name,\n -- \u00ABCorrigir\u00BB e \u00ABAprovar\u00BB eram um ato s\u00F3 no V1: auditado \u00E9 o conferido\n cs.audited_at AS corrected_at,\n cs.audit_notes AS correction_reason,\n cs.is_audited AS is_corrected,\n 'clinical'::text AS source,\n false AS stock_posted\n FROM public.plg_clinical_session_products cs\n LEFT JOIN public.plg_clinical_sessions s\n ON s.tenant_id = cs.tenant_id AND s.id = cs.session_id\n LEFT JOIN public.products p ON p.id = cs.product_id\n LEFT JOIN public.products sv ON sv.id = s.service_id\n LEFT JOIN public.plg_inventory_measurement_units u ON u.id = cs.used_unit_id\n LEFT JOIN public.items i ON i.id = cs.order_item_id AND i.tenant_id = cs.tenant_id\n LEFT JOIN public.orders o ON o.id = i.order_id\n LEFT JOIN public.people perf ON perf.id = coalesce(cs.assignee_id, s.performed_by_id, s.assignee_id)\n LEFT JOIN public.people cust ON cust.id = s.party_id\n -- A MESMA LINHA N\u00C3O CONTA DUAS VEZES. Se a declara\u00E7\u00E3o cl\u00EDnica j\u00E1 produziu\n -- uma sa\u00EDda no raz\u00E3o, quem manda \u00E9 o raz\u00E3o: \u00E9 ele que tem custo, posi\u00E7\u00E3o e\n -- a corre\u00E7\u00E3o que devolve saldo.\n --\n -- O teste de estorno N\u00C3O se repete aqui de prop\u00F3sito. A 001 decidiu que\n -- sa\u00EDda estornada sai da fila; se o g\u00EAmeo cl\u00EDnico reaparecesse quando o\n -- estorno acontece, a mesma linha voltaria \u00E0 confer\u00EAncia pela porta dos\n -- fundos e diria o contr\u00E1rio do que a 001 disse.\n WHERE NOT EXISTS (\n SELECT 1\n FROM public.plg_inventory_stock_movements m\n WHERE m.tenant_id = cs.tenant_id\n AND m.source_item_type = 'order_item'\n AND m.source_item_id = cs.order_item_id\n AND m.product_id = cs.product_id\n AND m.kind = 'out'\n )\n)\n\n-- O casco externo existe pelos TYPMODS. Um UNION apaga `numeric(14,4)` e deixa\n-- `numeric` puro, e o `CREATE OR REPLACE VIEW` recusa mudan\u00E7a de tipo de coluna\n-- existente \u2014 inclusive typmod. Ent\u00E3o cada coluna volta ao tipo que ela j\u00E1\n-- tinha, um a um. `variance` fica `numeric` SEM typmod porque \u00E9 assim que ela\n-- est\u00E1 no cat\u00E1logo hoje; apert\u00E1-la para `numeric(14,4)` seria o mesmo erro pelo\n-- lado oposto.\nSELECT a.tenant_id,\n a.unit_id,\n a.item_id,\n a.product_id,\n a.product_name,\n a.product_sku,\n a.used_at,\n a.movement_date,\n a.posted_quantity::numeric(14,4) AS posted_quantity,\n a.net_quantity::numeric(14,4) AS net_quantity,\n a.standard_quantity::numeric(14,4) AS standard_quantity,\n a.variance::numeric AS variance,\n a.unit_abbreviation,\n a.total_cost::numeric(16,4) AS total_cost,\n a.stock_location_id,\n a.stock_location_name,\n a.location_count,\n a.recipe_id,\n a.recipe_name,\n a.order_id,\n a.item_name,\n a.order_reference,\n a.performed_by_id,\n a.performed_by_name,\n a.customer_id,\n a.customer_name,\n a.corrected_at,\n a.correction_reason,\n a.is_corrected,\n -- \u2500\u2500 colunas novas, no fim, que \u00E9 onde o REPLACE aceita \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n a.source,\n a.stock_posted\n FROM (\n SELECT * FROM ledger\n UNION ALL\n SELECT * FROM clinical\n ) a;\n\nCOMMENT ON VIEW public.v_inventory_consumption_audit IS\n 'O consumo declarado em cada atendimento ao lado do padr\u00E3o, por linha e produto, nas suas duas origens: o que saiu do raz\u00E3o (source=ledger) e o que o V1 declarou e nunca baixou (source=clinical, stock_posted=false). N\u00E3o cria fila: os dois lugares j\u00E1 guardavam tudo (001, 011).';\n\nGRANT SELECT ON public.v_inventory_consumption_audit TO authenticated;\nGRANT SELECT ON public.v_inventory_consumption_audit TO service_role;\n\n-- \u2500\u2500 \u00EDndices: a confer\u00EAncia \u00E9 sempre (tenant, mais recente primeiro) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A tela pagina 50 por vez ordenando por `used_at DESC` sobre 116.911 linhas.\n-- Sem isto a vista varre a tabela inteira a cada p\u00E1gina.\nCREATE INDEX IF NOT EXISTS plg_clinical_session_products_audit_idx\n ON public.plg_clinical_session_products (tenant_id, executed_on DESC, id);\n\n-- O anti-join da deduplica\u00E7\u00E3o entra pelo par (linha vendida, produto)\nCREATE INDEX IF NOT EXISTS plg_clinical_session_products_order_item_idx\n ON public.plg_clinical_session_products (tenant_id, order_item_id, product_id)\n WHERE order_item_id IS NOT NULL;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- CORRIGIR, NA ORIGEM CL\u00CDNICA\n--\n-- A 002 fez de `app.inventory_correct_usage_line` o miolo \u00FAnico: a corre\u00E7\u00E3o\n-- avulsa e o lote s\u00E3o o mesmo c\u00F3digo. Ele procura a baixa no raz\u00E3o e, n\u00E3o\n-- achando, levanta \u00ABno usage of product % on item % to correct\u00BB. Com a vista\n-- servindo 116.890 linhas cl\u00EDnicas, esse erro passaria a ser a resposta normal\n-- da tela \u2014 o bot\u00E3o existiria s\u00F3 para falhar.\n--\n-- O QUE CORRIGIR SIGNIFICA EM CADA ORIGEM:\n--\n-- raz\u00E3o \u2014 a baixa aconteceu; corrigir 50 para 40 DEVOLVE 10 ao saldo, como\n-- ajuste, porque o raz\u00E3o \u00E9 append-only. (inalterado)\n-- cl\u00EDnica \u2014 nada saiu; n\u00E3o h\u00E1 o que devolver. Corrigir \u00E9 o ato do V1:\n-- conserta a quantidade declarada, exige motivo, e APROVA junto \u2014\n-- \u00ABSalvar e Aprovar\u00BB era um bot\u00E3o s\u00F3, nunca dois.\n--\n-- Lan\u00E7ar um ajuste de estoque para a origem cl\u00EDnica seria pior que n\u00E3o\n-- corrigir: criaria movimento para um consumo que nunca teve movimento, e o\n-- saldo passaria a contar uma devolu\u00E7\u00E3o de algo que nunca saiu.\n--\n-- PERMISS\u00C3O, EXPLICITAMENTE. A fun\u00E7\u00E3o \u00E9 SECURITY DEFINER, ent\u00E3o o UPDATE n\u00E3o\n-- passa pela RLS de `plg_clinical_session_products` \u2014 quem tem de barrar \u00E9 este\n-- c\u00F3digo. Exige-se `inventory.edit` na unidade do consumo, que \u00E9 a mesma regra\n-- do caminho do raz\u00E3o (`inventory_authorize_location` cobra `inventory.edit` na\n-- unidade do local). Linha sem unidade (906 delas) cai no teste \u00ABem qualquer\n-- unidade\u00BB, espelhando o `unit_id IS NULL OR has_unit(...)` da pr\u00F3pria policy.\nCREATE OR REPLACE FUNCTION app.inventory_correct_usage_line(\n p_tenant uuid,\n p_op uuid,\n p_key text,\n p_line_no integer,\n p_item_id uuid,\n p_product_id uuid,\n p_quantity numeric,\n p_reason text\n) RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_row record;\n v_cs record;\n v_delta numeric;\n v_unit uuid;\n v_id uuid;\nBEGIN\n IF p_quantity IS NULL OR p_quantity < 0 THEN\n RAISE EXCEPTION 'inventory: the corrected quantity cannot be negative' USING ERRCODE = '22023';\n END IF;\n\n -- o consumido de hoje: a baixa original mais o que auditorias j\u00E1 corrigiram\n SELECT sum(- m.quantity) AS net,\n (array_agg(m.source_location_id ORDER BY m.created_at DESC, m.id DESC))[1] AS location_id,\n (array_agg(m.measurement_unit_id ORDER BY m.created_at DESC, m.id DESC))[1] AS measurement_unit_id,\n (array_agg(m.batch_number ORDER BY m.created_at DESC, m.id DESC))[1] AS batch_number,\n (array_agg(m.expiration_date ORDER BY m.created_at DESC, m.id DESC))[1] AS expiration_date,\n (array_agg(m.unit_cost ORDER BY m.created_at DESC, m.id DESC))[1] AS unit_cost\n INTO v_row\n FROM public.plg_inventory_stock_movements m\n WHERE m.tenant_id = p_tenant\n AND m.source_item_id = p_item_id\n AND m.product_id = p_product_id\n AND ((m.source_item_type = 'order_item' AND m.kind = 'out')\n OR (m.source_item_type = 'usage_audit' AND m.kind = 'adjust'))\n AND NOT EXISTS (\n SELECT 1 FROM public.plg_inventory_stock_movements r\n WHERE r.reverses_movement_id = m.id\n );\n\n -- \u2500\u2500 a origem cl\u00EDnica: consumo declarado que nunca virou baixa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n IF v_row.net IS NULL THEN\n SELECT cs.id, cs.unit_id, cs.used_quantity, cs.is_audited\n INTO v_cs\n FROM public.plg_clinical_session_products cs\n WHERE cs.tenant_id = p_tenant\n AND cs.id = p_item_id\n AND cs.product_id = p_product_id;\n\n IF v_cs.id IS NULL THEN\n RAISE EXCEPTION 'inventory: no usage of product % on item % to correct', p_product_id, p_item_id\n USING ERRCODE = '22023';\n END IF;\n\n IF v_cs.unit_id IS NOT NULL THEN\n IF NOT app.has_unit(v_cs.unit_id) THEN\n RAISE EXCEPTION 'inventory: no access to the unit of usage %', p_item_id USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('inventory.edit', v_cs.unit_id) THEN\n RAISE EXCEPTION 'inventory: inventory.edit required at the unit of usage %', p_item_id\n USING ERRCODE = '42501';\n END IF;\n ELSIF NOT app.has_permission_anywhere('inventory.edit') THEN\n RAISE EXCEPTION 'inventory: inventory.edit required to correct usage %', p_item_id\n USING ERRCODE = '42501';\n END IF;\n\n -- Conferir e n\u00E3o mudar n\u00FAmero AINDA \u00E9 conferir: a linha pendente vira\n -- auditada. S\u00F3 \u00E9 \u00ABunchanged\u00BB o que j\u00E1 estava auditado com este n\u00FAmero \u2014\n -- reenviar o mesmo lote n\u00E3o reescreve a data da auditoria.\n IF coalesce(v_cs.used_quantity, 0) = p_quantity AND v_cs.is_audited THEN\n RETURN jsonb_build_object('status', 'unchanged', 'origin', 'clinical',\n 'item_id', p_item_id, 'product_id', p_product_id,\n 'previous_quantity', coalesce(v_cs.used_quantity, 0),\n 'quantity', p_quantity, 'delta', 0);\n END IF;\n\n UPDATE public.plg_clinical_session_products\n SET used_quantity = p_quantity,\n is_audited = true,\n audited_at = now(),\n audited_by = auth.uid(),\n audit_notes = p_reason,\n updated_at = now()\n WHERE tenant_id = p_tenant AND id = p_item_id AND product_id = p_product_id;\n\n -- Sem `movements`: nada entrou no raz\u00E3o, e devolver uma lista vazia \u00E9 o que\n -- diz isso a quem chamou. O lote continua contando a linha como aplicada.\n RETURN jsonb_build_object('status', 'corrected', 'origin', 'clinical',\n 'item_id', p_item_id, 'product_id', p_product_id,\n 'unit_id', v_cs.unit_id,\n 'previous_quantity', coalesce(v_cs.used_quantity, 0),\n 'quantity', p_quantity,\n 'delta', coalesce(v_cs.used_quantity, 0) - p_quantity,\n 'movements', '[]'::jsonb);\n END IF;\n\n -- \u2500\u2500 a origem do raz\u00E3o: inalterada desde a 002 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n -- positivo devolve ao estoque, negativo tira mais\n v_delta := v_row.net - p_quantity;\n IF v_delta = 0 THEN\n RETURN jsonb_build_object('status', 'unchanged', 'origin', 'ledger', 'item_id', p_item_id,\n 'product_id', p_product_id, 'previous_quantity', v_row.net,\n 'quantity', p_quantity, 'delta', 0);\n END IF;\n\n IF v_delta < 0\n AND app.inventory_available(p_tenant, p_product_id, v_row.location_id,\n v_row.batch_number, v_row.expiration_date) < - v_delta THEN\n RAISE EXCEPTION 'inventory: correcting to % would take product % below its available quantity at location %',\n p_quantity, p_product_id, v_row.location_id USING ERRCODE = '23514';\n END IF;\n\n v_unit := app.inventory_authorize_location(p_tenant, v_row.location_id, 'inventory.edit');\n\n INSERT INTO public.plg_inventory_stock_movements\n (tenant_id, product_id, kind, quantity, unit_cost, source_location_id,\n batch_number, expiration_date, measurement_unit_id, document_type,\n reason, source_item_type, source_item_id, idempotency_key, line_no,\n operation_id, metadata)\n VALUES\n (p_tenant, p_product_id, 'adjust', v_delta, coalesce(v_row.unit_cost, 0), v_row.location_id,\n v_row.batch_number, v_row.expiration_date, v_row.measurement_unit_id, 'adjustment',\n p_reason, 'usage_audit', p_item_id, p_key, p_line_no, p_op,\n jsonb_build_object('source', 'usage_audit', 'order_item_id', p_item_id,\n 'posted_quantity', v_row.net, 'corrected_quantity', p_quantity))\n RETURNING id INTO v_id;\n\n RETURN jsonb_build_object('status', 'corrected', 'origin', 'ledger', 'item_id', p_item_id,\n 'product_id', p_product_id, 'location_id', v_row.location_id,\n 'unit_id', v_unit, 'previous_quantity', v_row.net,\n 'quantity', p_quantity, 'delta', v_delta,\n 'movements', app.inventory_movement_json(v_id));\nEND $$;\n\nCOMMENT ON FUNCTION app.inventory_correct_usage_line(uuid, uuid, text, integer, uuid, uuid, numeric, text) IS\n 'Uma linha de corre\u00E7\u00E3o de consumo dentro de uma opera\u00E7\u00E3o j\u00E1 aberta. Origem raz\u00E3o: lan\u00E7a a diferen\u00E7a como ajuste. Origem cl\u00EDnica (consumo do V1 que nunca baixou): conserta a quantidade declarada e aprova, sem tocar no saldo (002, 011).';\n\nREVOKE ALL ON FUNCTION app.inventory_correct_usage_line(uuid, uuid, text, integer, uuid, uuid, numeric, text) FROM PUBLIC;\n";
|
|
13
|
+
export declare const MIGRATION_012_O_CONSUMO_CLINICO_NAO_PRECISA_DA_TABELA_DE_ITENS_INTEIRA = "-- O consumo cl\u00EDnico n\u00E3o precisa da tabela de itens inteira.\n--\n-- A auditoria de uso continuava estourando o `statement_timeout` do papel\n-- `authenticated` (8 s) mesmo depois de a clinical/003 baratear a pol\u00EDtica. O\n-- plano, lido com o tempo solto, mostrou onde:\n--\n-- Hash (cost=154686.67 rows=23255) (actual rows=455925)\n--\n-- O ramo cl\u00EDnico da vista faz `LEFT JOIN items i ON i.id = cs.order_item_id`, e\n-- o planejador resolve isso montando um hash de `public.items` INTEIRA \u2014\n-- 455.925 linhas \u2014 para casar com 116.911 consumos. S\u00F3 que **767** desses\n-- consumos t\u00EAm `order_item_id`: no V1 o produto usado pendura na execu\u00E7\u00E3o, n\u00E3o\n-- na linha vendida. O hash existe para 0,7% das linhas e \u00E9 pago por 100%.\n--\n-- O mesmo vale para `orders` e para as duas idas a `people` (120.808 linhas\n-- cada uma, uma para quem executou e outra para a cliente).\n--\n-- \u2500\u2500 A corre\u00E7\u00E3o \u00E9 a forma do join, n\u00E3o o dado \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Cada um vira LATERAL com a condi\u00E7\u00E3o de exist\u00EAncia dentro. Assim o planejador\n-- deixa de montar hash e passa a fazer busca por chave prim\u00E1ria \u2014 767 buscas em\n-- vez de um hash de 455 mil linhas \u2014, e as linhas sem `order_item_id` n\u00E3o\n-- procuram nada.\n--\n-- `LEFT JOIN LATERAL ... ON true` preserva a sem\u00E2ntica do LEFT JOIN: a linha do\n-- consumo sobrevive mesmo quando n\u00E3o h\u00E1 item, que \u00E9 o caso de 116.144 delas.\n--\n-- Medido no cluster, como o usu\u00E1rio que estava com a tela aberta, com ROLLBACK:\n--\n-- 50 linhas ordenadas por data 1,94 s (antes: estourava os 8 s)\n-- contagem total 1,27 s 116.911 linhas \u2014 a MESMA de antes\n--\n-- A contagem id\u00EAntica \u00E9 o teste que importa: uma vista que fica r\u00E1pida\n-- devolvendo menos linha n\u00E3o ficou r\u00E1pida, ficou errada.\n--\n-- Idempotente. `CREATE OR REPLACE` mant\u00E9m nome, ordem e tipo das colunas \u2014 s\u00F3\n-- os joins mudam \u2014 e mant\u00E9m `security_invoker`, sem o qual a vista passaria a\n-- ler com os direitos de quem a criou e furaria a RLS.\n\nCREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS\n WITH consumed AS (\n SELECT m.tenant_id,\n m.unit_id,\n m.source_item_id AS item_id,\n m.product_id,\n sum(- m.quantity) AS posted_quantity,\n sum(m.total_cost) AS total_cost,\n min(m.created_at) AS used_at,\n min(m.movement_date) AS movement_date,\n (array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,\n count(DISTINCT m.source_location_id)::integer AS location_count,\n (array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,\n (array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id\n FROM plg_inventory_stock_movements m\n WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements r\n WHERE r.reverses_movement_id = m.id))\n GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id\n ), corrected AS (\n SELECT c.tenant_id,\n c.source_item_id AS item_id,\n c.product_id,\n sum(- c.quantity) AS correction_delta,\n sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,\n max(c.created_at) AS corrected_at,\n (array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason\n FROM plg_inventory_stock_movements c\n WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL\n GROUP BY c.tenant_id, c.source_item_id, c.product_id\n ), ledger AS (\n SELECT c.tenant_id,\n c.unit_id,\n c.item_id,\n c.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n c.used_at,\n c.movement_date,\n c.posted_quantity,\n c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,\n std.standard_quantity,\n CASE\n WHEN std.standard_quantity IS NULL THEN NULL::numeric\n ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,\n c.stock_location_id,\n sl.name AS stock_location_name,\n c.location_count,\n r.id AS recipe_id,\n r.name AS recipe_name,\n i.order_id,\n i.name AS item_name,\n o.reference_number AS order_reference,\n i.assignee_id AS performed_by_id,\n perf.name AS performed_by_name,\n o.party_id AS customer_id,\n cust.name AS customer_name,\n k.corrected_at,\n k.correction_reason,\n k.item_id IS NOT NULL AS is_corrected,\n 'ledger'::text AS source,\n true AS stock_posted\n FROM consumed c\n LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id\n LEFT JOIN products p ON p.id = c.product_id\n LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id\n LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id\n LEFT JOIN orders o ON o.id = i.order_id\n LEFT JOIN people perf ON perf.id = i.assignee_id\n LEFT JOIN people cust ON cust.id = o.party_id\n LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id\n LEFT JOIN LATERAL ( SELECT\n CASE\n WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round\n FROM plg_inventory_recipe_ingredients ri\n WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)\n WHEN r.id IS NULL THEN i.quantity\n ELSE NULL::numeric\n END AS standard_quantity) std ON true\n ), clinical AS (\n SELECT cs.tenant_id,\n cs.unit_id,\n cs.id AS item_id,\n cs.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n COALESCE(cs.executed_on::timestamp with time zone, cs.created_at) AS used_at,\n COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,\n COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,\n COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,\n NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,\n CASE\n WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric\n ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n 0::numeric AS total_cost,\n NULL::uuid AS stock_location_id,\n NULL::text AS stock_location_name,\n 1 AS location_count,\n NULL::uuid AS recipe_id,\n NULL::text AS recipe_name,\n i.order_id,\n sv.name AS item_name,\n o.reference_number AS order_reference,\n COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,\n perf.name AS performed_by_name,\n s.party_id AS customer_id,\n cust.name AS customer_name,\n cs.audited_at AS corrected_at,\n cs.audit_notes AS correction_reason,\n cs.is_audited AS is_corrected,\n 'clinical'::text AS source,\n false AS stock_posted\n FROM plg_clinical_session_products cs\n LEFT JOIN plg_clinical_sessions s ON s.tenant_id = cs.tenant_id AND s.id = cs.session_id\n LEFT JOIN products p ON p.id = cs.product_id\n LEFT JOIN products sv ON sv.id = s.service_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id\n LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id\n FROM items it\n WHERE cs.order_item_id IS NOT NULL\n AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id) i ON true\n LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id\n FROM orders ord\n WHERE i.order_id IS NOT NULL AND ord.id = i.order_id) o ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id)) perf ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = s.party_id) cust ON true\n WHERE NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements m\n WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))\n )\n SELECT tenant_id,\n unit_id,\n item_id,\n product_id,\n product_name,\n product_sku,\n used_at,\n movement_date,\n posted_quantity::numeric(14,4) AS posted_quantity,\n net_quantity::numeric(14,4) AS net_quantity,\n standard_quantity::numeric(14,4) AS standard_quantity,\n variance,\n unit_abbreviation,\n total_cost::numeric(16,4) AS total_cost,\n stock_location_id,\n stock_location_name,\n location_count,\n recipe_id,\n recipe_name,\n order_id,\n item_name,\n order_reference,\n performed_by_id,\n performed_by_name,\n customer_id,\n customer_name,\n corrected_at,\n correction_reason,\n is_corrected,\n source,\n stock_posted\n FROM ( SELECT ledger.tenant_id,\n ledger.unit_id,\n ledger.item_id,\n ledger.product_id,\n ledger.product_name,\n ledger.product_sku,\n ledger.used_at,\n ledger.movement_date,\n ledger.posted_quantity,\n ledger.net_quantity,\n ledger.standard_quantity,\n ledger.variance,\n ledger.unit_abbreviation,\n ledger.total_cost,\n ledger.stock_location_id,\n ledger.stock_location_name,\n ledger.location_count,\n ledger.recipe_id,\n ledger.recipe_name,\n ledger.order_id,\n ledger.item_name,\n ledger.order_reference,\n ledger.performed_by_id,\n ledger.performed_by_name,\n ledger.customer_id,\n ledger.customer_name,\n ledger.corrected_at,\n ledger.correction_reason,\n ledger.is_corrected,\n ledger.source,\n ledger.stock_posted\n FROM ledger\n UNION ALL\n SELECT clinical.tenant_id,\n clinical.unit_id,\n clinical.item_id,\n clinical.product_id,\n clinical.product_name,\n clinical.product_sku,\n clinical.used_at,\n clinical.movement_date,\n clinical.posted_quantity,\n clinical.net_quantity,\n clinical.standard_quantity,\n clinical.variance,\n clinical.unit_abbreviation,\n clinical.total_cost,\n clinical.stock_location_id,\n clinical.stock_location_name,\n clinical.location_count,\n clinical.recipe_id,\n clinical.recipe_name,\n clinical.order_id,\n clinical.item_name,\n clinical.order_reference,\n clinical.performed_by_id,\n clinical.performed_by_name,\n clinical.customer_id,\n clinical.customer_name,\n clinical.corrected_at,\n clinical.correction_reason,\n clinical.is_corrected,\n clinical.source,\n clinical.stock_posted\n FROM clinical) a;;\n";
|
|
14
|
+
export declare const MIGRATION_013_A_FILA_DE_AUDITORIA_TEM_UMA_JANELA = "-- A fila de auditoria tem uma janela, e a janela precisa de \u00EDndice.\n--\n-- `v_inventory_consumption_audit` atravessa a hist\u00F3ria inteira do consumo. No Espa\u00E7o\n-- Facial isso \u00E9 116.939 linhas, e pedir as 50 mais recentes custava 7 s de\n-- banco: a tela voltava `canceling statement due to statement timeout` e, pior,\n-- mostrava \"Nenhum consumo fora do padr\u00E3o\" por cima do erro.\n--\n-- A tela passou a pedir uma janela (`used_at >= agora - 30 dias`), mas o ramo\n-- cl\u00EDnico n\u00E3o tinha como responder por ela: `used_at` \u00E9\n-- `coalesce(executed_on::timestamptz, created_at)` e essa express\u00E3o N\u00C3O \u00E9\n-- index\u00E1vel \u2014 o cast de `date` para `timestamptz` depende do fuso da sess\u00E3o e\n-- o Postgres o recusa em \u00EDndice.\n--\n-- `timezone('UTC', executed_on::timestamp)` \u00E9 o MESMO valor (o cluster roda em\n-- UTC) e \u00E9 imut\u00E1vel. Trocada a express\u00E3o, o \u00EDndice abaixo existe e a janela\n-- deixa de ser uma varredura.\n\nCREATE INDEX IF NOT EXISTS plg_clinical_session_products_used_at_idx\n ON public.plg_clinical_session_products (\n tenant_id,\n (COALESCE(timezone('UTC', executed_on::timestamp), created_at)) DESC\n );\n\nCREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS\n WITH consumed AS (\n SELECT m.tenant_id,\n m.unit_id,\n m.source_item_id AS item_id,\n m.product_id,\n sum(- m.quantity) AS posted_quantity,\n sum(m.total_cost) AS total_cost,\n min(m.created_at) AS used_at,\n min(m.movement_date) AS movement_date,\n (array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,\n count(DISTINCT m.source_location_id)::integer AS location_count,\n (array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,\n (array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id\n FROM plg_inventory_stock_movements m\n WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements r\n WHERE r.reverses_movement_id = m.id))\n GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id\n ), corrected AS (\n SELECT c.tenant_id,\n c.source_item_id AS item_id,\n c.product_id,\n sum(- c.quantity) AS correction_delta,\n sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,\n max(c.created_at) AS corrected_at,\n (array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason\n FROM plg_inventory_stock_movements c\n WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL\n GROUP BY c.tenant_id, c.source_item_id, c.product_id\n ), ledger AS (\n SELECT c.tenant_id,\n c.unit_id,\n c.item_id,\n c.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n c.used_at,\n c.movement_date,\n c.posted_quantity,\n c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,\n std.standard_quantity,\n CASE\n WHEN std.standard_quantity IS NULL THEN NULL::numeric\n ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,\n c.stock_location_id,\n sl.name AS stock_location_name,\n c.location_count,\n r.id AS recipe_id,\n r.name AS recipe_name,\n i.order_id,\n i.name AS item_name,\n o.reference_number AS order_reference,\n i.assignee_id AS performed_by_id,\n perf.name AS performed_by_name,\n o.party_id AS customer_id,\n cust.name AS customer_name,\n k.corrected_at,\n k.correction_reason,\n k.item_id IS NOT NULL AS is_corrected,\n 'ledger'::text AS source,\n true AS stock_posted\n FROM consumed c\n LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id\n LEFT JOIN products p ON p.id = c.product_id\n LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id\n LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id\n LEFT JOIN orders o ON o.id = i.order_id\n LEFT JOIN people perf ON perf.id = i.assignee_id\n LEFT JOIN people cust ON cust.id = o.party_id\n LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id\n LEFT JOIN LATERAL ( SELECT\n CASE\n WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round\n FROM plg_inventory_recipe_ingredients ri\n WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)\n WHEN r.id IS NULL THEN i.quantity\n ELSE NULL::numeric\n END AS standard_quantity) std ON true\n ), clinical AS (\n SELECT cs.tenant_id,\n cs.unit_id,\n cs.id AS item_id,\n cs.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n COALESCE(timezone('UTC', cs.executed_on::timestamp), cs.created_at) AS used_at,\n COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,\n COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,\n COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,\n NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,\n CASE\n WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric\n ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n 0::numeric AS total_cost,\n NULL::uuid AS stock_location_id,\n NULL::text AS stock_location_name,\n 1 AS location_count,\n NULL::uuid AS recipe_id,\n NULL::text AS recipe_name,\n i.order_id,\n sv.name AS item_name,\n o.reference_number AS order_reference,\n COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,\n perf.name AS performed_by_name,\n s.party_id AS customer_id,\n cust.name AS customer_name,\n cs.audited_at AS corrected_at,\n cs.audit_notes AS correction_reason,\n cs.is_audited AS is_corrected,\n 'clinical'::text AS source,\n false AS stock_posted\n FROM plg_clinical_session_products cs\n LEFT JOIN plg_clinical_sessions s ON s.tenant_id = cs.tenant_id AND s.id = cs.session_id\n LEFT JOIN products p ON p.id = cs.product_id\n LEFT JOIN products sv ON sv.id = s.service_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id\n LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id\n FROM items it\n WHERE cs.order_item_id IS NOT NULL\n AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id) i ON true\n LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id\n FROM orders ord\n WHERE i.order_id IS NOT NULL AND ord.id = i.order_id) o ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id)) perf ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = s.party_id) cust ON true\n WHERE NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements m\n WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))\n )\n SELECT tenant_id,\n unit_id,\n item_id,\n product_id,\n product_name,\n product_sku,\n used_at,\n movement_date,\n posted_quantity::numeric(14,4) AS posted_quantity,\n net_quantity::numeric(14,4) AS net_quantity,\n standard_quantity::numeric(14,4) AS standard_quantity,\n variance,\n unit_abbreviation,\n total_cost::numeric(16,4) AS total_cost,\n stock_location_id,\n stock_location_name,\n location_count,\n recipe_id,\n recipe_name,\n order_id,\n item_name,\n order_reference,\n performed_by_id,\n performed_by_name,\n customer_id,\n customer_name,\n corrected_at,\n correction_reason,\n is_corrected,\n source,\n stock_posted\n FROM ( SELECT ledger.tenant_id,\n ledger.unit_id,\n ledger.item_id,\n ledger.product_id,\n ledger.product_name,\n ledger.product_sku,\n ledger.used_at,\n ledger.movement_date,\n ledger.posted_quantity,\n ledger.net_quantity,\n ledger.standard_quantity,\n ledger.variance,\n ledger.unit_abbreviation,\n ledger.total_cost,\n ledger.stock_location_id,\n ledger.stock_location_name,\n ledger.location_count,\n ledger.recipe_id,\n ledger.recipe_name,\n ledger.order_id,\n ledger.item_name,\n ledger.order_reference,\n ledger.performed_by_id,\n ledger.performed_by_name,\n ledger.customer_id,\n ledger.customer_name,\n ledger.corrected_at,\n ledger.correction_reason,\n ledger.is_corrected,\n ledger.source,\n ledger.stock_posted\n FROM ledger\n UNION ALL\n SELECT clinical.tenant_id,\n clinical.unit_id,\n clinical.item_id,\n clinical.product_id,\n clinical.product_name,\n clinical.product_sku,\n clinical.used_at,\n clinical.movement_date,\n clinical.posted_quantity,\n clinical.net_quantity,\n clinical.standard_quantity,\n clinical.variance,\n clinical.unit_abbreviation,\n clinical.total_cost,\n clinical.stock_location_id,\n clinical.stock_location_name,\n clinical.location_count,\n clinical.recipe_id,\n clinical.recipe_name,\n clinical.order_id,\n clinical.item_name,\n clinical.order_reference,\n clinical.performed_by_id,\n clinical.performed_by_name,\n clinical.customer_id,\n clinical.customer_name,\n clinical.corrected_at,\n clinical.correction_reason,\n clinical.is_corrected,\n clinical.source,\n clinical.stock_posted\n FROM clinical) a;;\n";
|
|
15
|
+
export declare const MIGRATION_014_A_SESSAO_SE_BUSCA_UMA_A_UMA = "-- O atendimento entra na fila UM A UM, n\u00E3o a tabela inteira.\n--\n-- A vista buscava a sess\u00E3o de cada consumo com um LEFT JOIN comum. Sob RLS\n-- isso \u00E9 caro de um jeito que n\u00E3o aparece no EXPLAIN de quem tem chave de\n-- servi\u00E7o: a pol\u00EDtica de `plg_clinical_sessions` chama `app.has_unit` e\n-- `app.has_permission` POR LINHA, e o planejador varre a tabela inteira antes\n-- de casar com os poucos consumos da janela. Medido aqui: 232.264 chamadas,\n-- 6,4 s \u2014 e a tela voltava `canceling statement due to statement timeout` com\n-- a janela de 30 dias j\u00E1 aplicada.\n--\n-- LATERAL amarra a busca \u00E0 linha de fora: a sess\u00E3o \u00E9 procurada pela chave\n-- (tenant, id) uma vez por consumo, e a RLS roda dezenas de vezes em vez de\n-- centenas de milhares. \u00C9 a mesma corre\u00E7\u00E3o que a 012 j\u00E1 tinha feito para\n-- `items`, `orders` e `people` \u2014 a sess\u00E3o tinha ficado de fora.\n\nCREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS\n WITH consumed AS (\n SELECT m.tenant_id,\n m.unit_id,\n m.source_item_id AS item_id,\n m.product_id,\n sum(- m.quantity) AS posted_quantity,\n sum(m.total_cost) AS total_cost,\n min(m.created_at) AS used_at,\n min(m.movement_date) AS movement_date,\n (array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,\n count(DISTINCT m.source_location_id)::integer AS location_count,\n (array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,\n (array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id\n FROM plg_inventory_stock_movements m\n WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements r\n WHERE r.reverses_movement_id = m.id))\n GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id\n ), corrected AS (\n SELECT c.tenant_id,\n c.source_item_id AS item_id,\n c.product_id,\n sum(- c.quantity) AS correction_delta,\n sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,\n max(c.created_at) AS corrected_at,\n (array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason\n FROM plg_inventory_stock_movements c\n WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL\n GROUP BY c.tenant_id, c.source_item_id, c.product_id\n ), ledger AS (\n SELECT c.tenant_id,\n c.unit_id,\n c.item_id,\n c.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n c.used_at,\n c.movement_date,\n c.posted_quantity,\n c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,\n std.standard_quantity,\n CASE\n WHEN std.standard_quantity IS NULL THEN NULL::numeric\n ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,\n c.stock_location_id,\n sl.name AS stock_location_name,\n c.location_count,\n r.id AS recipe_id,\n r.name AS recipe_name,\n i.order_id,\n i.name AS item_name,\n o.reference_number AS order_reference,\n i.assignee_id AS performed_by_id,\n perf.name AS performed_by_name,\n o.party_id AS customer_id,\n cust.name AS customer_name,\n k.corrected_at,\n k.correction_reason,\n k.item_id IS NOT NULL AS is_corrected,\n 'ledger'::text AS source,\n true AS stock_posted\n FROM consumed c\n LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id\n LEFT JOIN products p ON p.id = c.product_id\n LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id\n LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id\n LEFT JOIN orders o ON o.id = i.order_id\n LEFT JOIN people perf ON perf.id = i.assignee_id\n LEFT JOIN people cust ON cust.id = o.party_id\n LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id\n LEFT JOIN LATERAL ( SELECT\n CASE\n WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round\n FROM plg_inventory_recipe_ingredients ri\n WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)\n WHEN r.id IS NULL THEN i.quantity\n ELSE NULL::numeric\n END AS standard_quantity) std ON true\n ), clinical AS (\n SELECT cs.tenant_id,\n cs.unit_id,\n cs.id AS item_id,\n cs.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n COALESCE(timezone('UTC', cs.executed_on::timestamp), cs.created_at) AS used_at,\n COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,\n COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,\n COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,\n NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,\n CASE\n WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric\n ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n 0::numeric AS total_cost,\n NULL::uuid AS stock_location_id,\n NULL::text AS stock_location_name,\n 1 AS location_count,\n NULL::uuid AS recipe_id,\n NULL::text AS recipe_name,\n i.order_id,\n sv.name AS item_name,\n o.reference_number AS order_reference,\n COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,\n perf.name AS performed_by_name,\n s.party_id AS customer_id,\n cust.name AS customer_name,\n cs.audited_at AS corrected_at,\n cs.audit_notes AS correction_reason,\n cs.is_audited AS is_corrected,\n 'clinical'::text AS source,\n false AS stock_posted\n FROM plg_clinical_session_products cs\n LEFT JOIN LATERAL ( SELECT ss.party_id, ss.performed_by_id, ss.assignee_id, ss.service_id\n FROM plg_clinical_sessions ss\n WHERE ss.tenant_id = cs.tenant_id AND ss.id = cs.session_id) s ON true\n LEFT JOIN products p ON p.id = cs.product_id\n LEFT JOIN products sv ON sv.id = s.service_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id\n LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id\n FROM items it\n WHERE cs.order_item_id IS NOT NULL\n AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id) i ON true\n LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id\n FROM orders ord\n WHERE i.order_id IS NOT NULL AND ord.id = i.order_id) o ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id)) perf ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = s.party_id) cust ON true\n WHERE NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements m\n WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))\n )\n SELECT tenant_id,\n unit_id,\n item_id,\n product_id,\n product_name,\n product_sku,\n used_at,\n movement_date,\n posted_quantity::numeric(14,4) AS posted_quantity,\n net_quantity::numeric(14,4) AS net_quantity,\n standard_quantity::numeric(14,4) AS standard_quantity,\n variance,\n unit_abbreviation,\n total_cost::numeric(16,4) AS total_cost,\n stock_location_id,\n stock_location_name,\n location_count,\n recipe_id,\n recipe_name,\n order_id,\n item_name,\n order_reference,\n performed_by_id,\n performed_by_name,\n customer_id,\n customer_name,\n corrected_at,\n correction_reason,\n is_corrected,\n source,\n stock_posted\n FROM ( SELECT ledger.tenant_id,\n ledger.unit_id,\n ledger.item_id,\n ledger.product_id,\n ledger.product_name,\n ledger.product_sku,\n ledger.used_at,\n ledger.movement_date,\n ledger.posted_quantity,\n ledger.net_quantity,\n ledger.standard_quantity,\n ledger.variance,\n ledger.unit_abbreviation,\n ledger.total_cost,\n ledger.stock_location_id,\n ledger.stock_location_name,\n ledger.location_count,\n ledger.recipe_id,\n ledger.recipe_name,\n ledger.order_id,\n ledger.item_name,\n ledger.order_reference,\n ledger.performed_by_id,\n ledger.performed_by_name,\n ledger.customer_id,\n ledger.customer_name,\n ledger.corrected_at,\n ledger.correction_reason,\n ledger.is_corrected,\n ledger.source,\n ledger.stock_posted\n FROM ledger\n UNION ALL\n SELECT clinical.tenant_id,\n clinical.unit_id,\n clinical.item_id,\n clinical.product_id,\n clinical.product_name,\n clinical.product_sku,\n clinical.used_at,\n clinical.movement_date,\n clinical.posted_quantity,\n clinical.net_quantity,\n clinical.standard_quantity,\n clinical.variance,\n clinical.unit_abbreviation,\n clinical.total_cost,\n clinical.stock_location_id,\n clinical.stock_location_name,\n clinical.location_count,\n clinical.recipe_id,\n clinical.recipe_name,\n clinical.order_id,\n clinical.item_name,\n clinical.order_reference,\n clinical.performed_by_id,\n clinical.performed_by_name,\n clinical.customer_id,\n clinical.customer_name,\n clinical.corrected_at,\n clinical.correction_reason,\n clinical.is_corrected,\n clinical.source,\n clinical.stock_posted\n FROM clinical) a;;\n";
|
|
16
|
+
export declare const MIGRATION_015_LATERAL_SEM_CERCA_NAO_E_LATERAL = "-- LATERAL sem cerca n\u00E3o \u00E9 LATERAL.\n--\n-- A 012 e a 014 escreveram as buscas de `items`, `orders`, `people` e da\n-- sess\u00E3o como LATERAL para que cada uma rodasse por linha. O planejador\n-- desfez o gesto: subconsulta simples \u00E9 ACHATADA de volta a jun\u00E7\u00E3o comum, e o\n-- plano voltava a varrer `people` inteira (61.854 linhas) e a sess\u00E3o inteira.\n-- Sob RLS cada uma dessas linhas paga `app.has_unit` + `app.has_permission`,\n-- e a tela morria nos 8 s do PostgREST.\n--\n-- `LIMIT 1` \u00E9 a cerca: o Postgres n\u00E3o achata subconsulta com limite. A chave\n-- \u00E9 \u00FAnica nos quatro casos, ent\u00E3o o limite n\u00E3o muda resultado nenhum \u2014 s\u00F3\n-- impede que a busca de UMA linha vire uma varredura de todas.\n\nCREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS\n WITH consumed AS (\n SELECT m.tenant_id,\n m.unit_id,\n m.source_item_id AS item_id,\n m.product_id,\n sum(- m.quantity) AS posted_quantity,\n sum(m.total_cost) AS total_cost,\n min(m.created_at) AS used_at,\n min(m.movement_date) AS movement_date,\n (array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,\n count(DISTINCT m.source_location_id)::integer AS location_count,\n (array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,\n (array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id\n FROM plg_inventory_stock_movements m\n WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements r\n WHERE r.reverses_movement_id = m.id))\n GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id\n ), corrected AS (\n SELECT c.tenant_id,\n c.source_item_id AS item_id,\n c.product_id,\n sum(- c.quantity) AS correction_delta,\n sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,\n max(c.created_at) AS corrected_at,\n (array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason\n FROM plg_inventory_stock_movements c\n WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL\n GROUP BY c.tenant_id, c.source_item_id, c.product_id\n ), ledger AS (\n SELECT c.tenant_id,\n c.unit_id,\n c.item_id,\n c.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n c.used_at,\n c.movement_date,\n c.posted_quantity,\n c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,\n std.standard_quantity,\n CASE\n WHEN std.standard_quantity IS NULL THEN NULL::numeric\n ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,\n c.stock_location_id,\n sl.name AS stock_location_name,\n c.location_count,\n r.id AS recipe_id,\n r.name AS recipe_name,\n i.order_id,\n i.name AS item_name,\n o.reference_number AS order_reference,\n i.assignee_id AS performed_by_id,\n perf.name AS performed_by_name,\n o.party_id AS customer_id,\n cust.name AS customer_name,\n k.corrected_at,\n k.correction_reason,\n k.item_id IS NOT NULL AS is_corrected,\n 'ledger'::text AS source,\n true AS stock_posted\n FROM consumed c\n LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id\n LEFT JOIN products p ON p.id = c.product_id\n LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id\n LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id\n LEFT JOIN orders o ON o.id = i.order_id\n LEFT JOIN people perf ON perf.id = i.assignee_id\n LEFT JOIN people cust ON cust.id = o.party_id\n LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id\n LEFT JOIN LATERAL ( SELECT\n CASE\n WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round\n FROM plg_inventory_recipe_ingredients ri\n WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)\n WHEN r.id IS NULL THEN i.quantity\n ELSE NULL::numeric\n END AS standard_quantity) std ON true\n ), clinical AS (\n SELECT cs.tenant_id,\n cs.unit_id,\n cs.id AS item_id,\n cs.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n COALESCE(timezone('UTC', cs.executed_on::timestamp), cs.created_at) AS used_at,\n COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,\n COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,\n COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,\n NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,\n CASE\n WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric\n ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n 0::numeric AS total_cost,\n NULL::uuid AS stock_location_id,\n NULL::text AS stock_location_name,\n 1 AS location_count,\n NULL::uuid AS recipe_id,\n NULL::text AS recipe_name,\n i.order_id,\n sv.name AS item_name,\n o.reference_number AS order_reference,\n COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,\n perf.name AS performed_by_name,\n s.party_id AS customer_id,\n cust.name AS customer_name,\n cs.audited_at AS corrected_at,\n cs.audit_notes AS correction_reason,\n cs.is_audited AS is_corrected,\n 'clinical'::text AS source,\n false AS stock_posted\n FROM plg_clinical_session_products cs\n LEFT JOIN LATERAL ( SELECT ss.party_id, ss.performed_by_id, ss.assignee_id, ss.service_id\n FROM plg_clinical_sessions ss\n WHERE ss.tenant_id = cs.tenant_id AND ss.id = cs.session_id LIMIT 1) s ON true\n LEFT JOIN products p ON p.id = cs.product_id\n LEFT JOIN products sv ON sv.id = s.service_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id\n LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id\n FROM items it\n WHERE cs.order_item_id IS NOT NULL\n AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id LIMIT 1) i ON true\n LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id\n FROM orders ord\n WHERE i.order_id IS NOT NULL AND ord.id = i.order_id LIMIT 1) o ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) LIMIT 1) perf ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = s.party_id LIMIT 1) cust ON true\n WHERE NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements m\n WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))\n )\n SELECT tenant_id,\n unit_id,\n item_id,\n product_id,\n product_name,\n product_sku,\n used_at,\n movement_date,\n posted_quantity::numeric(14,4) AS posted_quantity,\n net_quantity::numeric(14,4) AS net_quantity,\n standard_quantity::numeric(14,4) AS standard_quantity,\n variance,\n unit_abbreviation,\n total_cost::numeric(16,4) AS total_cost,\n stock_location_id,\n stock_location_name,\n location_count,\n recipe_id,\n recipe_name,\n order_id,\n item_name,\n order_reference,\n performed_by_id,\n performed_by_name,\n customer_id,\n customer_name,\n corrected_at,\n correction_reason,\n is_corrected,\n source,\n stock_posted\n FROM ( SELECT ledger.tenant_id,\n ledger.unit_id,\n ledger.item_id,\n ledger.product_id,\n ledger.product_name,\n ledger.product_sku,\n ledger.used_at,\n ledger.movement_date,\n ledger.posted_quantity,\n ledger.net_quantity,\n ledger.standard_quantity,\n ledger.variance,\n ledger.unit_abbreviation,\n ledger.total_cost,\n ledger.stock_location_id,\n ledger.stock_location_name,\n ledger.location_count,\n ledger.recipe_id,\n ledger.recipe_name,\n ledger.order_id,\n ledger.item_name,\n ledger.order_reference,\n ledger.performed_by_id,\n ledger.performed_by_name,\n ledger.customer_id,\n ledger.customer_name,\n ledger.corrected_at,\n ledger.correction_reason,\n ledger.is_corrected,\n ledger.source,\n ledger.stock_posted\n FROM ledger\n UNION ALL\n SELECT clinical.tenant_id,\n clinical.unit_id,\n clinical.item_id,\n clinical.product_id,\n clinical.product_name,\n clinical.product_sku,\n clinical.used_at,\n clinical.movement_date,\n clinical.posted_quantity,\n clinical.net_quantity,\n clinical.standard_quantity,\n clinical.variance,\n clinical.unit_abbreviation,\n clinical.total_cost,\n clinical.stock_location_id,\n clinical.stock_location_name,\n clinical.location_count,\n clinical.recipe_id,\n clinical.recipe_name,\n clinical.order_id,\n clinical.item_name,\n clinical.order_reference,\n clinical.performed_by_id,\n clinical.performed_by_name,\n clinical.customer_id,\n clinical.customer_name,\n clinical.corrected_at,\n clinical.correction_reason,\n clinical.is_corrected,\n clinical.source,\n clinical.stock_posted\n FROM clinical) a;;\n";
|
|
17
|
+
export declare const MIGRATION_016_A_DATA_DO_CONSUMO_E_UMA_COLUNA = "-- A data do consumo passa a ser uma COLUNA, e por isso a janela usa \u00EDndice.\n--\n-- Sob RLS o Postgres roda as regras da pol\u00EDtica ANTES das condi\u00E7\u00F5es de quem\n-- pergunta \u2014 a n\u00E3o ser que a condi\u00E7\u00E3o seja `leakproof`. A janela da fila\n-- (`used_at >= agora - 30 dias`) era uma EXPRESS\u00C3O com `timezone()`, que n\u00E3o \u00E9\n-- leakproof: o \u00EDndice da 013 existia e o plano o ignorava, varrendo as 116.934\n-- linhas e chamando `app.has_unit` 116.028 vezes. Medido: 7,9 s, e a tela\n-- morria nos 8 s do PostgREST.\n--\n-- Coluna gerada e gravada resolve pela raiz: a compara\u00E7\u00E3o vira `coluna >=\n-- constante`, que \u00E9 leakproof, entra como condi\u00E7\u00E3o de \u00EDndice e a RLS passa a\n-- rodar sobre as dezenas de linhas da janela em vez da tabela inteira. O valor\n-- \u00E9 o mesmo de antes (o cluster roda em UTC), calculado uma vez na escrita.\n\nALTER TABLE public.plg_clinical_session_products\n ADD COLUMN IF NOT EXISTS used_at timestamptz\n GENERATED ALWAYS AS (COALESCE(timezone('UTC', executed_on::timestamp), created_at)) STORED;\n\nDROP INDEX IF EXISTS public.plg_clinical_session_products_used_at_idx;\nCREATE INDEX IF NOT EXISTS plg_clinical_session_products_used_at_idx\n ON public.plg_clinical_session_products (tenant_id, used_at DESC);\n\nCREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS\n WITH consumed AS (\n SELECT m.tenant_id,\n m.unit_id,\n m.source_item_id AS item_id,\n m.product_id,\n sum(- m.quantity) AS posted_quantity,\n sum(m.total_cost) AS total_cost,\n min(m.created_at) AS used_at,\n min(m.movement_date) AS movement_date,\n (array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,\n count(DISTINCT m.source_location_id)::integer AS location_count,\n (array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,\n (array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id\n FROM plg_inventory_stock_movements m\n WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements r\n WHERE r.reverses_movement_id = m.id))\n GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id\n ), corrected AS (\n SELECT c.tenant_id,\n c.source_item_id AS item_id,\n c.product_id,\n sum(- c.quantity) AS correction_delta,\n sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,\n max(c.created_at) AS corrected_at,\n (array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason\n FROM plg_inventory_stock_movements c\n WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL\n GROUP BY c.tenant_id, c.source_item_id, c.product_id\n ), ledger AS (\n SELECT c.tenant_id,\n c.unit_id,\n c.item_id,\n c.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n c.used_at,\n c.movement_date,\n c.posted_quantity,\n c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,\n std.standard_quantity,\n CASE\n WHEN std.standard_quantity IS NULL THEN NULL::numeric\n ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,\n c.stock_location_id,\n sl.name AS stock_location_name,\n c.location_count,\n r.id AS recipe_id,\n r.name AS recipe_name,\n i.order_id,\n i.name AS item_name,\n o.reference_number AS order_reference,\n i.assignee_id AS performed_by_id,\n perf.name AS performed_by_name,\n o.party_id AS customer_id,\n cust.name AS customer_name,\n k.corrected_at,\n k.correction_reason,\n k.item_id IS NOT NULL AS is_corrected,\n 'ledger'::text AS source,\n true AS stock_posted\n FROM consumed c\n LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id\n LEFT JOIN products p ON p.id = c.product_id\n LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id\n LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id\n LEFT JOIN orders o ON o.id = i.order_id\n LEFT JOIN people perf ON perf.id = i.assignee_id\n LEFT JOIN people cust ON cust.id = o.party_id\n LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id\n LEFT JOIN LATERAL ( SELECT\n CASE\n WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round\n FROM plg_inventory_recipe_ingredients ri\n WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)\n WHEN r.id IS NULL THEN i.quantity\n ELSE NULL::numeric\n END AS standard_quantity) std ON true\n ), clinical AS (\n SELECT cs.tenant_id,\n cs.unit_id,\n cs.id AS item_id,\n cs.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n cs.used_at,\n COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,\n COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,\n COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,\n NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,\n CASE\n WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric\n ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n 0::numeric AS total_cost,\n NULL::uuid AS stock_location_id,\n NULL::text AS stock_location_name,\n 1 AS location_count,\n NULL::uuid AS recipe_id,\n NULL::text AS recipe_name,\n i.order_id,\n sv.name AS item_name,\n o.reference_number AS order_reference,\n COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,\n perf.name AS performed_by_name,\n s.party_id AS customer_id,\n cust.name AS customer_name,\n cs.audited_at AS corrected_at,\n cs.audit_notes AS correction_reason,\n cs.is_audited AS is_corrected,\n 'clinical'::text AS source,\n false AS stock_posted\n FROM plg_clinical_session_products cs\n LEFT JOIN LATERAL ( SELECT ss.party_id, ss.performed_by_id, ss.assignee_id, ss.service_id\n FROM plg_clinical_sessions ss\n WHERE ss.tenant_id = cs.tenant_id AND ss.id = cs.session_id LIMIT 1) s ON true\n LEFT JOIN products p ON p.id = cs.product_id\n LEFT JOIN products sv ON sv.id = s.service_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id\n LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id\n FROM items it\n WHERE cs.order_item_id IS NOT NULL\n AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id LIMIT 1) i ON true\n LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id\n FROM orders ord\n WHERE i.order_id IS NOT NULL AND ord.id = i.order_id LIMIT 1) o ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) LIMIT 1) perf ON true\n LEFT JOIN LATERAL ( SELECT pe.id, pe.name\n FROM people pe\n WHERE pe.id = s.party_id LIMIT 1) cust ON true\n WHERE NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements m\n WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))\n )\n SELECT tenant_id,\n unit_id,\n item_id,\n product_id,\n product_name,\n product_sku,\n used_at,\n movement_date,\n posted_quantity::numeric(14,4) AS posted_quantity,\n net_quantity::numeric(14,4) AS net_quantity,\n standard_quantity::numeric(14,4) AS standard_quantity,\n variance,\n unit_abbreviation,\n total_cost::numeric(16,4) AS total_cost,\n stock_location_id,\n stock_location_name,\n location_count,\n recipe_id,\n recipe_name,\n order_id,\n item_name,\n order_reference,\n performed_by_id,\n performed_by_name,\n customer_id,\n customer_name,\n corrected_at,\n correction_reason,\n is_corrected,\n source,\n stock_posted\n FROM ( SELECT ledger.tenant_id,\n ledger.unit_id,\n ledger.item_id,\n ledger.product_id,\n ledger.product_name,\n ledger.product_sku,\n ledger.used_at,\n ledger.movement_date,\n ledger.posted_quantity,\n ledger.net_quantity,\n ledger.standard_quantity,\n ledger.variance,\n ledger.unit_abbreviation,\n ledger.total_cost,\n ledger.stock_location_id,\n ledger.stock_location_name,\n ledger.location_count,\n ledger.recipe_id,\n ledger.recipe_name,\n ledger.order_id,\n ledger.item_name,\n ledger.order_reference,\n ledger.performed_by_id,\n ledger.performed_by_name,\n ledger.customer_id,\n ledger.customer_name,\n ledger.corrected_at,\n ledger.correction_reason,\n ledger.is_corrected,\n ledger.source,\n ledger.stock_posted\n FROM ledger\n UNION ALL\n SELECT clinical.tenant_id,\n clinical.unit_id,\n clinical.item_id,\n clinical.product_id,\n clinical.product_name,\n clinical.product_sku,\n clinical.used_at,\n clinical.movement_date,\n clinical.posted_quantity,\n clinical.net_quantity,\n clinical.standard_quantity,\n clinical.variance,\n clinical.unit_abbreviation,\n clinical.total_cost,\n clinical.stock_location_id,\n clinical.stock_location_name,\n clinical.location_count,\n clinical.recipe_id,\n clinical.recipe_name,\n clinical.order_id,\n clinical.item_name,\n clinical.order_reference,\n clinical.performed_by_id,\n clinical.performed_by_name,\n clinical.customer_id,\n clinical.customer_name,\n clinical.corrected_at,\n clinical.correction_reason,\n clinical.is_corrected,\n clinical.source,\n clinical.stock_posted\n FROM clinical) a;;\n";
|
|
18
|
+
export declare const MIGRATION_017_APROVAR_E_BAIXAR_SAO_DOIS_GESTOS = "-- Aprovar e baixar no estoque s\u00E3o DOIS gestos, e faltavam os dois.\n--\n-- A fila da Auditoria de Uso existe desde o come\u00E7o deste porte: o injetor\n-- lan\u00E7a o que usou, a linha chega aqui, e o estoquista confere. S\u00F3 que a tela\n-- s\u00F3 sabia CORRIGIR a quantidade \u2014 n\u00E3o havia como aceitar. O consumo entrava\n-- na fila e o saldo nunca ca\u00EDa. A pergunta que a jornada 4 do V1 responde\n-- (\"executei e o estoque n\u00E3o mudou\") continuava sem resposta, porque o bot\u00E3o\n-- que a responde n\u00E3o existia.\n--\n-- O V1 tem tr\u00EAs a\u00E7\u00F5es por linha, e elas s\u00E3o diferentes de prop\u00F3sito:\n--\n-- \uD83D\uDCCD Baixar no estoque aceita E desconta do saldo\n-- \u270F\uFE0F Corrigir conserta a quantidade digitada errada\n-- \u2705 Aprovar aceita o consumo, sem mexer no saldo\n--\n-- \"Aprovar\" sem baixar existe para o consumo que n\u00E3o sai do estoque da casa \u2014\n-- produto do pr\u00F3prio cliente, amostra do fornecedor, item j\u00E1 baixado por fora.\n--\n-- SOBRE SALDO NEGATIVO. Esta fun\u00E7\u00E3o N\u00C3O exige saldo. O movimento \u00E9 o registro\n-- do que foi usado numa pessoa, e isso aconteceu independentemente do que o\n-- sistema achava que havia na prateleira. Recusar a baixa por falta de saldo\n-- esconderia o consumo e deixaria a diferen\u00E7a invis\u00EDvel; deixar o saldo ficar\n-- negativo \u00E9 exatamente o sinal que o estoquista precisa ver.\n\nCREATE OR REPLACE FUNCTION public.inventory_approve_usage_batch(\n p_lines jsonb,\n p_post boolean DEFAULT false,\n p_reason text DEFAULT NULL\n)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $fn$\nDECLARE\n v_tenant uuid := app.inventory_require_tenant();\n v_actor uuid := auth.uid();\n v_key text;\n v_op record;\n r record;\n v_cs record;\n v_location uuid;\n v_cost numeric;\n v_movement uuid;\n v_no integer := 0;\n v_aprovadas integer := 0;\n v_baixadas integer := 0;\n v_puladas integer := 0;\n v_movements jsonb := '[]'::jsonb;\nBEGIN\n PERFORM app.inventory_check_lines(p_lines);\n\n SELECT 'usage_audit_approve:' || CASE WHEN p_post THEN 'post:' ELSE 'ack:' END\n || md5(string_agg((l->>'item_id'), ',' ORDER BY (l->>'item_id')))\n INTO v_key\n FROM jsonb_array_elements(p_lines) AS l;\n\n SELECT * INTO v_op FROM app.inventory_begin_operation(\n -- `confirm` \u00E9 a opera\u00E7\u00E3o de quem TIRA do estoque por consumo \u2014 o mesmo\n -- verbo que o consumo de item de pedido usa. 'out' n\u00E3o existe no cat\u00E1logo\n -- de opera\u00E7\u00F5es (s\u00F3 de movimentos), e o CHECK recusava.\n v_tenant, CASE WHEN p_post THEN 'confirm' ELSE 'adjust' END, v_key,\n jsonb_build_object('source', 'usage_audit_approve', 'post', p_post, 'lines', p_lines));\n IF v_op.existing IS NOT NULL THEN\n RETURN v_op.existing;\n END IF;\n\n FOR r IN\n SELECT (l->>'item_id')::uuid AS item_id, ord\n FROM jsonb_array_elements(p_lines) WITH ORDINALITY AS t(l, ord)\n ORDER BY ord\n LOOP\n v_no := v_no + 1;\n\n SELECT cs.* INTO v_cs\n FROM public.plg_clinical_session_products cs\n WHERE cs.tenant_id = v_tenant AND cs.id = r.item_id\n FOR UPDATE;\n\n -- Linha que n\u00E3o \u00E9 consumo cl\u00EDnico j\u00E1 nasceu como movimento do raz\u00E3o: ela\n -- n\u00E3o tem o que aprovar, e some da fila por outro caminho.\n IF NOT FOUND THEN\n v_puladas := v_puladas + 1;\n CONTINUE;\n END IF;\n\n IF p_post AND coalesce(v_cs.used_quantity, 0) > 0 THEN\n -- ONDE A BAIXA ACONTECE. Prefer\u00EAncia: o local padr\u00E3o do produto na\n -- unidade, depois o da ficha, depois o primeiro local ativo dela.\n SELECT candidate.location_id INTO v_location\n FROM (\n SELECT s.default_location_id AS location_id,\n CASE WHEN v_cs.unit_id IS NOT NULL AND s.unit_id = v_cs.unit_id THEN 1 ELSE 2 END AS priority\n FROM public.plg_inventory_product_settings s\n WHERE s.tenant_id = v_tenant\n AND s.product_id = v_cs.product_id\n AND s.default_location_id IS NOT NULL\n AND (s.unit_id IS NULL OR s.unit_id = v_cs.unit_id)\n UNION ALL\n SELECT d.default_location_id, 3\n FROM public.plg_inventory_product_details d\n WHERE d.tenant_id = v_tenant\n AND d.product_id = v_cs.product_id\n AND d.default_location_id IS NOT NULL\n UNION ALL\n SELECT l.id, 4\n FROM public.plg_inventory_stock_locations l\n WHERE l.tenant_id = v_tenant AND l.is_active\n ) candidate\n JOIN public.plg_inventory_stock_locations l\n ON l.id = candidate.location_id AND l.tenant_id = v_tenant AND l.is_active\n WHERE v_cs.unit_id IS NULL OR l.unit_id = v_cs.unit_id OR l.unit_id IS NULL\n ORDER BY candidate.priority, l.created_at, l.id\n LIMIT 1;\n\n IF v_location IS NULL THEN\n RAISE EXCEPTION 'inventory: nenhum local de estoque ativo para a unidade deste consumo'\n USING ERRCODE = '22023';\n END IF;\n\n SELECT p.unit_cost INTO v_cost\n FROM public.plg_inventory_stock_positions p\n WHERE p.tenant_id = v_tenant AND p.product_id = v_cs.product_id\n AND p.stock_location_id = v_location\n ORDER BY p.expiration_date NULLS LAST, p.created_at\n LIMIT 1;\n\n INSERT INTO public.plg_inventory_stock_movements\n (tenant_id, unit_id, product_id, kind, quantity, unit_cost, source_location_id,\n measurement_unit_id, document_type, document_number,\n source_item_type, source_item_id, idempotency_key, line_no, operation_id,\n user_id, metadata)\n VALUES\n (v_tenant, v_cs.unit_id, v_cs.product_id, 'out', -v_cs.used_quantity,\n coalesce(v_cost, 0), v_location, v_cs.used_unit_id, 'consumption',\n coalesce(v_cs.session_id::text, v_cs.id::text),\n 'usage_audit', v_cs.id, v_key, v_no, v_op.op_id, v_actor,\n jsonb_strip_nulls(jsonb_build_object(\n 'source', 'usage_audit_approve',\n 'session_id', v_cs.session_id,\n 'order_item_id', v_cs.order_item_id,\n 'reason', nullif(btrim(p_reason), ''))))\n RETURNING id INTO v_movement;\n\n v_movements := v_movements || app.inventory_movement_json(v_movement);\n v_baixadas := v_baixadas + 1;\n END IF;\n\n -- CONFERIDO \u00E9 o mesmo carimbo nos dois gestos: o que sai da fila \u00E9 o que\n -- algu\u00E9m olhou, tenha ou n\u00E3o baixado.\n UPDATE public.plg_clinical_session_products cs\n SET is_audited = true,\n audited_at = now(),\n audited_by = coalesce(cs.audited_by, v_actor),\n audit_notes = coalesce(nullif(btrim(p_reason), ''), cs.audit_notes),\n updated_at = now()\n WHERE cs.id = v_cs.id;\n v_aprovadas := v_aprovadas + 1;\n END LOOP;\n\n RETURN app.inventory_finish_operation(\n v_tenant, v_op.op_id, CASE WHEN p_post THEN 'confirm' ELSE 'adjust' END, v_key,\n jsonb_build_object(\n 'operation_id', v_op.op_id, 'kind', 'usage_audit_approve',\n 'idempotency_key', v_key, 'status', CASE WHEN p_post THEN 'posted' ELSE 'approved' END,\n 'lines_approved', v_aprovadas, 'lines_posted', v_baixadas,\n 'lines_skipped', v_puladas, 'movements', v_movements),\n jsonb_build_object('source', 'usage_audit_approve', 'post', p_post,\n 'lines_approved', v_aprovadas, 'lines_posted', v_baixadas));\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.inventory_approve_usage_batch(jsonb, boolean, text) TO authenticated;\n";
|
|
19
|
+
export declare const MIGRATION_018_O_NOME_NAO_SAI_DA_VISTA = "-- O NOME N\u00C3O SAI DA VISTA. A vista devolve o id; quem pinta busca o nome.\n--\n-- `people` tem 120.379 linhas neste tenant e uma pol\u00EDtica de RLS que passa por\n-- `resource_grants`. Buscar o nome do profissional e o da cliente DENTRO da\n-- vista fazia essa pol\u00EDtica rodar linha a linha: a mesma consulta que custa\n-- 9 ms com chave de servi\u00E7o levava **30 segundos** com a sess\u00E3o de quem usa a\n-- tela, e o PostgREST desistia aos 8.\n--\n-- Nenhum \u00EDndice conserta isso, porque o problema n\u00E3o \u00E9 achar a pessoa \u2014 \u00E9 a\n-- pol\u00EDtica ser avaliada uma vez por candidato. A sa\u00EDda \u00E9 n\u00E3o perguntar ali: a\n-- vista entrega `performed_by_id` e `customer_id`, e a tela busca os ~50 nomes\n-- de uma vez, numa consulta s\u00F3, com a RLS rodando uma vez para todos.\n\nCREATE OR REPLACE VIEW public.v_inventory_consumption_audit WITH (security_invoker = true) AS\n WITH consumed AS (\n SELECT m.tenant_id,\n m.unit_id,\n m.source_item_id AS item_id,\n m.product_id,\n sum(- m.quantity) AS posted_quantity,\n sum(m.total_cost) AS total_cost,\n min(m.created_at) AS used_at,\n min(m.movement_date) AS movement_date,\n (array_agg(m.source_location_id ORDER BY m.created_at, m.id))[1] AS stock_location_id,\n count(DISTINCT m.source_location_id)::integer AS location_count,\n (array_agg(m.measurement_unit_id ORDER BY m.created_at, m.id))[1] AS measurement_unit_id,\n (array_agg(m.metadata ->> 'recipe_id'::text ORDER BY m.created_at, m.id))[1] AS recipe_id\n FROM plg_inventory_stock_movements m\n WHERE m.source_item_type = 'order_item'::text AND m.source_item_id IS NOT NULL AND m.kind = 'out'::text AND NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements r\n WHERE r.reverses_movement_id = m.id))\n GROUP BY m.tenant_id, m.unit_id, m.source_item_id, m.product_id\n ), corrected AS (\n SELECT c.tenant_id,\n c.source_item_id AS item_id,\n c.product_id,\n sum(- c.quantity) AS correction_delta,\n sum((- sign(c.quantity)) * c.total_cost) AS correction_cost,\n max(c.created_at) AS corrected_at,\n (array_agg(c.reason ORDER BY c.created_at DESC, c.id DESC))[1] AS correction_reason\n FROM plg_inventory_stock_movements c\n WHERE c.source_item_type = 'usage_audit'::text AND c.source_item_id IS NOT NULL\n GROUP BY c.tenant_id, c.source_item_id, c.product_id\n ), ledger AS (\n SELECT c.tenant_id,\n c.unit_id,\n c.item_id,\n c.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n c.used_at,\n c.movement_date,\n c.posted_quantity,\n c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) AS net_quantity,\n std.standard_quantity,\n CASE\n WHEN std.standard_quantity IS NULL THEN NULL::numeric\n ELSE c.posted_quantity + COALESCE(k.correction_delta, 0::numeric) - std.standard_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n c.total_cost + COALESCE(k.correction_cost, 0::numeric) AS total_cost,\n c.stock_location_id,\n sl.name AS stock_location_name,\n c.location_count,\n r.id AS recipe_id,\n r.name AS recipe_name,\n i.order_id,\n i.name AS item_name,\n o.reference_number AS order_reference,\n i.assignee_id AS performed_by_id,\n NULL::text AS performed_by_name,\n o.party_id AS customer_id,\n NULL::text AS customer_name,\n k.corrected_at,\n k.correction_reason,\n k.item_id IS NOT NULL AS is_corrected,\n 'ledger'::text AS source,\n true AS stock_posted\n FROM consumed c\n LEFT JOIN corrected k ON k.tenant_id = c.tenant_id AND k.item_id = c.item_id AND k.product_id = c.product_id\n LEFT JOIN products p ON p.id = c.product_id\n LEFT JOIN plg_inventory_stock_locations sl ON sl.id = c.stock_location_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = c.measurement_unit_id\n LEFT JOIN items i ON i.id = c.item_id AND i.tenant_id = c.tenant_id\n LEFT JOIN orders o ON o.id = i.order_id\n LEFT JOIN people perf ON perf.id = i.assignee_id\n LEFT JOIN people cust ON cust.id = o.party_id\n LEFT JOIN plg_inventory_recipes r ON r.id = NULLIF(c.recipe_id, ''::text)::uuid AND r.tenant_id = c.tenant_id\n LEFT JOIN LATERAL ( SELECT\n CASE\n WHEN r.id IS NOT NULL AND COALESCE(r.yield_quantity, 0::numeric) > 0::numeric THEN ( SELECT round(sum(ri.quantity) * COALESCE(i.quantity, 1::numeric) / r.yield_quantity, 4) AS round\n FROM plg_inventory_recipe_ingredients ri\n WHERE ri.tenant_id = c.tenant_id AND ri.recipe_id = r.id AND ri.product_id = c.product_id)\n WHEN r.id IS NULL THEN i.quantity\n ELSE NULL::numeric\n END AS standard_quantity) std ON true\n ), clinical AS (\n SELECT cs.tenant_id,\n cs.unit_id,\n cs.id AS item_id,\n cs.product_id,\n p.name AS product_name,\n p.sku AS product_sku,\n cs.used_at,\n COALESCE(cs.executed_on, cs.created_at::date) AS movement_date,\n COALESCE(cs.used_quantity, 0::numeric) AS posted_quantity,\n COALESCE(cs.used_quantity, 0::numeric) AS net_quantity,\n NULLIF(cs.default_quantity, 0::numeric) AS standard_quantity,\n CASE\n WHEN NULLIF(cs.default_quantity, 0::numeric) IS NULL THEN NULL::numeric\n ELSE COALESCE(cs.used_quantity, 0::numeric) - cs.default_quantity\n END AS variance,\n u.abbreviation AS unit_abbreviation,\n 0::numeric AS total_cost,\n NULL::uuid AS stock_location_id,\n NULL::text AS stock_location_name,\n 1 AS location_count,\n NULL::uuid AS recipe_id,\n NULL::text AS recipe_name,\n i.order_id,\n sv.name AS item_name,\n o.reference_number AS order_reference,\n COALESCE(cs.assignee_id, s.performed_by_id, s.assignee_id) AS performed_by_id,\n NULL::text AS performed_by_name,\n s.party_id AS customer_id,\n NULL::text AS customer_name,\n cs.audited_at AS corrected_at,\n cs.audit_notes AS correction_reason,\n cs.is_audited AS is_corrected,\n 'clinical'::text AS source,\n false AS stock_posted\n FROM plg_clinical_session_products cs\n LEFT JOIN LATERAL ( SELECT ss.party_id, ss.performed_by_id, ss.assignee_id, ss.service_id\n FROM plg_clinical_sessions ss\n WHERE ss.tenant_id = cs.tenant_id AND ss.id = cs.session_id LIMIT 1) s ON true\n LEFT JOIN products p ON p.id = cs.product_id\n LEFT JOIN products sv ON sv.id = s.service_id\n LEFT JOIN plg_inventory_measurement_units u ON u.id = cs.used_unit_id\n LEFT JOIN LATERAL ( SELECT it.id, it.order_id, it.name, it.assignee_id\n FROM items it\n WHERE cs.order_item_id IS NOT NULL\n AND it.id = cs.order_item_id AND it.tenant_id = cs.tenant_id LIMIT 1) i ON true\n LEFT JOIN LATERAL ( SELECT ord.id, ord.reference_number, ord.party_id\n FROM orders ord\n WHERE i.order_id IS NOT NULL AND ord.id = i.order_id LIMIT 1) o ON true\n WHERE NOT (EXISTS ( SELECT 1\n FROM plg_inventory_stock_movements m\n WHERE m.tenant_id = cs.tenant_id AND m.source_item_type = 'order_item'::text AND m.source_item_id = cs.order_item_id AND m.product_id = cs.product_id AND m.kind = 'out'::text))\n )\n SELECT tenant_id,\n unit_id,\n item_id,\n product_id,\n product_name,\n product_sku,\n used_at,\n movement_date,\n posted_quantity::numeric(14,4) AS posted_quantity,\n net_quantity::numeric(14,4) AS net_quantity,\n standard_quantity::numeric(14,4) AS standard_quantity,\n variance,\n unit_abbreviation,\n total_cost::numeric(16,4) AS total_cost,\n stock_location_id,\n stock_location_name,\n location_count,\n recipe_id,\n recipe_name,\n order_id,\n item_name,\n order_reference,\n performed_by_id,\n performed_by_name,\n customer_id,\n customer_name,\n corrected_at,\n correction_reason,\n is_corrected,\n source,\n stock_posted\n FROM ( SELECT ledger.tenant_id,\n ledger.unit_id,\n ledger.item_id,\n ledger.product_id,\n ledger.product_name,\n ledger.product_sku,\n ledger.used_at,\n ledger.movement_date,\n ledger.posted_quantity,\n ledger.net_quantity,\n ledger.standard_quantity,\n ledger.variance,\n ledger.unit_abbreviation,\n ledger.total_cost,\n ledger.stock_location_id,\n ledger.stock_location_name,\n ledger.location_count,\n ledger.recipe_id,\n ledger.recipe_name,\n ledger.order_id,\n ledger.item_name,\n ledger.order_reference,\n ledger.performed_by_id,\n ledger.performed_by_name,\n ledger.customer_id,\n ledger.customer_name,\n ledger.corrected_at,\n ledger.correction_reason,\n ledger.is_corrected,\n ledger.source,\n ledger.stock_posted\n FROM ledger\n UNION ALL\n SELECT clinical.tenant_id,\n clinical.unit_id,\n clinical.item_id,\n clinical.product_id,\n clinical.product_name,\n clinical.product_sku,\n clinical.used_at,\n clinical.movement_date,\n clinical.posted_quantity,\n clinical.net_quantity,\n clinical.standard_quantity,\n clinical.variance,\n clinical.unit_abbreviation,\n clinical.total_cost,\n clinical.stock_location_id,\n clinical.stock_location_name,\n clinical.location_count,\n clinical.recipe_id,\n clinical.recipe_name,\n clinical.order_id,\n clinical.item_name,\n clinical.order_reference,\n clinical.performed_by_id,\n clinical.performed_by_name,\n clinical.customer_id,\n clinical.customer_name,\n clinical.corrected_at,\n clinical.correction_reason,\n clinical.is_corrected,\n clinical.source,\n clinical.stock_posted\n FROM clinical) a;;\n";
|
|
20
|
+
export declare const MIGRATION_019_SEM_SALDO_NAO_SE_BAIXA = "-- Sem saldo n\u00E3o se baixa \u2014 e a fila diz qual produto faltou.\n--\n-- A 017 assumiu que a baixa sempre cabe. N\u00E3o cabe: o gatilho de posi\u00E7\u00E3o da\n-- plataforma recusa sa\u00EDda sem lastro, e o estoque deste tenant est\u00E1 zerado em\n-- 223 dos 225 produtos. O clique voltava com o erro cru do Postgres\n-- (\"insufficient stock (no position for product \u2026)\") e a linha sumia do\n-- alcance de quem tentou.\n--\n-- Agora a linha sem saldo \u00E9 SEPARADA: n\u00E3o \u00E9 baixada, n\u00E3o \u00E9 carimbada como\n-- conferida \u2014 continua pendente, que \u00E9 o estado certo enquanto a entrada n\u00E3o\n-- for dada \u2014, e volta com nome de produto, quanto precisa e quanto h\u00E1.\n\nCREATE OR REPLACE FUNCTION public.inventory_approve_usage_batch(\n p_lines jsonb,\n p_post boolean DEFAULT false,\n p_reason text DEFAULT NULL\n)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $fn$\nDECLARE\n v_tenant uuid := app.inventory_require_tenant();\n v_actor uuid := auth.uid();\n v_key text;\n v_op record;\n r record;\n v_cs record;\n v_location uuid;\n v_cost numeric;\n v_movement uuid;\n v_no integer := 0;\n v_aprovadas integer := 0;\n v_baixadas integer := 0;\n v_puladas integer := 0;\n v_sem_saldo integer := 0;\n v_faltando jsonb := '[]'::jsonb;\n v_disp numeric;\n v_movements jsonb := '[]'::jsonb;\nBEGIN\n PERFORM app.inventory_check_lines(p_lines);\n\n SELECT 'usage_audit_approve:' || CASE WHEN p_post THEN 'post:' ELSE 'ack:' END\n || md5(string_agg((l->>'item_id'), ',' ORDER BY (l->>'item_id')))\n INTO v_key\n FROM jsonb_array_elements(p_lines) AS l;\n\n SELECT * INTO v_op FROM app.inventory_begin_operation(\n -- `confirm` \u00E9 a opera\u00E7\u00E3o de quem TIRA do estoque por consumo \u2014 o mesmo\n -- verbo que o consumo de item de pedido usa. 'out' n\u00E3o existe no cat\u00E1logo\n -- de opera\u00E7\u00F5es (s\u00F3 de movimentos), e o CHECK recusava.\n v_tenant, CASE WHEN p_post THEN 'confirm' ELSE 'adjust' END, v_key,\n jsonb_build_object('source', 'usage_audit_approve', 'post', p_post, 'lines', p_lines));\n IF v_op.existing IS NOT NULL THEN\n RETURN v_op.existing;\n END IF;\n\n FOR r IN\n SELECT (l->>'item_id')::uuid AS item_id, ord\n FROM jsonb_array_elements(p_lines) WITH ORDINALITY AS t(l, ord)\n ORDER BY ord\n LOOP\n v_no := v_no + 1;\n\n SELECT cs.* INTO v_cs\n FROM public.plg_clinical_session_products cs\n WHERE cs.tenant_id = v_tenant AND cs.id = r.item_id\n FOR UPDATE;\n\n -- Linha que n\u00E3o \u00E9 consumo cl\u00EDnico j\u00E1 nasceu como movimento do raz\u00E3o: ela\n -- n\u00E3o tem o que aprovar, e some da fila por outro caminho.\n IF NOT FOUND THEN\n v_puladas := v_puladas + 1;\n CONTINUE;\n END IF;\n\n IF p_post AND coalesce(v_cs.used_quantity, 0) > 0 THEN\n -- ONDE A BAIXA ACONTECE. Prefer\u00EAncia: o local padr\u00E3o do produto na\n -- unidade, depois o da ficha, depois o primeiro local ativo dela.\n SELECT candidate.location_id INTO v_location\n FROM (\n SELECT s.default_location_id AS location_id,\n CASE WHEN v_cs.unit_id IS NOT NULL AND s.unit_id = v_cs.unit_id THEN 1 ELSE 2 END AS priority\n FROM public.plg_inventory_product_settings s\n WHERE s.tenant_id = v_tenant\n AND s.product_id = v_cs.product_id\n AND s.default_location_id IS NOT NULL\n AND (s.unit_id IS NULL OR s.unit_id = v_cs.unit_id)\n UNION ALL\n SELECT d.default_location_id, 3\n FROM public.plg_inventory_product_details d\n WHERE d.tenant_id = v_tenant\n AND d.product_id = v_cs.product_id\n AND d.default_location_id IS NOT NULL\n UNION ALL\n SELECT l.id, 4\n FROM public.plg_inventory_stock_locations l\n WHERE l.tenant_id = v_tenant AND l.is_active\n ) candidate\n JOIN public.plg_inventory_stock_locations l\n ON l.id = candidate.location_id AND l.tenant_id = v_tenant AND l.is_active\n WHERE v_cs.unit_id IS NULL OR l.unit_id = v_cs.unit_id OR l.unit_id IS NULL\n ORDER BY candidate.priority, l.created_at, l.id\n LIMIT 1;\n\n IF v_location IS NULL THEN\n RAISE EXCEPTION 'inventory: nenhum local de estoque ativo para a unidade deste consumo'\n USING ERRCODE = '22023';\n END IF;\n\n -- SEM SALDO N\u00C3O SE BAIXA, e quem clicou precisa saber por qu\u00EA.\n --\n -- A plataforma n\u00E3o deixa o saldo ficar negativo: o gatilho de posi\u00E7\u00E3o\n -- recusa a sa\u00EDda sem lastro. Em vez de devolver o erro cru do banco, a\n -- linha \u00E9 separada e volta nomeada \u2014 a fila mant\u00E9m o consumo pendente,\n -- que \u00E9 o estado certo enquanto a entrada n\u00E3o for dada.\n SELECT coalesce(sum(p.quantity), 0) INTO v_disp\n FROM public.plg_inventory_stock_positions p\n WHERE p.tenant_id = v_tenant AND p.product_id = v_cs.product_id\n AND p.stock_location_id = v_location;\n\n IF coalesce(v_disp, 0) < v_cs.used_quantity THEN\n v_sem_saldo := v_sem_saldo + 1;\n v_faltando := v_faltando || jsonb_build_array(jsonb_build_object(\n 'item_id', v_cs.id,\n 'product', (SELECT pr.name FROM public.products pr WHERE pr.id = v_cs.product_id),\n 'needed', v_cs.used_quantity,\n 'available', coalesce(v_disp, 0)));\n CONTINUE;\n END IF;\n\n SELECT p.unit_cost INTO v_cost\n FROM public.plg_inventory_stock_positions p\n WHERE p.tenant_id = v_tenant AND p.product_id = v_cs.product_id\n AND p.stock_location_id = v_location\n ORDER BY p.expiration_date NULLS LAST, p.created_at\n LIMIT 1;\n\n INSERT INTO public.plg_inventory_stock_movements\n (tenant_id, unit_id, product_id, kind, quantity, unit_cost, source_location_id,\n measurement_unit_id, document_type, document_number,\n source_item_type, source_item_id, idempotency_key, line_no, operation_id,\n user_id, metadata)\n VALUES\n (v_tenant, v_cs.unit_id, v_cs.product_id, 'out', -v_cs.used_quantity,\n coalesce(v_cost, 0), v_location, v_cs.used_unit_id, 'consumption',\n coalesce(v_cs.session_id::text, v_cs.id::text),\n 'usage_audit', v_cs.id, v_key, v_no, v_op.op_id, v_actor,\n jsonb_strip_nulls(jsonb_build_object(\n 'source', 'usage_audit_approve',\n 'session_id', v_cs.session_id,\n 'order_item_id', v_cs.order_item_id,\n 'reason', nullif(btrim(p_reason), ''))))\n RETURNING id INTO v_movement;\n\n v_movements := v_movements || app.inventory_movement_json(v_movement);\n v_baixadas := v_baixadas + 1;\n END IF;\n\n -- CONFERIDO \u00E9 o mesmo carimbo nos dois gestos: o que sai da fila \u00E9 o que\n -- algu\u00E9m olhou, tenha ou n\u00E3o baixado.\n UPDATE public.plg_clinical_session_products cs\n SET is_audited = true,\n audited_at = now(),\n audited_by = coalesce(cs.audited_by, v_actor),\n audit_notes = coalesce(nullif(btrim(p_reason), ''), cs.audit_notes),\n updated_at = now()\n WHERE cs.id = v_cs.id;\n v_aprovadas := v_aprovadas + 1;\n END LOOP;\n\n RETURN app.inventory_finish_operation(\n v_tenant, v_op.op_id, CASE WHEN p_post THEN 'confirm' ELSE 'adjust' END, v_key,\n jsonb_build_object(\n 'operation_id', v_op.op_id, 'kind', 'usage_audit_approve',\n 'idempotency_key', v_key, 'status', CASE WHEN p_post THEN 'posted' ELSE 'approved' END,\n 'lines_approved', v_aprovadas, 'lines_posted', v_baixadas,\n 'lines_skipped', v_puladas, 'lines_no_stock', v_sem_saldo,\n 'missing', v_faltando, 'movements', v_movements),\n jsonb_build_object('source', 'usage_audit_approve', 'post', p_post,\n 'lines_approved', v_aprovadas, 'lines_posted', v_baixadas,\n 'lines_no_stock', v_sem_saldo));\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.inventory_approve_usage_batch(jsonb, boolean, text) TO authenticated;\n";
|
|
7
21
|
export declare const MIGRATIONS: Array<{
|
|
8
22
|
id: string;
|
|
9
23
|
sql: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,yn7NAy+GlC,CAAA;AAED,eAAO,MAAM,sDAAsD,g0bA+QlE,CAAA;AAED,eAAO,MAAM,uCAAuC,ymbAsRnD,CAAA;AAED,eAAO,MAAM,+CAA+C,+zNAmK3D,CAAA;AAED,eAAO,MAAM,4CAA4C,szYAsLxD,CAAA;AAED,eAAO,MAAM,mDAAmD,g5JA0E/D,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,yn7NAy+GlC,CAAA;AAED,eAAO,MAAM,sDAAsD,g0bA+QlE,CAAA;AAED,eAAO,MAAM,uCAAuC,ymbAsRnD,CAAA;AAED,eAAO,MAAM,+CAA+C,+zNAmK3D,CAAA;AAED,eAAO,MAAM,4CAA4C,szYAsLxD,CAAA;AAED,eAAO,MAAM,mDAAmD,g5JA0E/D,CAAA;AAED,eAAO,MAAM,4CAA4C,60nBAuUxD,CAAA;AAED,eAAO,MAAM,kCAAkC,oxSA+L9C,CAAA;AAED,eAAO,MAAM,qDAAqD,6yCA0BjE,CAAA;AAED,eAAO,MAAM,uDAAuD,s+XA4KnE,CAAA;AAED,eAAO,MAAM,0CAA0C,gm4BA4atD,CAAA;AAED,eAAO,MAAM,wCAAwC,y43BAsgBpD,CAAA;AAED,eAAO,MAAM,sEAAsE,o1ZAmRlF,CAAA;AAED,eAAO,MAAM,gDAAgD,2uXAmQ5D,CAAA;AAED,eAAO,MAAM,yCAAyC,umXA6PrD,CAAA;AAED,eAAO,MAAM,6CAA6C,w9WA2PzD,CAAA;AAED,eAAO,MAAM,4CAA4C,k8XAoQxD,CAAA;AAED,eAAO,MAAM,8CAA8C,okPA4K1D,CAAA;AAED,eAAO,MAAM,qCAAqC,umWAqPjD,CAAA;AAED,eAAO,MAAM,oCAAoC,skQA0LhD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAqBzD,CAAA"}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type StoreApi } from 'zustand/vanilla';
|
|
2
2
|
import type { InventoryDataProvider } from './data/types';
|
|
3
|
-
import type { Product, StockMovement, StockLocation, Recipe, InventorySummary, RestockLine, CategoryCount, ProductQuery, MovementQuery, UsageAuditEntry, UsageAuditQuery, UsageAuditCorrection, UsageAuditBatchResult, CreateProductInput, CreateStockMovementInput, CreateRecipeInput } from './types';
|
|
3
|
+
import type { Product, StockMovement, StockLocation, Recipe, InventorySummary, RestockLine, CategoryCount, ProductQuery, MovementQuery, UsageAuditEntry, UsageAuditQuery, UsageAuditCorrection, UsageAuditBatchResult, UsageAuditApproval, UsageAuditApproveResult, CreateProductInput, CreateStockMovementInput, CreateRecipeInput } from './types';
|
|
4
4
|
export interface InventoryUIState {
|
|
5
5
|
products: Product[];
|
|
6
6
|
productsTotal: number;
|
|
@@ -38,6 +38,13 @@ export interface InventoryUIState {
|
|
|
38
38
|
reason: string;
|
|
39
39
|
lines: UsageAuditCorrection[];
|
|
40
40
|
}): Promise<UsageAuditBatchResult>;
|
|
41
|
+
approveUsageBatch(input: {
|
|
42
|
+
post: boolean;
|
|
43
|
+
lines: UsageAuditApproval[];
|
|
44
|
+
reason?: string;
|
|
45
|
+
}): Promise<UsageAuditApproveResult>;
|
|
46
|
+
/** Muda as linhas NA LISTA que já está na tela, sem ir ao servidor. */
|
|
47
|
+
patchUsageAuditEntries(lines: UsageAuditApproval[], patch: Partial<UsageAuditEntry>): void;
|
|
41
48
|
fetchLocations(): Promise<void>;
|
|
42
49
|
fetchRecipes(): Promise<void>;
|
|
43
50
|
createProduct(input: CreateProductInput): Promise<Product>;
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAG5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EACV,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAC7C,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EACzE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,qBAAqB,EAC7E,kBAAkB,EAAE,wBAAwB,EAAE,iBAAiB,EAChE,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,OAAO,CAAA;IACxB,YAAY,EAAE,YAAY,CAAA;IAE1B,SAAS,EAAE,aAAa,EAAE,CAAA;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,OAAO,CAAA;IAEzB,UAAU,EAAE,eAAe,EAAE,CAAA;IAC7B,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,eAAe,EAAE,eAAe,CAAA;IAEhC,SAAS,EAAE,aAAa,EAAE,CAAA;IAC1B,gBAAgB,EAAE,OAAO,CAAA;IAEzB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,cAAc,EAAE,OAAO,CAAA;IAEvB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAChC,cAAc,EAAE,OAAO,CAAA;IAEvB,4EAA4E;IAC5E,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;IAC7B,WAAW,EAAE,aAAa,EAAE,GAAG,IAAI,CAAA;IAEnC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,YAAY,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3G,iBAAiB,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,oBAAoB,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC3G,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1D,cAAc,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IACvE,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACxD;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAG5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EACV,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAC7C,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EACzE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,qBAAqB,EAC7E,kBAAkB,EAAE,uBAAuB,EAC3C,kBAAkB,EAAE,wBAAwB,EAAE,iBAAiB,EAChE,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,OAAO,CAAA;IACxB,YAAY,EAAE,YAAY,CAAA;IAE1B,SAAS,EAAE,aAAa,EAAE,CAAA;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,OAAO,CAAA;IAEzB,UAAU,EAAE,eAAe,EAAE,CAAA;IAC7B,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,eAAe,EAAE,eAAe,CAAA;IAEhC,SAAS,EAAE,aAAa,EAAE,CAAA;IAC1B,gBAAgB,EAAE,OAAO,CAAA;IAEzB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,cAAc,EAAE,OAAO,CAAA;IAEvB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAChC,cAAc,EAAE,OAAO,CAAA;IAEvB,4EAA4E;IAC5E,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;IAC7B,WAAW,EAAE,aAAa,EAAE,GAAG,IAAI,CAAA;IAEnC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,YAAY,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3G,iBAAiB,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,oBAAoB,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC3G,iBAAiB,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,kBAAkB,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;IAC3H,uEAAuE;IACvE,sBAAsB,CAAC,KAAK,EAAE,kBAAkB,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAAA;IAC1F,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1D,cAAc,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IACvE,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACxD;AAqBD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAsLhG"}
|