@fayz-ai/plugin-fiscal-br 0.3.7 → 0.4.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/data/inbox.d.ts +78 -0
- package/dist/data/inbox.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1693 -120
- package/dist/index.js.map +1 -1
- package/dist/lib/nfe-xml.d.ts +2 -0
- package/dist/lib/nfe-xml.d.ts.map +1 -1
- package/dist/lib/note-suggestion.d.ts +196 -0
- package/dist/lib/note-suggestion.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +7 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InboundNotesPage.d.ts.map +1 -1
- package/dist/views/ManifestView.d.ts +3 -1
- package/dist/views/ManifestView.d.ts.map +1 -1
- package/dist/views/NoteReconcileView.d.ts +7 -1
- package/dist/views/NoteReconcileView.d.ts.map +1 -1
- package/dist/views/UnitFiscalWidgets.d.ts +21 -0
- package/dist/views/UnitFiscalWidgets.d.ts.map +1 -0
- package/dist/views/inventoryView.d.ts +31 -0
- package/dist/views/inventoryView.d.ts.map +1 -1
- package/functions/fiscalbr-suggest-note-items/index.ts +425 -0
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { recordRailFields, RECORD_GROUP, activeFacet, connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus,
|
|
2
|
-
import { useState,
|
|
3
|
-
import {
|
|
4
|
-
import { useFilterDraft, Button, PageHeaderActions, Card, FilterPanel, FilterPanelField, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SearchField, DataTable, SubpageHeader, Skeleton, EmBreve, SearchSelect, Input, DatePicker, Hint, ListFilterBar, ComingSoon } from '@fayz-ai/ui';
|
|
1
|
+
import { recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus, maskCpfCnpj } from '@fayz-ai/core';
|
|
2
|
+
import React5, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { ModalHeader, ModalTitle, ModalBody, Skeleton, AiActionButton, Card, Button, EmBreve, SearchSelect, Input, DatePicker, Hint, ModalFooter, SubpageHeader, useFilterDraft, PageHeaderActions, FilterPanel, FilterPanelField, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SearchField, DataTable, ListFilterBar, Modal, ModalContent, ComingSoon, CardHeader, CardTitle, CardContent, toast, Badge, CardDescription, Switch } from '@fayz-ai/ui';
|
|
5
4
|
export { ComingSoon, EmBreve } from '@fayz-ai/ui';
|
|
6
|
-
import { useUnitFilter } from '@fayz-ai/admin';
|
|
5
|
+
import { registerDocumentOpener, getActiveTenantId, useUnitFilter, UNIT_DETAIL_ZONES, adminNavigateTo } from '@fayz-ai/admin';
|
|
6
|
+
import { Gift, RefreshCw, Loader2, FileDown, AlertTriangle, ChevronRight, Check, Trash2, Plus, Inbox, UploadCloud, ShieldCheck, FileText, ExternalLink, X, Info } from 'lucide-react';
|
|
7
7
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
@@ -2832,6 +2832,821 @@ END $function$;
|
|
|
2832
2832
|
REVOKE ALL ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) FROM PUBLIC;
|
|
2833
2833
|
GRANT EXECUTE ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) TO authenticated;
|
|
2834
2834
|
`;
|
|
2835
|
+
var MIGRATION_011_O_IMPOSTO_DO_PRODUTO_E_DO_FISCAL = `-- O imposto do PRODUTO mora no fiscal, n\xE3o no estoque.
|
|
2836
|
+
--
|
|
2837
|
+
-- No V1 as dezessete colunas de tributo (NCM, CFOP, CST, PIS, COFINS, CEST, FCP
|
|
2838
|
+
-- e os dois campos novos da reforma) ficam numa tabela do m\xF3dulo de estoque,
|
|
2839
|
+
-- ao lado de quantidade m\xEDnima e temperatura de armazenamento. O sal\xE3o que
|
|
2840
|
+
-- nunca emitiu uma nota carrega as dezessete do mesmo jeito.
|
|
2841
|
+
--
|
|
2842
|
+
-- Aqui elas ficam onde o assunto \xE9: \`plg_fiscalbr_tax_groups\` j\xE1 guarda a
|
|
2843
|
+
-- mesma lista para o GRUPO fiscal, e esta tabela \xE9 o que um produto tem de
|
|
2844
|
+
-- diferente do grupo dele. Produto sem nada de fiscal n\xE3o tem linha nenhuma, e
|
|
2845
|
+
-- o estoque de quem n\xE3o emite nota n\xE3o muda de forma.
|
|
2846
|
+
--
|
|
2847
|
+
-- \`tax_group_id\` \xE9 a heran\xE7a e as outras colunas s\xE3o a exce\xE7\xE3o: NULL aqui quer
|
|
2848
|
+
-- dizer \xABvale o que o grupo diz\xBB, n\xE3o \xABzero\xBB. Por isso nenhuma tem DEFAULT 0 \u2014
|
|
2849
|
+
-- um zero gravado afirmaria al\xEDquota zero e sobrescreveria o grupo em sil\xEAncio.
|
|
2850
|
+
|
|
2851
|
+
CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_product_tax_info (
|
|
2852
|
+
product_id uuid PRIMARY KEY REFERENCES public.products(id) ON DELETE CASCADE,
|
|
2853
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
2854
|
+
tax_group_id uuid REFERENCES public.plg_fiscalbr_tax_groups(id) ON DELETE SET NULL,
|
|
2855
|
+
|
|
2856
|
+
-- Nomenclatura Comum do Mercosul: \xE9 do PRODUTO, nunca do grupo. \xC9 o campo que
|
|
2857
|
+
-- o V1 preenche mesmo quando nenhum outro est\xE1 preenchido.
|
|
2858
|
+
ncm text,
|
|
2859
|
+
cest text,
|
|
2860
|
+
icms_origin text,
|
|
2861
|
+
icms_rate numeric(9,4),
|
|
2862
|
+
cfop text,
|
|
2863
|
+
cst text,
|
|
2864
|
+
pis_situation text,
|
|
2865
|
+
pis_rate numeric(9,4),
|
|
2866
|
+
cofins_situation text,
|
|
2867
|
+
cofins_rate numeric(9,4),
|
|
2868
|
+
base_calculation_mode text,
|
|
2869
|
+
benefit_code text,
|
|
2870
|
+
poverty_fund_rate numeric(9,4),
|
|
2871
|
+
|
|
2872
|
+
-- Reforma tribut\xE1ria. Entram agora pelo mesmo motivo que entraram no grupo:
|
|
2873
|
+
-- deixar de fora obrigaria a recadastrar o cat\xE1logo inteiro no dia em que
|
|
2874
|
+
-- IBS e CBS valerem.
|
|
2875
|
+
ibs_cbs_situation text,
|
|
2876
|
+
ibs_cbs_classification text,
|
|
2877
|
+
|
|
2878
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
2879
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
2880
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
2881
|
+
);
|
|
2882
|
+
|
|
2883
|
+
CREATE INDEX IF NOT EXISTS plg_fiscalbr_product_tax_info_tenant_idx
|
|
2884
|
+
ON public.plg_fiscalbr_product_tax_info (tenant_id);
|
|
2885
|
+
CREATE INDEX IF NOT EXISTS plg_fiscalbr_product_tax_info_group_idx
|
|
2886
|
+
ON public.plg_fiscalbr_product_tax_info (tax_group_id);
|
|
2887
|
+
|
|
2888
|
+
ALTER TABLE public.plg_fiscalbr_product_tax_info ENABLE ROW LEVEL SECURITY;
|
|
2889
|
+
|
|
2890
|
+
-- Quem l\xEA e escreve \xE9 quem cuida do CADASTRO do produto, n\xE3o quem emite a
|
|
2891
|
+
-- nota: a aba Fiscal do V1 vive dentro do formul\xE1rio do produto, e exigir
|
|
2892
|
+
-- permiss\xE3o de financeiro ali esconderia a se\xE7\xE3o do estoquista que digita o
|
|
2893
|
+
-- NCM da mercadoria que acabou de chegar.
|
|
2894
|
+
DROP POLICY IF EXISTS plg_fiscalbr_product_tax_info_authz_select ON public.plg_fiscalbr_product_tax_info;
|
|
2895
|
+
CREATE POLICY plg_fiscalbr_product_tax_info_authz_select
|
|
2896
|
+
ON public.plg_fiscalbr_product_tax_info FOR SELECT TO authenticated
|
|
2897
|
+
USING (
|
|
2898
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
2899
|
+
AND (SELECT app.has_permission('inventory.read', NULL::uuid)));
|
|
2900
|
+
|
|
2901
|
+
DROP POLICY IF EXISTS plg_fiscalbr_product_tax_info_authz_write ON public.plg_fiscalbr_product_tax_info;
|
|
2902
|
+
CREATE POLICY plg_fiscalbr_product_tax_info_authz_write
|
|
2903
|
+
ON public.plg_fiscalbr_product_tax_info FOR ALL TO authenticated
|
|
2904
|
+
USING (
|
|
2905
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
2906
|
+
AND (SELECT app.has_permission('inventory.edit', NULL::uuid)))
|
|
2907
|
+
WITH CHECK (
|
|
2908
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
2909
|
+
AND (SELECT app.has_permission('inventory.edit', NULL::uuid)));
|
|
2910
|
+
|
|
2911
|
+
GRANT ALL ON TABLE public.plg_fiscalbr_product_tax_info TO authenticated;
|
|
2912
|
+
GRANT ALL ON TABLE public.plg_fiscalbr_product_tax_info TO service_role;
|
|
2913
|
+
|
|
2914
|
+
COMMENT ON TABLE public.plg_fiscalbr_product_tax_info IS
|
|
2915
|
+
'Tributos do produto \u2014 a exce\xE7\xE3o ao grupo fiscal. Espelha a aba Fiscal do cadastro de produto do V1 (product_fiscal_info). NULL herda do grupo.';
|
|
2916
|
+
`;
|
|
2917
|
+
var MIGRATION_012_QUEM_E_O_PRODUTO_SE_PERGUNTA_AO_INDICE = `-- ---------------------------------------------------------------------------
|
|
2918
|
+
-- Quem \xE9 o produto desta linha da nota: pergunta-se ao \xEDndice, n\xE3o ao modelo.
|
|
2919
|
+
-- ---------------------------------------------------------------------------
|
|
2920
|
+
-- A sugest\xE3o de IA da entrada de notas mandava o CAT\xC1LOGO para o modelo. O V1
|
|
2921
|
+
-- mandava 400 produtos em todo turno (\`.slice(0, 400)\` sobre um \`.limit(800)\`
|
|
2922
|
+
-- sem \`order by\` \u2014 o corte era arbitr\xE1rio, e acima de 400 produtos a function
|
|
2923
|
+
-- respondia \xABcrie este produto\xBB para produto cadastrado, sem aviso).
|
|
2924
|
+
--
|
|
2925
|
+
-- O custo disso \xE9 \`cat\xE1logo \xD7 notas\`. Num tenant de 171 produtos s\xE3o ~3,5k
|
|
2926
|
+
-- tokens por nota; num distribuidor de 8.000 s\xE3o ~160k e n\xE3o cabe no turno. E a
|
|
2927
|
+
-- varredura de cat\xE1logo n\xE3o \xE9 trabalho de modelo: \xE9 trabalho de \xEDndice.
|
|
2928
|
+
--
|
|
2929
|
+
-- Esta fun\xE7\xE3o \xE9 a pr\xE9-sele\xE7\xE3o, em TR\xCAS fases, e a primeira que acerta encerra a
|
|
2930
|
+
-- linha:
|
|
2931
|
+
--
|
|
2932
|
+
-- supplier_map o de-para do fornecedor. O que aquele CNPJ j\xE1 ensinou n\xE3o se
|
|
2933
|
+
-- pergunta de novo \u2014 e numa nota repetida isso resolve a nota
|
|
2934
|
+
-- inteira, ent\xE3o a IA nem roda.
|
|
2935
|
+
-- ean c\xF3digo de barras contra o cadastro. Igualdade, n\xE3o palpite.
|
|
2936
|
+
-- text semelhan\xE7a de texto, com os candidatos que sobraram.
|
|
2937
|
+
--
|
|
2938
|
+
-- \`reason\` \xE9 o que a tela usa para decidir: \`supplier_map\` e \`ean\` s\xE3o V\xCDNCULO
|
|
2939
|
+
-- (liga sozinho), \`text\` \xE9 CANDIDATO (vai para o modelo desambiguar). Sem essa
|
|
2940
|
+
-- coluna a tela trataria um EAN exato como um chute de trigrama.
|
|
2941
|
+
--
|
|
2942
|
+
-- A fase de texto N\xC3O reimplementa busca: ela chama \`public.global_search\`, que
|
|
2943
|
+
-- j\xE1 tem \xEDndice GIN de trigrama, \`fayz_norm()\` (tira acento, min\xFAscula, colapsa
|
|
2944
|
+
-- pontua\xE7\xE3o) e a escada de pontua\xE7\xE3o da casa. Reescrever isso pior era o erro
|
|
2945
|
+
-- que o \`ILIKE\` da descri\xE7\xE3o fatiada em 24 caracteres j\xE1 estava cometendo do
|
|
2946
|
+
-- lado do cliente \u2014 \`name.ilike.%SACO PLASTICO 10X15 TRAN%\` n\xE3o casa acento,
|
|
2947
|
+
-- ordem de palavra nem abrevia\xE7\xE3o, e o resultado mais comum era zero candidatos.
|
|
2948
|
+
--
|
|
2949
|
+
-- O RPC foi renomeado na 137 (\`fayz_global_search\` \u2192 \`global_search\`) e h\xE1 pool
|
|
2950
|
+
-- em cada lado. Aqui se descobre o nome em tempo de execu\xE7\xE3o, como o cliente j\xE1
|
|
2951
|
+
-- faz em \`@fayz-ai/core/search\`.
|
|
2952
|
+
--
|
|
2953
|
+
-- N\xC3O \xE9 SECURITY DEFINER. Roda como quem chamou, a RLS de \`products\` vale, e o
|
|
2954
|
+
-- tenant vem de \`app.current_tenant_id()\` \u2014 o cliente n\xE3o passa tenant e por
|
|
2955
|
+
-- isso n\xE3o pode passar o de outra conta.
|
|
2956
|
+
--
|
|
2957
|
+
-- \`SET search_path\` \xE9 expl\xEDcito porque \`similarity\`/\`word_similarity\` moram em
|
|
2958
|
+
-- \`extensions\`: corpo com search_path vazio passa no DDL e estoura na primeira
|
|
2959
|
+
-- chamada.
|
|
2960
|
+
-- ---------------------------------------------------------------------------
|
|
2961
|
+
|
|
2962
|
+
CREATE OR REPLACE FUNCTION public.fiscalbr_match_product_candidates(
|
|
2963
|
+
-- [{ "ref": "<id do item>", "text": "SACO PLASTICO 10X15", "ean": "789\u2026", "code": "ABC1" }]
|
|
2964
|
+
p_queries jsonb,
|
|
2965
|
+
p_supplier_cnpj text DEFAULT NULL,
|
|
2966
|
+
p_per_query integer DEFAULT 8
|
|
2967
|
+
) RETURNS TABLE (
|
|
2968
|
+
ref text,
|
|
2969
|
+
product_id uuid,
|
|
2970
|
+
name text,
|
|
2971
|
+
sku text,
|
|
2972
|
+
unit text,
|
|
2973
|
+
cost numeric,
|
|
2974
|
+
score real,
|
|
2975
|
+
reason text
|
|
2976
|
+
)
|
|
2977
|
+
LANGUAGE plpgsql
|
|
2978
|
+
STABLE
|
|
2979
|
+
SET search_path TO 'public', 'extensions', 'pg_temp'
|
|
2980
|
+
AS $fn$
|
|
2981
|
+
DECLARE
|
|
2982
|
+
v_tenant uuid := (SELECT app.current_tenant_id());
|
|
2983
|
+
v_cnpj text := public.fayz_digits(coalesce(p_supplier_cnpj, ''));
|
|
2984
|
+
v_barcodes boolean := to_regclass('public.plg_inventory_product_barcodes') IS NOT NULL;
|
|
2985
|
+
v_map boolean := to_regclass('public.plg_fiscalbr_supplier_product_map') IS NOT NULL;
|
|
2986
|
+
v_search text;
|
|
2987
|
+
v_q record;
|
|
2988
|
+
v_found uuid;
|
|
2989
|
+
BEGIN
|
|
2990
|
+
IF v_tenant IS NULL THEN RETURN; END IF;
|
|
2991
|
+
IF p_queries IS NULL OR jsonb_typeof(p_queries) <> 'array' THEN RETURN; END IF;
|
|
2992
|
+
p_per_query := least(greatest(coalesce(p_per_query, 8), 1), 50);
|
|
2993
|
+
|
|
2994
|
+
v_search := CASE
|
|
2995
|
+
WHEN to_regprocedure('public.global_search(text,uuid,text[],integer,integer)') IS NOT NULL
|
|
2996
|
+
THEN 'public.global_search'
|
|
2997
|
+
WHEN to_regprocedure('public.fayz_global_search(text,uuid,text[],integer,integer)') IS NOT NULL
|
|
2998
|
+
THEN 'public.fayz_global_search'
|
|
2999
|
+
ELSE NULL
|
|
3000
|
+
END;
|
|
3001
|
+
|
|
3002
|
+
FOR v_q IN
|
|
3003
|
+
SELECT e->>'ref' AS ref,
|
|
3004
|
+
coalesce(e->>'text', '') AS text,
|
|
3005
|
+
public.fayz_digits(coalesce(e->>'ean', '')) AS ean,
|
|
3006
|
+
nullif(btrim(coalesce(e->>'code', '')), '') AS code
|
|
3007
|
+
FROM jsonb_array_elements(p_queries) AS e
|
|
3008
|
+
WHERE e->>'ref' IS NOT NULL
|
|
3009
|
+
LOOP
|
|
3010
|
+
v_found := NULL;
|
|
3011
|
+
|
|
3012
|
+
-- \u2460 O de-para do fornecedor. Aprendizado da casa, e ele ganha de tudo.
|
|
3013
|
+
IF v_map AND v_cnpj <> '' AND v_q.code IS NOT NULL THEN
|
|
3014
|
+
SELECT p.id INTO v_found
|
|
3015
|
+
FROM public.plg_fiscalbr_supplier_product_map m
|
|
3016
|
+
JOIN public.products p ON p.id = m.product_id
|
|
3017
|
+
WHERE m.supplier_cnpj = v_cnpj
|
|
3018
|
+
AND m.supplier_product_code = v_q.code
|
|
3019
|
+
AND p.is_active
|
|
3020
|
+
ORDER BY m.hit_count DESC NULLS LAST, m.last_used_at DESC NULLS LAST
|
|
3021
|
+
LIMIT 1;
|
|
3022
|
+
|
|
3023
|
+
IF v_found IS NOT NULL THEN
|
|
3024
|
+
RETURN QUERY
|
|
3025
|
+
SELECT v_q.ref, p.id, p.name, p.sku, p.unit, p.cost, 1.0::real, 'supplier_map'::text
|
|
3026
|
+
FROM public.products p WHERE p.id = v_found;
|
|
3027
|
+
CONTINUE;
|
|
3028
|
+
END IF;
|
|
3029
|
+
END IF;
|
|
3030
|
+
|
|
3031
|
+
-- \u2461 O c\xF3digo de barras. O produto guarda o principal em \`metadata->>barcode\`
|
|
3032
|
+
-- (no V1 era coluna) e os demais na tabela de c\xF3digos do estoque.
|
|
3033
|
+
IF length(v_q.ean) >= 8 THEN
|
|
3034
|
+
SELECT p.id INTO v_found
|
|
3035
|
+
FROM public.products p
|
|
3036
|
+
WHERE p.is_active
|
|
3037
|
+
AND (
|
|
3038
|
+
public.fayz_digits(coalesce(p.metadata->>'barcode', '')) = v_q.ean
|
|
3039
|
+
OR (v_barcodes AND EXISTS (
|
|
3040
|
+
SELECT 1 FROM public.plg_inventory_product_barcodes b
|
|
3041
|
+
WHERE b.product_id = p.id
|
|
3042
|
+
AND public.fayz_digits(coalesce(b.barcode, '')) = v_q.ean))
|
|
3043
|
+
)
|
|
3044
|
+
LIMIT 1;
|
|
3045
|
+
|
|
3046
|
+
IF v_found IS NOT NULL THEN
|
|
3047
|
+
RETURN QUERY
|
|
3048
|
+
SELECT v_q.ref, p.id, p.name, p.sku, p.unit, p.cost, 1.0::real, 'ean'::text
|
|
3049
|
+
FROM public.products p WHERE p.id = v_found;
|
|
3050
|
+
CONTINUE;
|
|
3051
|
+
END IF;
|
|
3052
|
+
END IF;
|
|
3053
|
+
|
|
3054
|
+
-- \u2462 Semelhan\xE7a de texto, pela busca indexada que j\xE1 existe.
|
|
3055
|
+
CONTINUE WHEN v_search IS NULL OR length(public.fayz_norm(v_q.text)) < 3;
|
|
3056
|
+
|
|
3057
|
+
RETURN QUERY EXECUTE format($q$
|
|
3058
|
+
SELECT $4::text, p.id, p.name, p.sku, p.unit, p.cost, g.score, 'text'::text
|
|
3059
|
+
FROM %s($1, $2, ARRAY['product']::text[], $3, $3) g
|
|
3060
|
+
JOIN public.products p ON p.id = g.record_id::uuid
|
|
3061
|
+
WHERE p.is_active
|
|
3062
|
+
ORDER BY g.score DESC
|
|
3063
|
+
$q$, v_search)
|
|
3064
|
+
USING v_q.text, v_tenant, p_per_query, v_q.ref;
|
|
3065
|
+
END LOOP;
|
|
3066
|
+
END
|
|
3067
|
+
$fn$;
|
|
3068
|
+
|
|
3069
|
+
GRANT EXECUTE ON FUNCTION public.fiscalbr_match_product_candidates(jsonb, text, integer) TO authenticated;
|
|
3070
|
+
|
|
3071
|
+
COMMENT ON FUNCTION public.fiscalbr_match_product_candidates(jsonb, text, integer) IS
|
|
3072
|
+
'Pr\xE9-sele\xE7\xE3o de produtos para a sugest\xE3o de IA da entrada de notas: de-para do fornecedor e EAN resolvem sozinhos (reason supplier_map/ean, v\xEDnculo direto); o resto volta como candidatos de texto (reason text) para o modelo desambiguar. Uma viagem para a nota inteira \u2014 substitui o despejo de 400 produtos do V1 e o ILIKE da descri\xE7\xE3o fatiada.';
|
|
3073
|
+
`;
|
|
3074
|
+
var MIGRATION_013_A_SUGESTAO_SOBREVIVE_A_FECHAR_A_JANELA = `-- ---------------------------------------------------------------------------
|
|
3075
|
+
-- A sugest\xE3o sobrevive a fechar a janela \u2014 e diz QUEM escreveu no campo.
|
|
3076
|
+
-- ---------------------------------------------------------------------------
|
|
3077
|
+
-- A concilia\xE7\xE3o virou modal para a lista n\xE3o ser desmontada. O efeito colateral
|
|
3078
|
+
-- \xE9 que o modal fecha com clique fora e com Esc, e a sugest\xE3o s\xF3 existia em
|
|
3079
|
+
-- mem\xF3ria at\xE9 algu\xE9m clicar em \xABSalvar concilia\xE7\xE3o\xBB:
|
|
3080
|
+
--
|
|
3081
|
+
-- sugerir (30s de IA) \u2192 revisar 7 de 11 itens \u2192 clique fora \u2192 perdeu os 11
|
|
3082
|
+
--
|
|
3083
|
+
-- No V1 fechar n\xE3o custava nada, porque a edge function j\xE1 tinha gravado. Ent\xE3o
|
|
3084
|
+
-- a V2 grava tamb\xE9m, logo depois da sugest\xE3o, pelo caminho que j\xE1 existia
|
|
3085
|
+
-- (\`fiscalbr_save_inbox_decision\`, que n\xE3o marca \`reconciled\`) \u2014 nenhum segundo
|
|
3086
|
+
-- caminho de escrita, nenhuma coluna \`ai_*\` paralela aos campos de verdade.
|
|
3087
|
+
--
|
|
3088
|
+
-- O que faltava era a MEM\xD3RIA DE AUTORIA. Depois de recarregar, a tela via um
|
|
3089
|
+
-- fator 22,056 e n\xE3o sabia se foi a Laissa que digitou ou a m\xE1quina que chutou \u2014
|
|
3090
|
+
-- e as duas coisas se conferem de maneiras diferentes.
|
|
3091
|
+
--
|
|
3092
|
+
-- O V1 guardava um booleano, \`ai_suggested\`. Aqui \xE9 texto, porque a V2 tem TR\xCAS
|
|
3093
|
+
-- autores de m\xE1quina e eles n\xE3o merecem o mesmo selo:
|
|
3094
|
+
--
|
|
3095
|
+
-- supplier_map o de-para deste fornecedor. Igualdade, e aprendizado da casa.
|
|
3096
|
+
-- ean c\xF3digo de barras. Igualdade.
|
|
3097
|
+
-- ai semelhan\xE7a julgada por modelo. \xC9 o \xFAnico que se confere item
|
|
3098
|
+
-- a item, e \`suggestion_confidence\` diz o quanto.
|
|
3099
|
+
--
|
|
3100
|
+
-- NULL \xE9 a resposta mais importante: significa \xABuma pessoa escolheu isto\xBB. O
|
|
3101
|
+
-- cliente limpa a coluna em qualquer edi\xE7\xE3o do campo, ent\xE3o o selo nunca mente
|
|
3102
|
+
-- sobre um valor que algu\xE9m corrigiu \xE0 m\xE3o.
|
|
3103
|
+
-- ---------------------------------------------------------------------------
|
|
3104
|
+
|
|
3105
|
+
ALTER TABLE public.plg_fiscalbr_inbox_items
|
|
3106
|
+
ADD COLUMN IF NOT EXISTS suggested_by text,
|
|
3107
|
+
ADD COLUMN IF NOT EXISTS suggestion_confidence numeric;
|
|
3108
|
+
|
|
3109
|
+
DO $$
|
|
3110
|
+
BEGIN
|
|
3111
|
+
IF NOT EXISTS (
|
|
3112
|
+
SELECT 1 FROM pg_constraint
|
|
3113
|
+
WHERE conname = 'plg_fiscalbr_inbox_items_suggested_by_check'
|
|
3114
|
+
AND conrelid = 'public.plg_fiscalbr_inbox_items'::regclass
|
|
3115
|
+
) THEN
|
|
3116
|
+
ALTER TABLE public.plg_fiscalbr_inbox_items
|
|
3117
|
+
ADD CONSTRAINT plg_fiscalbr_inbox_items_suggested_by_check
|
|
3118
|
+
CHECK (suggested_by IS NULL OR suggested_by IN ('ai', 'ean', 'supplier_map'));
|
|
3119
|
+
END IF;
|
|
3120
|
+
END $$;
|
|
3121
|
+
|
|
3122
|
+
COMMENT ON COLUMN public.plg_fiscalbr_inbox_items.suggested_by IS
|
|
3123
|
+
'Quem escreveu a decis\xE3o desta linha: ai (modelo, confira), ean / supplier_map (igualdade), NULL (uma pessoa). O cliente limpa em qualquer edi\xE7\xE3o manual.';
|
|
3124
|
+
COMMENT ON COLUMN public.plg_fiscalbr_inbox_items.suggestion_confidence IS
|
|
3125
|
+
'0..1, s\xF3 para suggested_by = ai. Abaixo de 0,6 a tela marca a linha como \xABconfira\xBB.';
|
|
3126
|
+
|
|
3127
|
+
-- \u2500\u2500 A grava\xE7\xE3o da decis\xE3o passa a carregar a autoria \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3128
|
+
-- Mesma assinatura, ent\xE3o \xE9 \`CREATE OR REPLACE\` e nenhum chamador muda. O corpo
|
|
3129
|
+
-- \xE9 o da 009 com duas linhas a mais: o resto fica palavra por palavra, porque
|
|
3130
|
+
-- esta fun\xE7\xE3o \xE9 o \xFAnico caminho de escrita da decis\xE3o e reescrev\xEA-la de mem\xF3ria
|
|
3131
|
+
-- \xE9 como se perde uma regra (a de \`reconciled\`, por exemplo).
|
|
3132
|
+
|
|
3133
|
+
CREATE OR REPLACE FUNCTION public.fiscalbr_save_inbox_decision(
|
|
3134
|
+
p_document_id uuid,
|
|
3135
|
+
p_items jsonb,
|
|
3136
|
+
p_default_chart_account_id uuid DEFAULT NULL,
|
|
3137
|
+
p_default_location_id uuid DEFAULT NULL,
|
|
3138
|
+
p_duplicates jsonb DEFAULT NULL)
|
|
3139
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$
|
|
3140
|
+
DECLARE
|
|
3141
|
+
v_tenant uuid := public.plg_financial_require_tenant();
|
|
3142
|
+
v_doc public.plg_fiscalbr_inbox_documents%ROWTYPE;
|
|
3143
|
+
v_row jsonb;
|
|
3144
|
+
v_done integer := 0;
|
|
3145
|
+
v_total integer;
|
|
3146
|
+
BEGIN
|
|
3147
|
+
SELECT * INTO v_doc FROM public.plg_fiscalbr_inbox_documents
|
|
3148
|
+
WHERE tenant_id = v_tenant AND id = p_document_id;
|
|
3149
|
+
IF NOT FOUND THEN
|
|
3150
|
+
RAISE EXCEPTION 'fiscalbr: nota % n\xE3o encontrada neste tenant', p_document_id USING ERRCODE = 'P0002';
|
|
3151
|
+
END IF;
|
|
3152
|
+
PERFORM public.plg_financial_require_permission('financial.edit', v_doc.unit_id);
|
|
3153
|
+
|
|
3154
|
+
IF v_doc.stocked_at IS NOT NULL OR v_doc.invoice_id IS NOT NULL THEN
|
|
3155
|
+
RAISE EXCEPTION 'fiscalbr: nota % j\xE1 foi aceita \u2014 a decis\xE3o n\xE3o muda depois do estoque', p_document_id
|
|
3156
|
+
USING ERRCODE = '23514';
|
|
3157
|
+
END IF;
|
|
3158
|
+
|
|
3159
|
+
UPDATE public.plg_fiscalbr_inbox_documents SET
|
|
3160
|
+
default_chart_account_id = coalesce(p_default_chart_account_id, default_chart_account_id),
|
|
3161
|
+
default_location_id = coalesce(p_default_location_id, default_location_id),
|
|
3162
|
+
reviewed_at = now(), updated_at = now()
|
|
3163
|
+
WHERE id = v_doc.id;
|
|
3164
|
+
|
|
3165
|
+
FOR v_row IN SELECT * FROM jsonb_array_elements(coalesce(p_items, '[]'::jsonb)) LOOP
|
|
3166
|
+
UPDATE public.plg_fiscalbr_inbox_items i SET
|
|
3167
|
+
product_id = nullif(v_row->>'product_id', '')::uuid,
|
|
3168
|
+
entry_type = nullif(v_row->>'entry_type', ''),
|
|
3169
|
+
chart_account_id = nullif(v_row->>'chart_account_id', '')::uuid,
|
|
3170
|
+
stock_unit_id = nullif(v_row->>'stock_unit_id', '')::uuid,
|
|
3171
|
+
conversion_factor = coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1),
|
|
3172
|
+
cost_mode = coalesce(nullif(v_row->>'cost_mode', ''), 'novo'),
|
|
3173
|
+
shortage_quantity = nullif(v_row->>'shortage_quantity', '')::numeric,
|
|
3174
|
+
shortage_reason = nullif(v_row->>'shortage_reason', ''),
|
|
3175
|
+
shortage_note = nullif(v_row->>'shortage_note', ''),
|
|
3176
|
+
-- Quem escreveu. Ausente na chave \xE9 NULL, que \xE9 \xABuma pessoa escolheu\xBB \u2014 e \xE9
|
|
3177
|
+
-- o que acontece em todo salvamento comum, sem a IA ter passado por aqui.
|
|
3178
|
+
suggested_by = nullif(v_row->>'suggested_by', ''),
|
|
3179
|
+
suggestion_confidence = nullif(v_row->>'suggestion_confidence', '')::numeric,
|
|
3180
|
+
-- Derivadas, gravadas: \xE9 o que o operador viu.
|
|
3181
|
+
stock_quantity = i.quantity_commercial * coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1)
|
|
3182
|
+
- coalesce(nullif(v_row->>'shortage_quantity', '')::numeric, 0),
|
|
3183
|
+
unit_cost_new = CASE WHEN coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1) > 0
|
|
3184
|
+
THEN i.unit_price_commercial / coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1) END,
|
|
3185
|
+
-- Uma linha est\xE1 conciliada quando sabe o que \xE9. Despesa e servi\xE7o n\xE3o
|
|
3186
|
+
-- pedem produto: elas nunca v\xE3o virar estoque.
|
|
3187
|
+
reconciled = CASE
|
|
3188
|
+
WHEN nullif(v_row->>'entry_type', '') IN ('despesa','servico') THEN true
|
|
3189
|
+
ELSE nullif(v_row->>'product_id', '') IS NOT NULL
|
|
3190
|
+
END,
|
|
3191
|
+
updated_at = now()
|
|
3192
|
+
WHERE i.tenant_id = v_tenant AND i.inbox_document_id = v_doc.id
|
|
3193
|
+
AND i.id = (v_row->>'id')::uuid;
|
|
3194
|
+
v_done := v_done + 1;
|
|
3195
|
+
END LOOP;
|
|
3196
|
+
|
|
3197
|
+
IF p_duplicates IS NOT NULL THEN
|
|
3198
|
+
DELETE FROM public.plg_fiscalbr_inbox_duplicates
|
|
3199
|
+
WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
|
|
3200
|
+
INSERT INTO public.plg_fiscalbr_inbox_duplicates (
|
|
3201
|
+
tenant_id, inbox_document_id, duplicate_index, number, due_date, value,
|
|
3202
|
+
payment_type, barcode, pix_key, payment_method_id)
|
|
3203
|
+
SELECT v_tenant, v_doc.id, coalesce((d->>'index')::int, ord), d->>'number',
|
|
3204
|
+
nullif(d->>'due_date', '')::date, (d->>'value')::numeric,
|
|
3205
|
+
nullif(d->>'payment_type', ''), nullif(d->>'barcode', ''),
|
|
3206
|
+
nullif(d->>'pix_key', ''), nullif(d->>'payment_method_id', '')::uuid
|
|
3207
|
+
FROM jsonb_array_elements(p_duplicates) WITH ORDINALITY AS t(d, ord);
|
|
3208
|
+
END IF;
|
|
3209
|
+
|
|
3210
|
+
SELECT count(*) INTO v_total FROM public.plg_fiscalbr_inbox_items
|
|
3211
|
+
WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
|
|
3212
|
+
|
|
3213
|
+
UPDATE public.plg_fiscalbr_inbox_documents SET status = CASE
|
|
3214
|
+
WHEN (SELECT count(*) FROM public.plg_fiscalbr_inbox_items
|
|
3215
|
+
WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id AND reconciled) = v_total
|
|
3216
|
+
THEN 'accepted' ELSE 'partial' END
|
|
3217
|
+
WHERE id = v_doc.id;
|
|
3218
|
+
|
|
3219
|
+
RETURN jsonb_build_object('document_id', v_doc.id, 'saved', v_done, 'items', v_total,
|
|
3220
|
+
'reconciled', (SELECT count(*) FROM public.plg_fiscalbr_inbox_items
|
|
3221
|
+
WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id AND reconciled));
|
|
3222
|
+
END $function$;
|
|
3223
|
+
|
|
3224
|
+
REVOKE ALL ON FUNCTION public.fiscalbr_save_inbox_decision(uuid, jsonb, uuid, uuid, jsonb) FROM PUBLIC;
|
|
3225
|
+
GRANT EXECUTE ON FUNCTION public.fiscalbr_save_inbox_decision(uuid, jsonb, uuid, uuid, jsonb) TO authenticated;
|
|
3226
|
+
`;
|
|
3227
|
+
var MIGRATION_014_A_DESCRICAO_DA_NOTA_TEM_LIXO_NO_MEIO = `-- ---------------------------------------------------------------------------
|
|
3228
|
+
-- A descri\xE7\xE3o da nota tem lixo no meio, e um token de lixo matava a busca.
|
|
3229
|
+
-- ---------------------------------------------------------------------------
|
|
3230
|
+
-- Caso real, primeira nota de verdade: o item \xAB20X35 AR VACUO\xBB voltou SEM
|
|
3231
|
+
-- candidato, e o produto \`EMBALAGEM VACUO 20X35\` est\xE1 cadastrado.
|
|
3232
|
+
--
|
|
3233
|
+
-- \`public.global_search\` exige que TODOS os tokens apare\xE7am no haystack \u2014 \xE9 a
|
|
3234
|
+
-- regra certa para gente digitando (\xABjo\xE3o silva\xBB n\xE3o deve trazer todos os jo\xE3os)
|
|
3235
|
+
-- e \xE9 a regra errada para descri\xE7\xE3o de NF-e. Os tokens de \`20X35 AR VACUO\` s\xE3o
|
|
3236
|
+
-- \`20x35\`, \`ar\`, \`vacuo\`; o produto \xE9 \`embalagem vacuo 20x35\`. Dois casam. \`ar\`
|
|
3237
|
+
-- n\xE3o aparece em lugar nenhum, e o AND derruba a linha inteira.
|
|
3238
|
+
--
|
|
3239
|
+
-- A segunda fase (\`word_similarity\`) tamb\xE9m n\xE3o salva: ela compara a frase
|
|
3240
|
+
-- inteira contra o haystack, e com um ter\xE7o da frase sendo ru\xEDdo a semelhan\xE7a
|
|
3241
|
+
-- fica abaixo do limiar.
|
|
3242
|
+
--
|
|
3243
|
+
-- Descri\xE7\xE3o de nota fiscal \xE9 assim por natureza: abreviada, em ordem pr\xF3pria, com
|
|
3244
|
+
-- sigla de unidade, micragem e palavra solta no meio. Ent\xE3o o que entra na busca
|
|
3245
|
+
-- deixa de ser a frase e passa a ser O QUE NELA DISTINGUE:
|
|
3246
|
+
--
|
|
3247
|
+
-- \u2460 tokens de 3 letras ou mais. \xABar\xBB, \xABkg\xBB, \xABun\xBB, \xABc/\xBB, \xABp/\xBB saem. \xAB20x35\xBB
|
|
3248
|
+
-- e \xABvacuo\xBB ficam \u2014 e s\xE3o eles que identificam o produto.
|
|
3249
|
+
-- \u2461 se mesmo assim n\xE3o vier nada, UMA \xFAltima tentativa com o token mais longo
|
|
3250
|
+
-- sozinho. \xC9 o recuo que acha \xABembalagem a v\xE1cuo 15 x 25\xBB a partir de
|
|
3251
|
+
-- \xABVACUO\xBB, para a pessoa escolher entre poucos em vez de nenhum.
|
|
3252
|
+
--
|
|
3253
|
+
-- N\xE3o se mexe em \`global_search\`: l\xE1 a regra est\xE1 certa para quem ela serve.
|
|
3254
|
+
-- ---------------------------------------------------------------------------
|
|
3255
|
+
|
|
3256
|
+
CREATE OR REPLACE FUNCTION public.fiscalbr_match_product_candidates(
|
|
3257
|
+
p_queries jsonb,
|
|
3258
|
+
p_supplier_cnpj text DEFAULT NULL,
|
|
3259
|
+
p_per_query integer DEFAULT 8
|
|
3260
|
+
) RETURNS TABLE (
|
|
3261
|
+
ref text,
|
|
3262
|
+
product_id uuid,
|
|
3263
|
+
name text,
|
|
3264
|
+
sku text,
|
|
3265
|
+
unit text,
|
|
3266
|
+
cost numeric,
|
|
3267
|
+
score real,
|
|
3268
|
+
reason text
|
|
3269
|
+
)
|
|
3270
|
+
LANGUAGE plpgsql
|
|
3271
|
+
STABLE
|
|
3272
|
+
SET search_path TO 'public', 'extensions', 'pg_temp'
|
|
3273
|
+
AS $fn$
|
|
3274
|
+
DECLARE
|
|
3275
|
+
v_tenant uuid := (SELECT app.current_tenant_id());
|
|
3276
|
+
v_cnpj text := public.fayz_digits(coalesce(p_supplier_cnpj, ''));
|
|
3277
|
+
v_barcodes boolean := to_regclass('public.plg_inventory_product_barcodes') IS NOT NULL;
|
|
3278
|
+
v_map boolean := to_regclass('public.plg_fiscalbr_supplier_product_map') IS NOT NULL;
|
|
3279
|
+
v_search text;
|
|
3280
|
+
v_q record;
|
|
3281
|
+
v_found uuid;
|
|
3282
|
+
v_tokens text[];
|
|
3283
|
+
v_strong text[];
|
|
3284
|
+
v_busca text;
|
|
3285
|
+
v_recuo text;
|
|
3286
|
+
BEGIN
|
|
3287
|
+
IF v_tenant IS NULL THEN RETURN; END IF;
|
|
3288
|
+
IF p_queries IS NULL OR jsonb_typeof(p_queries) <> 'array' THEN RETURN; END IF;
|
|
3289
|
+
p_per_query := least(greatest(coalesce(p_per_query, 8), 1), 50);
|
|
3290
|
+
|
|
3291
|
+
v_search := CASE
|
|
3292
|
+
WHEN to_regprocedure('public.global_search(text,uuid,text[],integer,integer)') IS NOT NULL
|
|
3293
|
+
THEN 'public.global_search'
|
|
3294
|
+
WHEN to_regprocedure('public.fayz_global_search(text,uuid,text[],integer,integer)') IS NOT NULL
|
|
3295
|
+
THEN 'public.fayz_global_search'
|
|
3296
|
+
ELSE NULL
|
|
3297
|
+
END;
|
|
3298
|
+
|
|
3299
|
+
FOR v_q IN
|
|
3300
|
+
SELECT e->>'ref' AS ref,
|
|
3301
|
+
coalesce(e->>'text', '') AS text,
|
|
3302
|
+
public.fayz_digits(coalesce(e->>'ean', '')) AS ean,
|
|
3303
|
+
nullif(btrim(coalesce(e->>'code', '')), '') AS code
|
|
3304
|
+
FROM jsonb_array_elements(p_queries) AS e
|
|
3305
|
+
WHERE e->>'ref' IS NOT NULL
|
|
3306
|
+
LOOP
|
|
3307
|
+
v_found := NULL;
|
|
3308
|
+
|
|
3309
|
+
-- \u2460 O de-para do fornecedor. Aprendizado da casa, e ele ganha de tudo.
|
|
3310
|
+
IF v_map AND v_cnpj <> '' AND v_q.code IS NOT NULL THEN
|
|
3311
|
+
SELECT p.id INTO v_found
|
|
3312
|
+
FROM public.plg_fiscalbr_supplier_product_map m
|
|
3313
|
+
JOIN public.products p ON p.id = m.product_id
|
|
3314
|
+
WHERE m.supplier_cnpj = v_cnpj
|
|
3315
|
+
AND m.supplier_product_code = v_q.code
|
|
3316
|
+
AND p.is_active
|
|
3317
|
+
ORDER BY m.hit_count DESC NULLS LAST, m.last_used_at DESC NULLS LAST
|
|
3318
|
+
LIMIT 1;
|
|
3319
|
+
|
|
3320
|
+
IF v_found IS NOT NULL THEN
|
|
3321
|
+
RETURN QUERY
|
|
3322
|
+
SELECT v_q.ref, p.id, p.name, p.sku, p.unit, p.cost, 1.0::real, 'supplier_map'::text
|
|
3323
|
+
FROM public.products p WHERE p.id = v_found;
|
|
3324
|
+
CONTINUE;
|
|
3325
|
+
END IF;
|
|
3326
|
+
END IF;
|
|
3327
|
+
|
|
3328
|
+
-- \u2461 O c\xF3digo de barras. \xABSEM GTIN\xBB \xE9 o que a NF-e escreve quando n\xE3o h\xE1 \u2014 e
|
|
3329
|
+
-- \`fayz_digits\` o reduz a vazio, ent\xE3o o teste de comprimento j\xE1 o barra.
|
|
3330
|
+
IF length(v_q.ean) >= 8 THEN
|
|
3331
|
+
SELECT p.id INTO v_found
|
|
3332
|
+
FROM public.products p
|
|
3333
|
+
WHERE p.is_active
|
|
3334
|
+
AND (
|
|
3335
|
+
public.fayz_digits(coalesce(p.metadata->>'barcode', '')) = v_q.ean
|
|
3336
|
+
OR (v_barcodes AND EXISTS (
|
|
3337
|
+
SELECT 1 FROM public.plg_inventory_product_barcodes b
|
|
3338
|
+
WHERE b.product_id = p.id
|
|
3339
|
+
AND public.fayz_digits(coalesce(b.barcode, '')) = v_q.ean))
|
|
3340
|
+
)
|
|
3341
|
+
LIMIT 1;
|
|
3342
|
+
|
|
3343
|
+
IF v_found IS NOT NULL THEN
|
|
3344
|
+
RETURN QUERY
|
|
3345
|
+
SELECT v_q.ref, p.id, p.name, p.sku, p.unit, p.cost, 1.0::real, 'ean'::text
|
|
3346
|
+
FROM public.products p WHERE p.id = v_found;
|
|
3347
|
+
CONTINUE;
|
|
3348
|
+
END IF;
|
|
3349
|
+
END IF;
|
|
3350
|
+
|
|
3351
|
+
-- \u2462 Semelhan\xE7a de texto, com o que DISTINGUE em vez da frase inteira.
|
|
3352
|
+
CONTINUE WHEN v_search IS NULL;
|
|
3353
|
+
|
|
3354
|
+
v_tokens := array_remove(string_to_array(public.fayz_norm(v_q.text), ' '), '');
|
|
3355
|
+
SELECT array_agg(t ORDER BY length(t) DESC) INTO v_strong
|
|
3356
|
+
FROM unnest(v_tokens) t WHERE length(t) >= 3;
|
|
3357
|
+
|
|
3358
|
+
v_busca := array_to_string(coalesce(v_strong, v_tokens), ' ');
|
|
3359
|
+
-- O recuo \xE9 o token mais longo sozinho: numa descri\xE7\xE3o de nota ele \xE9 quase
|
|
3360
|
+
-- sempre o nome da coisa, e os outros s\xE3o medida, micragem ou conjun\xE7\xE3o.
|
|
3361
|
+
v_recuo := coalesce(v_strong[1], '');
|
|
3362
|
+
|
|
3363
|
+
CONTINUE WHEN length(v_busca) < 3;
|
|
3364
|
+
|
|
3365
|
+
RETURN QUERY EXECUTE format($q$
|
|
3366
|
+
WITH forte AS (
|
|
3367
|
+
SELECT g.record_id, g.score FROM %1$s($1, $2, ARRAY['product']::text[], $3, $3) g
|
|
3368
|
+
),
|
|
3369
|
+
-- S\xF3 quando a busca forte n\xE3o achou nada: um recuo que rodasse sempre
|
|
3370
|
+
-- encheria a lista de parecidos e empurraria o certo para fora do teto.
|
|
3371
|
+
fraco AS (
|
|
3372
|
+
SELECT g.record_id, g.score * 0.7 AS score
|
|
3373
|
+
FROM %1$s($4, $2, ARRAY['product']::text[], $3, $3) g
|
|
3374
|
+
WHERE $4 <> '' AND $4 <> $1 AND NOT EXISTS (SELECT 1 FROM forte)
|
|
3375
|
+
)
|
|
3376
|
+
SELECT $5::text, p.id, p.name, p.sku, p.unit, p.cost, g.score::real, 'text'::text
|
|
3377
|
+
FROM (SELECT * FROM forte UNION ALL SELECT * FROM fraco) g
|
|
3378
|
+
JOIN public.products p ON p.id = g.record_id::uuid
|
|
3379
|
+
WHERE p.is_active
|
|
3380
|
+
ORDER BY g.score DESC
|
|
3381
|
+
$q$, v_search)
|
|
3382
|
+
USING v_busca, v_tenant, p_per_query, v_recuo, v_q.ref;
|
|
3383
|
+
END LOOP;
|
|
3384
|
+
END
|
|
3385
|
+
$fn$;
|
|
3386
|
+
|
|
3387
|
+
GRANT EXECUTE ON FUNCTION public.fiscalbr_match_product_candidates(jsonb, text, integer) TO authenticated;
|
|
3388
|
+
|
|
3389
|
+
COMMENT ON FUNCTION public.fiscalbr_match_product_candidates(jsonb, text, integer) IS
|
|
3390
|
+
'Pr\xE9-sele\xE7\xE3o de produtos para a sugest\xE3o de IA da entrada de notas: de-para do fornecedor e EAN resolvem sozinhos (reason supplier_map/ean); o resto volta como candidatos de texto. A busca usa os tokens de 3+ letras da descri\xE7\xE3o, n\xE3o a frase \u2014 \xAB20X35 AR VACUO\xBB achava nada porque o AND de global_search exige o token \xABar\xBB, que n\xE3o existe em \xABembalagem vacuo 20x35\xBB.';
|
|
3391
|
+
`;
|
|
3392
|
+
var MIGRATION_015_O_MOVIMENTO_DIZ_QUE_VEIO_DA_NOTA = `-- ---------------------------------------------------------------------------
|
|
3393
|
+
-- O movimento de estoque diz, por escrito, que veio desta nota.
|
|
3394
|
+
-- ---------------------------------------------------------------------------
|
|
3395
|
+
-- A aceita\xE7\xE3o j\xE1 mandava a frase para \`inventory_receive\`, na chave \`notes\` do
|
|
3396
|
+
-- cabe\xE7alho \u2014 e ela nunca chegava: l\xE1 dentro o \`coalesce(v_ln.notes, \u2026)\` d\xE1 a vez
|
|
3397
|
+
-- \xE0 anota\xE7\xE3o DA LINHA, que aqui \xE9 sempre a descri\xE7\xE3o do item. A tela de
|
|
3398
|
+
-- Movimenta\xE7\xF5es ent\xE3o escrevia \xABSem motivo registrado\xBB sobre a entrada mais
|
|
3399
|
+
-- rastre\xE1vel que existe.
|
|
3400
|
+
--
|
|
3401
|
+
-- \`inventory/021\` abriu a chave \`reason\` no cabe\xE7alho. Esta migration passa a
|
|
3402
|
+
-- us\xE1-la, com a mesma frase que as linhas migradas do V1 carregam \u2014 para que a
|
|
3403
|
+
-- entrada de ontem e a de hoje se leiam igual no mesmo raz\xE3o.
|
|
3404
|
+
--
|
|
3405
|
+
-- S\xF3 a chamada muda. O resto do corpo \xE9 o da 009, palavra por palavra: esta
|
|
3406
|
+
-- fun\xE7\xE3o escreve estoque e t\xEDtulo na mesma transa\xE7\xE3o, e reescrev\xEA-la de mem\xF3ria
|
|
3407
|
+
-- \xE9 como se perde uma regra.
|
|
3408
|
+
-- ---------------------------------------------------------------------------
|
|
3409
|
+
|
|
3410
|
+
DO $m$
|
|
3411
|
+
DECLARE
|
|
3412
|
+
v_def text;
|
|
3413
|
+
v_antes text := E' ''ref'', ''NF-e '' || coalesce(v_doc.nfe_number, ''?''),\\n ''notes'', ''Entrada via DANFE NF-e '' || coalesce(v_doc.nfe_number, ''?'')),';
|
|
3414
|
+
v_depois text := E' ''ref'', ''NF-e '' || coalesce(v_doc.nfe_number, ''?''),\\n ''notes'', ''Entrada via DANFE NF-e '' || coalesce(v_doc.nfe_number, ''?''),\\n ''reason'', ''Entrada via DANFE NF-e '' || coalesce(v_doc.nfe_number, ''?'')),';
|
|
3415
|
+
BEGIN
|
|
3416
|
+
SELECT pg_get_functiondef(p.oid) INTO v_def
|
|
3417
|
+
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
3418
|
+
WHERE n.nspname = 'public' AND p.proname = 'fiscalbr_accept_inbox_document';
|
|
3419
|
+
|
|
3420
|
+
IF v_def IS NULL THEN
|
|
3421
|
+
RAISE NOTICE 'fiscalbr: accept ainda n\xE3o existe neste pool \u2014 009 roda antes e j\xE1 traz o reason';
|
|
3422
|
+
RETURN;
|
|
3423
|
+
END IF;
|
|
3424
|
+
|
|
3425
|
+
-- J\xE1 aplicada: sair sem tocar em nada \xE9 o que torna esta migration repet\xEDvel.
|
|
3426
|
+
IF position('''reason'', ''Entrada via DANFE' in v_def) > 0 THEN RETURN; END IF;
|
|
3427
|
+
|
|
3428
|
+
IF position(v_antes in v_def) = 0 THEN
|
|
3429
|
+
RAISE EXCEPTION 'fiscalbr/015: o corpo de fiscalbr_accept_inbox_document n\xE3o \xE9 o esperado \u2014 confira \xE0 m\xE3o em vez de deixar passar calado';
|
|
3430
|
+
END IF;
|
|
3431
|
+
|
|
3432
|
+
EXECUTE replace(v_def, v_antes, v_depois);
|
|
3433
|
+
END $m$;
|
|
3434
|
+
`;
|
|
3435
|
+
var MIGRATION_016_A_NOTA_GUARDA_O_PROPRIO_XML = `-- ---------------------------------------------------------------------------
|
|
3436
|
+
-- A nota guarda o pr\xF3prio XML, em vez de l\xEA-lo e jogar fora.
|
|
3437
|
+
-- ---------------------------------------------------------------------------
|
|
3438
|
+
-- O V1 abre o XML e o DANFE direto da lista de notas. A V2 mostrava os dois
|
|
3439
|
+
-- bot\xF5es desligados porque n\xE3o tinha o arquivo: das 889 notas deste cluster,
|
|
3440
|
+
-- ZERO t\xEAm \`document_id\`, zero t\xEAm bytes em \`documents\`, zero t\xEAm nada no
|
|
3441
|
+
-- metadata.
|
|
3442
|
+
--
|
|
3443
|
+
-- E n\xE3o \xE9 que o arquivo nunca tenha passado por aqui \u2014 ele passa INTEIRO pela
|
|
3444
|
+
-- m\xE3o do est\xE1gio: o navegador l\xEA o \`.xml\` que a pessoa arrastou, o parser extrai
|
|
3445
|
+
-- itens, parcelas e totais, e os bytes s\xE3o descartados no fim da fun\xE7\xE3o. A nota
|
|
3446
|
+
-- fiscal \xE9 documento com guarda legal de cinco anos, e a \xFAnica c\xF3pia ficava na
|
|
3447
|
+
-- pasta de downloads de quem importou.
|
|
3448
|
+
--
|
|
3449
|
+
-- \`xml_content text\` \xE9 o mesmo nome e o mesmo tipo do \`dfe_inbox.xml_content\` do
|
|
3450
|
+
-- V1 \u2014 a ponte da 003 j\xE1 citava essa coluna, e repetir o nome \xE9 o que faz uma
|
|
3451
|
+
-- futura migra\xE7\xE3o de dados ser um \`INSERT ... SELECT\` em vez de um de-para.
|
|
3452
|
+
--
|
|
3453
|
+
-- Guardar no banco e n\xE3o no bucket, de prop\xF3sito: s\xE3o 10\u201350 KB de texto por nota
|
|
3454
|
+
-- e s\xE3o o \xDANICO conte\xFAdo que torna a linha audit\xE1vel. Storage \xE9 outro sistema,
|
|
3455
|
+
-- com outra vida \xFAtil e outra pol\xEDtica de acesso, e um XML fiscal que vive em
|
|
3456
|
+
-- bucket \xE9 um XML que algu\xE9m apaga limpando bucket.
|
|
3457
|
+
--
|
|
3458
|
+
-- Isto N\xC3O preenche as 889 notas antigas: o que n\xE3o foi guardado n\xE3o se inventa.
|
|
3459
|
+
-- A tela desliga o bot\xE3o por nota, n\xE3o por feature.
|
|
3460
|
+
-- ---------------------------------------------------------------------------
|
|
3461
|
+
|
|
3462
|
+
ALTER TABLE public.plg_fiscalbr_inbox_documents
|
|
3463
|
+
ADD COLUMN IF NOT EXISTS xml_content text;
|
|
3464
|
+
|
|
3465
|
+
COMMENT ON COLUMN public.plg_fiscalbr_inbox_documents.xml_content IS
|
|
3466
|
+
'O XML autorizado da NF-e, como chegou. Nulo nas notas importadas antes desta coluna existir \u2014 o que n\xE3o foi guardado n\xE3o se inventa.';
|
|
3467
|
+
|
|
3468
|
+
-- \u2500\u2500 O est\xE1gio passa a guardar o que j\xE1 tinha em m\xE3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3469
|
+
-- Patch sobre o corpo que est\xE1 no cluster, e n\xE3o reescrita: esta fun\xE7\xE3o insere
|
|
3470
|
+
-- documento, itens, duplicatas e pagamentos numa transa\xE7\xE3o s\xF3, e reescrev\xEA-la de
|
|
3471
|
+
-- mem\xF3ria \xE9 como se perde uma regra.
|
|
3472
|
+
|
|
3473
|
+
DO $m$
|
|
3474
|
+
DECLARE
|
|
3475
|
+
v_def text;
|
|
3476
|
+
v_cols_antes text := E' total_freight, total_insurance, total_other, additional_info, metadata)';
|
|
3477
|
+
v_cols_depois text := E' total_freight, total_insurance, total_other, additional_info, metadata, xml_content)';
|
|
3478
|
+
v_vals_antes text := E' jsonb_build_object(''source'', coalesce(p_note->>''source'', ''xml_upload'')))';
|
|
3479
|
+
v_vals_depois text := E' jsonb_build_object(''source'', coalesce(p_note->>''source'', ''xml_upload'')),' || E'\\n' ||
|
|
3480
|
+
E' nullif(p_note->>''xml'', ''''))';
|
|
3481
|
+
v_upd_antes text := E' total_value = EXCLUDED.total_value,';
|
|
3482
|
+
v_upd_depois text := E' total_value = EXCLUDED.total_value,' || E'\\n' ||
|
|
3483
|
+
E' -- Reimportar com o arquivo preenche o que faltava; reimportar sem ele' || E'\\n' ||
|
|
3484
|
+
E' -- n\xE3o apaga o que j\xE1 estava guardado.' || E'\\n' ||
|
|
3485
|
+
E' xml_content = coalesce(EXCLUDED.xml_content, public.plg_fiscalbr_inbox_documents.xml_content),';
|
|
3486
|
+
BEGIN
|
|
3487
|
+
SELECT pg_get_functiondef(p.oid) INTO v_def
|
|
3488
|
+
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
3489
|
+
WHERE n.nspname = 'public' AND p.proname = 'fiscalbr_stage_inbox_document';
|
|
3490
|
+
|
|
3491
|
+
IF v_def IS NULL THEN
|
|
3492
|
+
RAISE NOTICE 'fiscalbr/016: o est\xE1gio ainda n\xE3o existe neste pool \u2014 a 009 roda antes';
|
|
3493
|
+
RETURN;
|
|
3494
|
+
END IF;
|
|
3495
|
+
|
|
3496
|
+
-- J\xE1 aplicada: sair sem tocar em nada \xE9 o que torna esta migration repet\xEDvel.
|
|
3497
|
+
IF position('xml_content' in v_def) > 0 THEN RETURN; END IF;
|
|
3498
|
+
|
|
3499
|
+
IF position(v_cols_antes in v_def) = 0
|
|
3500
|
+
OR position(v_vals_antes in v_def) = 0
|
|
3501
|
+
OR position(v_upd_antes in v_def) = 0 THEN
|
|
3502
|
+
RAISE EXCEPTION 'fiscalbr/016: o corpo de fiscalbr_stage_inbox_document n\xE3o \xE9 o esperado \u2014 confira \xE0 m\xE3o em vez de deixar passar calado';
|
|
3503
|
+
END IF;
|
|
3504
|
+
|
|
3505
|
+
v_def := replace(v_def, v_cols_antes, v_cols_depois);
|
|
3506
|
+
v_def := replace(v_def, v_vals_antes, v_vals_depois);
|
|
3507
|
+
v_def := replace(v_def, v_upd_antes, v_upd_depois);
|
|
3508
|
+
EXECUTE v_def;
|
|
3509
|
+
END $m$;
|
|
3510
|
+
`;
|
|
3511
|
+
var MIGRATION_017_QUEM_EMITE_E_O_CNPJ_NAO_A_LOJA = `-- Quem emite \xE9 o CNPJ, n\xE3o a loja.
|
|
3512
|
+
--
|
|
3513
|
+
-- As abas "Certificado" e "Emiss\xE3o Fiscal" do cadastro de empresa do V1
|
|
3514
|
+
-- guardam, em companies, como aquela pessoa jur\xEDdica emite NFS-e e NFC-e e se
|
|
3515
|
+
-- recebe DANFes da SEFAZ. No V2 a pessoa jur\xEDdica \xE9
|
|
3516
|
+
-- public.company_legal_entities (base), e a filial chega nela por
|
|
3517
|
+
-- app.units.company_legal_entity_id (168). Esta tabela \xE9 uma linha por pessoa
|
|
3518
|
+
-- jur\xEDdica:
|
|
3519
|
+
--
|
|
3520
|
+
-- unidade \u2192 app.units.company_legal_entity_id \u2192 company_legal_entities.id
|
|
3521
|
+
-- \u2192 plg_fiscalbr_emitter_settings.legal_entity_id (UNIQUE)
|
|
3522
|
+
--
|
|
3523
|
+
-- Duas filiais com o mesmo CNPJ dividem a mesma configura\xE7\xE3o, como no V1.
|
|
3524
|
+
--
|
|
3525
|
+
-- O certificado A1 N\xC3O mora aqui. O .pfx e a senha v\xE3o direto ao provedor
|
|
3526
|
+
-- (PlugNotas) e o que fica \xE9 o recibo: id no provedor, titular e validade. O
|
|
3527
|
+
-- CSC da NFC-e \xE9 segredo \u2014 o navegador grava, mas n\xE3o l\xEA de volta: a coluna
|
|
3528
|
+
-- fica fora do GRANT SELECT e a tela v\xEA s\xF3 \`nfce_csc_token_set\`.
|
|
3529
|
+
--
|
|
3530
|
+
-- Plano aprovado em docs/port/empresa.schema.json. Idempotente.
|
|
3531
|
+
|
|
3532
|
+
CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_emitter_settings (
|
|
3533
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3534
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3535
|
+
legal_entity_id uuid NOT NULL,
|
|
3536
|
+
|
|
3537
|
+
nfse_enabled boolean NOT NULL DEFAULT false,
|
|
3538
|
+
nfse_production boolean NOT NULL DEFAULT false,
|
|
3539
|
+
nfse_national boolean NOT NULL DEFAULT false,
|
|
3540
|
+
nfse_service_code_national text,
|
|
3541
|
+
nfse_service_code_municipal text,
|
|
3542
|
+
nfse_nbs_code text,
|
|
3543
|
+
nfse_cnae text,
|
|
3544
|
+
nfse_iss_rate numeric(5,2) CHECK (nfse_iss_rate IS NULL OR (nfse_iss_rate >= 0 AND nfse_iss_rate <= 100)),
|
|
3545
|
+
nfse_iss_taxation smallint,
|
|
3546
|
+
nfse_iss_exigibility smallint,
|
|
3547
|
+
nfse_ibscbs_cst text,
|
|
3548
|
+
nfse_ibscbs_classification text,
|
|
3549
|
+
nfse_simples_regime smallint,
|
|
3550
|
+
nfse_net_value boolean NOT NULL DEFAULT false,
|
|
3551
|
+
nfse_additional_info text,
|
|
3552
|
+
nfse_city_features jsonb,
|
|
3553
|
+
|
|
3554
|
+
nfce_enabled boolean NOT NULL DEFAULT false,
|
|
3555
|
+
nfce_production boolean NOT NULL DEFAULT false,
|
|
3556
|
+
nfce_series integer CHECK (nfce_series IS NULL OR nfce_series > 0),
|
|
3557
|
+
nfce_csc_id text,
|
|
3558
|
+
nfce_csc_token text,
|
|
3559
|
+
nfce_csc_token_set boolean GENERATED ALWAYS AS (nfce_csc_token IS NOT NULL AND nfce_csc_token <> '') STORED,
|
|
3560
|
+
nfce_generic_item jsonb,
|
|
3561
|
+
|
|
3562
|
+
nfe_enabled boolean NOT NULL DEFAULT false,
|
|
3563
|
+
|
|
3564
|
+
certificate_provider_id text,
|
|
3565
|
+
certificate_cn text,
|
|
3566
|
+
certificate_expires_at timestamptz,
|
|
3567
|
+
certificate_uploaded_at timestamptz,
|
|
3568
|
+
|
|
3569
|
+
dfe_sync_enabled boolean NOT NULL DEFAULT false,
|
|
3570
|
+
dfe_sync_cursor text,
|
|
3571
|
+
dfe_last_sync_at timestamptz,
|
|
3572
|
+
provider_synced_at timestamptz,
|
|
3573
|
+
|
|
3574
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3575
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
3576
|
+
UNIQUE (tenant_id, id)
|
|
3577
|
+
);
|
|
3578
|
+
|
|
3579
|
+
DO $$
|
|
3580
|
+
BEGIN
|
|
3581
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_emitter_settings_legal_entity_fkey') THEN
|
|
3582
|
+
-- company_legal_entities \xE9 da base, n\xE3o de outro plugin: a FK f\xEDsica \xE9
|
|
3583
|
+
-- permitida (a regra do 000 pro\xEDbe FK entre PLUGINS).
|
|
3584
|
+
ALTER TABLE ONLY public.plg_fiscalbr_emitter_settings
|
|
3585
|
+
ADD CONSTRAINT plg_fiscalbr_emitter_settings_legal_entity_fkey
|
|
3586
|
+
FOREIGN KEY (legal_entity_id) REFERENCES public.company_legal_entities(id) ON DELETE CASCADE;
|
|
3587
|
+
END IF;
|
|
3588
|
+
END $$;
|
|
3589
|
+
|
|
3590
|
+
CREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_emitter_settings_legal_entity_uidx
|
|
3591
|
+
ON public.plg_fiscalbr_emitter_settings USING btree (legal_entity_id);
|
|
3592
|
+
|
|
3593
|
+
COMMENT ON TABLE public.plg_fiscalbr_emitter_settings IS
|
|
3594
|
+
'V1: companies (abas Certificado + Emiss\xE3o Fiscal). Como uma pessoa jur\xEDdica emite NFS-e/NFC-e e recebe DANFes. Uma linha por CNPJ; a filial chega por app.units.company_legal_entity_id.';
|
|
3595
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.legal_entity_id IS 'A chave que conecta: company_legal_entities.id. UNIQUE.';
|
|
3596
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_production IS 'V1: companies.nfse_producao \u2014 false = homologa\xE7\xE3o.';
|
|
3597
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_national IS 'V1: companies.nfse_nacional \u2014 padr\xE3o nacional \xD7 municipal.';
|
|
3598
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_service_code_national IS 'V1: companies.nfse_codigo_tributacao_nacional';
|
|
3599
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_service_code_municipal IS 'V1: companies.nfse_codigo_tributacao_municipio';
|
|
3600
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_nbs_code IS 'V1: companies.nfse_codigo_nbs';
|
|
3601
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_cnae IS 'V1: companies.nfse_cnae \u2014 CNAE do servi\xE7o, pode diferir do principal.';
|
|
3602
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_iss_rate IS 'V1: companies.nfse_iss_aliquota (%)';
|
|
3603
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_iss_taxation IS 'V1: companies.nfse_iss_tipo_tributacao';
|
|
3604
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_iss_exigibility IS 'V1: companies.nfse_iss_exigibilidade';
|
|
3605
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_simples_regime IS 'V1: companies.nfse_regime_apuracao_sn \u2014 s\xF3 para optante do Simples.';
|
|
3606
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_net_value IS 'V1: companies.nfse_valor_liquido \u2014 emitir pelo valor l\xEDquido.';
|
|
3607
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_city_features IS 'V1: companies.nfse_city_features \u2014 o que a prefeitura aceita, devolvido pelo provedor.';
|
|
3608
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfce_csc_token IS 'V1: companies.nfce_csc_token \u2014 SEGREDO. Fora do GRANT SELECT de authenticated.';
|
|
3609
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfce_generic_item IS 'V1: companies.nfce_item_generico \u2014 {ncm, cfop, csosn}.';
|
|
3610
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.certificate_provider_id IS 'V1: companies.certificate_plugnotas_id \u2014 recibo; o .pfx n\xE3o fica no banco.';
|
|
3611
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.dfe_sync_enabled IS 'V1: companies.dfe_sync_enabled \u2014 receber DANFes automaticamente.';
|
|
3612
|
+
COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.dfe_sync_cursor IS 'V1: companies.dfe_sync_cursor \u2014 NSU onde a busca na SEFAZ parou.';
|
|
3613
|
+
|
|
3614
|
+
ALTER TABLE public.plg_fiscalbr_emitter_settings ENABLE ROW LEVEL SECURITY;
|
|
3615
|
+
ALTER TABLE public.plg_fiscalbr_emitter_settings FORCE ROW LEVEL SECURITY;
|
|
3616
|
+
|
|
3617
|
+
-- O segredo fica fora do SELECT: grava-se, n\xE3o se l\xEA.
|
|
3618
|
+
REVOKE ALL ON TABLE public.plg_fiscalbr_emitter_settings FROM anon, authenticated;
|
|
3619
|
+
GRANT INSERT, UPDATE, DELETE ON TABLE public.plg_fiscalbr_emitter_settings TO authenticated;
|
|
3620
|
+
GRANT SELECT (
|
|
3621
|
+
id, tenant_id, legal_entity_id,
|
|
3622
|
+
nfse_enabled, nfse_production, nfse_national, nfse_service_code_national, nfse_service_code_municipal,
|
|
3623
|
+
nfse_nbs_code, nfse_cnae, nfse_iss_rate, nfse_iss_taxation, nfse_iss_exigibility, nfse_ibscbs_cst,
|
|
3624
|
+
nfse_ibscbs_classification, nfse_simples_regime, nfse_net_value, nfse_additional_info, nfse_city_features,
|
|
3625
|
+
nfce_enabled, nfce_production, nfce_series, nfce_csc_id, nfce_csc_token_set, nfce_generic_item,
|
|
3626
|
+
nfe_enabled, certificate_provider_id, certificate_cn, certificate_expires_at, certificate_uploaded_at,
|
|
3627
|
+
dfe_sync_enabled, dfe_sync_cursor, dfe_last_sync_at, provider_synced_at, created_at, updated_at
|
|
3628
|
+
) ON public.plg_fiscalbr_emitter_settings TO authenticated;
|
|
3629
|
+
GRANT ALL ON TABLE public.plg_fiscalbr_emitter_settings TO service_role;
|
|
3630
|
+
|
|
3631
|
+
DROP POLICY IF EXISTS plg_fiscalbr_emitter_settings_authz_select ON public.plg_fiscalbr_emitter_settings;
|
|
3632
|
+
CREATE POLICY plg_fiscalbr_emitter_settings_authz_select ON public.plg_fiscalbr_emitter_settings
|
|
3633
|
+
FOR SELECT TO authenticated USING (
|
|
3634
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
3635
|
+
AND (SELECT app.has_permission('financial.read', NULL::uuid)));
|
|
3636
|
+
|
|
3637
|
+
DROP POLICY IF EXISTS plg_fiscalbr_emitter_settings_authz_write ON public.plg_fiscalbr_emitter_settings;
|
|
3638
|
+
CREATE POLICY plg_fiscalbr_emitter_settings_authz_write ON public.plg_fiscalbr_emitter_settings
|
|
3639
|
+
FOR ALL TO authenticated
|
|
3640
|
+
USING (
|
|
3641
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
3642
|
+
AND (SELECT app.has_permission('settings.manage', NULL::uuid)))
|
|
3643
|
+
WITH CHECK (
|
|
3644
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
3645
|
+
AND (SELECT app.has_permission('settings.manage', NULL::uuid))
|
|
3646
|
+
AND EXISTS (SELECT 1 FROM public.company_legal_entities le
|
|
3647
|
+
WHERE le.id = plg_fiscalbr_emitter_settings.legal_entity_id
|
|
3648
|
+
AND le.tenant_id = plg_fiscalbr_emitter_settings.tenant_id));
|
|
3649
|
+
`;
|
|
2835
3650
|
var MIGRATIONS = [
|
|
2836
3651
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
2837
3652
|
{ id: "001_preserve_terminal_results_and_recover_leases", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },
|
|
@@ -2843,7 +3658,14 @@ var MIGRATIONS = [
|
|
|
2843
3658
|
{ id: "007_a_rls_do_documento_passa_a_ser_legivel", sql: MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL },
|
|
2844
3659
|
{ id: "008_a_nota_recebida_guarda_a_decisao_de_entrada", sql: MIGRATION_008_A_NOTA_RECEBIDA_GUARDA_A_DECISAO_DE_ENTRADA },
|
|
2845
3660
|
{ id: "009_aceitar_a_nota_escreve_estoque_e_titulo_de_uma_vez", sql: MIGRATION_009_ACEITAR_A_NOTA_ESCREVE_ESTOQUE_E_TITULO_DE_UMA_VEZ },
|
|
2846
|
-
{ id: "010_a_data_de_emissao_e_instante_e_a_competencia_e_dia", sql: MIGRATION_010_A_DATA_DE_EMISSAO_E_INSTANTE_E_A_COMPETENCIA_E_DIA }
|
|
3661
|
+
{ id: "010_a_data_de_emissao_e_instante_e_a_competencia_e_dia", sql: MIGRATION_010_A_DATA_DE_EMISSAO_E_INSTANTE_E_A_COMPETENCIA_E_DIA },
|
|
3662
|
+
{ id: "011_o_imposto_do_produto_e_do_fiscal", sql: MIGRATION_011_O_IMPOSTO_DO_PRODUTO_E_DO_FISCAL },
|
|
3663
|
+
{ id: "012_quem_e_o_produto_se_pergunta_ao_indice", sql: MIGRATION_012_QUEM_E_O_PRODUTO_SE_PERGUNTA_AO_INDICE },
|
|
3664
|
+
{ id: "013_a_sugestao_sobrevive_a_fechar_a_janela", sql: MIGRATION_013_A_SUGESTAO_SOBREVIVE_A_FECHAR_A_JANELA },
|
|
3665
|
+
{ id: "014_a_descricao_da_nota_tem_lixo_no_meio", sql: MIGRATION_014_A_DESCRICAO_DA_NOTA_TEM_LIXO_NO_MEIO },
|
|
3666
|
+
{ id: "015_o_movimento_diz_que_veio_da_nota", sql: MIGRATION_015_O_MOVIMENTO_DIZ_QUE_VEIO_DA_NOTA },
|
|
3667
|
+
{ id: "016_a_nota_guarda_o_proprio_xml", sql: MIGRATION_016_A_NOTA_GUARDA_O_PROPRIO_XML },
|
|
3668
|
+
{ id: "017_quem_emite_e_o_cnpj_nao_a_loja", sql: MIGRATION_017_QUEM_EMITE_E_O_CNPJ_NAO_A_LOJA }
|
|
2847
3669
|
];
|
|
2848
3670
|
var taxGroupEntity = {
|
|
2849
3671
|
name: "Grupo fiscal",
|
|
@@ -3175,6 +3997,10 @@ function parseNfeXml(xml) {
|
|
|
3175
3997
|
}
|
|
3176
3998
|
function toStagePayload(note, options = {}) {
|
|
3177
3999
|
return {
|
|
4000
|
+
// O ARQUIVO VIAJA JUNTO. Ele já está na mão aqui — o parser acabou de lê-lo —
|
|
4001
|
+
// e sem isto os bytes morriam no fim da função: nota fiscal tem guarda legal
|
|
4002
|
+
// de cinco anos e a única cópia ficava na pasta de downloads de quem importou.
|
|
4003
|
+
...options.xml ? { xml: options.xml } : {},
|
|
3178
4004
|
access_key: note.accessKey,
|
|
3179
4005
|
nfe_number: note.nfeNumber,
|
|
3180
4006
|
nfe_series: note.nfeSeries,
|
|
@@ -3249,7 +4075,7 @@ function sb() {
|
|
|
3249
4075
|
var NOTE_COLUMNS = `
|
|
3250
4076
|
id, access_key, nfe_number, nfe_series, issue_date, received_at, status,
|
|
3251
4077
|
manifestation_status, manifested_at, manifestation_attempts,
|
|
3252
|
-
nature_operation, supplier_name, supplier_cnpj, supplier_party_id,
|
|
4078
|
+
nature_operation, supplier_name, supplier_cnpj, supplier_party_id, xml_content,
|
|
3253
4079
|
total_value, total_products, total_discount, total_freight,
|
|
3254
4080
|
invoice_id, inventory_operation_id, stocked_at, reviewed_at,
|
|
3255
4081
|
default_chart_account_id, default_location_id, unit_id, document_id, metadata,
|
|
@@ -3286,6 +4112,7 @@ function rowToNote(row) {
|
|
|
3286
4112
|
operationId: row.inventory_operation_id ?? null,
|
|
3287
4113
|
stockedAt: row.stocked_at ?? null,
|
|
3288
4114
|
documentId: row.document_id ?? null,
|
|
4115
|
+
hasXml: Boolean(row.xml_content),
|
|
3289
4116
|
unitId: row.unit_id ?? null
|
|
3290
4117
|
};
|
|
3291
4118
|
}
|
|
@@ -3312,10 +4139,11 @@ async function listInboxNotes(options = {}) {
|
|
|
3312
4139
|
var ITEM_COLUMNS = `
|
|
3313
4140
|
id, item_number, supplier_product_code, ean_barcode, description, ncm, cest, cfop,
|
|
3314
4141
|
unit_commercial, quantity_commercial, unit_price_commercial, total_price, discount,
|
|
4142
|
+
unit_taxable, quantity_taxable,
|
|
3315
4143
|
icms_cst, icms_csosn, icms_base, icms_rate, icms_value, pis_value, cofins_value, ipi_value,
|
|
3316
4144
|
product_id, entry_type, chart_account_id, stock_unit_id, stock_quantity,
|
|
3317
4145
|
conversion_factor, cost_mode, unit_cost_new, reconciled, stock_movement_id,
|
|
3318
|
-
shortage_quantity, shortage_reason, shortage_note
|
|
4146
|
+
shortage_quantity, shortage_reason, shortage_note, suggested_by, suggestion_confidence
|
|
3319
4147
|
`;
|
|
3320
4148
|
function rowToItem(row) {
|
|
3321
4149
|
const quantity = Number(row.quantity_commercial ?? 0);
|
|
@@ -3333,6 +4161,8 @@ function rowToItem(row) {
|
|
|
3333
4161
|
quantity,
|
|
3334
4162
|
unitPrice: Number(row.unit_price_commercial ?? 0),
|
|
3335
4163
|
total: Number(row.total_price ?? 0),
|
|
4164
|
+
unitTaxable: row.unit_taxable ?? null,
|
|
4165
|
+
quantityTaxable: row.quantity_taxable == null ? null : Number(row.quantity_taxable),
|
|
3336
4166
|
discount: row.discount == null ? null : Number(row.discount),
|
|
3337
4167
|
icmsCst: row.icms_cst ?? null,
|
|
3338
4168
|
icmsCsosn: row.icms_csosn ?? null,
|
|
@@ -3349,6 +4179,8 @@ function rowToItem(row) {
|
|
|
3349
4179
|
costMode: row.cost_mode ?? "novo",
|
|
3350
4180
|
unitCostNew: row.unit_cost_new == null ? null : Number(row.unit_cost_new),
|
|
3351
4181
|
reconciled: Boolean(row.reconciled),
|
|
4182
|
+
suggestedBy: row.suggested_by === "ai" || row.suggested_by === "ean" || row.suggested_by === "supplier_map" ? row.suggested_by : null,
|
|
4183
|
+
suggestionConfidence: row.suggestion_confidence == null ? null : Number(row.suggestion_confidence),
|
|
3352
4184
|
stockMovementId: row.stock_movement_id ?? null,
|
|
3353
4185
|
shortageQuantity: row.shortage_quantity == null ? null : Number(row.shortage_quantity),
|
|
3354
4186
|
shortageReason: row.shortage_reason ?? null,
|
|
@@ -3394,7 +4226,7 @@ async function getInboxNote(id) {
|
|
|
3394
4226
|
async function stageNoteFromXml(xml, options = {}) {
|
|
3395
4227
|
const parsed = parseNfeXml(xml);
|
|
3396
4228
|
const { data, error } = await sb().rpc("fiscalbr_stage_inbox_document", {
|
|
3397
|
-
p_note: toStagePayload(parsed, { unitId: options.unitId ?? null })
|
|
4229
|
+
p_note: toStagePayload(parsed, { unitId: options.unitId ?? null, xml })
|
|
3398
4230
|
});
|
|
3399
4231
|
if (error) throw error;
|
|
3400
4232
|
const res = data ?? {};
|
|
@@ -3419,6 +4251,8 @@ async function saveNoteDecision(input) {
|
|
|
3419
4251
|
stock_unit_id: i.stockUnitId ?? null,
|
|
3420
4252
|
conversion_factor: i.conversionFactor ?? 1,
|
|
3421
4253
|
cost_mode: i.costMode ?? "novo",
|
|
4254
|
+
suggested_by: i.suggestedBy ?? null,
|
|
4255
|
+
suggestion_confidence: i.suggestionConfidence ?? null,
|
|
3422
4256
|
shortage_quantity: i.shortageQuantity ?? null,
|
|
3423
4257
|
shortage_reason: i.shortageReason ?? null,
|
|
3424
4258
|
shortage_note: i.shortageNote ?? null
|
|
@@ -3482,6 +4316,57 @@ async function searchProducts(term, limit = 20) {
|
|
|
3482
4316
|
kind: p.kind ?? null
|
|
3483
4317
|
}));
|
|
3484
4318
|
}
|
|
4319
|
+
async function getInboxNoteXml(id) {
|
|
4320
|
+
const { data, error } = await sb().from("plg_fiscalbr_inbox_documents").select("xml_content").eq("id", id).maybeSingle();
|
|
4321
|
+
if (error) throw error;
|
|
4322
|
+
return data?.xml_content ?? null;
|
|
4323
|
+
}
|
|
4324
|
+
async function findInboxNoteByNumber(nfeNumber) {
|
|
4325
|
+
const n = (nfeNumber ?? "").replace(/\D/g, "");
|
|
4326
|
+
if (!n) return null;
|
|
4327
|
+
const { data, error } = await sb().from("plg_fiscalbr_inbox_documents").select("id").eq("nfe_number", n).limit(2);
|
|
4328
|
+
if (error) throw error;
|
|
4329
|
+
const linhas = data ?? [];
|
|
4330
|
+
return linhas.length === 1 ? linhas[0].id : null;
|
|
4331
|
+
}
|
|
4332
|
+
async function findInboxNoteByOperation(operationId) {
|
|
4333
|
+
if (!operationId) return null;
|
|
4334
|
+
const { data, error } = await sb().from("plg_fiscalbr_inbox_documents").select("id").eq("inventory_operation_id", operationId).limit(1).maybeSingle();
|
|
4335
|
+
if (error) throw error;
|
|
4336
|
+
return data?.id ?? null;
|
|
4337
|
+
}
|
|
4338
|
+
async function suggestNoteItems(documentId, reevaluate = false) {
|
|
4339
|
+
const { data, error } = await sb().functions.invoke("fiscalbr-suggest-note-items", {
|
|
4340
|
+
body: { documentId, reevaluate, tenantId: getActiveTenantId() ?? null }
|
|
4341
|
+
});
|
|
4342
|
+
if (error) {
|
|
4343
|
+
const corpo = await error.context?.json?.().catch(() => null);
|
|
4344
|
+
throw new Error(corpo?.error ?? error.message);
|
|
4345
|
+
}
|
|
4346
|
+
const res = data ?? {};
|
|
4347
|
+
if (res.ok === false) throw new Error(String(res.error ?? "A sugest\xE3o falhou."));
|
|
4348
|
+
return {
|
|
4349
|
+
applied: Number(res.applied ?? 0),
|
|
4350
|
+
typedOnly: Number(res.typedOnly ?? 0),
|
|
4351
|
+
bound: Number(res.bound ?? 0),
|
|
4352
|
+
boundByEan: Number(res.boundByEan ?? 0),
|
|
4353
|
+
newProductNames: Array.isArray(res.newProductNames) ? res.newProductNames.filter((n) => typeof n === "string") : [],
|
|
4354
|
+
warnings: Array.isArray(res.warnings) ? res.warnings.filter((w) => typeof w === "string") : []
|
|
4355
|
+
};
|
|
4356
|
+
}
|
|
4357
|
+
async function listProductsByIds(ids) {
|
|
4358
|
+
if (ids.length === 0) return [];
|
|
4359
|
+
const { data, error } = await sb().from("products").select("id, name, sku, unit, cost, kind").in("id", ids);
|
|
4360
|
+
if (error) throw error;
|
|
4361
|
+
return (data ?? []).map((p) => ({
|
|
4362
|
+
id: String(p.id),
|
|
4363
|
+
name: p.name ?? "",
|
|
4364
|
+
sku: p.sku ?? null,
|
|
4365
|
+
unit: p.unit ?? null,
|
|
4366
|
+
cost: p.cost == null ? null : Number(p.cost),
|
|
4367
|
+
kind: p.kind ?? null
|
|
4368
|
+
}));
|
|
4369
|
+
}
|
|
3485
4370
|
async function listMeasurementUnits() {
|
|
3486
4371
|
const { data, error } = await sb().from("plg_inventory_measurement_units").select("id, name, abbreviation, is_base").eq("is_active", true).order("name");
|
|
3487
4372
|
if (error) throw error;
|
|
@@ -3711,13 +4596,13 @@ function InboundNotesView({ onOpenNote, onOpenManifest }) {
|
|
|
3711
4596
|
] }) : /* @__PURE__ */ jsxs("div", { children: [
|
|
3712
4597
|
/* @__PURE__ */ jsx(Inbox, { className: "mx-auto h-8 w-8 text-muted-foreground", "aria-hidden": true }),
|
|
3713
4598
|
/* @__PURE__ */ jsx("p", { className: "mt-2 text-sm font-medium", children: "Nenhuma nota de entrada" }),
|
|
3714
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "As notas chegam da SEFAZ
|
|
4599
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "As notas chegam da SEFAZ pela fila de manifesta\xE7\xE3o, ou voc\xEA arrasta o XML aqui." })
|
|
3715
4600
|
] });
|
|
3716
4601
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
3717
4602
|
/* @__PURE__ */ jsxs(PageHeaderActions, { children: [
|
|
3718
4603
|
onOpenManifest && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onOpenManifest, children: [
|
|
3719
4604
|
/* @__PURE__ */ jsx(Inbox, { className: "h-4 w-4 mr-1.5" }),
|
|
3720
|
-
"
|
|
4605
|
+
"Fila da SEFAZ"
|
|
3721
4606
|
] }),
|
|
3722
4607
|
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => fileInput.current?.click(), disabled: busy, children: [
|
|
3723
4608
|
busy ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 mr-1.5 animate-spin" }) : /* @__PURE__ */ jsx(UploadCloud, { className: "h-4 w-4 mr-1.5" }),
|
|
@@ -3862,6 +4747,24 @@ function StockBadge({ stockedAt, status }) {
|
|
|
3862
4747
|
// src/types.ts
|
|
3863
4748
|
var STOCK_BEARING_ENTRY_TYPES = ["insumo", "revenda", "patrimonio"];
|
|
3864
4749
|
var isStockBearing = (type) => STOCK_BEARING_ENTRY_TYPES.includes(type);
|
|
4750
|
+
|
|
4751
|
+
// src/lib/note-suggestion.ts
|
|
4752
|
+
var PACKAGING_COUNT = {
|
|
4753
|
+
MI: 1e3,
|
|
4754
|
+
MIL: 1e3,
|
|
4755
|
+
MLH: 1e3,
|
|
4756
|
+
MILHEIRO: 1e3,
|
|
4757
|
+
"MI/1000": 1e3,
|
|
4758
|
+
DZ: 12,
|
|
4759
|
+
DUZIA: 12,
|
|
4760
|
+
"D\xDAZIA": 12,
|
|
4761
|
+
CENTO: 100,
|
|
4762
|
+
CENT: 100
|
|
4763
|
+
};
|
|
4764
|
+
var norm = (u) => (u ?? "").trim().toUpperCase().replace(/\.$/, "");
|
|
4765
|
+
function packagingCount(commercialUnit) {
|
|
4766
|
+
return PACKAGING_COUNT[norm(commercialUnit)] ?? null;
|
|
4767
|
+
}
|
|
3865
4768
|
var money2 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(v || 0);
|
|
3866
4769
|
var qty = (v) => new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 4 }).format(v || 0);
|
|
3867
4770
|
var day2 = (iso) => iso ? iso.slice(0, 10).split("-").reverse().join("/") : "\u2014";
|
|
@@ -3885,9 +4788,15 @@ var draftOf = (item) => ({
|
|
|
3885
4788
|
stockUnitId: item.stockUnitId,
|
|
3886
4789
|
conversionFactor: String(item.conversionFactor ?? 1),
|
|
3887
4790
|
costMode: item.costMode === "media" ? "media" : "novo",
|
|
3888
|
-
shortageQuantity: item.shortageQuantity == null ? "" : String(item.shortageQuantity)
|
|
4791
|
+
shortageQuantity: item.shortageQuantity == null ? "" : String(item.shortageQuantity),
|
|
4792
|
+
// Vem do SERVIDOR: é o que faz o selo sobreviver a recarregar a página.
|
|
4793
|
+
suggestedBy: item.suggestedBy,
|
|
4794
|
+
suggestionConfidence: item.suggestionConfidence
|
|
3889
4795
|
});
|
|
3890
|
-
function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
4796
|
+
function NoteReconcileView({ noteId, onBack, onOpenInvoice, chrome = "page" }) {
|
|
4797
|
+
const emModal = chrome === "modal";
|
|
4798
|
+
const sangria = emModal ? "-mx-6 px-6" : "-mx-4 px-4";
|
|
4799
|
+
const cabecalho = (titulo) => emModal ? null : /* @__PURE__ */ jsx(SubpageHeader, { title: titulo, onBack, parentLabel: "Manifesto de Notas" });
|
|
3891
4800
|
const [note, setNote] = useState(null);
|
|
3892
4801
|
const [loading, setLoading] = useState(true);
|
|
3893
4802
|
const [drafts, setDrafts] = useState({});
|
|
@@ -3904,6 +4813,9 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
3904
4813
|
const [registryWarning, setRegistryWarning] = useState(null);
|
|
3905
4814
|
const [error, setError] = useState(null);
|
|
3906
4815
|
const [accepted, setAccepted] = useState(null);
|
|
4816
|
+
const [baixandoXml, setBaixandoXml] = useState(false);
|
|
4817
|
+
const [suggesting, setSuggesting] = useState(false);
|
|
4818
|
+
const [suggestionNote, setSuggestionNote] = useState(null);
|
|
3907
4819
|
const load = useCallback(async () => {
|
|
3908
4820
|
setLoading(true);
|
|
3909
4821
|
try {
|
|
@@ -3929,9 +4841,13 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
3929
4841
|
setLocations(l);
|
|
3930
4842
|
setRegistryWarning(failed.length ? `N\xE3o consegui carregar ${failed.join(", ")}. Estes seletores v\xE3o aparecer vazios.` : null);
|
|
3931
4843
|
if (detail) {
|
|
3932
|
-
|
|
3933
|
-
const
|
|
3934
|
-
|
|
4844
|
+
const vinculados = [...new Set(detail.items.map((i) => i.productId).filter((id) => !!id))];
|
|
4845
|
+
const nomes = vinculados.length ? new Map((await listProductsByIds(vinculados).catch(() => [])).map((p) => [p.id, p.name])) : /* @__PURE__ */ new Map();
|
|
4846
|
+
setDrafts(Object.fromEntries(detail.items.map((i) => [
|
|
4847
|
+
i.id,
|
|
4848
|
+
{ ...draftOf(i), productLabel: i.productId && nomes.get(i.productId) || "" }
|
|
4849
|
+
])));
|
|
4850
|
+
setChecked(Object.fromEntries(detail.items.filter((i) => i.reconciled).map((i) => [i.id, true])));
|
|
3935
4851
|
const pending = detail.items.find((i) => !i.reconciled);
|
|
3936
4852
|
setOpenIds(pending ? [pending.id] : []);
|
|
3937
4853
|
setInstalments(detail.instalments.map((d) => ({
|
|
@@ -3955,7 +4871,14 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
3955
4871
|
useEffect(() => {
|
|
3956
4872
|
void load();
|
|
3957
4873
|
}, [load]);
|
|
3958
|
-
const patch = (id, change) => setDrafts((prev) => ({
|
|
4874
|
+
const patch = (id, change) => setDrafts((prev) => ({
|
|
4875
|
+
...prev,
|
|
4876
|
+
[id]: {
|
|
4877
|
+
...prev[id],
|
|
4878
|
+
..."suggestedBy" in change ? {} : { suggestedBy: null, suggestionConfidence: null },
|
|
4879
|
+
...change
|
|
4880
|
+
}
|
|
4881
|
+
}));
|
|
3959
4882
|
const accountOptions = useMemo(
|
|
3960
4883
|
() => accounts.map((a) => ({ id: a.id, label: `${a.code} \xB7 ${a.name}` })),
|
|
3961
4884
|
[accounts]
|
|
@@ -3968,10 +4891,10 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
3968
4891
|
() => locations.map((l) => ({ id: l.id, label: l.name })),
|
|
3969
4892
|
[locations]
|
|
3970
4893
|
);
|
|
3971
|
-
const
|
|
4894
|
+
const buildDecisions = useCallback((rascunhos) => {
|
|
3972
4895
|
if (!note) return [];
|
|
3973
4896
|
return note.items.map((item) => {
|
|
3974
|
-
const d =
|
|
4897
|
+
const d = rascunhos[item.id] ?? draftOf(item);
|
|
3975
4898
|
return {
|
|
3976
4899
|
id: item.id,
|
|
3977
4900
|
productId: isStockBearing(d.entryType) ? d.productId : null,
|
|
@@ -3981,16 +4904,21 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
3981
4904
|
stockUnitId: d.stockUnitId,
|
|
3982
4905
|
conversionFactor: Number(d.conversionFactor.replace(",", ".")) || 1,
|
|
3983
4906
|
costMode: d.costMode,
|
|
4907
|
+
suggestedBy: d.suggestedBy,
|
|
4908
|
+
suggestionConfidence: d.suggestionConfidence,
|
|
3984
4909
|
shortageQuantity: d.shortageQuantity === "" ? null : Number(d.shortageQuantity.replace(",", "."))
|
|
3985
4910
|
};
|
|
3986
4911
|
});
|
|
3987
|
-
}, [note,
|
|
4912
|
+
}, [note, defaultAccount]);
|
|
4913
|
+
const decisions = useMemo(() => buildDecisions(drafts), [buildDecisions, drafts]);
|
|
3988
4914
|
const stockLines = decisions.filter((d) => isStockBearing(d.entryType));
|
|
3989
4915
|
const missingProduct = stockLines.filter((d) => !d.productId).length;
|
|
3990
4916
|
const missingType = decisions.filter((d) => !d.entryType).length;
|
|
4917
|
+
const suspectFactor = (item, d) => packagingCount(item.unit) !== null && isStockBearing(d.entryType) && ((Number(d.conversionFactor.replace(",", ".")) || 1) === 1 || !d.stockUnitId);
|
|
4918
|
+
const unverifiedFactor = note ? note.items.filter((i) => suspectFactor(i, drafts[i.id] ?? draftOf(i))).length : 0;
|
|
3991
4919
|
const instalmentSum = instalments.reduce((acc, d) => acc + (Number(d.amount) || 0), 0);
|
|
3992
4920
|
const readOnly = Boolean(note?.stockedAt || note?.invoiceId);
|
|
3993
|
-
const isReady = (d) => Boolean(d.entryType) && (!isStockBearing(d.entryType) || Boolean(d.productId));
|
|
4921
|
+
const isReady = (item, d) => Boolean(d.entryType) && (!isStockBearing(d.entryType) || Boolean(d.productId)) && !suspectFactor(item, d);
|
|
3994
4922
|
const doneCount = note ? note.items.filter((i) => checked[i.id]).length : 0;
|
|
3995
4923
|
const toggleItem = (id) => setOpenIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
|
|
3996
4924
|
const confirmItem = (id) => {
|
|
@@ -4001,6 +4929,106 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
4001
4929
|
return next && !rest.includes(next.id) ? [...rest, next.id] : rest;
|
|
4002
4930
|
});
|
|
4003
4931
|
};
|
|
4932
|
+
const persistir = useCallback(async (rascunhos) => {
|
|
4933
|
+
if (!note) return;
|
|
4934
|
+
await saveNoteDecision({
|
|
4935
|
+
documentId: note.id,
|
|
4936
|
+
items: buildDecisions(rascunhos),
|
|
4937
|
+
defaultChartAccountId: defaultAccount,
|
|
4938
|
+
defaultLocationId: location,
|
|
4939
|
+
instalments
|
|
4940
|
+
});
|
|
4941
|
+
}, [note, buildDecisions, defaultAccount, location, instalments]);
|
|
4942
|
+
const pendingItems = useMemo(
|
|
4943
|
+
() => (note?.items ?? []).filter((item) => {
|
|
4944
|
+
const d = drafts[item.id] ?? draftOf(item);
|
|
4945
|
+
return !d.entryType || isStockBearing(d.entryType) && !d.productId;
|
|
4946
|
+
}),
|
|
4947
|
+
[note, drafts]
|
|
4948
|
+
);
|
|
4949
|
+
const machineItems = useMemo(
|
|
4950
|
+
() => (note?.items ?? []).filter((item) => (drafts[item.id] ?? draftOf(item)).suggestedBy !== null),
|
|
4951
|
+
[note, drafts]
|
|
4952
|
+
);
|
|
4953
|
+
const sugerir = useCallback(async (reavaliar2 = false) => {
|
|
4954
|
+
if (!note) return;
|
|
4955
|
+
setSuggesting(true);
|
|
4956
|
+
setSuggestionNote(null);
|
|
4957
|
+
setError(null);
|
|
4958
|
+
try {
|
|
4959
|
+
await persistir(drafts);
|
|
4960
|
+
const res = await suggestNoteItems(note.id, reavaliar2);
|
|
4961
|
+
await load();
|
|
4962
|
+
if (res.applied === 0 && res.bound === 0 && res.typedOnly === 0) {
|
|
4963
|
+
setSuggestionNote(res.warnings[0] ?? "N\xE3o achei nada para preencher nestes itens.");
|
|
4964
|
+
return;
|
|
4965
|
+
}
|
|
4966
|
+
const semIA = res.bound ? `Sem IA: ${res.boundByEan ? `${res.boundByEan} pelo c\xF3digo de barras` : ""}` + (res.boundByEan && res.bound - res.boundByEan ? " e " : "") + (res.bound - res.boundByEan ? `${res.bound - res.boundByEan} pelo cat\xE1logo deste fornecedor` : "") + "." : "";
|
|
4967
|
+
setSuggestionNote([
|
|
4968
|
+
semIA,
|
|
4969
|
+
res.applied ? `${res.applied} ${res.applied === 1 ? "item preenchido" : "itens preenchidos"} pela IA.` : "",
|
|
4970
|
+
res.typedOnly ? `${res.typedOnly} ${res.typedOnly === 1 ? "item ficou" : "itens ficaram"} sem produto no cat\xE1logo` + (res.newProductNames.length ? ` (${res.newProductNames.slice(0, 3).join(", ")})` : "") + " \u2014 escolha o produto ou cadastre antes de aceitar." : "",
|
|
4971
|
+
res.applied || res.bound ? "Salvo." : "",
|
|
4972
|
+
...res.warnings
|
|
4973
|
+
].filter(Boolean).join(" "));
|
|
4974
|
+
} catch (e) {
|
|
4975
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
4976
|
+
} finally {
|
|
4977
|
+
setSuggesting(false);
|
|
4978
|
+
}
|
|
4979
|
+
}, [note, drafts, persistir, load]);
|
|
4980
|
+
const reavaliar = pendingItems.length === 0 && machineItems.length > 0;
|
|
4981
|
+
const botaoIA = readOnly || pendingItems.length === 0 && !reavaliar ? null : /* @__PURE__ */ jsx(
|
|
4982
|
+
AiActionButton,
|
|
4983
|
+
{
|
|
4984
|
+
size: "sm",
|
|
4985
|
+
className: "shrink-0",
|
|
4986
|
+
tone: reavaliar ? "soft" : "solid",
|
|
4987
|
+
busy: suggesting,
|
|
4988
|
+
busyLabel: "Pensando\u2026",
|
|
4989
|
+
onClick: () => {
|
|
4990
|
+
void sugerir(reavaliar);
|
|
4991
|
+
},
|
|
4992
|
+
children: reavaliar ? "Rodar IA novamente" : "Sugerir com IA"
|
|
4993
|
+
}
|
|
4994
|
+
);
|
|
4995
|
+
const moldura = (node, titulo, rodape2) => emModal ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4996
|
+
/* @__PURE__ */ jsxs(ModalHeader, { className: "shrink-0 flex-row items-center justify-between gap-3 space-y-0 border-b px-6 py-3 pr-14 text-left sm:flex-row", children: [
|
|
4997
|
+
/* @__PURE__ */ jsx(ModalTitle, { className: "min-w-0 truncate", children: titulo }),
|
|
4998
|
+
botaoIA
|
|
4999
|
+
] }),
|
|
5000
|
+
/* @__PURE__ */ jsx(ModalBody, { className: "px-6 py-4", children: node }),
|
|
5001
|
+
rodape2 && /* @__PURE__ */ jsx(ModalFooter, { className: "shrink-0 flex-row items-center justify-between gap-3 space-x-0 border-t px-6 py-3 pr-16", children: rodape2 })
|
|
5002
|
+
] }) : (
|
|
5003
|
+
// Em tela cheia não há moldura onde pendurar o rodapé: ele volta para dentro
|
|
5004
|
+
// do conteúdo, grudado na base da janela enquanto a página rola.
|
|
5005
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5006
|
+
node,
|
|
5007
|
+
rodape2 && /* @__PURE__ */ jsx("div", { className: `sticky bottom-0 ${sangria} flex items-center justify-between gap-3 border-t bg-card/95 py-3 pr-16 backdrop-blur`, children: rodape2 })
|
|
5008
|
+
] })
|
|
5009
|
+
);
|
|
5010
|
+
const baixarXml = useCallback(async () => {
|
|
5011
|
+
if (!note) return;
|
|
5012
|
+
setBaixandoXml(true);
|
|
5013
|
+
setError(null);
|
|
5014
|
+
try {
|
|
5015
|
+
const xml = await getInboxNoteXml(note.id);
|
|
5016
|
+
if (!xml) {
|
|
5017
|
+
setError("O arquivo desta nota n\xE3o est\xE1 guardado.");
|
|
5018
|
+
return;
|
|
5019
|
+
}
|
|
5020
|
+
const url = URL.createObjectURL(new Blob([xml], { type: "application/xml" }));
|
|
5021
|
+
const a = document.createElement("a");
|
|
5022
|
+
a.href = url;
|
|
5023
|
+
a.download = `${note.accessKey || note.number || "nfe"}.xml`;
|
|
5024
|
+
a.click();
|
|
5025
|
+
URL.revokeObjectURL(url);
|
|
5026
|
+
} catch (e) {
|
|
5027
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
5028
|
+
} finally {
|
|
5029
|
+
setBaixandoXml(false);
|
|
5030
|
+
}
|
|
5031
|
+
}, [note]);
|
|
4004
5032
|
const save = async () => {
|
|
4005
5033
|
if (!note) return;
|
|
4006
5034
|
setSaving(true);
|
|
@@ -4041,8 +5069,8 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
4041
5069
|
}
|
|
4042
5070
|
};
|
|
4043
5071
|
if (loading) {
|
|
4044
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
4045
|
-
|
|
5072
|
+
return moldura(/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
5073
|
+
cabecalho("Concilia\xE7\xE3o"),
|
|
4046
5074
|
/* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
|
|
4047
5075
|
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" }),
|
|
4048
5076
|
/* @__PURE__ */ jsx("div", { className: "mt-3 grid gap-x-6 gap-y-3 sm:grid-cols-2 lg:grid-cols-3", children: [0, 1, 2, 3, 4, 5].map((i) => /* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -4066,26 +5094,64 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
4066
5094
|
] }),
|
|
4067
5095
|
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-20" })
|
|
4068
5096
|
] }, i)) })
|
|
4069
|
-
] });
|
|
5097
|
+
] }), "Concilia\xE7\xE3o");
|
|
4070
5098
|
}
|
|
4071
5099
|
if (!note) {
|
|
4072
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
4073
|
-
|
|
5100
|
+
return moldura(/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
5101
|
+
cabecalho("Concilia\xE7\xE3o"),
|
|
4074
5102
|
/* @__PURE__ */ jsx(Card, { className: "p-8 text-center text-sm", children: error ? /* @__PURE__ */ jsxs("span", { className: "text-destructive", children: [
|
|
4075
5103
|
"N\xE3o consegui carregar a nota: ",
|
|
4076
5104
|
error
|
|
4077
5105
|
] }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Nota n\xE3o encontrada." }) })
|
|
4078
|
-
] });
|
|
5106
|
+
] }), "Concilia\xE7\xE3o");
|
|
4079
5107
|
}
|
|
4080
|
-
|
|
4081
|
-
/* @__PURE__ */
|
|
4082
|
-
|
|
5108
|
+
const rodape = readOnly ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5109
|
+
/* @__PURE__ */ jsxs(
|
|
5110
|
+
Button,
|
|
4083
5111
|
{
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
5112
|
+
variant: "ghost",
|
|
5113
|
+
onClick: async () => {
|
|
5114
|
+
await rejectNote(note.id);
|
|
5115
|
+
onBack();
|
|
5116
|
+
},
|
|
5117
|
+
disabled: saving || accepting,
|
|
5118
|
+
children: [
|
|
5119
|
+
/* @__PURE__ */ jsx(X, { className: "mr-1.5 h-4 w-4" }),
|
|
5120
|
+
"Rejeitar"
|
|
5121
|
+
]
|
|
4087
5122
|
}
|
|
4088
5123
|
),
|
|
5124
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5125
|
+
(missingType > 0 || missingProduct > 0) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
5126
|
+
unverifiedFactor > 0 && `${unverifiedFactor} com fator por conferir`,
|
|
5127
|
+
unverifiedFactor > 0 && (missingType > 0 || missingProduct > 0) && " \xB7 ",
|
|
5128
|
+
missingType > 0 && `${missingType} sem tipo`,
|
|
5129
|
+
missingType > 0 && missingProduct > 0 && " \xB7 ",
|
|
5130
|
+
missingProduct > 0 && `${missingProduct} sem produto`
|
|
5131
|
+
] }),
|
|
5132
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => {
|
|
5133
|
+
void save();
|
|
5134
|
+
}, disabled: saving || accepting, children: [
|
|
5135
|
+
saving && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
|
|
5136
|
+
"Salvar concilia\xE7\xE3o"
|
|
5137
|
+
] }),
|
|
5138
|
+
/* @__PURE__ */ jsxs(
|
|
5139
|
+
Button,
|
|
5140
|
+
{
|
|
5141
|
+
onClick: () => {
|
|
5142
|
+
void accept();
|
|
5143
|
+
},
|
|
5144
|
+
disabled: accepting || saving || missingType > 0 || missingProduct > 0 || unverifiedFactor > 0,
|
|
5145
|
+
children: [
|
|
5146
|
+
accepting ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Check, { className: "mr-1.5 h-4 w-4" }),
|
|
5147
|
+
"Aceitar"
|
|
5148
|
+
]
|
|
5149
|
+
}
|
|
5150
|
+
)
|
|
5151
|
+
] })
|
|
5152
|
+
] });
|
|
5153
|
+
return moldura(/* @__PURE__ */ jsxs("div", { className: `space-y-4 ${emModal ? "" : "pb-24"}`, children: [
|
|
5154
|
+
cabecalho(`Concilia\xE7\xE3o \xB7 NF-e ${note.number}`),
|
|
4089
5155
|
accepted && /* @__PURE__ */ jsxs(Card, { className: "border-success bg-success-soft p-4", children: [
|
|
4090
5156
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-success-soft-foreground", children: accepted.alreadyAccepted ? "Esta nota j\xE1 havia sido aceita \u2014 nada foi lan\xE7ado de novo." : `Aceita: ${accepted.stockLines} ${accepted.stockLines === 1 ? "linha" : "linhas"} de estoque e um t\xEDtulo de ${money2(accepted.invoiceTotal)} em ${accepted.instalments} ${accepted.instalments === 1 ? "parcela" : "parcelas"}.` }),
|
|
4091
5157
|
accepted.invoiceId && onOpenInvoice && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "mt-2", onClick: () => onOpenInvoice(accepted.invoiceId), children: "Abrir a conta a pagar" })
|
|
@@ -4130,6 +5196,25 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
4130
5196
|
/* @__PURE__ */ jsx(RefreshCw, { className: "mr-1.5 h-4 w-4" }),
|
|
4131
5197
|
"Atualizar status"
|
|
4132
5198
|
] }),
|
|
5199
|
+
note.hasXml ? /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", disabled: baixandoXml, onClick: () => {
|
|
5200
|
+
void baixarXml();
|
|
5201
|
+
}, children: [
|
|
5202
|
+
baixandoXml ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(FileDown, { className: "mr-1.5 h-4 w-4" }),
|
|
5203
|
+
"Baixar XML"
|
|
5204
|
+
] }) : /* @__PURE__ */ jsx(
|
|
5205
|
+
EmBreve,
|
|
5206
|
+
{
|
|
5207
|
+
label: "Baixar XML",
|
|
5208
|
+
o_que: "O arquivo desta nota n\xE3o foi guardado \u2014 ela entrou antes de o est\xE1gio passar a gravar o XML. Reimportar o .xml preenche"
|
|
5209
|
+
}
|
|
5210
|
+
),
|
|
5211
|
+
/* @__PURE__ */ jsx(
|
|
5212
|
+
EmBreve,
|
|
5213
|
+
{
|
|
5214
|
+
label: "DANFE (PDF)",
|
|
5215
|
+
o_que: "O espelho impresso da nota. Tem layout fixado pela SEFAZ e vinha renderizado da PlugNotas no V1 \u2014 n\xE3o sai do XML sozinho"
|
|
5216
|
+
}
|
|
5217
|
+
),
|
|
4133
5218
|
/* @__PURE__ */ jsx(
|
|
4134
5219
|
EmBreve,
|
|
4135
5220
|
{
|
|
@@ -4218,6 +5303,19 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
4218
5303
|
onClick: () => setOpenIds(openIds.length ? [] : note.items.map((i) => i.id)),
|
|
4219
5304
|
children: openIds.length ? "Recolher todos" : "Abrir todos"
|
|
4220
5305
|
}
|
|
5306
|
+
),
|
|
5307
|
+
!emModal && botaoIA
|
|
5308
|
+
] }),
|
|
5309
|
+
suggestionNote && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 rounded-md border border-magic/30 bg-magic-soft px-3 py-2", children: [
|
|
5310
|
+
/* @__PURE__ */ jsx("p", { className: "flex-1 text-xs text-magic-soft-foreground", children: suggestionNote }),
|
|
5311
|
+
/* @__PURE__ */ jsx(
|
|
5312
|
+
"button",
|
|
5313
|
+
{
|
|
5314
|
+
type: "button",
|
|
5315
|
+
className: "shrink-0 text-xs text-magic-soft-foreground/70 hover:underline",
|
|
5316
|
+
onClick: () => setSuggestionNote(null),
|
|
5317
|
+
children: "Fechar"
|
|
5318
|
+
}
|
|
4221
5319
|
)
|
|
4222
5320
|
] }),
|
|
4223
5321
|
note.items.map((item) => {
|
|
@@ -4229,7 +5327,7 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
4229
5327
|
const bearing = isStockBearing(d.entryType);
|
|
4230
5328
|
const open = openIds.includes(item.id);
|
|
4231
5329
|
const done = Boolean(checked[item.id]);
|
|
4232
|
-
const ready = isReady(d);
|
|
5330
|
+
const ready = isReady(item, d);
|
|
4233
5331
|
const typeLabel = ENTRY_TYPES.find((t) => t.value === d.entryType)?.label;
|
|
4234
5332
|
return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
|
|
4235
5333
|
/* @__PURE__ */ jsxs(
|
|
@@ -4260,7 +5358,8 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
4260
5358
|
] })
|
|
4261
5359
|
] }),
|
|
4262
5360
|
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums", children: money2(item.total) }),
|
|
4263
|
-
/* @__PURE__ */ jsx(
|
|
5361
|
+
d.suggestedBy && !done && /* @__PURE__ */ jsx(AuthorBadge, { draft: d }),
|
|
5362
|
+
/* @__PURE__ */ jsx(ItemState, { done: done && ready, ready })
|
|
4264
5363
|
]
|
|
4265
5364
|
}
|
|
4266
5365
|
),
|
|
@@ -4363,6 +5462,28 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
4363
5462
|
)
|
|
4364
5463
|
] })
|
|
4365
5464
|
] }),
|
|
5465
|
+
suspectFactor(item, d) && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-warning bg-warning-soft px-3 py-2", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-warning-soft-foreground", children: [
|
|
5466
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "mr-1 inline h-3.5 w-3.5", "aria-hidden": true }),
|
|
5467
|
+
"Este item vem em ",
|
|
5468
|
+
/* @__PURE__ */ jsx("strong", { children: item.unit }),
|
|
5469
|
+
", que s\xE3o",
|
|
5470
|
+
" ",
|
|
5471
|
+
qty(packagingCount(item.unit) ?? 0),
|
|
5472
|
+
" por embalagem.",
|
|
5473
|
+
" ",
|
|
5474
|
+
!d.stockUnitId ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5475
|
+
"Escolha a ",
|
|
5476
|
+
/* @__PURE__ */ jsx("strong", { children: "unidade de estoque" }),
|
|
5477
|
+
" para confirmar o fator \u2014 o produto est\xE1 cadastrado sem unidade."
|
|
5478
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5479
|
+
"O fator ainda \xE9 1: do jeito que est\xE1 entrariam ",
|
|
5480
|
+
qty(item.quantity),
|
|
5481
|
+
" ",
|
|
5482
|
+
"em vez de ",
|
|
5483
|
+
qty(item.quantity * (packagingCount(item.unit) ?? 1)),
|
|
5484
|
+
"."
|
|
5485
|
+
] })
|
|
5486
|
+
] }) }),
|
|
4366
5487
|
/* @__PURE__ */ jsxs("div", { className: "rounded-md bg-muted/50 px-3 py-2", children: [
|
|
4367
5488
|
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [
|
|
4368
5489
|
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Qtd estoque (calc.)" }),
|
|
@@ -4583,51 +5704,18 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
|
|
|
4583
5704
|
)
|
|
4584
5705
|
] })
|
|
4585
5706
|
] }),
|
|
4586
|
-
error && /* @__PURE__ */ jsx(Card, { className: "border-destructive bg-destructive/5 p-4", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }) })
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
/* @__PURE__ */ jsx(X, { className: "mr-1.5 h-4 w-4" }),
|
|
4599
|
-
"Rejeitar"
|
|
4600
|
-
]
|
|
4601
|
-
}
|
|
4602
|
-
),
|
|
4603
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4604
|
-
(missingType > 0 || missingProduct > 0) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
4605
|
-
missingType > 0 && `${missingType} sem tipo`,
|
|
4606
|
-
missingType > 0 && missingProduct > 0 && " \xB7 ",
|
|
4607
|
-
missingProduct > 0 && `${missingProduct} sem produto`
|
|
4608
|
-
] }),
|
|
4609
|
-
/* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => {
|
|
4610
|
-
void save();
|
|
4611
|
-
}, disabled: saving || accepting, children: [
|
|
4612
|
-
saving && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
|
|
4613
|
-
"Salvar concilia\xE7\xE3o"
|
|
4614
|
-
] }),
|
|
4615
|
-
/* @__PURE__ */ jsxs(
|
|
4616
|
-
Button,
|
|
4617
|
-
{
|
|
4618
|
-
onClick: () => {
|
|
4619
|
-
void accept();
|
|
4620
|
-
},
|
|
4621
|
-
disabled: accepting || saving || missingType > 0 || missingProduct > 0,
|
|
4622
|
-
children: [
|
|
4623
|
-
accepting ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Check, { className: "mr-1.5 h-4 w-4" }),
|
|
4624
|
-
"Aceitar"
|
|
4625
|
-
]
|
|
4626
|
-
}
|
|
4627
|
-
)
|
|
4628
|
-
] })
|
|
4629
|
-
] })
|
|
4630
|
-
] });
|
|
5707
|
+
error && /* @__PURE__ */ jsx(Card, { className: "border-destructive bg-destructive/5 p-4", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }) })
|
|
5708
|
+
] }), `Concilia\xE7\xE3o \xB7 NF-e ${note.number}`, rodape);
|
|
5709
|
+
}
|
|
5710
|
+
function AuthorBadge({ draft }) {
|
|
5711
|
+
if (draft.suggestedBy === "ai") {
|
|
5712
|
+
const confira = draft.suggestionConfidence != null && draft.suggestionConfidence < 0.6;
|
|
5713
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center rounded-full bg-magic-soft px-2 py-0.5 text-[11px] font-medium text-magic-soft-foreground", children: [
|
|
5714
|
+
"IA",
|
|
5715
|
+
confira ? " \xB7 confira" : ""
|
|
5716
|
+
] });
|
|
5717
|
+
}
|
|
5718
|
+
return /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0 items-center rounded-full bg-info-soft px-2 py-0.5 text-[11px] font-medium text-info-soft-foreground", children: draft.suggestedBy === "ean" ? "EAN" : "fornecedor" });
|
|
4631
5719
|
}
|
|
4632
5720
|
function ItemState({ done, ready }) {
|
|
4633
5721
|
if (done) {
|
|
@@ -4651,7 +5739,7 @@ var MANIFEST_TONE = {
|
|
|
4651
5739
|
processando: "bg-info-soft text-info-soft-foreground",
|
|
4652
5740
|
erro: "bg-destructive/10 text-destructive"
|
|
4653
5741
|
};
|
|
4654
|
-
function ManifestView({ onOpenNote }) {
|
|
5742
|
+
function ManifestView({ onOpenNote, onBack }) {
|
|
4655
5743
|
const [notes, setNotes] = useState([]);
|
|
4656
5744
|
const [loading, setLoading] = useState(true);
|
|
4657
5745
|
const [tab, setTab] = useState("pendentes");
|
|
@@ -4687,6 +5775,7 @@ function ManifestView({ onOpenNote }) {
|
|
|
4687
5775
|
]
|
|
4688
5776
|
}], [tab]);
|
|
4689
5777
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
5778
|
+
onBack && /* @__PURE__ */ jsx(SubpageHeader, { title: "Fila da SEFAZ", onBack, parentLabel: "Manifesto de Notas" }),
|
|
4690
5779
|
/* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
|
|
4691
5780
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
4692
5781
|
/* @__PURE__ */ jsx(ShieldCheck, { className: "mt-0.5 h-5 w-5 text-muted-foreground", "aria-hidden": true }),
|
|
@@ -4755,13 +5844,17 @@ var pathOf = (view) => view.kind === "note" ? `${HASH_PREFIX}/note/${view.id}` :
|
|
|
4755
5844
|
var STACK_MARK = { inboundNotes: true };
|
|
4756
5845
|
function InboundNotesPage({ payableRoute } = {}) {
|
|
4757
5846
|
const [view, setView] = useState(viewFromLocation);
|
|
5847
|
+
const [background, setBackground] = useState(
|
|
5848
|
+
() => viewFromLocation().kind === "manifest" ? "manifest" : "list"
|
|
5849
|
+
);
|
|
4758
5850
|
const go = useCallback((next) => {
|
|
4759
5851
|
setView(next);
|
|
5852
|
+
if (next.kind !== "note") setBackground(next.kind);
|
|
4760
5853
|
if (typeof window === "undefined") return;
|
|
4761
5854
|
const path = pathOf(next);
|
|
4762
5855
|
if (window.location.hash === path) return;
|
|
4763
5856
|
const empilhado = window.history.state?.inboundNotes;
|
|
4764
|
-
if (next.kind
|
|
5857
|
+
if (next.kind !== "note" && empilhado) {
|
|
4765
5858
|
window.history.back();
|
|
4766
5859
|
return;
|
|
4767
5860
|
}
|
|
@@ -4770,7 +5863,11 @@ function InboundNotesPage({ payableRoute } = {}) {
|
|
|
4770
5863
|
}, []);
|
|
4771
5864
|
useEffect(() => {
|
|
4772
5865
|
if (typeof window === "undefined") return;
|
|
4773
|
-
const onPop = () =>
|
|
5866
|
+
const onPop = () => {
|
|
5867
|
+
const next = viewFromLocation();
|
|
5868
|
+
setView(next);
|
|
5869
|
+
if (next.kind !== "note") setBackground(next.kind);
|
|
5870
|
+
};
|
|
4774
5871
|
window.addEventListener("popstate", onPop);
|
|
4775
5872
|
window.addEventListener("hashchange", onPop);
|
|
4776
5873
|
return () => {
|
|
@@ -4778,34 +5875,436 @@ function InboundNotesPage({ payableRoute } = {}) {
|
|
|
4778
5875
|
window.removeEventListener("hashchange", onPop);
|
|
4779
5876
|
};
|
|
4780
5877
|
}, []);
|
|
4781
|
-
|
|
4782
|
-
|
|
5878
|
+
const fecharNota = useCallback(() => go({ kind: background }), [go, background]);
|
|
5879
|
+
useEffect(() => {
|
|
5880
|
+
if (typeof window === "undefined") return;
|
|
5881
|
+
const op = window.location.hash.match(/[?&]operation=([0-9a-f-]{36})/i)?.[1];
|
|
5882
|
+
if (!op) return;
|
|
5883
|
+
let vivo = true;
|
|
5884
|
+
findInboxNoteByOperation(op).then((id) => {
|
|
5885
|
+
if (vivo && id) go({ kind: "note", id });
|
|
5886
|
+
}).catch(() => {
|
|
5887
|
+
});
|
|
5888
|
+
return () => {
|
|
5889
|
+
vivo = false;
|
|
5890
|
+
};
|
|
5891
|
+
}, [go]);
|
|
5892
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5893
|
+
background === "manifest" ? /* @__PURE__ */ jsx(ManifestView, { onOpenNote: (id) => go({ kind: "note", id }), onBack: () => go({ kind: "list" }) }) : /* @__PURE__ */ jsx(
|
|
5894
|
+
InboundNotesView,
|
|
5895
|
+
{
|
|
5896
|
+
onOpenNote: (id) => go({ kind: "note", id }),
|
|
5897
|
+
onOpenManifest: () => go({ kind: "manifest" })
|
|
5898
|
+
}
|
|
5899
|
+
),
|
|
5900
|
+
/* @__PURE__ */ jsx(Modal, { open: view.kind === "note", onOpenChange: (open) => {
|
|
5901
|
+
if (!open) fecharNota();
|
|
5902
|
+
}, children: view.kind === "note" && // `noPadding`: a conciliação desenha o próprio cabeçalho e o próprio
|
|
5903
|
+
// corpo — o título e o «Sugerir com IA» ficam fora da área que rola, e o
|
|
5904
|
+
// rodapé «Aceitar» precisa encostar nas bordas do modal.
|
|
5905
|
+
/* @__PURE__ */ jsx(ModalContent, { size: "5xl", noPadding: true, className: "sm:max-h-[90vh]", children: /* @__PURE__ */ jsx(
|
|
4783
5906
|
NoteReconcileView,
|
|
4784
5907
|
{
|
|
5908
|
+
chrome: "modal",
|
|
4785
5909
|
noteId: view.id,
|
|
4786
|
-
onBack:
|
|
5910
|
+
onBack: fecharNota,
|
|
4787
5911
|
onOpenInvoice: payableRoute ? (invoiceId) => {
|
|
4788
5912
|
window.location.hash = payableRoute(invoiceId);
|
|
4789
5913
|
} : void 0
|
|
4790
5914
|
}
|
|
4791
|
-
)
|
|
4792
|
-
}
|
|
4793
|
-
if (view.kind === "manifest") {
|
|
4794
|
-
return /* @__PURE__ */ jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsx(ManifestView, { onOpenNote: (id) => go({ kind: "note", id }) }) });
|
|
4795
|
-
}
|
|
4796
|
-
return /* @__PURE__ */ jsx(
|
|
4797
|
-
InboundNotesView,
|
|
4798
|
-
{
|
|
4799
|
-
onOpenNote: (id) => go({ kind: "note", id }),
|
|
4800
|
-
onOpenManifest: () => go({ kind: "manifest" })
|
|
4801
|
-
}
|
|
4802
|
-
);
|
|
5915
|
+
) }) })
|
|
5916
|
+
] });
|
|
4803
5917
|
}
|
|
4804
5918
|
function makeInboundNotesRoute(options = {}) {
|
|
4805
5919
|
return function InboundNotesRoute() {
|
|
4806
5920
|
return /* @__PURE__ */ jsx(InboundNotesPage, { ...options });
|
|
4807
5921
|
};
|
|
4808
5922
|
}
|
|
5923
|
+
var COLUMNS = [
|
|
5924
|
+
"id",
|
|
5925
|
+
"legal_entity_id",
|
|
5926
|
+
"nfse_enabled",
|
|
5927
|
+
"nfse_production",
|
|
5928
|
+
"nfse_national",
|
|
5929
|
+
"nfse_service_code_national",
|
|
5930
|
+
"nfse_service_code_municipal",
|
|
5931
|
+
"nfse_nbs_code",
|
|
5932
|
+
"nfse_cnae",
|
|
5933
|
+
"nfse_iss_rate",
|
|
5934
|
+
"nfse_iss_taxation",
|
|
5935
|
+
"nfse_ibscbs_cst",
|
|
5936
|
+
"nfse_ibscbs_classification",
|
|
5937
|
+
"nfse_simples_regime",
|
|
5938
|
+
"nfse_net_value",
|
|
5939
|
+
"nfse_additional_info",
|
|
5940
|
+
"nfce_enabled",
|
|
5941
|
+
"nfce_production",
|
|
5942
|
+
"nfce_series",
|
|
5943
|
+
"nfce_csc_id",
|
|
5944
|
+
"nfce_csc_token_set",
|
|
5945
|
+
"nfce_generic_item",
|
|
5946
|
+
"certificate_cn",
|
|
5947
|
+
"certificate_expires_at",
|
|
5948
|
+
"certificate_uploaded_at",
|
|
5949
|
+
"dfe_sync_enabled",
|
|
5950
|
+
"dfe_last_sync_at",
|
|
5951
|
+
"provider_synced_at"
|
|
5952
|
+
].join(", ");
|
|
5953
|
+
var sb2 = () => getSupabaseClientOptional();
|
|
5954
|
+
function useEmitterSettings(legalEntityId) {
|
|
5955
|
+
const [row, setRow] = React5.useState(null);
|
|
5956
|
+
const [loading, setLoading] = React5.useState(true);
|
|
5957
|
+
React5.useEffect(() => {
|
|
5958
|
+
if (!legalEntityId) {
|
|
5959
|
+
setLoading(false);
|
|
5960
|
+
return;
|
|
5961
|
+
}
|
|
5962
|
+
let alive = true;
|
|
5963
|
+
void sb2()?.from("plg_fiscalbr_emitter_settings").select(COLUMNS).eq("legal_entity_id", legalEntityId).maybeSingle().then(({ data, error }) => {
|
|
5964
|
+
if (!alive) return;
|
|
5965
|
+
if (error) toast.error("N\xE3o foi poss\xEDvel ler a configura\xE7\xE3o fiscal", { description: error.message });
|
|
5966
|
+
setRow(data ?? {});
|
|
5967
|
+
setLoading(false);
|
|
5968
|
+
});
|
|
5969
|
+
return () => {
|
|
5970
|
+
alive = false;
|
|
5971
|
+
};
|
|
5972
|
+
}, [legalEntityId]);
|
|
5973
|
+
const save = React5.useCallback(async (tenantId, patch) => {
|
|
5974
|
+
if (!legalEntityId) return false;
|
|
5975
|
+
const payload = { ...patch, tenant_id: tenantId, legal_entity_id: legalEntityId, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5976
|
+
const { data, error } = await sb2().from("plg_fiscalbr_emitter_settings").upsert(payload, { onConflict: "legal_entity_id" }).select(COLUMNS).single();
|
|
5977
|
+
if (error) {
|
|
5978
|
+
toast.error("N\xE3o foi poss\xEDvel salvar", { description: error.message });
|
|
5979
|
+
return false;
|
|
5980
|
+
}
|
|
5981
|
+
setRow(data);
|
|
5982
|
+
return true;
|
|
5983
|
+
}, [legalEntityId]);
|
|
5984
|
+
return { row, loading, save };
|
|
5985
|
+
}
|
|
5986
|
+
function NeedsLegalEntity({ item, what }) {
|
|
5987
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col items-start gap-3 py-6", children: [
|
|
5988
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Falta o CNPJ desta unidade" }),
|
|
5989
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
5990
|
+
what,
|
|
5991
|
+
" pertence \xE0 pessoa jur\xEDdica. Preencha a raz\xE3o social e o CNPJ em Dados da empresa para configurar aqui."
|
|
5992
|
+
] }),
|
|
5993
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => adminNavigateTo(`/settings/locations/${item.id}/edit`), children: "Preencher CNPJ" })
|
|
5994
|
+
] }) });
|
|
5995
|
+
}
|
|
5996
|
+
function LoadingCard() {
|
|
5997
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "space-y-3 py-6", children: [
|
|
5998
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-48" }),
|
|
5999
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }),
|
|
6000
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" })
|
|
6001
|
+
] }) });
|
|
6002
|
+
}
|
|
6003
|
+
function ToggleRow({ title, description, checked, onChange, disabled, children }) {
|
|
6004
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4", children: [
|
|
6005
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
6006
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: title }),
|
|
6007
|
+
description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: description })
|
|
6008
|
+
] }),
|
|
6009
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
6010
|
+
children,
|
|
6011
|
+
/* @__PURE__ */ jsx(Switch, { checked, disabled, onCheckedChange: onChange })
|
|
6012
|
+
] })
|
|
6013
|
+
] });
|
|
6014
|
+
}
|
|
6015
|
+
function Field2({ label, hint, children }) {
|
|
6016
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
6017
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: label }),
|
|
6018
|
+
children,
|
|
6019
|
+
hint && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: hint })
|
|
6020
|
+
] });
|
|
6021
|
+
}
|
|
6022
|
+
function SummaryCard({ title, lines, badge, onConfigure }) {
|
|
6023
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardHeader, { className: "flex-row items-start justify-between gap-3 space-y-0", children: [
|
|
6024
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
6025
|
+
/* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: title }),
|
|
6026
|
+
/* @__PURE__ */ jsx("dl", { className: "mt-2 grid gap-x-6 gap-y-1 text-sm", style: { gridTemplateColumns: "auto 1fr" }, children: lines.map((l) => /* @__PURE__ */ jsxs(React5.Fragment, { children: [
|
|
6027
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: l.label }),
|
|
6028
|
+
/* @__PURE__ */ jsx("dd", { className: "text-foreground", children: l.value })
|
|
6029
|
+
] }, l.label)) })
|
|
6030
|
+
] }),
|
|
6031
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
6032
|
+
badge && /* @__PURE__ */ jsx(
|
|
6033
|
+
Badge,
|
|
6034
|
+
{
|
|
6035
|
+
variant: badge.tone === "bad" ? "destructive" : "outline",
|
|
6036
|
+
className: badge.tone === "ok" ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : badge.tone === "muted" ? "text-muted-foreground" : void 0,
|
|
6037
|
+
children: badge.text
|
|
6038
|
+
}
|
|
6039
|
+
),
|
|
6040
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onConfigure, children: "Configurar" })
|
|
6041
|
+
] })
|
|
6042
|
+
] }) });
|
|
6043
|
+
}
|
|
6044
|
+
var fmtDateTime = (v) => v ? new Date(v).toLocaleString("pt-BR") : null;
|
|
6045
|
+
function UnitCertificateWidget({ item }) {
|
|
6046
|
+
const { row, loading, save } = useEmitterSettings(item?.legalEntityId);
|
|
6047
|
+
const [open, setOpen] = React5.useState(false);
|
|
6048
|
+
if (!item?.id) return null;
|
|
6049
|
+
if (!item.legalEntityId) return /* @__PURE__ */ jsx(NeedsLegalEntity, { item, what: "O certificado digital" });
|
|
6050
|
+
if (loading || !row) return /* @__PURE__ */ jsx(LoadingCard, {});
|
|
6051
|
+
const expires = row.certificate_expires_at ? new Date(row.certificate_expires_at) : null;
|
|
6052
|
+
const expired = expires ? expires.getTime() < Date.now() : false;
|
|
6053
|
+
const hasCertificate = !!row.certificate_uploaded_at;
|
|
6054
|
+
if (!open) {
|
|
6055
|
+
return /* @__PURE__ */ jsx(
|
|
6056
|
+
SummaryCard,
|
|
6057
|
+
{
|
|
6058
|
+
title: "Certificado digital e DANFes",
|
|
6059
|
+
lines: [
|
|
6060
|
+
{ label: "Certificado A1", value: hasCertificate ? [row.certificate_cn, expires ? `v\xE1lido at\xE9 ${expires.toLocaleDateString("pt-BR")}` : null].filter(Boolean).join(" \xB7 ") || "Enviado" : "Nenhum certificado enviado" },
|
|
6061
|
+
{ label: "Receber DANFes", value: row.dfe_sync_enabled ? `Ligado \xB7 \xFAltima sincroniza\xE7\xE3o ${fmtDateTime(row.dfe_last_sync_at) ?? "nunca"}` : "Desligado" }
|
|
6062
|
+
],
|
|
6063
|
+
badge: hasCertificate ? { text: expired ? "Vencido" : "Ativo", tone: expired ? "bad" : "ok" } : { text: "Sem certificado", tone: "muted" },
|
|
6064
|
+
onConfigure: () => setOpen(true)
|
|
6065
|
+
}
|
|
6066
|
+
);
|
|
6067
|
+
}
|
|
6068
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
6069
|
+
/* @__PURE__ */ jsxs(CardHeader, { className: "flex-row items-start justify-between gap-3 space-y-0", children: [
|
|
6070
|
+
/* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Certificado digital e DANFes" }),
|
|
6071
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpen(false), children: "Fechar" })
|
|
6072
|
+
] }),
|
|
6073
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "space-y-5", children: [
|
|
6074
|
+
/* @__PURE__ */ jsx(
|
|
6075
|
+
ToggleRow,
|
|
6076
|
+
{
|
|
6077
|
+
title: "Receber DANFes automaticamente",
|
|
6078
|
+
description: "Busca por hora as NF-e emitidas contra o CNPJ desta empresa.",
|
|
6079
|
+
checked: !!row.dfe_sync_enabled,
|
|
6080
|
+
onChange: (v) => void save(item.tenantId, { dfe_sync_enabled: v }).then((ok) => ok && toast.success(v ? "Recep\xE7\xE3o ligada" : "Recep\xE7\xE3o desligada"))
|
|
6081
|
+
}
|
|
6082
|
+
),
|
|
6083
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-muted/40 p-3 text-sm", children: [
|
|
6084
|
+
/* @__PURE__ */ jsxs("p", { className: "mb-1.5 flex items-center gap-1.5 font-medium", children: [
|
|
6085
|
+
/* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }),
|
|
6086
|
+
" Como funciona"
|
|
6087
|
+
] }),
|
|
6088
|
+
/* @__PURE__ */ jsxs("ul", { className: "ml-5 list-disc space-y-1 text-muted-foreground", children: [
|
|
6089
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
6090
|
+
/* @__PURE__ */ jsx("strong", { className: "text-foreground", children: "O que faz:" }),
|
|
6091
|
+
" consulta a SEFAZ e lista as NF-e emitidas contra o CNPJ, para voc\xEA aceitar ou rejeitar e gerar entrada de estoque ou conta a pagar."
|
|
6092
|
+
] }),
|
|
6093
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
6094
|
+
/* @__PURE__ */ jsx("strong", { className: "text-foreground", children: "Requer certificado digital A1" }),
|
|
6095
|
+
" (.pfx) v\xE1lido desta empresa. O arquivo vai direto ao servi\xE7o fiscal e n\xE3o fica armazenado no nosso sistema."
|
|
6096
|
+
] }),
|
|
6097
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
6098
|
+
/* @__PURE__ */ jsx("strong", { className: "text-foreground", children: "N\xE3o faz manifesta\xE7\xE3o na SEFAZ" }),
|
|
6099
|
+
" (ci\xEAncia, confirma\xE7\xE3o, desconhecimento) nem emite NF-e."
|
|
6100
|
+
] })
|
|
6101
|
+
] })
|
|
6102
|
+
] }),
|
|
6103
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-md border p-4 space-y-4", children: [
|
|
6104
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
|
|
6105
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
6106
|
+
/* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1.5 text-sm font-medium", children: [
|
|
6107
|
+
/* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4" }),
|
|
6108
|
+
" Certificado digital A1"
|
|
6109
|
+
] }),
|
|
6110
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: hasCertificate ? [row.certificate_cn, expires ? `v\xE1lido at\xE9 ${expires.toLocaleDateString("pt-BR")}` : null].filter(Boolean).join(" \xB7 ") : "Nenhum certificado enviado" })
|
|
6111
|
+
] }),
|
|
6112
|
+
hasCertificate && /* @__PURE__ */ jsx(Badge, { variant: expired ? "destructive" : "secondary", children: expired ? "Vencido" : "Ativo" })
|
|
6113
|
+
] }),
|
|
6114
|
+
/* @__PURE__ */ jsx("div", { className: "fz-container", children: /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-4", children: [
|
|
6115
|
+
/* @__PURE__ */ jsx(Field2, { label: "Arquivo (.pfx)", children: /* @__PURE__ */ jsx(Input, { type: "file", accept: ".pfx,.p12", disabled: true }) }),
|
|
6116
|
+
/* @__PURE__ */ jsx(Field2, { label: "Senha do certificado", children: /* @__PURE__ */ jsx(Input, { type: "password", disabled: true }) })
|
|
6117
|
+
] }) }),
|
|
6118
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "O envio do certificado ainda n\xE3o est\xE1 dispon\xEDvel nesta vers\xE3o: falta a fun\xE7\xE3o no servidor que repassa o arquivo ao PlugNotas sem guard\xE1-lo." })
|
|
6119
|
+
] }),
|
|
6120
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
6121
|
+
"\xDAltima sincroniza\xE7\xE3o: ",
|
|
6122
|
+
fmtDateTime(row.dfe_last_sync_at) ?? "nunca"
|
|
6123
|
+
] })
|
|
6124
|
+
] })
|
|
6125
|
+
] });
|
|
6126
|
+
}
|
|
6127
|
+
var ISS_TAXATION = [
|
|
6128
|
+
{ value: "6", label: "Tribut\xE1vel no munic\xEDpio" },
|
|
6129
|
+
{ value: "7", label: "Tribut\xE1vel fora do munic\xEDpio" },
|
|
6130
|
+
{ value: "1", label: "Isento" },
|
|
6131
|
+
{ value: "2", label: "Imune" }
|
|
6132
|
+
];
|
|
6133
|
+
var SIMPLES_REGIME = [
|
|
6134
|
+
{ value: "auto", label: "Autom\xE1tico (1 \u2014 tudo pelo Simples; excesso \u2192 3)" },
|
|
6135
|
+
{ value: "1", label: "1 \u2014 Federais e ISS pelo Simples (DAS)" },
|
|
6136
|
+
{ value: "2", label: "2 \u2014 Federais pelo Simples; ISS pela NFS-e (lei municipal)" },
|
|
6137
|
+
{ value: "3", label: "3 \u2014 Federais e ISS fora do Simples (sublimite)" }
|
|
6138
|
+
];
|
|
6139
|
+
function EnvSelect({ value, onChange, disabled }) {
|
|
6140
|
+
return /* @__PURE__ */ jsxs(Select, { value: value ? "prod" : "hom", onValueChange: (v) => onChange(v === "prod"), disabled, children: [
|
|
6141
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-36", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
6142
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
6143
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "hom", children: "Homologa\xE7\xE3o" }),
|
|
6144
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "prod", children: "Produ\xE7\xE3o" })
|
|
6145
|
+
] })
|
|
6146
|
+
] });
|
|
6147
|
+
}
|
|
6148
|
+
function UnitFiscalWidget({ item }) {
|
|
6149
|
+
const { row, loading, save } = useEmitterSettings(item?.legalEntityId);
|
|
6150
|
+
const [draft, setDraft] = React5.useState({});
|
|
6151
|
+
const [cscToken, setCscToken] = React5.useState("");
|
|
6152
|
+
const [saving, setSaving] = React5.useState(false);
|
|
6153
|
+
const [open, setOpen] = React5.useState(false);
|
|
6154
|
+
React5.useEffect(() => {
|
|
6155
|
+
if (row) setDraft(row);
|
|
6156
|
+
}, [row]);
|
|
6157
|
+
if (!item?.id) return null;
|
|
6158
|
+
if (!item.legalEntityId) return /* @__PURE__ */ jsx(NeedsLegalEntity, { item, what: "A emiss\xE3o fiscal" });
|
|
6159
|
+
if (loading || !row) return /* @__PURE__ */ jsx(LoadingCard, {});
|
|
6160
|
+
const set = (patch) => setDraft((d) => ({ ...d, ...patch }));
|
|
6161
|
+
const generic = draft.nfce_generic_item ?? {};
|
|
6162
|
+
const setGeneric = (patch) => set({ nfce_generic_item: { ...generic, ...patch } });
|
|
6163
|
+
const isSimples = (item.taxRegime ?? "").startsWith("simples");
|
|
6164
|
+
const checks = [
|
|
6165
|
+
{ ok: !!row.certificate_uploaded_at && !(row.certificate_expires_at && Date.parse(row.certificate_expires_at) < Date.now()), label: "Certificado A1 v\xE1lido" },
|
|
6166
|
+
// Only what the switched-on documents need: NFS-e off does not block a restaurant that issues NFC-e.
|
|
6167
|
+
...draft.nfse_enabled ? [
|
|
6168
|
+
{ ok: !!draft.nfse_service_code_national, label: "C\xF3digo de tributa\xE7\xE3o do servi\xE7o" },
|
|
6169
|
+
{ ok: draft.nfse_iss_rate != null && draft.nfse_iss_rate !== "", label: "Al\xEDquota de ISS" }
|
|
6170
|
+
] : [],
|
|
6171
|
+
...draft.nfce_enabled ? [
|
|
6172
|
+
{ ok: !!item.stateRegistration && !!draft.nfce_csc_id && (!!row.nfce_csc_token_set || !!cscToken), label: "IE + CSC (NFC-e)" }
|
|
6173
|
+
] : [],
|
|
6174
|
+
{ ok: !!row.provider_synced_at, label: "Empresa registrada para emitir" }
|
|
6175
|
+
];
|
|
6176
|
+
const ready = (draft.nfse_enabled || draft.nfce_enabled) && checks.every((c) => c.ok);
|
|
6177
|
+
const onSave = async () => {
|
|
6178
|
+
setSaving(true);
|
|
6179
|
+
const {
|
|
6180
|
+
id: _id,
|
|
6181
|
+
legal_entity_id: _le,
|
|
6182
|
+
nfce_csc_token_set: _t,
|
|
6183
|
+
certificate_cn: _cn,
|
|
6184
|
+
certificate_expires_at: _e,
|
|
6185
|
+
certificate_uploaded_at: _u,
|
|
6186
|
+
dfe_last_sync_at: _d,
|
|
6187
|
+
provider_synced_at: _p,
|
|
6188
|
+
...patch
|
|
6189
|
+
} = draft;
|
|
6190
|
+
const issRate = patch.nfse_iss_rate === "" || patch.nfse_iss_rate == null ? null : Number(String(patch.nfse_iss_rate).replace(",", "."));
|
|
6191
|
+
const ok = await save(item.tenantId, {
|
|
6192
|
+
...patch,
|
|
6193
|
+
nfse_iss_rate: Number.isFinite(issRate) ? issRate : null,
|
|
6194
|
+
nfse_iss_taxation: patch.nfse_iss_taxation ? Number(patch.nfse_iss_taxation) : null,
|
|
6195
|
+
nfse_simples_regime: patch.nfse_simples_regime && patch.nfse_simples_regime !== "auto" ? Number(patch.nfse_simples_regime) : null,
|
|
6196
|
+
nfce_series: patch.nfce_series ? Number(patch.nfce_series) : null,
|
|
6197
|
+
...cscToken ? { nfce_csc_token: cscToken } : {}
|
|
6198
|
+
});
|
|
6199
|
+
setSaving(false);
|
|
6200
|
+
if (ok) {
|
|
6201
|
+
setCscToken("");
|
|
6202
|
+
setOpen(false);
|
|
6203
|
+
toast.success("Configura\xE7\xE3o fiscal salva");
|
|
6204
|
+
}
|
|
6205
|
+
};
|
|
6206
|
+
if (!open) {
|
|
6207
|
+
const env = (prod) => prod ? "Produ\xE7\xE3o" : "Homologa\xE7\xE3o";
|
|
6208
|
+
return /* @__PURE__ */ jsx(
|
|
6209
|
+
SummaryCard,
|
|
6210
|
+
{
|
|
6211
|
+
title: "Emiss\xE3o fiscal (NFS-e / NFC-e)",
|
|
6212
|
+
lines: [
|
|
6213
|
+
{ label: "NFS-e", value: row.nfse_enabled ? `Ligada \xB7 ${env(!!row.nfse_production)}` : "Desligada" },
|
|
6214
|
+
{ label: "NFC-e", value: row.nfce_enabled ? `Ligada \xB7 ${env(!!row.nfce_production)}${row.nfce_series ? ` \xB7 s\xE9rie ${row.nfce_series}` : ""}` : "Desligada" }
|
|
6215
|
+
],
|
|
6216
|
+
badge: ready ? { text: "Pronto para emitir", tone: "ok" } : { text: "Configura\xE7\xE3o incompleta", tone: "muted" },
|
|
6217
|
+
onConfigure: () => setOpen(true)
|
|
6218
|
+
}
|
|
6219
|
+
);
|
|
6220
|
+
}
|
|
6221
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
6222
|
+
/* @__PURE__ */ jsxs(CardHeader, { className: "flex-row items-start justify-between gap-3 space-y-0", children: [
|
|
6223
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
6224
|
+
/* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Emiss\xE3o fiscal (NFS-e / NFC-e)" }),
|
|
6225
|
+
/* @__PURE__ */ jsxs(CardDescription, { children: [
|
|
6226
|
+
item.legalName ?? "Pessoa jur\xEDdica",
|
|
6227
|
+
item.cnpj ? ` \xB7 CNPJ ${maskCpfCnpj(item.cnpj)}` : ""
|
|
6228
|
+
] })
|
|
6229
|
+
] }),
|
|
6230
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
6231
|
+
/* @__PURE__ */ jsx(Badge, { variant: ready ? "default" : "secondary", children: ready ? "Pronto para emitir" : "Configura\xE7\xE3o incompleta" }),
|
|
6232
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
6233
|
+
setDraft(row);
|
|
6234
|
+
setCscToken("");
|
|
6235
|
+
setOpen(false);
|
|
6236
|
+
}, children: "Cancelar" })
|
|
6237
|
+
] })
|
|
6238
|
+
] }),
|
|
6239
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "space-y-5", children: [
|
|
6240
|
+
/* @__PURE__ */ jsxs("section", { className: "space-y-4 rounded-md border p-4", children: [
|
|
6241
|
+
/* @__PURE__ */ jsx(
|
|
6242
|
+
ToggleRow,
|
|
6243
|
+
{
|
|
6244
|
+
title: "NFS-e \u2014 Nota de Servi\xE7o",
|
|
6245
|
+
description: "Padr\xE3o nacional (RJ, Vit\xF3ria e demais aderentes)",
|
|
6246
|
+
checked: !!draft.nfse_enabled,
|
|
6247
|
+
onChange: (v) => set({ nfse_enabled: v }),
|
|
6248
|
+
children: /* @__PURE__ */ jsx(EnvSelect, { value: !!draft.nfse_production, onChange: (v) => set({ nfse_production: v }), disabled: !draft.nfse_enabled })
|
|
6249
|
+
}
|
|
6250
|
+
),
|
|
6251
|
+
draft.nfse_enabled && /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-4", children: [
|
|
6252
|
+
/* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-4", children: [
|
|
6253
|
+
/* @__PURE__ */ jsx(Field2, { label: "C\xF3d. tributa\xE7\xE3o nacional", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_service_code_national ?? "", placeholder: "ex.: 06.02.01", onChange: (e) => set({ nfse_service_code_national: e.target.value }) }) }),
|
|
6254
|
+
/* @__PURE__ */ jsx(Field2, { label: "C\xF3d. tributa\xE7\xE3o municipal", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_service_code_municipal ?? "", placeholder: "ex.: 001", onChange: (e) => set({ nfse_service_code_municipal: e.target.value }) }) }),
|
|
6255
|
+
/* @__PURE__ */ jsx(Field2, { label: "C\xF3digo NBS", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_nbs_code ?? "", placeholder: "ex.: 1.2602.90.00", onChange: (e) => set({ nfse_nbs_code: e.target.value }) }) }),
|
|
6256
|
+
/* @__PURE__ */ jsx(Field2, { label: "CNAE do servi\xE7o", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_cnae ?? "", placeholder: "ex.: 9602-5/02", onChange: (e) => set({ nfse_cnae: e.target.value }) }) }),
|
|
6257
|
+
/* @__PURE__ */ jsx(Field2, { label: "Al\xEDquota ISS (%)", children: /* @__PURE__ */ jsx(Input, { inputMode: "decimal", value: draft.nfse_iss_rate ?? "", placeholder: "ex.: 5", onChange: (e) => set({ nfse_iss_rate: e.target.value }) }) }),
|
|
6258
|
+
/* @__PURE__ */ jsx(Field2, { label: "Tributa\xE7\xE3o ISS", children: /* @__PURE__ */ jsxs(Select, { value: draft.nfse_iss_taxation != null ? String(draft.nfse_iss_taxation) : void 0, onValueChange: (v) => set({ nfse_iss_taxation: v }), children: [
|
|
6259
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Selecione" }) }),
|
|
6260
|
+
/* @__PURE__ */ jsx(SelectContent, { children: ISS_TAXATION.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.value, children: o.label }, o.value)) })
|
|
6261
|
+
] }) }),
|
|
6262
|
+
/* @__PURE__ */ jsx(Field2, { label: "IBS/CBS \u2014 CST", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_ibscbs_cst ?? "", placeholder: "ex.: 000", onChange: (e) => set({ nfse_ibscbs_cst: e.target.value }) }) }),
|
|
6263
|
+
/* @__PURE__ */ jsx(Field2, { label: "IBS/CBS \u2014 Classifica\xE7\xE3o", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_ibscbs_classification ?? "", placeholder: "ex.: 000001", onChange: (e) => set({ nfse_ibscbs_classification: e.target.value }) }) }),
|
|
6264
|
+
isSimples && /* @__PURE__ */ jsx(Field2, { label: "Apura\xE7\xE3o dos tributos no Simples", children: /* @__PURE__ */ jsxs(Select, { value: draft.nfse_simples_regime != null ? String(draft.nfse_simples_regime) : "auto", onValueChange: (v) => set({ nfse_simples_regime: v }), children: [
|
|
6265
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
6266
|
+
/* @__PURE__ */ jsx(SelectContent, { children: SIMPLES_REGIME.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.value, children: o.label }, o.value)) })
|
|
6267
|
+
] }) })
|
|
6268
|
+
] }),
|
|
6269
|
+
/* @__PURE__ */ jsx(ToggleRow, { title: "Emitir pelo valor l\xEDquido", checked: !!draft.nfse_net_value, onChange: (v) => set({ nfse_net_value: v }) }),
|
|
6270
|
+
/* @__PURE__ */ jsx(Field2, { label: "Informa\xE7\xF5es complementares da nota", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_additional_info ?? "", placeholder: "ex.: Totais Aproximados dos Tributos cfe. Lei n\xBA 12.741/2012\u2026", onChange: (e) => set({ nfse_additional_info: e.target.value }) }) })
|
|
6271
|
+
] })
|
|
6272
|
+
] }),
|
|
6273
|
+
/* @__PURE__ */ jsxs("section", { className: "space-y-4 rounded-md border p-4", children: [
|
|
6274
|
+
/* @__PURE__ */ jsx(
|
|
6275
|
+
ToggleRow,
|
|
6276
|
+
{
|
|
6277
|
+
title: "NFC-e \u2014 Cupom do Consumidor",
|
|
6278
|
+
description: "Produtos / comanda",
|
|
6279
|
+
checked: !!draft.nfce_enabled,
|
|
6280
|
+
onChange: (v) => set({ nfce_enabled: v }),
|
|
6281
|
+
children: /* @__PURE__ */ jsx(EnvSelect, { value: !!draft.nfce_production, onChange: (v) => set({ nfce_production: v }), disabled: !draft.nfce_enabled })
|
|
6282
|
+
}
|
|
6283
|
+
),
|
|
6284
|
+
draft.nfce_enabled && /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-4", children: [
|
|
6285
|
+
/* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-4", children: [
|
|
6286
|
+
/* @__PURE__ */ jsx(Field2, { label: "S\xE9rie", children: /* @__PURE__ */ jsx(Input, { inputMode: "numeric", value: draft.nfce_series ?? "", onChange: (e) => set({ nfce_series: e.target.value.replace(/\D/g, "") }) }) }),
|
|
6287
|
+
/* @__PURE__ */ jsx(Field2, { label: "CSC \u2014 ID", children: /* @__PURE__ */ jsx(Input, { value: draft.nfce_csc_id ?? "", onChange: (e) => set({ nfce_csc_id: e.target.value }) }) })
|
|
6288
|
+
] }),
|
|
6289
|
+
/* @__PURE__ */ jsx(Field2, { label: "CSC \u2014 Token", hint: "O CSC (id num\xE9rico + token) \xE9 gerado no portal da SEFAZ do estado \u2014 n\xE3o \xE9 o login de nenhum sistema.", children: /* @__PURE__ */ jsx(Input, { type: "password", value: cscToken, placeholder: row.nfce_csc_token_set ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (configurado \u2014 digite para trocar)" : "", onChange: (e) => setCscToken(e.target.value) }) }),
|
|
6290
|
+
/* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-4", children: [
|
|
6291
|
+
/* @__PURE__ */ jsx(Field2, { label: "Item gen\xE9rico \u2014 NCM", children: /* @__PURE__ */ jsx(Input, { value: generic.ncm ?? "", onChange: (e) => setGeneric({ ncm: e.target.value }) }) }),
|
|
6292
|
+
/* @__PURE__ */ jsx(Field2, { label: "Item gen\xE9rico \u2014 CFOP", children: /* @__PURE__ */ jsx(Input, { value: generic.cfop ?? "", onChange: (e) => setGeneric({ cfop: e.target.value }) }) }),
|
|
6293
|
+
/* @__PURE__ */ jsx(Field2, { label: "Item gen\xE9rico \u2014 CSOSN/CST", children: /* @__PURE__ */ jsx(Input, { value: generic.csosn ?? "", onChange: (e) => setGeneric({ csosn: e.target.value }) }) })
|
|
6294
|
+
] })
|
|
6295
|
+
] })
|
|
6296
|
+
] }),
|
|
6297
|
+
/* @__PURE__ */ jsx("ul", { className: "flex flex-wrap gap-2", children: checks.map((c) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: c.ok ? "gap-1 border-emerald-500/40 text-emerald-700 dark:text-emerald-400" : "gap-1 text-muted-foreground", children: [
|
|
6298
|
+
c.ok ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }),
|
|
6299
|
+
c.label
|
|
6300
|
+
] }) }, c.label)) }),
|
|
6301
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-end gap-3", children: [
|
|
6302
|
+
/* @__PURE__ */ jsx("p", { className: "mr-auto text-xs text-muted-foreground", children: "Registrar a empresa no emissor ainda n\xE3o est\xE1 dispon\xEDvel nesta vers\xE3o \u2014 a configura\xE7\xE3o fica salva para quando estiver." }),
|
|
6303
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => void onSave(), disabled: saving, children: saving ? "Salvando\u2026" : "Salvar configura\xE7\xE3o" })
|
|
6304
|
+
] })
|
|
6305
|
+
] })
|
|
6306
|
+
] });
|
|
6307
|
+
}
|
|
4809
6308
|
|
|
4810
6309
|
// src/lib/financial-provider.ts
|
|
4811
6310
|
function fiscalReader(provider) {
|
|
@@ -4813,13 +6312,13 @@ function fiscalReader(provider) {
|
|
|
4813
6312
|
return provider.getNotaById.bind(provider);
|
|
4814
6313
|
}
|
|
4815
6314
|
async function committed(read, id) {
|
|
4816
|
-
const
|
|
4817
|
-
if (!
|
|
4818
|
-
return
|
|
6315
|
+
const document2 = await read(id);
|
|
6316
|
+
if (!document2) throw new Error("Fiscal document was not found after the server operation.");
|
|
6317
|
+
return document2;
|
|
4819
6318
|
}
|
|
4820
|
-
function resultError(result,
|
|
6319
|
+
function resultError(result, document2, fallback) {
|
|
4821
6320
|
if (result.status !== "error") return;
|
|
4822
|
-
throw new Error(result.message?.trim() ||
|
|
6321
|
+
throw new Error(result.message?.trim() || document2.errorMessage?.trim() || fallback);
|
|
4823
6322
|
}
|
|
4824
6323
|
function cancellationPending(metadata) {
|
|
4825
6324
|
const provider = metadata?.plugnotas;
|
|
@@ -4841,11 +6340,11 @@ function createPlugNotasFinancialProvider(options) {
|
|
|
4841
6340
|
const { base, gateway } = options;
|
|
4842
6341
|
const issueNota = async (id) => {
|
|
4843
6342
|
const read = fiscalReader(base);
|
|
4844
|
-
const
|
|
4845
|
-
if (!
|
|
4846
|
-
if (
|
|
4847
|
-
if (!
|
|
4848
|
-
if (!["draft", "error"].includes(
|
|
6343
|
+
const document2 = await read(id);
|
|
6344
|
+
if (!document2) throw new Error("Fiscal document not found.");
|
|
6345
|
+
if (document2.model !== "nfce") throw new Error("PlugNotas currently authorizes NFC-e only.");
|
|
6346
|
+
if (!document2.orderId?.trim()) throw new Error("PlugNotas NFC-e must be created from an order.");
|
|
6347
|
+
if (!["draft", "error"].includes(document2.status)) return document2;
|
|
4849
6348
|
const result = await gateway.issue({ notaId: id, idempotencyKey: `nfce:${id}` });
|
|
4850
6349
|
const saved = await committed(read, id);
|
|
4851
6350
|
resultError(result, saved, "PlugNotas could not issue this NFC-e.");
|
|
@@ -4853,9 +6352,9 @@ function createPlugNotasFinancialProvider(options) {
|
|
|
4853
6352
|
};
|
|
4854
6353
|
const refreshNota = async (id) => {
|
|
4855
6354
|
const read = fiscalReader(base);
|
|
4856
|
-
const
|
|
4857
|
-
if (!
|
|
4858
|
-
if (!["processing", "error"].includes(
|
|
6355
|
+
const document2 = await read(id);
|
|
6356
|
+
if (!document2) throw new Error("Fiscal document not found.");
|
|
6357
|
+
if (!["processing", "error"].includes(document2.status) && !cancellationPending(document2.metadata)) return document2;
|
|
4859
6358
|
const result = await gateway.refresh({ notaId: id });
|
|
4860
6359
|
const saved = await committed(read, id);
|
|
4861
6360
|
resultError(result, saved, "PlugNotas could not refresh this NFC-e.");
|
|
@@ -4863,10 +6362,10 @@ function createPlugNotasFinancialProvider(options) {
|
|
|
4863
6362
|
};
|
|
4864
6363
|
const cancelNota = async (id, reason) => {
|
|
4865
6364
|
const read = fiscalReader(base);
|
|
4866
|
-
const
|
|
4867
|
-
if (!
|
|
4868
|
-
if (
|
|
4869
|
-
if (cancellationPending(
|
|
6365
|
+
const document2 = await read(id);
|
|
6366
|
+
if (!document2) throw new Error("Fiscal document not found.");
|
|
6367
|
+
if (document2.status !== "issued") throw new Error("Only an issued NFC-e can be cancelled.");
|
|
6368
|
+
if (cancellationPending(document2.metadata)) throw new Error("NFC-e cancellation is already in progress.");
|
|
4870
6369
|
const normalized = reason?.trim() ?? "";
|
|
4871
6370
|
if (normalized.length < 15 || normalized.length > 255) {
|
|
4872
6371
|
throw new Error("Cancellation reason must contain between 15 and 255 characters.");
|
|
@@ -4878,12 +6377,12 @@ function createPlugNotasFinancialProvider(options) {
|
|
|
4878
6377
|
};
|
|
4879
6378
|
const getNotaArtifactUrl = async (id, kind) => {
|
|
4880
6379
|
const read = fiscalReader(base);
|
|
4881
|
-
const
|
|
4882
|
-
if (!
|
|
4883
|
-
if (!["issued", "cancelled"].includes(
|
|
6380
|
+
const document2 = await read(id);
|
|
6381
|
+
if (!document2) throw new Error("Fiscal document not found.");
|
|
6382
|
+
if (!["issued", "cancelled"].includes(document2.status)) {
|
|
4884
6383
|
throw new Error("Fiscal artifacts are available only after authorization.");
|
|
4885
6384
|
}
|
|
4886
|
-
if (kind === "cancellation_xml" &&
|
|
6385
|
+
if (kind === "cancellation_xml" && document2.status !== "cancelled") {
|
|
4887
6386
|
throw new Error("Cancellation XML is available only after cancellation.");
|
|
4888
6387
|
}
|
|
4889
6388
|
return artifactUrl((await gateway.artifact({ notaId: id, kind })).url);
|
|
@@ -4955,10 +6454,14 @@ function ReturnedNotesView({ onBack } = {}) {
|
|
|
4955
6454
|
);
|
|
4956
6455
|
}
|
|
4957
6456
|
function inboundNotesInventoryView(options = {}) {
|
|
6457
|
+
registerDocumentOpener(NFE_OPENER);
|
|
4958
6458
|
return {
|
|
4959
6459
|
id: "inbound-notes",
|
|
4960
6460
|
label: "Notas Fiscais",
|
|
4961
6461
|
icon: "FileText",
|
|
6462
|
+
// O nome do V1 para a fila de notas de compra. «Entrada» era o nome genérico
|
|
6463
|
+
// que o Estoque dá a qualquer tela emprestada, e dizia menos do que a tela faz.
|
|
6464
|
+
defaultChildLabel: "Manifesto de Notas",
|
|
4962
6465
|
render: () => /* @__PURE__ */ jsx(InboundNotesPage, { ...options }),
|
|
4963
6466
|
// A ordem é a do V1: Grupos Fiscais vem antes das duas de emissão, que é
|
|
4964
6467
|
// onde ele é usado.
|
|
@@ -4976,6 +6479,60 @@ function inboundNotesInventoryView(options = {}) {
|
|
|
4976
6479
|
]
|
|
4977
6480
|
};
|
|
4978
6481
|
}
|
|
6482
|
+
function InboundNoteDocumentModal({ movement, onClose }) {
|
|
6483
|
+
const [noteId, setNoteId] = React5.useState(null);
|
|
6484
|
+
const [erro, setErro] = React5.useState(null);
|
|
6485
|
+
React5.useEffect(() => {
|
|
6486
|
+
let vivo = true;
|
|
6487
|
+
const numero = nfeNumberOf(movement);
|
|
6488
|
+
const busca = movement.operationId ? findInboxNoteByOperation(movement.operationId).then((id) => id ?? (numero ? findInboxNoteByNumber(numero) : null)) : numero ? findInboxNoteByNumber(numero) : Promise.resolve(null);
|
|
6489
|
+
busca.then((id) => {
|
|
6490
|
+
if (!vivo) return;
|
|
6491
|
+
if (id) setNoteId(id);
|
|
6492
|
+
else setErro("N\xE3o achei a nota desta entrada nesta conta.");
|
|
6493
|
+
}).catch((e) => {
|
|
6494
|
+
if (vivo) setErro(e instanceof Error ? e.message : String(e));
|
|
6495
|
+
});
|
|
6496
|
+
return () => {
|
|
6497
|
+
vivo = false;
|
|
6498
|
+
};
|
|
6499
|
+
}, [movement.operationId, movement.documentNumber, movement.reason]);
|
|
6500
|
+
if (erro) {
|
|
6501
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6502
|
+
/* @__PURE__ */ jsx(ModalHeader, { className: "shrink-0 border-b px-6 py-3 pr-14 text-left", children: /* @__PURE__ */ jsx(ModalTitle, { children: "Nota de entrada" }) }),
|
|
6503
|
+
/* @__PURE__ */ jsx(ModalBody, { className: "px-6 py-8", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: erro }) })
|
|
6504
|
+
] });
|
|
6505
|
+
}
|
|
6506
|
+
if (!noteId) {
|
|
6507
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6508
|
+
/* @__PURE__ */ jsx(ModalHeader, { className: "shrink-0 border-b px-6 py-3 pr-14 text-left", children: /* @__PURE__ */ jsx(ModalTitle, { children: "Nota de entrada" }) }),
|
|
6509
|
+
/* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3 px-6 py-4", children: [
|
|
6510
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-48" }),
|
|
6511
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" }),
|
|
6512
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" })
|
|
6513
|
+
] })
|
|
6514
|
+
] });
|
|
6515
|
+
}
|
|
6516
|
+
return /* @__PURE__ */ jsx(NoteReconcileView, { chrome: "modal", noteId, onBack: onClose });
|
|
6517
|
+
}
|
|
6518
|
+
function inboundNoteDocumentModal(movement, close) {
|
|
6519
|
+
if (movement.documentType !== "nfe" || !movement.operationId) return void 0;
|
|
6520
|
+
return /* @__PURE__ */ jsx(InboundNoteDocumentModal, { movement, onClose: close });
|
|
6521
|
+
}
|
|
6522
|
+
var nfeNumberOf = (ref) => (ref.documentNumber ?? ref.reason ?? "").match(/NF-e\s*([0-9]{2,})/i)?.[1] ?? null;
|
|
6523
|
+
var NFE_OPENER = {
|
|
6524
|
+
documentType: "nfe",
|
|
6525
|
+
// O razão é APPEND-ONLY — um gatilho recusa `UPDATE`, e com razão — então as
|
|
6526
|
+
// 3.088 linhas que a importação do V1 trouxe sem `document_type` nunca vão
|
|
6527
|
+
// ganhá-lo. O que elas têm é «Entrada via DANFE NF-e 10592» no motivo, frase
|
|
6528
|
+
// que este mesmo sistema escreveu. Reconhecê-la é ler o que se escreveu.
|
|
6529
|
+
claims: (ref) => Boolean(nfeNumberOf(ref)),
|
|
6530
|
+
// Const de módulo e não arrow inline: o registro compara por identidade para
|
|
6531
|
+
// distinguir recarga quente de dois plugins disputando o mesmo tipo, e uma
|
|
6532
|
+
// função nova a cada chamada pareceria disputa.
|
|
6533
|
+
render: (ref, close) => /* @__PURE__ */ jsx(InboundNoteDocumentModal, { movement: ref, onClose: close })
|
|
6534
|
+
};
|
|
6535
|
+
registerDocumentOpener(NFE_OPENER);
|
|
4979
6536
|
|
|
4980
6537
|
// src/index.ts
|
|
4981
6538
|
function createPlugNotasPlugin(options = {}) {
|
|
@@ -5013,6 +6570,22 @@ function createPlugNotasPlugin(options = {}) {
|
|
|
5013
6570
|
// O grupo fiscal é CADASTRO, e por isso entra como registry e não como
|
|
5014
6571
|
// rota própria: ele aparece onde os outros cadastros do produto aparecem.
|
|
5015
6572
|
registries: fiscalRegistries,
|
|
6573
|
+
widgets: [
|
|
6574
|
+
{
|
|
6575
|
+
id: "fiscal-br-unit-certificate",
|
|
6576
|
+
zone: UNIT_DETAIL_ZONES.certificate,
|
|
6577
|
+
component: UnitCertificateWidget,
|
|
6578
|
+
order: 0,
|
|
6579
|
+
permission: { feature: "financial", action: "read" }
|
|
6580
|
+
},
|
|
6581
|
+
{
|
|
6582
|
+
id: "fiscal-br-unit-fiscal",
|
|
6583
|
+
zone: UNIT_DETAIL_ZONES.fiscal,
|
|
6584
|
+
component: UnitFiscalWidget,
|
|
6585
|
+
order: 0,
|
|
6586
|
+
permission: { feature: "financial", action: "read" }
|
|
6587
|
+
}
|
|
6588
|
+
],
|
|
5016
6589
|
connectors: [createPlugNotasConnector(options)],
|
|
5017
6590
|
functions: [{
|
|
5018
6591
|
slug: "plugnotas-bridge",
|
|
@@ -5084,6 +6657,6 @@ function createPlugNotasPlugin(options = {}) {
|
|
|
5084
6657
|
});
|
|
5085
6658
|
}
|
|
5086
6659
|
|
|
5087
|
-
export { InboundNotesPage, InboundNotesView, IssuedNotesView, ManifestView, NfeParseError, NoteReconcileView, PLUGNOTAS_CONNECTOR_ID, PLUGNOTAS_FIELDS, PLUGNOTAS_SETTINGS, ReturnedNotesView, STOCK_BEARING_ENTRY_TYPES, TaxGroupsView, acceptNote, createPlugNotasConnector, createPlugNotasFinancialProvider, createPlugNotasPlugin, getInboxNote, inboundNotesInventoryView, isStockBearing, isValidCnpj, listChartAccounts, listInboxNotes, listMeasurementUnits, listStockLocations, listSupplierMap, markPlugNotasEmitterReady, onlyDigits, parseNfeXml, paymentLabel, paymentTypeFromCode, rejectNote, saveNoteDecision, searchProducts, stageNoteFromXml, toStagePayload };
|
|
6660
|
+
export { InboundNoteDocumentModal, InboundNotesPage, InboundNotesView, IssuedNotesView, ManifestView, NfeParseError, NoteReconcileView, PLUGNOTAS_CONNECTOR_ID, PLUGNOTAS_FIELDS, PLUGNOTAS_SETTINGS, ReturnedNotesView, STOCK_BEARING_ENTRY_TYPES, TaxGroupsView, acceptNote, createPlugNotasConnector, createPlugNotasFinancialProvider, createPlugNotasPlugin, findInboxNoteByNumber, findInboxNoteByOperation, getInboxNote, getInboxNoteXml, inboundNoteDocumentModal, inboundNotesInventoryView, isStockBearing, isValidCnpj, listChartAccounts, listInboxNotes, listMeasurementUnits, listProductsByIds, listStockLocations, listSupplierMap, markPlugNotasEmitterReady, onlyDigits, parseNfeXml, paymentLabel, paymentTypeFromCode, rejectNote, saveNoteDecision, searchProducts, stageNoteFromXml, suggestNoteItems, toStagePayload };
|
|
5088
6661
|
//# sourceMappingURL=index.js.map
|
|
5089
6662
|
//# sourceMappingURL=index.js.map
|