@fayz-ai/plugin-fiscal-br 0.3.4 → 0.3.5

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/index.js CHANGED
@@ -1,4 +1,9 @@
1
- import { recordRailFields, RECORD_GROUP, activeFacet, connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus } from '@fayz-ai/core';
1
+ import { recordRailFields, RECORD_GROUP, activeFacet, connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus, getSupabaseClientOptional } from '@fayz-ai/core';
2
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
3
+ import { Inbox, Loader2, UploadCloud, Trash2, FileDown, Gift, RefreshCw, Check, Plus, AlertTriangle, X, ShieldCheck, FileText, ExternalLink } from 'lucide-react';
4
+ import { PageHeaderActions, Button, Card, ListFilterBar, Skeleton, SubpageHeader, EmBreve, SearchSelect, Input, DatePicker, Hint, ComingSoon } from '@fayz-ai/ui';
5
+ export { ComingSoon, EmBreve } from '@fayz-ai/ui';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
7
 
3
8
  // src/index.ts
4
9
  var PLUGNOTAS_CONNECTOR_ID = "plugnotas";
@@ -1833,6 +1838,999 @@ ALTER TABLE public.plg_fiscalbr_documents ENABLE ROW LEVEL SECURITY;
1833
1838
  ALTER TABLE public.plg_fiscalbr_document_events ENABLE ROW LEVEL SECURITY;
1834
1839
  ALTER TABLE public.plg_fiscalbr_document_orders ENABLE ROW LEVEL SECURITY;
1835
1840
  `;
1841
+ var MIGRATION_008_A_NOTA_RECEBIDA_GUARDA_A_DECISAO_DE_ENTRADA = `-- ---------------------------------------------------------------------------
1842
+ -- 008_a_nota_recebida_guarda_a_decisao_de_entrada.sql
1843
+ --
1844
+ -- A 002 trouxe a nota; esta guarda o que se DECIDE sobre ela.
1845
+ --
1846
+ -- O inbox do V2 j\xE1 sabe tudo que o XML diz: fornecedor, item, NCM, CFOP, os 30
1847
+ -- campos de tributo. O que n\xE3o tem onde morar \xE9 a decis\xE3o que o operador toma
1848
+ -- em cima da nota, e \xE9 ela que transforma documento em estoque e em dinheiro:
1849
+ --
1850
+ -- a qual PRODUTO esta linha corresponde (product_id, j\xE1 existia)
1851
+ -- que TIPO de entrada ela \xE9 (entry_type)
1852
+ -- em que UNIDADE de estoque, com que FATOR (stock_unit_id, conversion_factor)
1853
+ -- em que PLANO DE CONTAS ela cai (chart_account_id)
1854
+ -- com que CUSTO o produto fica (cost_mode, unit_cost_new)
1855
+ --
1856
+ -- Sem essas colunas a tela de concilia\xE7\xE3o n\xE3o tem onde salvar, e "importar nota
1857
+ -- fiscal" viraria um leitor de XML \u2014 que \xE9 exatamente o que o V2 tinha.
1858
+ --
1859
+ -- \u2500\u2500 O vocabul\xE1rio vem do V1 medido, n\xE3o de um enum inventado \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1860
+ --
1861
+ -- \`danfe_import_items.entry_type\` no V1 de produ\xE7\xE3o (29-09-2026), 3.402 linhas:
1862
+ --
1863
+ -- insumo 2.303 revenda 546 patrimonio 280
1864
+ -- despesa 262 servico 11 (null) 5
1865
+ --
1866
+ -- S\xE3o cinco, e duas delas \u2014 \`despesa\` e \`servico\` \u2014 N\xC3O viram estoque. \xC9 por
1867
+ -- isso que o tipo \xE9 coluna da linha e n\xE3o da nota: a mesma NF-e traz o insumo
1868
+ -- que entra no estoque e a taxa de entrega que s\xF3 vira despesa.
1869
+ --
1870
+ -- \`cost_mode\` tem UM valor em 3.407 linhas: \`novo\`. A m\xE9dia ponderada existe na
1871
+ -- tela do V1 e ningu\xE9m nunca escolheu. Fica no CHECK por paridade \u2014 tirar a
1872
+ -- op\xE7\xE3o \xE9 decis\xE3o de produto, e o dado n\xE3o autoriza ningu\xE9m a tom\xE1-la por aqui.
1873
+ --
1874
+ -- \u2500\u2500 Por que \`stock_movement_id\` na linha, e n\xE3o s\xF3 na nota \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1875
+ --
1876
+ -- Uma nota pode virar estoque PARCIALMENTE: o V1 tem os status \`partial\` e
1877
+ -- \`partial_stocked\` justamente porque o operador concilia 1 de 6 itens hoje e o
1878
+ -- resto amanh\xE3. Guardar o movimento s\xF3 no cabe\xE7alho responderia "esta nota
1879
+ -- entrou?" e nunca "esta LINHA entrou?" \u2014 e \xE9 a segunda pergunta que decide se
1880
+ -- aceitar de novo duplica saldo.
1881
+ --
1882
+ -- \u2500\u2500 O de-para \xE9 o que faz a segunda nota ser r\xE1pida \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1883
+ --
1884
+ -- \`supplier_product_mappings\` no V1: (CNPJ do fornecedor, c\xF3digo do produto no
1885
+ -- fornecedor) \u2192 produto, tipo, plano, unidade, fator. 66 linhas na Artorius, e \xE9
1886
+ -- o que faz a nota seguinte do mesmo fornecedor chegar j\xE1 vinculada. Sem ele,
1887
+ -- toda importa\xE7\xE3o recome\xE7a do zero e o ganho de importar XML desaparece.
1888
+ --
1889
+ -- A chave \xE9 (tenant, CNPJ, c\xF3digo) e N\xC3O inclui unidade: o de-para \xE9
1890
+ -- conhecimento sobre o cat\xE1logo do fornecedor, n\xE3o sobre uma loja.
1891
+ --
1892
+ -- Aditivo e idempotente. Nada aqui apaga, renomeia ou reescreve dado existente.
1893
+ -- ---------------------------------------------------------------------------
1894
+
1895
+ -- \u2500\u2500 A decis\xE3o, linha a linha \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1896
+
1897
+ ALTER TABLE public.plg_fiscalbr_inbox_items
1898
+ ADD COLUMN IF NOT EXISTS entry_type text,
1899
+ ADD COLUMN IF NOT EXISTS chart_account_id uuid,
1900
+ ADD COLUMN IF NOT EXISTS stock_unit_id uuid,
1901
+ -- A quantidade que entra no estoque, j\xE1 convertida. Derivada
1902
+ -- (quantity_commercial \xD7 conversion_factor), mas GRAVADA: \xE9 o n\xFAmero que o
1903
+ -- operador viu na tela antes de aceitar, e recalcular depois com um fator que
1904
+ -- algu\xE9m editou daria outra resposta para a mesma entrada.
1905
+ ADD COLUMN IF NOT EXISTS stock_quantity numeric(18,4),
1906
+ ADD COLUMN IF NOT EXISTS conversion_factor numeric(14,4) NOT NULL DEFAULT 1,
1907
+ ADD COLUMN IF NOT EXISTS cost_mode text,
1908
+ ADD COLUMN IF NOT EXISTS unit_cost_new numeric(18,6),
1909
+ -- O movimento que ESTA linha produziu. Nulo = n\xE3o entrou.
1910
+ ADD COLUMN IF NOT EXISTS stock_movement_id uuid,
1911
+ -- Diverg\xEAncia: chegou menos do que a nota diz. O V1 guarda quantidade, motivo
1912
+ -- e observa\xE7\xE3o, e a nota continua v\xE1lida \u2014 quem responde pela diferen\xE7a \xE9 o
1913
+ -- fornecedor, n\xE3o o estoque.
1914
+ ADD COLUMN IF NOT EXISTS shortage_quantity numeric(18,4),
1915
+ ADD COLUMN IF NOT EXISTS shortage_reason text,
1916
+ ADD COLUMN IF NOT EXISTS shortage_note text;
1917
+
1918
+ DO $$
1919
+ BEGIN
1920
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_items_entry_type_chk') THEN
1921
+ ALTER TABLE public.plg_fiscalbr_inbox_items
1922
+ ADD CONSTRAINT plg_fiscalbr_inbox_items_entry_type_chk
1923
+ CHECK (entry_type IS NULL OR entry_type IN ('insumo','revenda','patrimonio','despesa','servico'));
1924
+ END IF;
1925
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_items_cost_mode_chk') THEN
1926
+ ALTER TABLE public.plg_fiscalbr_inbox_items
1927
+ ADD CONSTRAINT plg_fiscalbr_inbox_items_cost_mode_chk
1928
+ CHECK (cost_mode IS NULL OR cost_mode IN ('novo','media'));
1929
+ END IF;
1930
+ -- Um fator zero ou negativo n\xE3o converte nada: transformaria a quantidade da
1931
+ -- nota em zero estoque sem ningu\xE9m acusar. \xC9 o defeito silencioso que o
1932
+ -- FLOW.md do V1 nomeia (\xABfator de convers\xE3o errado \xE9 silencioso\xBB), e aqui ele
1933
+ -- deixa de ser poss\xEDvel.
1934
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_items_factor_chk') THEN
1935
+ ALTER TABLE public.plg_fiscalbr_inbox_items
1936
+ ADD CONSTRAINT plg_fiscalbr_inbox_items_factor_chk CHECK (conversion_factor > 0);
1937
+ END IF;
1938
+ END $$;
1939
+
1940
+ -- FK l\xF3gica para o plano de contas e a unidade: as duas s\xE3o de OUTROS plugins
1941
+ -- (financial e inventory), e a 000 deste plugin j\xE1 estabelece que FK f\xEDsica
1942
+ -- entre plugins \xE9 proibida \u2014 instalar ou remover um addon n\xE3o reescreve o
1943
+ -- schema do vizinho. A integridade \xE9 verificada na RPC, que \xE9 quem escreve.
1944
+ COMMENT ON COLUMN public.plg_fiscalbr_inbox_items.chart_account_id IS
1945
+ 'Logical reference to plg_financial_chart_of_accounts.id \u2014 validated by the accept RPC, not by a cross-plugin FK.';
1946
+ COMMENT ON COLUMN public.plg_fiscalbr_inbox_items.stock_unit_id IS
1947
+ 'Logical reference to plg_inventory_measurement_units.id \u2014 validated by the accept RPC.';
1948
+ COMMENT ON COLUMN public.plg_fiscalbr_inbox_items.stock_quantity IS
1949
+ 'quantity_commercial x conversion_factor, stored as shown on screen before accepting.';
1950
+
1951
+ CREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_items_pending_idx
1952
+ ON public.plg_fiscalbr_inbox_items (tenant_id, inbox_document_id)
1953
+ WHERE reconciled IS NOT TRUE;
1954
+
1955
+ -- \u2500\u2500 A decis\xE3o no cabe\xE7alho da nota \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1956
+
1957
+ ALTER TABLE public.plg_fiscalbr_inbox_documents
1958
+ -- \xABitens sem plano herdar\xE3o este\xBB \u2014 o plano da nota \xE9 padr\xE3o com HERAN\xC7A, n\xE3o
1959
+ -- obriga\xE7\xE3o por item. A frase \xE9 do V1 e a coluna existe para ela.
1960
+ ADD COLUMN IF NOT EXISTS default_chart_account_id uuid,
1961
+ -- O t\xEDtulo a pagar que esta nota gerou. Mesma ideia do inventory_operation_id
1962
+ -- que a 002 j\xE1 trouxe para o estoque: a nota aponta para as duas travessias.
1963
+ ADD COLUMN IF NOT EXISTS invoice_id uuid,
1964
+ ADD COLUMN IF NOT EXISTS stocked_at timestamptz,
1965
+ -- A natureza da opera\xE7\xE3o declarada na NF-e (\xABREM BONIFICACAO NAO
1966
+ -- CONTRIBUINTE\xBB). O bloco de situa\xE7\xE3o fiscal da concilia\xE7\xE3o mostra isso, e
1967
+ -- sem a coluna a tela mostraria um campo vazio.
1968
+ ADD COLUMN IF NOT EXISTS nature_operation text,
1969
+ ADD COLUMN IF NOT EXISTS default_location_id uuid;
1970
+
1971
+ COMMENT ON COLUMN public.plg_fiscalbr_inbox_documents.invoice_id IS
1972
+ 'Logical reference to plg_financial_invoices.id created by fiscalbr_accept_inbox_document.';
1973
+
1974
+ -- \u2500\u2500 A parcela sabe como se paga \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1975
+ --
1976
+ -- A duplicata do XML traz n\xFAmero, vencimento e valor. O que o operador acrescenta
1977
+ -- na tela \xE9 COMO ela se paga \u2014 e o boleto que ele anexa carrega o c\xF3digo de
1978
+ -- barras, que \xE9 o que a automa\xE7\xE3o do V1 l\xEA para preencher a parcela.
1979
+
1980
+ ALTER TABLE public.plg_fiscalbr_inbox_duplicates
1981
+ ADD COLUMN IF NOT EXISTS payment_type text,
1982
+ ADD COLUMN IF NOT EXISTS barcode text,
1983
+ ADD COLUMN IF NOT EXISTS pix_key text,
1984
+ ADD COLUMN IF NOT EXISTS payment_method_id uuid;
1985
+
1986
+ -- O vocabul\xE1rio \xE9 o do V1, medido em \`danfe_payments.payment_type\` (1.069 linhas):
1987
+ -- boleto 660 \xB7 sem_pagamento 108 \xB7 outro 107 \xB7 dinheiro 65 \xB7
1988
+ -- duplicata_mercantil 45 \xB7 pix 35 \xB7 cartao_credito 35 \xB7 cartao_debito 9 \xB7
1989
+ -- credito_loja 4 \xB7 vale_presente 1
1990
+ DO $$
1991
+ BEGIN
1992
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_duplicates_payment_type_chk') THEN
1993
+ ALTER TABLE public.plg_fiscalbr_inbox_duplicates
1994
+ ADD CONSTRAINT plg_fiscalbr_inbox_duplicates_payment_type_chk
1995
+ CHECK (payment_type IS NULL OR payment_type IN (
1996
+ 'boleto','pix','dinheiro','cartao_credito','cartao_debito',
1997
+ 'duplicata_mercantil','credito_loja','vale_presente','sem_pagamento','outro'));
1998
+ END IF;
1999
+ END $$;
2000
+
2001
+ -- \u2500\u2500 O de-para fornecedor \xD7 produto \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2002
+
2003
+ CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_supplier_product_map (
2004
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
2005
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
2006
+ -- S\xF3 d\xEDgitos, normalizado na escrita: o XML traz o CNPJ cru e a tela do V1
2007
+ -- aceitava pontua\xE7\xE3o, ent\xE3o duas grafias do mesmo fornecedor viravam dois
2008
+ -- de-para e a segunda nota n\xE3o achava o primeiro.
2009
+ supplier_cnpj text NOT NULL,
2010
+ supplier_product_code text NOT NULL,
2011
+ product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
2012
+ entry_type text,
2013
+ chart_account_id uuid,
2014
+ stock_unit_id uuid,
2015
+ conversion_factor numeric(14,4) NOT NULL DEFAULT 1,
2016
+ cost_mode text,
2017
+ -- Quantas vezes este de-para acertou. Serve para a tela mostrar de onde veio o
2018
+ -- v\xEDnculo (\xABj\xE1 usado 12 vezes\xBB) em vez de um produto que apareceu sozinho.
2019
+ hit_count integer NOT NULL DEFAULT 0,
2020
+ last_used_at timestamptz,
2021
+ owner_id uuid,
2022
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
2023
+ created_at timestamptz NOT NULL DEFAULT now(),
2024
+ updated_at timestamptz NOT NULL DEFAULT now(),
2025
+ CONSTRAINT plg_fiscalbr_supplier_product_map_factor_chk CHECK (conversion_factor > 0),
2026
+ CONSTRAINT plg_fiscalbr_supplier_product_map_entry_type_chk
2027
+ CHECK (entry_type IS NULL OR entry_type IN ('insumo','revenda','patrimonio','despesa','servico')),
2028
+ CONSTRAINT plg_fiscalbr_supplier_product_map_cost_mode_chk
2029
+ CHECK (cost_mode IS NULL OR cost_mode IN ('novo','media'))
2030
+ );
2031
+
2032
+ CREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_supplier_product_map_uq
2033
+ ON public.plg_fiscalbr_supplier_product_map (tenant_id, supplier_cnpj, supplier_product_code);
2034
+
2035
+ ALTER TABLE public.plg_fiscalbr_supplier_product_map ENABLE ROW LEVEL SECURITY;
2036
+
2037
+ -- Mesmas pol\xEDticas das tabelas do inbox (002): o de-para \xE9 conhecimento do
2038
+ -- financeiro sobre o fornecedor, ent\xE3o l\xEA com \`financial.read\` e escreve com
2039
+ -- \`financial.edit\`. N\xE3o tem unit_id \u2014 \xE9 cat\xE1logo do fornecedor, n\xE3o da loja.
2040
+ DROP POLICY IF EXISTS plg_fiscalbr_supplier_product_map_authz_select ON public.plg_fiscalbr_supplier_product_map;
2041
+ CREATE POLICY plg_fiscalbr_supplier_product_map_authz_select
2042
+ ON public.plg_fiscalbr_supplier_product_map FOR SELECT TO authenticated
2043
+ USING (
2044
+ tenant_id = (SELECT app.current_tenant_id())
2045
+ AND (SELECT app.has_permission('financial.read', NULL::uuid)));
2046
+
2047
+ DROP POLICY IF EXISTS plg_fiscalbr_supplier_product_map_authz_write ON public.plg_fiscalbr_supplier_product_map;
2048
+ CREATE POLICY plg_fiscalbr_supplier_product_map_authz_write
2049
+ ON public.plg_fiscalbr_supplier_product_map FOR ALL TO authenticated
2050
+ USING (
2051
+ tenant_id = (SELECT app.current_tenant_id())
2052
+ AND (SELECT app.has_permission('financial.edit', NULL::uuid)))
2053
+ WITH CHECK (
2054
+ tenant_id = (SELECT app.current_tenant_id())
2055
+ AND (SELECT app.has_permission('financial.edit', NULL::uuid)));
2056
+
2057
+ COMMENT ON TABLE public.plg_fiscalbr_supplier_product_map IS
2058
+ 'Supplier catalogue memory: (CNPJ, supplier product code) -> the entry decision, so the next note from the same supplier arrives already bound. Mirrors V1 supplier_product_mappings.';
2059
+ `;
2060
+ var MIGRATION_009_ACEITAR_A_NOTA_ESCREVE_ESTOQUE_E_TITULO_DE_UMA_VEZ = `-- ---------------------------------------------------------------------------
2061
+ -- 009_aceitar_a_nota_escreve_estoque_e_titulo_de_uma_vez.sql
2062
+ --
2063
+ -- \xC9 o arquivo que fecha o porte: \xABAceitar\xBB na concilia\xE7\xE3o d\xE1 entrada no estoque
2064
+ -- E cria a conta a pagar, numa transa\xE7\xE3o s\xF3.
2065
+ --
2066
+ -- Medido no V1 de produ\xE7\xE3o (tenant 12 da Artorius, 29-09-2026): **72 de 72**
2067
+ -- movimentos de estoque nasceram com \`danfe_import_item_id\`. Nenhum foi digitado.
2068
+ -- Dar ao V2 uma tela de movimenta\xE7\xE3o manual e chamar isso de paridade seria
2069
+ -- entregar a tela que eles nunca abriram.
2070
+ --
2071
+ -- \u2500\u2500 N\xE3o reimplementa estoque nem t\xEDtulo. CHAMA quem j\xE1 faz \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2072
+ --
2073
+ -- O V2 j\xE1 tem as duas primitivas, e as duas j\xE1 s\xE3o idempotentes:
2074
+ --
2075
+ -- public.inventory_receive(location, lines, document, idempotency_key)
2076
+ -- public.financial_create_invoice(direction, total, counterparty, \u2026, key)
2077
+ --
2078
+ -- Reescrever INSERT em \`plg_inventory_stock_movements\` aqui daria uma segunda
2079
+ -- resposta para \xABcomo entra estoque\xBB \u2014 e a posi\xE7\xE3o \xE9 derivada por gatilho, ent\xE3o
2080
+ -- a segunda resposta sairia com saldo certo e sem ningu\xE9m acusar. A entrada de
2081
+ -- nota \xE9 mais um CHAMADOR dessas portas, como a requisi\xE7\xE3o e a contagem.
2082
+ --
2083
+ -- Idempot\xEAncia vem de gra\xE7a da\xED: a chave \xE9 derivada da chave de acesso da NF-e,
2084
+ -- que \xE9 \xFAnica por documento fiscal no Brasil. Aceitar duas vezes devolve o mesmo
2085
+ -- resultado sem escrever de novo \u2014 o AC7 do plano.
2086
+ --
2087
+ -- \u2500\u2500 O total do t\xEDtulo \xE9 a soma das PARCELAS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2088
+ --
2089
+ -- Regra herdada da \`plugin-financial/084\`, que mediu: em 6,3% das notas a soma
2090
+ -- das duplicatas n\xE3o bate com o total da nota. \`plg_financial_invoices\` \xE9
2091
+ -- imut\xE1vel depois de emitido, ent\xE3o um t\xEDtulo aberto com o total errado fica
2092
+ -- com saldo errado para sempre em toda tela que faz \xABtotal \u2212 pago\xBB.
2093
+ -- O total da nota continua leg\xEDvel no \`metadata\` e na caixa de entrada.
2094
+ --
2095
+ -- E a MESMA chave de idempot\xEAncia da 084 (\`danfe:<chave>\`): uma nota que o porte
2096
+ -- hist\xF3rico j\xE1 transformou em t\xEDtulo n\xE3o pode ganhar um segundo aqui.
2097
+ --
2098
+ -- \u2500\u2500 Despesa e servi\xE7o n\xE3o viram estoque \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2099
+ --
2100
+ -- \`entry_type\` decide por LINHA. A mesma NF-e traz o insumo que entra no estoque
2101
+ -- e a taxa de entrega que s\xF3 vira despesa \u2014 e as duas precisam ir para o mesmo
2102
+ -- t\xEDtulo a pagar. Por isso a lista de linhas do estoque \xE9 um FILTRO sobre os
2103
+ -- itens, n\xE3o os itens.
2104
+ --
2105
+ -- Idempotente. S\xF3 cria fun\xE7\xE3o.
2106
+ -- ---------------------------------------------------------------------------
2107
+
2108
+ -- \u2500\u2500 Ajudantes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2109
+
2110
+ -- O XML traz o CNPJ cru e a tela aceita pontua\xE7\xE3o. Duas grafias do mesmo
2111
+ -- fornecedor viravam dois de-para no V1, e a segunda nota n\xE3o achava o primeiro.
2112
+ CREATE OR REPLACE FUNCTION public.plg_fiscalbr_digits(p_value text)
2113
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO '' AS $$
2114
+ SELECT regexp_replace(coalesce(p_value, ''), '[^0-9]', '', 'g')
2115
+ $$;
2116
+
2117
+ -- \u2500\u2500 Receber a nota: o XML j\xE1 lido vira linhas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2118
+ --
2119
+ -- O XML \xE9 decomposto no NAVEGADOR e chega aqui como jsonb. Parsear NF-e em
2120
+ -- PL/pgSQL seria escrever um leitor de XML dentro do banco, sem teste unit\xE1rio
2121
+ -- poss\xEDvel e sem a mensagem de erro que a tela precisa dar para um arquivo que
2122
+ -- n\xE3o \xE9 uma nota.
2123
+
2124
+ CREATE OR REPLACE FUNCTION public.fiscalbr_stage_inbox_document(p_note jsonb)
2125
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$
2126
+ DECLARE
2127
+ v_tenant uuid := public.plg_financial_require_tenant();
2128
+ v_key text := btrim(coalesce(p_note->>'access_key', ''));
2129
+ v_cnpj text := public.plg_fiscalbr_digits(p_note->'supplier'->>'cnpj');
2130
+ v_doc public.plg_fiscalbr_inbox_documents%ROWTYPE;
2131
+ v_unit uuid := nullif(p_note->>'unit_id', '')::uuid;
2132
+ v_party uuid;
2133
+ v_item jsonb;
2134
+ v_map public.plg_fiscalbr_supplier_product_map%ROWTYPE;
2135
+ v_n integer := 0;
2136
+ v_matched integer := 0;
2137
+ BEGIN
2138
+ PERFORM public.plg_financial_require_permission('financial.create', v_unit);
2139
+
2140
+ IF length(v_key) <> 44 THEN
2141
+ RAISE EXCEPTION 'fiscalbr: chave de acesso inv\xE1lida (esperado 44 d\xEDgitos, veio %)', length(v_key)
2142
+ USING ERRCODE = '22023';
2143
+ END IF;
2144
+
2145
+ SELECT * INTO v_doc FROM public.plg_fiscalbr_inbox_documents
2146
+ WHERE tenant_id = v_tenant AND access_key = v_key;
2147
+
2148
+ -- Nota j\xE1 aceita n\xE3o \xE9 reimportada: o XML \xE9 o mesmo documento fiscal, e
2149
+ -- reescrever os itens apagaria a decis\xE3o que virou estoque e t\xEDtulo.
2150
+ IF FOUND AND (v_doc.stocked_at IS NOT NULL OR v_doc.invoice_id IS NOT NULL) THEN
2151
+ RETURN jsonb_build_object(
2152
+ 'document_id', v_doc.id, 'access_key', v_key, 'already_accepted', true,
2153
+ 'invoice_id', v_doc.invoice_id, 'items', 0, 'matched', 0);
2154
+ END IF;
2155
+
2156
+ -- O fornecedor \xE9 uma PESSOA, n\xE3o um nome. A 094 do financial documenta o custo
2157
+ -- de n\xE3o ter o ponteiro: praticamente todo t\xEDtulo nascido de DANFE ficou sem
2158
+ -- contraparte. Casar por CNPJ acerta; casar por nome acertaria 137 de 554.
2159
+ IF v_cnpj <> '' THEN
2160
+ SELECT id INTO v_party FROM public.people
2161
+ WHERE tenant_id = v_tenant AND public.plg_fiscalbr_digits(document_number) = v_cnpj
2162
+ LIMIT 1;
2163
+ END IF;
2164
+
2165
+ INSERT INTO public.plg_fiscalbr_inbox_documents (
2166
+ tenant_id, unit_id, access_key, nfe_number, nfe_series, nfe_model, issue_date,
2167
+ received_at, status, nature_operation, supplier_party_id, supplier_cnpj,
2168
+ supplier_name, supplier_ie, total_value, total_products, total_discount,
2169
+ total_freight, total_insurance, total_other, additional_info, metadata)
2170
+ VALUES (
2171
+ v_tenant, v_unit, v_key, p_note->>'nfe_number', p_note->>'nfe_series',
2172
+ coalesce(p_note->>'nfe_model', '55'), nullif(p_note->>'issue_date', '')::date,
2173
+ now(), 'pending', p_note->>'nature_operation', v_party, nullif(v_cnpj, ''),
2174
+ p_note->'supplier'->>'name', p_note->'supplier'->>'ie',
2175
+ (p_note->'totals'->>'total')::numeric, (p_note->'totals'->>'products')::numeric,
2176
+ (p_note->'totals'->>'discount')::numeric, (p_note->'totals'->>'freight')::numeric,
2177
+ (p_note->'totals'->>'insurance')::numeric, (p_note->'totals'->>'other')::numeric,
2178
+ p_note->>'additional_info',
2179
+ jsonb_build_object('source', coalesce(p_note->>'source', 'xml_upload')))
2180
+ -- O \xEDndice \xFAnico da 002 \xE9 PARCIAL (\`WHERE access_key IS NOT NULL\`), e a
2181
+ -- infer\xEAncia do ON CONFLICT s\xF3 o alcan\xE7a repetindo o predicado. Sem ele o
2182
+ -- INSERT levanta \xABno unique or exclusion constraint matching\xBB \u2014 e levantaria
2183
+ -- na primeira reimporta\xE7\xE3o, n\xE3o na aplica\xE7\xE3o.
2184
+ ON CONFLICT (tenant_id, access_key) WHERE access_key IS NOT NULL DO UPDATE SET
2185
+ -- Reimportar o mesmo XML corrige o cabe\xE7alho; a contraparte s\xF3 \xE9 preenchida
2186
+ -- quando est\xE1 vazia, para n\xE3o desfazer uma costura feita \xE0 m\xE3o.
2187
+ nfe_number = EXCLUDED.nfe_number,
2188
+ nature_operation = coalesce(EXCLUDED.nature_operation, public.plg_fiscalbr_inbox_documents.nature_operation),
2189
+ supplier_party_id = coalesce(public.plg_fiscalbr_inbox_documents.supplier_party_id, EXCLUDED.supplier_party_id),
2190
+ supplier_name = EXCLUDED.supplier_name,
2191
+ total_value = EXCLUDED.total_value,
2192
+ updated_at = now()
2193
+ RETURNING * INTO v_doc;
2194
+
2195
+ -- Os itens e as duplicatas s\xE3o do DOCUMENTO, n\xE3o do operador: v\xEAm do XML e
2196
+ -- podem ser reescritos enquanto a nota n\xE3o foi aceita.
2197
+ DELETE FROM public.plg_fiscalbr_inbox_items WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
2198
+ DELETE FROM public.plg_fiscalbr_inbox_duplicates WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
2199
+ DELETE FROM public.plg_fiscalbr_inbox_payments WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
2200
+
2201
+ FOR v_item IN SELECT * FROM jsonb_array_elements(coalesce(p_note->'items', '[]'::jsonb)) LOOP
2202
+ v_n := v_n + 1;
2203
+
2204
+ -- O de-para: a segunda nota do mesmo fornecedor chega j\xE1 vinculada. \xC9 o que
2205
+ -- faz importar XML valer a pena \u2014 sem ele toda nota recome\xE7a do zero.
2206
+ SELECT * INTO v_map FROM public.plg_fiscalbr_supplier_product_map
2207
+ WHERE tenant_id = v_tenant AND supplier_cnpj = v_cnpj
2208
+ AND supplier_product_code = coalesce(v_item->>'supplier_product_code', '');
2209
+ IF FOUND THEN v_matched := v_matched + 1; END IF;
2210
+
2211
+ INSERT INTO public.plg_fiscalbr_inbox_items (
2212
+ tenant_id, inbox_document_id, item_number, supplier_product_code, ean_barcode,
2213
+ description, ncm, cest, cfop, unit_commercial, quantity_commercial,
2214
+ unit_price_commercial, total_price, discount, freight, other_expenses,
2215
+ icms_cst, icms_csosn, icms_base, icms_rate, icms_value,
2216
+ pis_value, cofins_value, ipi_value, raw_xml_item,
2217
+ product_id, entry_type, chart_account_id, stock_unit_id, conversion_factor,
2218
+ cost_mode, reconciled, stock_quantity, unit_cost_new)
2219
+ VALUES (
2220
+ v_tenant, v_doc.id, coalesce((v_item->>'item_number')::int, v_n),
2221
+ v_item->>'supplier_product_code', v_item->>'ean_barcode',
2222
+ v_item->>'description', v_item->>'ncm', v_item->>'cest', v_item->>'cfop',
2223
+ v_item->>'unit', (v_item->>'quantity')::numeric,
2224
+ (v_item->>'unit_price')::numeric, (v_item->>'total')::numeric,
2225
+ (v_item->>'discount')::numeric, (v_item->>'freight')::numeric,
2226
+ (v_item->>'other')::numeric,
2227
+ v_item->>'icms_cst', v_item->>'icms_csosn', (v_item->>'icms_base')::numeric,
2228
+ (v_item->>'icms_rate')::numeric, (v_item->>'icms_value')::numeric,
2229
+ (v_item->>'pis_value')::numeric, (v_item->>'cofins_value')::numeric,
2230
+ (v_item->>'ipi_value')::numeric, v_item,
2231
+ v_map.product_id, v_map.entry_type, v_map.chart_account_id, v_map.stock_unit_id,
2232
+ coalesce(v_map.conversion_factor, 1), coalesce(v_map.cost_mode, 'novo'),
2233
+ v_map.product_id IS NOT NULL,
2234
+ -- A quantidade de estoque j\xE1 sai calculada quando o de-para deu o fator: \xE9
2235
+ -- o n\xFAmero que a tela mostra antes de aceitar.
2236
+ CASE WHEN v_map.product_id IS NOT NULL
2237
+ THEN (v_item->>'quantity')::numeric * coalesce(v_map.conversion_factor, 1) END,
2238
+ CASE WHEN v_map.product_id IS NOT NULL AND coalesce(v_map.conversion_factor, 1) > 0
2239
+ THEN (v_item->>'unit_price')::numeric / coalesce(v_map.conversion_factor, 1) END);
2240
+ END LOOP;
2241
+
2242
+ INSERT INTO public.plg_fiscalbr_inbox_duplicates (
2243
+ tenant_id, inbox_document_id, duplicate_index, number, due_date, value, payment_type)
2244
+ SELECT v_tenant, v_doc.id, coalesce((d->>'index')::int, ord), d->>'number',
2245
+ nullif(d->>'due_date', '')::date, (d->>'value')::numeric,
2246
+ nullif(d->>'payment_type', '')
2247
+ FROM jsonb_array_elements(coalesce(p_note->'duplicates', '[]'::jsonb)) WITH ORDINALITY AS t(d, ord);
2248
+
2249
+ INSERT INTO public.plg_fiscalbr_inbox_payments (
2250
+ tenant_id, inbox_document_id, payment_index, payment_indicator,
2251
+ payment_type_code, payment_type_label, value)
2252
+ SELECT v_tenant, v_doc.id, coalesce((p->>'index')::int, ord), p->>'indicator',
2253
+ p->>'code', p->>'label', (p->>'value')::numeric
2254
+ FROM jsonb_array_elements(coalesce(p_note->'payments', '[]'::jsonb)) WITH ORDINALITY AS t(p, ord);
2255
+
2256
+ RETURN jsonb_build_object(
2257
+ 'document_id', v_doc.id, 'access_key', v_key, 'already_accepted', false,
2258
+ 'supplier_party_id', v_party, 'items', v_n, 'matched', v_matched);
2259
+ END $function$;
2260
+
2261
+ REVOKE ALL ON FUNCTION public.fiscalbr_stage_inbox_document(jsonb) FROM PUBLIC;
2262
+ GRANT EXECUTE ON FUNCTION public.fiscalbr_stage_inbox_document(jsonb) TO authenticated;
2263
+
2264
+ -- \u2500\u2500 Salvar a concilia\xE7\xE3o sem aceitar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2265
+ --
2266
+ -- Conciliar e estocar s\xE3o duas coisas no V1 \u2014 da\xED as duas colunas na lista, e
2267
+ -- da\xED a fila de 55 notas conciliadas e n\xE3o estocadas. Quem salva o v\xEDnculo n\xE3o
2268
+ -- est\xE1 mandando dar entrada.
2269
+
2270
+ CREATE OR REPLACE FUNCTION public.fiscalbr_save_inbox_decision(
2271
+ p_document_id uuid,
2272
+ p_items jsonb,
2273
+ p_default_chart_account_id uuid DEFAULT NULL,
2274
+ p_default_location_id uuid DEFAULT NULL,
2275
+ p_duplicates jsonb DEFAULT NULL)
2276
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$
2277
+ DECLARE
2278
+ v_tenant uuid := public.plg_financial_require_tenant();
2279
+ v_doc public.plg_fiscalbr_inbox_documents%ROWTYPE;
2280
+ v_row jsonb;
2281
+ v_done integer := 0;
2282
+ v_total integer;
2283
+ BEGIN
2284
+ SELECT * INTO v_doc FROM public.plg_fiscalbr_inbox_documents
2285
+ WHERE tenant_id = v_tenant AND id = p_document_id;
2286
+ IF NOT FOUND THEN
2287
+ RAISE EXCEPTION 'fiscalbr: nota % n\xE3o encontrada neste tenant', p_document_id USING ERRCODE = 'P0002';
2288
+ END IF;
2289
+ PERFORM public.plg_financial_require_permission('financial.edit', v_doc.unit_id);
2290
+
2291
+ IF v_doc.stocked_at IS NOT NULL OR v_doc.invoice_id IS NOT NULL THEN
2292
+ RAISE EXCEPTION 'fiscalbr: nota % j\xE1 foi aceita \u2014 a decis\xE3o n\xE3o muda depois do estoque', p_document_id
2293
+ USING ERRCODE = '23514';
2294
+ END IF;
2295
+
2296
+ UPDATE public.plg_fiscalbr_inbox_documents SET
2297
+ default_chart_account_id = coalesce(p_default_chart_account_id, default_chart_account_id),
2298
+ default_location_id = coalesce(p_default_location_id, default_location_id),
2299
+ reviewed_at = now(), updated_at = now()
2300
+ WHERE id = v_doc.id;
2301
+
2302
+ FOR v_row IN SELECT * FROM jsonb_array_elements(coalesce(p_items, '[]'::jsonb)) LOOP
2303
+ UPDATE public.plg_fiscalbr_inbox_items i SET
2304
+ product_id = nullif(v_row->>'product_id', '')::uuid,
2305
+ entry_type = nullif(v_row->>'entry_type', ''),
2306
+ chart_account_id = nullif(v_row->>'chart_account_id', '')::uuid,
2307
+ stock_unit_id = nullif(v_row->>'stock_unit_id', '')::uuid,
2308
+ conversion_factor = coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1),
2309
+ cost_mode = coalesce(nullif(v_row->>'cost_mode', ''), 'novo'),
2310
+ shortage_quantity = nullif(v_row->>'shortage_quantity', '')::numeric,
2311
+ shortage_reason = nullif(v_row->>'shortage_reason', ''),
2312
+ shortage_note = nullif(v_row->>'shortage_note', ''),
2313
+ -- Derivadas, gravadas: \xE9 o que o operador viu.
2314
+ stock_quantity = i.quantity_commercial * coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1)
2315
+ - coalesce(nullif(v_row->>'shortage_quantity', '')::numeric, 0),
2316
+ unit_cost_new = CASE WHEN coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1) > 0
2317
+ THEN i.unit_price_commercial / coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1) END,
2318
+ -- Uma linha est\xE1 conciliada quando sabe o que \xE9. Despesa e servi\xE7o n\xE3o
2319
+ -- pedem produto: elas nunca v\xE3o virar estoque.
2320
+ reconciled = CASE
2321
+ WHEN nullif(v_row->>'entry_type', '') IN ('despesa','servico') THEN true
2322
+ ELSE nullif(v_row->>'product_id', '') IS NOT NULL
2323
+ END,
2324
+ updated_at = now()
2325
+ WHERE i.tenant_id = v_tenant AND i.inbox_document_id = v_doc.id
2326
+ AND i.id = (v_row->>'id')::uuid;
2327
+ v_done := v_done + 1;
2328
+ END LOOP;
2329
+
2330
+ IF p_duplicates IS NOT NULL THEN
2331
+ DELETE FROM public.plg_fiscalbr_inbox_duplicates
2332
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
2333
+ INSERT INTO public.plg_fiscalbr_inbox_duplicates (
2334
+ tenant_id, inbox_document_id, duplicate_index, number, due_date, value,
2335
+ payment_type, barcode, pix_key, payment_method_id)
2336
+ SELECT v_tenant, v_doc.id, coalesce((d->>'index')::int, ord), d->>'number',
2337
+ nullif(d->>'due_date', '')::date, (d->>'value')::numeric,
2338
+ nullif(d->>'payment_type', ''), nullif(d->>'barcode', ''),
2339
+ nullif(d->>'pix_key', ''), nullif(d->>'payment_method_id', '')::uuid
2340
+ FROM jsonb_array_elements(p_duplicates) WITH ORDINALITY AS t(d, ord);
2341
+ END IF;
2342
+
2343
+ SELECT count(*) INTO v_total FROM public.plg_fiscalbr_inbox_items
2344
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
2345
+
2346
+ UPDATE public.plg_fiscalbr_inbox_documents SET status = CASE
2347
+ WHEN (SELECT count(*) FROM public.plg_fiscalbr_inbox_items
2348
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id AND reconciled) = v_total
2349
+ THEN 'accepted' ELSE 'partial' END
2350
+ WHERE id = v_doc.id;
2351
+
2352
+ RETURN jsonb_build_object('document_id', v_doc.id, 'saved', v_done, 'items', v_total,
2353
+ 'reconciled', (SELECT count(*) FROM public.plg_fiscalbr_inbox_items
2354
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id AND reconciled));
2355
+ END $function$;
2356
+
2357
+ REVOKE ALL ON FUNCTION public.fiscalbr_save_inbox_decision(uuid, jsonb, uuid, uuid, jsonb) FROM PUBLIC;
2358
+ GRANT EXECUTE ON FUNCTION public.fiscalbr_save_inbox_decision(uuid, jsonb, uuid, uuid, jsonb) TO authenticated;
2359
+
2360
+ -- \u2500\u2500 Aceitar: estoque e t\xEDtulo, de uma vez \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2361
+
2362
+ CREATE OR REPLACE FUNCTION public.fiscalbr_accept_inbox_document(
2363
+ p_document_id uuid,
2364
+ p_location_id uuid DEFAULT NULL)
2365
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$
2366
+ DECLARE
2367
+ v_tenant uuid := public.plg_financial_require_tenant();
2368
+ v_doc public.plg_fiscalbr_inbox_documents%ROWTYPE;
2369
+ v_loc uuid;
2370
+ v_lines jsonb := '[]'::jsonb;
2371
+ v_insts jsonb := '[]'::jsonb;
2372
+ v_recv jsonb;
2373
+ v_inv jsonb;
2374
+ v_it record;
2375
+ v_no integer := 0;
2376
+ v_total numeric := 0;
2377
+ v_stock_n integer := 0;
2378
+ v_key text;
2379
+ v_invoice uuid;
2380
+ -- Posi\xE7\xE3o na lista de linhas \u2192 id do item da nota. \`line_no\` do movimento
2381
+ -- indexa este array direto.
2382
+ v_item_ids uuid[] := '{}';
2383
+ BEGIN
2384
+ SELECT * INTO v_doc FROM public.plg_fiscalbr_inbox_documents
2385
+ WHERE tenant_id = v_tenant AND id = p_document_id;
2386
+ IF NOT FOUND THEN
2387
+ RAISE EXCEPTION 'fiscalbr: nota % n\xE3o encontrada neste tenant', p_document_id USING ERRCODE = 'P0002';
2388
+ END IF;
2389
+ PERFORM public.plg_financial_require_permission('financial.create', v_doc.unit_id);
2390
+
2391
+ -- J\xE1 aceita devolve o que aceitou. N\xE3o \xE9 erro: \xE9 a segunda vez que o operador
2392
+ -- clicou, e o AC7 diz que isso n\xE3o pode dobrar nada.
2393
+ IF v_doc.stocked_at IS NOT NULL OR v_doc.invoice_id IS NOT NULL THEN
2394
+ RETURN jsonb_build_object('document_id', v_doc.id, 'already_accepted', true,
2395
+ 'invoice_id', v_doc.invoice_id, 'operation_id', v_doc.inventory_operation_id);
2396
+ END IF;
2397
+
2398
+ v_loc := coalesce(p_location_id, v_doc.default_location_id);
2399
+
2400
+ -- Toda linha tem de saber o que \xE9. Aceitar uma nota com item sem tipo gravaria
2401
+ -- um t\xEDtulo cujo rateio ningu\xE9m consegue explicar depois.
2402
+ IF EXISTS (SELECT 1 FROM public.plg_fiscalbr_inbox_items
2403
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id
2404
+ AND (entry_type IS NULL OR entry_type = '')) THEN
2405
+ RAISE EXCEPTION 'fiscalbr: h\xE1 item sem tipo de registro \u2014 conciliar antes de aceitar'
2406
+ USING ERRCODE = '23514';
2407
+ END IF;
2408
+
2409
+ -- As linhas que viram estoque. Despesa e servi\xE7o ficam fora daqui e entram s\xF3
2410
+ -- no t\xEDtulo.
2411
+ FOR v_it IN
2412
+ SELECT * FROM public.plg_fiscalbr_inbox_items
2413
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id
2414
+ AND entry_type IN ('insumo','revenda','patrimonio')
2415
+ ORDER BY item_number
2416
+ LOOP
2417
+ IF v_it.product_id IS NULL THEN
2418
+ RAISE EXCEPTION 'fiscalbr: item % \xE9 % e n\xE3o tem produto vinculado', v_it.item_number, v_it.entry_type
2419
+ USING ERRCODE = '23514';
2420
+ END IF;
2421
+ v_no := v_no + 1;
2422
+ -- A ordem desta lista \xC9 o \`line_no\` que o movimento vai carregar. Guardo o
2423
+ -- id da linha na mesma posi\xE7\xE3o para devolver o movimento ao item sem
2424
+ -- reconsultar \u2014 e sem depender de casar por produto, que repete quando a
2425
+ -- mesma mercadoria vem em duas linhas da nota.
2426
+ v_item_ids := v_item_ids || v_it.id;
2427
+ v_lines := v_lines || jsonb_build_object(
2428
+ 'product_id', v_it.product_id,
2429
+ 'quantity', coalesce(v_it.stock_quantity, v_it.quantity_commercial * v_it.conversion_factor),
2430
+ 'unit_cost', coalesce(v_it.unit_cost_new, v_it.unit_price_commercial / v_it.conversion_factor),
2431
+ 'measurement_unit_id', v_it.stock_unit_id,
2432
+ 'notes', v_it.description);
2433
+ END LOOP;
2434
+ v_stock_n := v_no;
2435
+
2436
+ IF v_stock_n > 0 THEN
2437
+ IF v_loc IS NULL THEN
2438
+ RAISE EXCEPTION 'fiscalbr: a nota tem % linha(s) de estoque e nenhum local de estoque escolhido', v_stock_n
2439
+ USING ERRCODE = '22023';
2440
+ END IF;
2441
+ -- A chave de acesso \xE9 \xFAnica por documento fiscal no Brasil: \xE9 a chave de
2442
+ -- idempot\xEAncia natural, e \xE9 ela que faz o segundo clique n\xE3o dobrar saldo.
2443
+ v_recv := public.inventory_receive(
2444
+ v_loc, v_lines,
2445
+ jsonb_build_object(
2446
+ 'supplier_id', v_doc.supplier_party_id,
2447
+ 'date', v_doc.issue_date,
2448
+ 'type', 'nfe',
2449
+ 'ref', 'NF-e ' || coalesce(v_doc.nfe_number, '?'),
2450
+ 'notes', 'Entrada via DANFE NF-e ' || coalesce(v_doc.nfe_number, '?')),
2451
+ 'nfe:' || v_doc.access_key);
2452
+
2453
+ -- Cada movimento volta para a SUA linha. Guardar s\xF3 no cabe\xE7alho responderia
2454
+ -- \xABesta nota entrou?\xBB e nunca \xABesta linha entrou?\xBB \u2014 e \xE9 a segunda pergunta
2455
+ -- que a concilia\xE7\xE3o parcial faz.
2456
+ --
2457
+ -- \`line_no\` do movimento \xE9 a posi\xE7\xE3o na lista que montei acima, e
2458
+ -- \`v_item_ids\` est\xE1 na mesma ordem: um join, n\xE3o uma varredura por posi\xE7\xE3o.
2459
+ UPDATE public.plg_fiscalbr_inbox_items i
2460
+ SET stock_movement_id = m.movement_id, updated_at = now()
2461
+ FROM (
2462
+ SELECT v_item_ids[(mov->>'line_no')::int] AS item_id, (mov->>'id')::uuid AS movement_id
2463
+ FROM jsonb_array_elements(coalesce(v_recv->'movements', '[]'::jsonb)) AS t(mov)
2464
+ WHERE (mov->>'line_no') IS NOT NULL
2465
+ ) m
2466
+ WHERE i.tenant_id = v_tenant AND i.id = m.item_id;
2467
+ END IF;
2468
+
2469
+ -- \u2500\u2500 O t\xEDtulo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2470
+ -- Total = soma das PARCELAS (regra da financial/084). Nota sem duplicata vira
2471
+ -- uma parcela s\xF3, no vencimento que a nota declarar ou na emiss\xE3o.
2472
+ SELECT coalesce(sum(value), 0),
2473
+ coalesce(jsonb_agg(jsonb_build_object(
2474
+ 'number', coalesce(duplicate_index, 1),
2475
+ 'due_date', coalesce(due_date, v_doc.issue_date, current_date),
2476
+ 'amount', round(value, 2),
2477
+ 'metadata', jsonb_strip_nulls(jsonb_build_object(
2478
+ 'barcode', barcode, 'pixKey', pix_key, 'paymentType', payment_type,
2479
+ 'danfeNumber', number))) ORDER BY duplicate_index), '[]'::jsonb)
2480
+ INTO v_total, v_insts
2481
+ FROM public.plg_fiscalbr_inbox_duplicates
2482
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
2483
+
2484
+ IF jsonb_array_length(v_insts) = 0 THEN
2485
+ v_total := round(coalesce(v_doc.total_value, 0), 2);
2486
+ v_insts := jsonb_build_array(jsonb_build_object(
2487
+ 'number', 1, 'due_date', coalesce(v_doc.issue_date, current_date), 'amount', v_total));
2488
+ ELSE
2489
+ v_total := round(v_total, 2);
2490
+ END IF;
2491
+
2492
+ -- A MESMA chave da 084: nota j\xE1 transformada em t\xEDtulo pelo porte hist\xF3rico
2493
+ -- n\xE3o ganha um segundo aqui.
2494
+ v_key := 'danfe:' || v_doc.access_key;
2495
+
2496
+ v_inv := public.financial_create_invoice(
2497
+ 'payable', v_total, v_doc.supplier_party_id, NULL, v_doc.unit_id, v_insts,
2498
+ v_doc.issue_date,
2499
+ 'NF-e ' || coalesce(v_doc.nfe_number, '?') || ' \xB7 ' || coalesce(v_doc.supplier_name, 'fornecedor'),
2500
+ NULL,
2501
+ jsonb_build_object(
2502
+ 'source', 'danfe',
2503
+ 'danfe', jsonb_strip_nulls(jsonb_build_object(
2504
+ 'accessKey', v_doc.access_key, 'number', v_doc.nfe_number,
2505
+ 'series', v_doc.nfe_series, 'supplierName', v_doc.supplier_name,
2506
+ 'supplierCnpj', v_doc.supplier_cnpj,
2507
+ -- O total da NOTA continua leg\xEDvel, mesmo quando difere do total do
2508
+ -- t\xEDtulo. \xC9 a diferen\xE7a que a 084 mediu em 6,3% das notas.
2509
+ 'noteTotal', v_doc.total_value,
2510
+ 'inboxDocumentId', v_doc.id))),
2511
+ v_key);
2512
+ v_invoice := (v_inv->>'invoice_id')::uuid;
2513
+
2514
+ -- As linhas do t\xEDtulo: TODOS os itens, inclusive despesa e servi\xE7o. \xC9 o que
2515
+ -- responde \xABde que essa conta \xE9 feita\xBB.
2516
+ INSERT INTO public.items (tenant_id, invoice_id, name, quantity, unit_price, total, sort_order, metadata)
2517
+ SELECT v_tenant, v_invoice, i.description, i.quantity_commercial, i.unit_price_commercial,
2518
+ round(coalesce(i.total_price, 0), 2), i.item_number,
2519
+ jsonb_strip_nulls(jsonb_build_object(
2520
+ 'itemKind', i.entry_type, 'referenceId', i.product_id,
2521
+ 'chartAccountId', i.chart_account_id, 'ncm', i.ncm, 'cfop', i.cfop,
2522
+ 'supplierProductCode', i.supplier_product_code))
2523
+ FROM public.plg_fiscalbr_inbox_items i
2524
+ WHERE i.tenant_id = v_tenant AND i.inbox_document_id = v_doc.id
2525
+ ORDER BY i.item_number
2526
+ ON CONFLICT DO NOTHING;
2527
+
2528
+ UPDATE public.plg_fiscalbr_inbox_documents SET
2529
+ invoice_id = v_invoice,
2530
+ inventory_operation_id = coalesce((v_recv->>'operation_id')::uuid, inventory_operation_id),
2531
+ stocked_at = CASE WHEN v_stock_n > 0 THEN now() ELSE stocked_at END,
2532
+ status = 'accepted', updated_at = now()
2533
+ WHERE id = v_doc.id;
2534
+
2535
+ -- \u2500\u2500 O de-para aprende \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2536
+ -- A decis\xE3o desta nota \xE9 o que faz a pr\xF3xima do mesmo fornecedor chegar
2537
+ -- pronta. Guardar isto \xE9 o que separa \xABimportei XML\xBB de \xABo sistema sabe o meu
2538
+ -- fornecedor\xBB.
2539
+ INSERT INTO public.plg_fiscalbr_supplier_product_map (
2540
+ tenant_id, supplier_cnpj, supplier_product_code, product_id, entry_type,
2541
+ chart_account_id, stock_unit_id, conversion_factor, cost_mode, hit_count, last_used_at)
2542
+ SELECT v_tenant, v_doc.supplier_cnpj, i.supplier_product_code, i.product_id,
2543
+ i.entry_type, coalesce(i.chart_account_id, v_doc.default_chart_account_id),
2544
+ i.stock_unit_id, i.conversion_factor, i.cost_mode, 1, now()
2545
+ FROM public.plg_fiscalbr_inbox_items i
2546
+ WHERE i.tenant_id = v_tenant AND i.inbox_document_id = v_doc.id
2547
+ AND i.product_id IS NOT NULL
2548
+ AND coalesce(v_doc.supplier_cnpj, '') <> ''
2549
+ AND coalesce(i.supplier_product_code, '') <> ''
2550
+ ON CONFLICT (tenant_id, supplier_cnpj, supplier_product_code) DO UPDATE SET
2551
+ product_id = EXCLUDED.product_id,
2552
+ entry_type = EXCLUDED.entry_type,
2553
+ chart_account_id = coalesce(EXCLUDED.chart_account_id, public.plg_fiscalbr_supplier_product_map.chart_account_id),
2554
+ stock_unit_id = coalesce(EXCLUDED.stock_unit_id, public.plg_fiscalbr_supplier_product_map.stock_unit_id),
2555
+ conversion_factor = EXCLUDED.conversion_factor,
2556
+ cost_mode = EXCLUDED.cost_mode,
2557
+ hit_count = public.plg_fiscalbr_supplier_product_map.hit_count + 1,
2558
+ last_used_at = now(),
2559
+ updated_at = now();
2560
+
2561
+ RETURN jsonb_build_object(
2562
+ 'document_id', v_doc.id, 'already_accepted', false,
2563
+ 'invoice_id', v_invoice, 'invoice_total', v_total,
2564
+ 'installments', jsonb_array_length(v_insts),
2565
+ 'operation_id', v_recv->>'operation_id',
2566
+ 'stock_lines', v_stock_n,
2567
+ 'movements', coalesce(v_recv->'movements', '[]'::jsonb));
2568
+ END $function$;
2569
+
2570
+ REVOKE ALL ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) FROM PUBLIC;
2571
+ GRANT EXECUTE ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) TO authenticated;
2572
+
2573
+ -- \u2500\u2500 Rejeitar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2574
+ -- O rodap\xE9 do di\xE1logo do V1 tem dois bot\xF5es, e o outro tamb\xE9m precisa existir:
2575
+ -- recusar a nota \xE9 dizer que ela n\xE3o vira nada aqui \u2014 sem apagar o documento,
2576
+ -- que \xE9 fiscal e tem de continuar leg\xEDvel.
2577
+
2578
+ CREATE OR REPLACE FUNCTION public.fiscalbr_reject_inbox_document(
2579
+ p_document_id uuid, p_reason text DEFAULT NULL)
2580
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$
2581
+ DECLARE
2582
+ v_tenant uuid := public.plg_financial_require_tenant();
2583
+ v_doc public.plg_fiscalbr_inbox_documents%ROWTYPE;
2584
+ BEGIN
2585
+ SELECT * INTO v_doc FROM public.plg_fiscalbr_inbox_documents
2586
+ WHERE tenant_id = v_tenant AND id = p_document_id;
2587
+ IF NOT FOUND THEN
2588
+ RAISE EXCEPTION 'fiscalbr: nota % n\xE3o encontrada neste tenant', p_document_id USING ERRCODE = 'P0002';
2589
+ END IF;
2590
+ PERFORM public.plg_financial_require_permission('financial.edit', v_doc.unit_id);
2591
+
2592
+ IF v_doc.stocked_at IS NOT NULL OR v_doc.invoice_id IS NOT NULL THEN
2593
+ RAISE EXCEPTION 'fiscalbr: nota % j\xE1 virou estoque ou t\xEDtulo \u2014 recusar n\xE3o desfaz', p_document_id
2594
+ USING ERRCODE = '23514';
2595
+ END IF;
2596
+
2597
+ UPDATE public.plg_fiscalbr_inbox_documents SET
2598
+ status = 'rejected',
2599
+ metadata = metadata || jsonb_strip_nulls(jsonb_build_object('rejectReason', p_reason)),
2600
+ reviewed_at = now(), updated_at = now()
2601
+ WHERE id = v_doc.id;
2602
+
2603
+ RETURN jsonb_build_object('document_id', v_doc.id, 'status', 'rejected');
2604
+ END $function$;
2605
+
2606
+ REVOKE ALL ON FUNCTION public.fiscalbr_reject_inbox_document(uuid, text) FROM PUBLIC;
2607
+ GRANT EXECUTE ON FUNCTION public.fiscalbr_reject_inbox_document(uuid, text) TO authenticated;
2608
+ `;
2609
+ var MIGRATION_010_A_DATA_DE_EMISSAO_E_INSTANTE_E_A_COMPETENCIA_E_DIA = `-- ---------------------------------------------------------------------------
2610
+ -- 010_a_data_de_emissao_e_instante_e_a_competencia_e_dia.sql
2611
+ --
2612
+ -- A 009 n\xE3o conseguia aceitar nota nenhuma. Descoberto no primeiro teste com
2613
+ -- usu\xE1rio logado de verdade, que \xE9 onde ela ia ser usada:
2614
+ --
2615
+ -- POST rpc/fiscalbr_accept_inbox_document \u2192 404
2616
+ -- function public.financial_create_invoice(unknown, numeric, uuid, unknown,
2617
+ -- uuid, jsonb, timestamp with time zone, text, unknown, jsonb, text)
2618
+ -- does not exist
2619
+ --
2620
+ -- \`plg_fiscalbr_inbox_documents.issue_date\` \xE9 **timestamptz** \u2014 a NF-e declara um
2621
+ -- INSTANTE (\`dhEmi\`, com fuso), e a 002 preservou isso. Eu escrevi a chamada
2622
+ -- supondo \`date\`, e \`financial_create_invoice\` pede \`p_competence_date date\`.
2623
+ --
2624
+ -- Nenhum teste pegou porque nenhum podia: o SQL aplica, a fun\xE7\xE3o compila (o corpo
2625
+ -- de PL/pgSQL n\xE3o resolve assinatura em CREATE), e o erro s\xF3 existe na chamada.
2626
+ -- Foi preciso um usu\xE1rio logado percorrendo o caminho.
2627
+ --
2628
+ -- \u2500\u2500 Duas corre\xE7\xF5es, n\xE3o uma \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2629
+ --
2630
+ -- 1. \`issue_date::date\` onde a compet\xEAncia \xE9 pedida. Instante \xE9 quando a nota foi
2631
+ -- emitida; compet\xEAncia \xE9 o DIA a que o t\xEDtulo pertence, e o m\xEAs do t\xEDtulo vem
2632
+ -- dela \u2014 um \`timestamptz\` viajando para l\xE1 levaria fuso para dentro de uma
2633
+ -- conta cont\xE1bil.
2634
+ --
2635
+ -- 2. **Todo argumento tipado na chamada.** A chamada posicional passava \`'payable'\`
2636
+ -- e \`NULL\` sem tipo, e o Postgres tem de adivinhar entre as sobrecargas. Adivinhar
2637
+ -- certo hoje e errado quando algu\xE9m acrescentar uma sobrecarga \xE9 o defeito que
2638
+ -- volta meses depois sem ningu\xE9m mexer neste arquivo. Agora vai \`::text\`,
2639
+ -- \`::uuid\`, \`::date\` em tudo.
2640
+ --
2641
+ -- Idempotente: s\xF3 substitui o corpo de uma fun\xE7\xE3o.
2642
+ -- ---------------------------------------------------------------------------
2643
+
2644
+ CREATE OR REPLACE FUNCTION public.fiscalbr_accept_inbox_document(
2645
+ p_document_id uuid,
2646
+ p_location_id uuid DEFAULT NULL)
2647
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$
2648
+ DECLARE
2649
+ v_tenant uuid := public.plg_financial_require_tenant();
2650
+ v_doc public.plg_fiscalbr_inbox_documents%ROWTYPE;
2651
+ v_loc uuid;
2652
+ v_lines jsonb := '[]'::jsonb;
2653
+ v_insts jsonb := '[]'::jsonb;
2654
+ v_recv jsonb;
2655
+ v_inv jsonb;
2656
+ v_it record;
2657
+ v_no integer := 0;
2658
+ v_total numeric := 0;
2659
+ v_stock_n integer := 0;
2660
+ v_key text;
2661
+ v_invoice uuid;
2662
+ v_item_ids uuid[] := '{}';
2663
+ -- A compet\xEAncia: o DIA da emiss\xE3o, n\xE3o o instante.
2664
+ v_competence date;
2665
+ BEGIN
2666
+ SELECT * INTO v_doc FROM public.plg_fiscalbr_inbox_documents
2667
+ WHERE tenant_id = v_tenant AND id = p_document_id;
2668
+ IF NOT FOUND THEN
2669
+ RAISE EXCEPTION 'fiscalbr: nota % n\xE3o encontrada neste tenant', p_document_id USING ERRCODE = 'P0002';
2670
+ END IF;
2671
+ PERFORM public.plg_financial_require_permission('financial.create', v_doc.unit_id);
2672
+
2673
+ IF v_doc.stocked_at IS NOT NULL OR v_doc.invoice_id IS NOT NULL THEN
2674
+ RETURN jsonb_build_object('document_id', v_doc.id, 'already_accepted', true,
2675
+ 'invoice_id', v_doc.invoice_id, 'operation_id', v_doc.inventory_operation_id);
2676
+ END IF;
2677
+
2678
+ v_loc := coalesce(p_location_id, v_doc.default_location_id);
2679
+ v_competence := (v_doc.issue_date)::date;
2680
+
2681
+ IF EXISTS (SELECT 1 FROM public.plg_fiscalbr_inbox_items
2682
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id
2683
+ AND (entry_type IS NULL OR entry_type = '')) THEN
2684
+ RAISE EXCEPTION 'fiscalbr: h\xE1 item sem tipo de registro \u2014 conciliar antes de aceitar'
2685
+ USING ERRCODE = '23514';
2686
+ END IF;
2687
+
2688
+ FOR v_it IN
2689
+ SELECT * FROM public.plg_fiscalbr_inbox_items
2690
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id
2691
+ AND entry_type IN ('insumo','revenda','patrimonio')
2692
+ ORDER BY item_number
2693
+ LOOP
2694
+ IF v_it.product_id IS NULL THEN
2695
+ RAISE EXCEPTION 'fiscalbr: item % \xE9 % e n\xE3o tem produto vinculado', v_it.item_number, v_it.entry_type
2696
+ USING ERRCODE = '23514';
2697
+ END IF;
2698
+ v_no := v_no + 1;
2699
+ v_item_ids := v_item_ids || v_it.id;
2700
+ v_lines := v_lines || jsonb_build_object(
2701
+ 'product_id', v_it.product_id,
2702
+ 'quantity', coalesce(v_it.stock_quantity, v_it.quantity_commercial * v_it.conversion_factor),
2703
+ 'unit_cost', coalesce(v_it.unit_cost_new, v_it.unit_price_commercial / v_it.conversion_factor),
2704
+ 'measurement_unit_id', v_it.stock_unit_id,
2705
+ 'notes', v_it.description);
2706
+ END LOOP;
2707
+ v_stock_n := v_no;
2708
+
2709
+ IF v_stock_n > 0 THEN
2710
+ IF v_loc IS NULL THEN
2711
+ RAISE EXCEPTION 'fiscalbr: a nota tem % linha(s) de estoque e nenhum local de estoque escolhido', v_stock_n
2712
+ USING ERRCODE = '22023';
2713
+ END IF;
2714
+ v_recv := public.inventory_receive(
2715
+ v_loc::uuid,
2716
+ v_lines::jsonb,
2717
+ jsonb_build_object(
2718
+ 'supplier_id', v_doc.supplier_party_id,
2719
+ -- O documento do estoque tamb\xE9m quer o DIA.
2720
+ 'date', v_competence,
2721
+ 'type', 'nfe',
2722
+ 'ref', 'NF-e ' || coalesce(v_doc.nfe_number, '?'),
2723
+ 'notes', 'Entrada via DANFE NF-e ' || coalesce(v_doc.nfe_number, '?')),
2724
+ ('nfe:' || v_doc.access_key)::text);
2725
+
2726
+ UPDATE public.plg_fiscalbr_inbox_items i
2727
+ SET stock_movement_id = m.movement_id, updated_at = now()
2728
+ FROM (
2729
+ SELECT v_item_ids[(mov->>'line_no')::int] AS item_id, (mov->>'id')::uuid AS movement_id
2730
+ FROM jsonb_array_elements(coalesce(v_recv->'movements', '[]'::jsonb)) AS t(mov)
2731
+ WHERE (mov->>'line_no') IS NOT NULL
2732
+ ) m
2733
+ WHERE i.tenant_id = v_tenant AND i.id = m.item_id;
2734
+ END IF;
2735
+
2736
+ SELECT coalesce(sum(value), 0),
2737
+ coalesce(jsonb_agg(jsonb_build_object(
2738
+ 'number', coalesce(duplicate_index, 1),
2739
+ 'due_date', coalesce(due_date, v_competence, current_date),
2740
+ 'amount', round(value, 2),
2741
+ 'metadata', jsonb_strip_nulls(jsonb_build_object(
2742
+ 'barcode', barcode, 'pixKey', pix_key, 'paymentType', payment_type,
2743
+ 'danfeNumber', number))) ORDER BY duplicate_index), '[]'::jsonb)
2744
+ INTO v_total, v_insts
2745
+ FROM public.plg_fiscalbr_inbox_duplicates
2746
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
2747
+
2748
+ IF jsonb_array_length(v_insts) = 0 THEN
2749
+ v_total := round(coalesce(v_doc.total_value, 0), 2);
2750
+ v_insts := jsonb_build_array(jsonb_build_object(
2751
+ 'number', 1, 'due_date', coalesce(v_competence, current_date), 'amount', v_total));
2752
+ ELSE
2753
+ v_total := round(v_total, 2);
2754
+ END IF;
2755
+
2756
+ v_key := 'danfe:' || v_doc.access_key;
2757
+
2758
+ -- TODO argumento tipado. A chamada posicional sem tipo obrigava o Postgres a
2759
+ -- adivinhar entre sobrecargas, e foi assim que esta fun\xE7\xE3o nasceu quebrada.
2760
+ v_inv := public.financial_create_invoice(
2761
+ 'payable'::text,
2762
+ v_total::numeric,
2763
+ v_doc.supplier_party_id::uuid,
2764
+ NULL::uuid,
2765
+ v_doc.unit_id::uuid,
2766
+ v_insts::jsonb,
2767
+ v_competence::date,
2768
+ ('NF-e ' || coalesce(v_doc.nfe_number, '?') || ' \xB7 ' || coalesce(v_doc.supplier_name, 'fornecedor'))::text,
2769
+ NULL::text,
2770
+ jsonb_build_object(
2771
+ 'source', 'danfe',
2772
+ 'danfe', jsonb_strip_nulls(jsonb_build_object(
2773
+ 'accessKey', v_doc.access_key, 'number', v_doc.nfe_number,
2774
+ 'series', v_doc.nfe_series, 'supplierName', v_doc.supplier_name,
2775
+ 'supplierCnpj', v_doc.supplier_cnpj,
2776
+ 'noteTotal', v_doc.total_value,
2777
+ 'inboxDocumentId', v_doc.id)))::jsonb,
2778
+ v_key::text);
2779
+ v_invoice := (v_inv->>'invoice_id')::uuid;
2780
+
2781
+ INSERT INTO public.items (tenant_id, invoice_id, name, quantity, unit_price, total, sort_order, metadata)
2782
+ SELECT v_tenant, v_invoice, i.description, i.quantity_commercial, i.unit_price_commercial,
2783
+ round(coalesce(i.total_price, 0), 2), i.item_number,
2784
+ jsonb_strip_nulls(jsonb_build_object(
2785
+ 'itemKind', i.entry_type, 'referenceId', i.product_id,
2786
+ 'chartAccountId', i.chart_account_id, 'ncm', i.ncm, 'cfop', i.cfop,
2787
+ 'supplierProductCode', i.supplier_product_code))
2788
+ FROM public.plg_fiscalbr_inbox_items i
2789
+ WHERE i.tenant_id = v_tenant AND i.inbox_document_id = v_doc.id
2790
+ ORDER BY i.item_number
2791
+ ON CONFLICT DO NOTHING;
2792
+
2793
+ UPDATE public.plg_fiscalbr_inbox_documents SET
2794
+ invoice_id = v_invoice,
2795
+ inventory_operation_id = coalesce((v_recv->>'operation_id')::uuid, inventory_operation_id),
2796
+ stocked_at = CASE WHEN v_stock_n > 0 THEN now() ELSE stocked_at END,
2797
+ status = 'accepted', updated_at = now()
2798
+ WHERE id = v_doc.id;
2799
+
2800
+ INSERT INTO public.plg_fiscalbr_supplier_product_map (
2801
+ tenant_id, supplier_cnpj, supplier_product_code, product_id, entry_type,
2802
+ chart_account_id, stock_unit_id, conversion_factor, cost_mode, hit_count, last_used_at)
2803
+ SELECT v_tenant, v_doc.supplier_cnpj, i.supplier_product_code, i.product_id,
2804
+ i.entry_type, coalesce(i.chart_account_id, v_doc.default_chart_account_id),
2805
+ i.stock_unit_id, i.conversion_factor, i.cost_mode, 1, now()
2806
+ FROM public.plg_fiscalbr_inbox_items i
2807
+ WHERE i.tenant_id = v_tenant AND i.inbox_document_id = v_doc.id
2808
+ AND i.product_id IS NOT NULL
2809
+ AND coalesce(v_doc.supplier_cnpj, '') <> ''
2810
+ AND coalesce(i.supplier_product_code, '') <> ''
2811
+ ON CONFLICT (tenant_id, supplier_cnpj, supplier_product_code) DO UPDATE SET
2812
+ product_id = EXCLUDED.product_id,
2813
+ entry_type = EXCLUDED.entry_type,
2814
+ chart_account_id = coalesce(EXCLUDED.chart_account_id, public.plg_fiscalbr_supplier_product_map.chart_account_id),
2815
+ stock_unit_id = coalesce(EXCLUDED.stock_unit_id, public.plg_fiscalbr_supplier_product_map.stock_unit_id),
2816
+ conversion_factor = EXCLUDED.conversion_factor,
2817
+ cost_mode = EXCLUDED.cost_mode,
2818
+ hit_count = public.plg_fiscalbr_supplier_product_map.hit_count + 1,
2819
+ last_used_at = now(),
2820
+ updated_at = now();
2821
+
2822
+ RETURN jsonb_build_object(
2823
+ 'document_id', v_doc.id, 'already_accepted', false,
2824
+ 'invoice_id', v_invoice, 'invoice_total', v_total,
2825
+ 'installments', jsonb_array_length(v_insts),
2826
+ 'operation_id', v_recv->>'operation_id',
2827
+ 'stock_lines', v_stock_n,
2828
+ 'movements', coalesce(v_recv->'movements', '[]'::jsonb));
2829
+ END $function$;
2830
+
2831
+ REVOKE ALL ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) FROM PUBLIC;
2832
+ GRANT EXECUTE ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) TO authenticated;
2833
+ `;
1836
2834
  var MIGRATIONS = [
1837
2835
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
1838
2836
  { id: "001_preserve_terminal_results_and_recover_leases", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },
@@ -1841,7 +2839,10 @@ var MIGRATIONS = [
1841
2839
  { id: "004_o_grupo_fiscal_do_v1_repete_nome", sql: MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME },
1842
2840
  { id: "005_a_nota_emitida_e_um_documento", sql: MIGRATION_005_A_NOTA_EMITIDA_E_UM_DOCUMENTO },
1843
2841
  { id: "006_o_grupo_fiscal_ganha_o_rail", sql: MIGRATION_006_O_GRUPO_FISCAL_GANHA_O_RAIL },
1844
- { id: "007_a_rls_do_documento_passa_a_ser_legivel", sql: MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL }
2842
+ { id: "007_a_rls_do_documento_passa_a_ser_legivel", sql: MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL },
2843
+ { id: "008_a_nota_recebida_guarda_a_decisao_de_entrada", sql: MIGRATION_008_A_NOTA_RECEBIDA_GUARDA_A_DECISAO_DE_ENTRADA },
2844
+ { 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 },
2845
+ { 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 }
1845
2846
  ];
1846
2847
  var taxGroupEntity = {
1847
2848
  name: "Grupo fiscal",
@@ -1929,6 +2930,1591 @@ var fiscalRegistries = [
1929
2930
  }
1930
2931
  ];
1931
2932
 
2933
+ // src/lib/nfe-xml.ts
2934
+ var NfeParseError = class extends Error {
2935
+ constructor(message) {
2936
+ super(message);
2937
+ this.name = "NfeParseError";
2938
+ }
2939
+ };
2940
+ var ENTITIES = {
2941
+ amp: "&",
2942
+ lt: "<",
2943
+ gt: ">",
2944
+ quot: '"',
2945
+ apos: "'"
2946
+ };
2947
+ function decode(raw) {
2948
+ return raw.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (whole, body) => {
2949
+ if (body.startsWith("#x") || body.startsWith("#X")) return String.fromCodePoint(parseInt(body.slice(2), 16));
2950
+ if (body.startsWith("#")) return String.fromCodePoint(parseInt(body.slice(1), 10));
2951
+ return ENTITIES[body] ?? whole;
2952
+ });
2953
+ }
2954
+ var bare = (name) => {
2955
+ const i = name.indexOf(":");
2956
+ return i < 0 ? name : name.slice(i + 1);
2957
+ };
2958
+ function parseXml(xml) {
2959
+ const src = xml.replace(/<\?[\s\S]*?\?>/g, "").replace(/<!--[\s\S]*?-->/g, "").replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_m, inner) => inner).replace(/<!DOCTYPE[^>]*>/gi, "");
2960
+ const root = { name: "#root", attrs: {}, children: [], text: "" };
2961
+ const stack = [root];
2962
+ const tag = /<\s*(\/?)\s*([A-Za-z_][\w.:-]*)((?:\s+[\w.:-]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)\s*>/g;
2963
+ let cursor = 0;
2964
+ let m;
2965
+ while ((m = tag.exec(src)) !== null) {
2966
+ const [whole, closing, rawName, rawAttrs, selfClosing] = m;
2967
+ const top = stack[stack.length - 1];
2968
+ const between = src.slice(cursor, m.index);
2969
+ if (between.trim()) top.text += decode(between);
2970
+ cursor = m.index + whole.length;
2971
+ if (closing) {
2972
+ if (stack.length > 1) stack.pop();
2973
+ continue;
2974
+ }
2975
+ const attrs = {};
2976
+ const attrRe = /([\w.:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
2977
+ let a;
2978
+ while ((a = attrRe.exec(rawAttrs ?? "")) !== null) {
2979
+ attrs[bare(a[1])] = decode(a[2] ?? a[3] ?? "");
2980
+ }
2981
+ const node = { name: bare(rawName), attrs, children: [], text: "" };
2982
+ top.children.push(node);
2983
+ if (!selfClosing) stack.push(node);
2984
+ }
2985
+ return root;
2986
+ }
2987
+ var kids = (node, name) => node ? node.children.filter((c) => c.name === name) : [];
2988
+ var kid = (node, name) => kids(node, name)[0];
2989
+ function at(node, path) {
2990
+ let cur = node;
2991
+ for (const step of path.split(".")) {
2992
+ cur = kid(cur, step);
2993
+ if (!cur) return void 0;
2994
+ }
2995
+ return cur;
2996
+ }
2997
+ var str = (node, path) => {
2998
+ const target = path ? at(node, path) : node;
2999
+ const value = target?.text.trim();
3000
+ return value ? value : void 0;
3001
+ };
3002
+ var num = (node, path) => {
3003
+ const value = str(node, path);
3004
+ if (value === void 0) return void 0;
3005
+ const parsed = Number(value);
3006
+ return Number.isFinite(parsed) ? parsed : void 0;
3007
+ };
3008
+ var zero = (node, path) => num(node, path) ?? 0;
3009
+ var PAYMENT_BY_CODE = {
3010
+ "01": "dinheiro",
3011
+ "02": "outro",
3012
+ // cheque
3013
+ "03": "cartao_credito",
3014
+ "04": "cartao_debito",
3015
+ "05": "credito_loja",
3016
+ "10": "outro",
3017
+ // vale alimentação
3018
+ "11": "outro",
3019
+ // vale refeição
3020
+ "12": "vale_presente",
3021
+ "13": "outro",
3022
+ // vale combustível
3023
+ "14": "duplicata_mercantil",
3024
+ "15": "boleto",
3025
+ "16": "outro",
3026
+ // depósito bancário
3027
+ "17": "pix",
3028
+ "18": "outro",
3029
+ // transferência bancária
3030
+ "19": "outro",
3031
+ // programa de fidelidade
3032
+ "90": "sem_pagamento",
3033
+ "99": "outro"
3034
+ };
3035
+ var PAYMENT_LABEL = {
3036
+ dinheiro: "Dinheiro",
3037
+ cartao_credito: "Cart\xE3o de cr\xE9dito",
3038
+ cartao_debito: "Cart\xE3o de d\xE9bito",
3039
+ credito_loja: "Cr\xE9dito na loja",
3040
+ vale_presente: "Vale presente",
3041
+ duplicata_mercantil: "Duplicata mercantil",
3042
+ boleto: "Boleto",
3043
+ pix: "PIX",
3044
+ sem_pagamento: "Sem pagamento",
3045
+ outro: "Outro"
3046
+ };
3047
+ function paymentTypeFromCode(code) {
3048
+ if (!code) return "outro";
3049
+ return PAYMENT_BY_CODE[code.padStart(2, "0")] ?? "outro";
3050
+ }
3051
+ function paymentLabel(type) {
3052
+ return PAYMENT_LABEL[type] ?? "Outro";
3053
+ }
3054
+ var onlyDigits = (value) => (value ?? "").replace(/\D+/g, "");
3055
+ function parseNfeXml(xml) {
3056
+ if (!xml || !xml.trim()) throw new NfeParseError("O arquivo est\xE1 vazio.");
3057
+ const root = parseXml(xml);
3058
+ const proc = kid(root, "nfeProc");
3059
+ const nfe = kid(proc ?? root, "NFe") ?? kid(root, "NFe");
3060
+ const inf = kid(nfe, "infNFe");
3061
+ if (!inf) {
3062
+ throw new NfeParseError(
3063
+ "Este arquivo n\xE3o \xE9 uma NF-e: n\xE3o encontrei <infNFe>. Se veio do fornecedor, pe\xE7a o XML da nota, n\xE3o o DANFE em PDF."
3064
+ );
3065
+ }
3066
+ const accessKey = onlyDigits(inf.attrs.Id ?? inf.attrs.id ?? "");
3067
+ if (accessKey.length !== 44) {
3068
+ throw new NfeParseError(
3069
+ `A chave de acesso tem ${accessKey.length} d\xEDgitos e deveria ter 44. O arquivo pode estar truncado.`
3070
+ );
3071
+ }
3072
+ const ide = kid(inf, "ide");
3073
+ const model = str(ide, "mod") ?? "55";
3074
+ if (model !== "55") {
3075
+ throw new NfeParseError(
3076
+ `Este XML \xE9 modelo ${model}${model === "65" ? " (NFC-e, nota de venda no balc\xE3o)" : ""}, e a entrada de notas recebe NF-e modelo 55 de fornecedor.`
3077
+ );
3078
+ }
3079
+ const emit = kid(inf, "emit");
3080
+ const ender = kid(emit, "enderEmit");
3081
+ const total = at(inf, "total.ICMSTot");
3082
+ const dhEmi = str(ide, "dhEmi") ?? str(ide, "dEmi") ?? "";
3083
+ const items = kids(inf, "det").map((det, i) => {
3084
+ const prod = kid(det, "prod");
3085
+ const imposto = kid(det, "imposto");
3086
+ const icms = kid(imposto, "ICMS")?.children[0];
3087
+ const pis = kid(imposto, "PIS")?.children[0];
3088
+ const cofins = kid(imposto, "COFINS")?.children[0];
3089
+ const ipi = kid(kid(imposto, "IPI"), "IPITrib");
3090
+ return {
3091
+ itemNumber: Number(det.attrs.nItem ?? i + 1) || i + 1,
3092
+ supplierProductCode: str(prod, "cProd") ?? "",
3093
+ eanBarcode: str(prod, "cEAN"),
3094
+ description: str(prod, "xProd") ?? "(sem descri\xE7\xE3o)",
3095
+ ncm: str(prod, "NCM"),
3096
+ cest: str(prod, "CEST"),
3097
+ cfop: str(prod, "CFOP"),
3098
+ unit: str(prod, "uCom"),
3099
+ quantity: zero(prod, "qCom"),
3100
+ unitPrice: zero(prod, "vUnCom"),
3101
+ total: zero(prod, "vProd"),
3102
+ discount: num(prod, "vDesc"),
3103
+ freight: num(prod, "vFrete"),
3104
+ other: num(prod, "vOutro"),
3105
+ icmsCst: str(icms, "CST"),
3106
+ icmsCsosn: str(icms, "CSOSN"),
3107
+ icmsOrigin: str(icms, "orig"),
3108
+ icmsBase: num(icms, "vBC"),
3109
+ icmsRate: num(icms, "pICMS"),
3110
+ icmsValue: num(icms, "vICMS"),
3111
+ pisValue: num(pis, "vPIS"),
3112
+ cofinsValue: num(cofins, "vCOFINS"),
3113
+ ipiValue: num(ipi, "vIPI")
3114
+ };
3115
+ });
3116
+ if (items.length === 0) {
3117
+ throw new NfeParseError("A nota n\xE3o tem nenhum item \u2014 n\xE3o h\xE1 o que dar entrada.");
3118
+ }
3119
+ const cobr = kid(inf, "cobr");
3120
+ const duplicates = kids(cobr, "dup").map((dup, i) => ({
3121
+ index: i + 1,
3122
+ number: str(dup, "nDup"),
3123
+ dueDate: str(dup, "dVenc"),
3124
+ value: zero(dup, "vDup")
3125
+ }));
3126
+ const payments = kids(kid(inf, "pag"), "detPag").map((det, i) => {
3127
+ const code = str(det, "tPag");
3128
+ const type = paymentTypeFromCode(code);
3129
+ return {
3130
+ index: i + 1,
3131
+ indicator: str(det, "indPag"),
3132
+ code,
3133
+ label: paymentLabel(type),
3134
+ value: zero(det, "vPag")
3135
+ };
3136
+ });
3137
+ const prot = at(proc, "protNFe.infProt");
3138
+ return {
3139
+ accessKey,
3140
+ nfeNumber: str(ide, "nNF") ?? "",
3141
+ nfeSeries: str(ide, "serie") ?? "",
3142
+ nfeModel: model,
3143
+ // The layout writes an ISO instant with offset; the inbox stores a date.
3144
+ issueDate: dhEmi.slice(0, 10),
3145
+ natureOperation: str(ide, "natOp"),
3146
+ supplier: {
3147
+ cnpj: onlyDigits(str(emit, "CNPJ") ?? str(emit, "CPF")),
3148
+ name: str(emit, "xNome") ?? "",
3149
+ tradeName: str(emit, "xFant"),
3150
+ ie: str(emit, "IE"),
3151
+ city: str(ender, "xMun"),
3152
+ state: str(ender, "UF")
3153
+ },
3154
+ totals: {
3155
+ total: zero(total, "vNF"),
3156
+ products: zero(total, "vProd"),
3157
+ discount: zero(total, "vDesc"),
3158
+ freight: zero(total, "vFrete"),
3159
+ insurance: zero(total, "vSeg"),
3160
+ other: zero(total, "vOutro"),
3161
+ icmsBase: num(total, "vBC"),
3162
+ icmsValue: num(total, "vICMS"),
3163
+ pisValue: num(total, "vPIS"),
3164
+ cofinsValue: num(total, "vCOFINS"),
3165
+ ipiValue: num(total, "vIPI")
3166
+ },
3167
+ items,
3168
+ duplicates,
3169
+ payments,
3170
+ additionalInfo: str(at(inf, "infAdic"), "infCpl"),
3171
+ protocol: str(prot, "nProt"),
3172
+ authorizedAt: str(prot, "dhRecbto")
3173
+ };
3174
+ }
3175
+ function toStagePayload(note, options = {}) {
3176
+ return {
3177
+ access_key: note.accessKey,
3178
+ nfe_number: note.nfeNumber,
3179
+ nfe_series: note.nfeSeries,
3180
+ nfe_model: note.nfeModel,
3181
+ issue_date: note.issueDate,
3182
+ nature_operation: note.natureOperation ?? null,
3183
+ additional_info: note.additionalInfo ?? null,
3184
+ unit_id: options.unitId ?? null,
3185
+ source: options.source ?? "xml_upload",
3186
+ supplier: {
3187
+ cnpj: note.supplier.cnpj,
3188
+ name: note.supplier.name,
3189
+ ie: note.supplier.ie ?? null
3190
+ },
3191
+ totals: {
3192
+ total: note.totals.total,
3193
+ products: note.totals.products,
3194
+ discount: note.totals.discount,
3195
+ freight: note.totals.freight,
3196
+ insurance: note.totals.insurance,
3197
+ other: note.totals.other
3198
+ },
3199
+ items: note.items.map((i) => ({
3200
+ item_number: i.itemNumber,
3201
+ supplier_product_code: i.supplierProductCode,
3202
+ ean_barcode: i.eanBarcode ?? null,
3203
+ description: i.description,
3204
+ ncm: i.ncm ?? null,
3205
+ cest: i.cest ?? null,
3206
+ cfop: i.cfop ?? null,
3207
+ unit: i.unit ?? null,
3208
+ quantity: i.quantity,
3209
+ unit_price: i.unitPrice,
3210
+ total: i.total,
3211
+ discount: i.discount ?? null,
3212
+ freight: i.freight ?? null,
3213
+ other: i.other ?? null,
3214
+ icms_cst: i.icmsCst ?? null,
3215
+ icms_csosn: i.icmsCsosn ?? null,
3216
+ icms_base: i.icmsBase ?? null,
3217
+ icms_rate: i.icmsRate ?? null,
3218
+ icms_value: i.icmsValue ?? null,
3219
+ pis_value: i.pisValue ?? null,
3220
+ cofins_value: i.cofinsValue ?? null,
3221
+ ipi_value: i.ipiValue ?? null
3222
+ })),
3223
+ // The instalment carries the payment form the note declared, so the
3224
+ // reconciliation dialog opens with `Boleto` already chosen instead of blank.
3225
+ duplicates: note.duplicates.map((d) => ({
3226
+ index: d.index,
3227
+ number: d.number ?? null,
3228
+ due_date: d.dueDate ?? null,
3229
+ value: d.value,
3230
+ payment_type: note.payments.length === 1 ? paymentTypeFromCode(note.payments[0].code) : null
3231
+ })),
3232
+ payments: note.payments.map((p) => ({
3233
+ index: p.index,
3234
+ indicator: p.indicator ?? null,
3235
+ code: p.code ?? null,
3236
+ label: p.label ?? null,
3237
+ value: p.value
3238
+ }))
3239
+ };
3240
+ }
3241
+
3242
+ // src/data/inbox.ts
3243
+ function sb() {
3244
+ const supabase = getSupabaseClientOptional();
3245
+ if (!supabase) throw new Error("Supabase not initialized");
3246
+ return supabase;
3247
+ }
3248
+ var NOTE_COLUMNS = `
3249
+ id, access_key, nfe_number, nfe_series, issue_date, received_at, status,
3250
+ manifestation_status, manifested_at, manifestation_attempts,
3251
+ nature_operation, supplier_name, supplier_cnpj, supplier_party_id,
3252
+ total_value, total_products, total_discount, total_freight,
3253
+ invoice_id, inventory_operation_id, stocked_at, reviewed_at,
3254
+ default_chart_account_id, default_location_id, unit_id, document_id, metadata,
3255
+ items:plg_fiscalbr_inbox_items(id, reconciled, entry_type),
3256
+ duplicates:plg_fiscalbr_inbox_duplicates(id, value)
3257
+ `;
3258
+ function rowToNote(row) {
3259
+ const items = row.items ?? [];
3260
+ const duplicates = row.duplicates ?? [];
3261
+ return {
3262
+ id: String(row.id),
3263
+ accessKey: String(row.access_key ?? ""),
3264
+ number: String(row.nfe_number ?? ""),
3265
+ series: String(row.nfe_series ?? ""),
3266
+ issueDate: row.issue_date ?? null,
3267
+ receivedAt: row.received_at ?? null,
3268
+ status: row.status ?? "pending",
3269
+ manifestationStatus: row.manifestation_status ?? null,
3270
+ manifestedAt: row.manifested_at ?? null,
3271
+ manifestationAttempts: Number(row.manifestation_attempts ?? 0),
3272
+ natureOperation: row.nature_operation ?? null,
3273
+ supplierName: row.supplier_name ?? "",
3274
+ supplierCnpj: row.supplier_cnpj ?? null,
3275
+ supplierPartyId: row.supplier_party_id ?? null,
3276
+ total: Number(row.total_value ?? 0),
3277
+ itemCount: items.length,
3278
+ reconciledCount: items.filter((i) => i.reconciled).length,
3279
+ instalmentCount: duplicates.length,
3280
+ // The sum of instalments, because that is the total the payable is opened
3281
+ // with (financial/084) — showing the note total here would make the list and
3282
+ // Contas a Pagar disagree on 6,3% of notes.
3283
+ instalmentTotal: duplicates.reduce((acc, d) => acc + Number(d.value ?? 0), 0),
3284
+ invoiceId: row.invoice_id ?? null,
3285
+ operationId: row.inventory_operation_id ?? null,
3286
+ stockedAt: row.stocked_at ?? null,
3287
+ documentId: row.document_id ?? null,
3288
+ unitId: row.unit_id ?? null
3289
+ };
3290
+ }
3291
+ async function listInboxNotes(options = {}) {
3292
+ const { filter = "all", search, limit = 200 } = options;
3293
+ let query = sb().from("plg_fiscalbr_inbox_documents").select(NOTE_COLUMNS);
3294
+ if (filter === "stocked") query = query.not("stocked_at", "is", null);
3295
+ if (filter === "pending") query = query.is("stocked_at", null);
3296
+ if (filter === "rejected") query = query.eq("status", "rejected");
3297
+ if (filter === "to_manifest") query = query.not("manifestation_status", "eq", "confirmada");
3298
+ if (search?.trim()) {
3299
+ const term = search.trim();
3300
+ query = query.or(
3301
+ `supplier_name.ilike.%${term}%,nfe_number.ilike.%${term}%,access_key.ilike.%${term}%`
3302
+ );
3303
+ }
3304
+ const { data, error } = await query.order("issue_date", { ascending: false, nullsFirst: false }).order("received_at", { ascending: false }).limit(limit);
3305
+ if (error) throw error;
3306
+ return (data ?? []).map(rowToNote);
3307
+ }
3308
+ var ITEM_COLUMNS = `
3309
+ id, item_number, supplier_product_code, ean_barcode, description, ncm, cest, cfop,
3310
+ unit_commercial, quantity_commercial, unit_price_commercial, total_price, discount,
3311
+ icms_cst, icms_csosn, icms_base, icms_rate, icms_value, pis_value, cofins_value, ipi_value,
3312
+ product_id, entry_type, chart_account_id, stock_unit_id, stock_quantity,
3313
+ conversion_factor, cost_mode, unit_cost_new, reconciled, stock_movement_id,
3314
+ shortage_quantity, shortage_reason, shortage_note
3315
+ `;
3316
+ function rowToItem(row) {
3317
+ const quantity = Number(row.quantity_commercial ?? 0);
3318
+ const factor = Number(row.conversion_factor ?? 1) || 1;
3319
+ return {
3320
+ id: String(row.id),
3321
+ itemNumber: Number(row.item_number ?? 0),
3322
+ supplierProductCode: row.supplier_product_code ?? "",
3323
+ eanBarcode: row.ean_barcode ?? null,
3324
+ description: row.description ?? "",
3325
+ ncm: row.ncm ?? null,
3326
+ cest: row.cest ?? null,
3327
+ cfop: row.cfop ?? null,
3328
+ unit: row.unit_commercial ?? null,
3329
+ quantity,
3330
+ unitPrice: Number(row.unit_price_commercial ?? 0),
3331
+ total: Number(row.total_price ?? 0),
3332
+ discount: row.discount == null ? null : Number(row.discount),
3333
+ icmsCst: row.icms_cst ?? null,
3334
+ icmsCsosn: row.icms_csosn ?? null,
3335
+ icmsValue: row.icms_value == null ? null : Number(row.icms_value),
3336
+ productId: row.product_id ?? null,
3337
+ entryType: row.entry_type ?? null,
3338
+ chartAccountId: row.chart_account_id ?? null,
3339
+ stockUnitId: row.stock_unit_id ?? null,
3340
+ conversionFactor: factor,
3341
+ // Derived AND stored by the RPC. Reading the stored value keeps the screen
3342
+ // showing the number the operator accepted, and falls back to the product of
3343
+ // the two only while the decision has not been saved yet.
3344
+ stockQuantity: row.stock_quantity == null ? quantity * factor : Number(row.stock_quantity),
3345
+ costMode: row.cost_mode ?? "novo",
3346
+ unitCostNew: row.unit_cost_new == null ? null : Number(row.unit_cost_new),
3347
+ reconciled: Boolean(row.reconciled),
3348
+ stockMovementId: row.stock_movement_id ?? null,
3349
+ shortageQuantity: row.shortage_quantity == null ? null : Number(row.shortage_quantity),
3350
+ shortageReason: row.shortage_reason ?? null,
3351
+ shortageNote: row.shortage_note ?? null
3352
+ };
3353
+ }
3354
+ async function getInboxNote(id) {
3355
+ const { data, error } = await sb().from("plg_fiscalbr_inbox_documents").select(NOTE_COLUMNS).eq("id", id).maybeSingle();
3356
+ if (error) throw error;
3357
+ if (!data) return null;
3358
+ const [items, instalments, payments] = await Promise.all([
3359
+ sb().from("plg_fiscalbr_inbox_items").select(ITEM_COLUMNS).eq("inbox_document_id", id).order("item_number"),
3360
+ sb().from("plg_fiscalbr_inbox_duplicates").select("id, duplicate_index, number, due_date, value, payment_type, barcode, pix_key, payment_method_id").eq("inbox_document_id", id).order("duplicate_index"),
3361
+ sb().from("plg_fiscalbr_inbox_payments").select("id, payment_index, payment_type_code, payment_type_label, value").eq("inbox_document_id", id).order("payment_index")
3362
+ ]);
3363
+ if (items.error) throw items.error;
3364
+ if (instalments.error) throw instalments.error;
3365
+ return {
3366
+ ...rowToNote(data),
3367
+ natureOperation: data.nature_operation ?? null,
3368
+ defaultChartAccountId: data.default_chart_account_id ?? null,
3369
+ defaultLocationId: data.default_location_id ?? null,
3370
+ items: (items.data ?? []).map(rowToItem),
3371
+ instalments: (instalments.data ?? []).map((d) => ({
3372
+ id: String(d.id),
3373
+ index: Number(d.duplicate_index ?? 0),
3374
+ number: d.number ?? null,
3375
+ dueDate: d.due_date ?? null,
3376
+ amount: Number(d.value ?? 0),
3377
+ paymentType: d.payment_type ?? null,
3378
+ barcode: d.barcode ?? null,
3379
+ pixKey: d.pix_key ?? null,
3380
+ paymentMethodId: d.payment_method_id ?? null
3381
+ })),
3382
+ declaredPayments: (payments.data ?? []).map((p) => ({
3383
+ index: Number(p.payment_index ?? 0),
3384
+ code: p.payment_type_code ?? null,
3385
+ label: p.payment_type_label ?? null,
3386
+ value: Number(p.value ?? 0)
3387
+ }))
3388
+ };
3389
+ }
3390
+ async function stageNoteFromXml(xml, options = {}) {
3391
+ const parsed = parseNfeXml(xml);
3392
+ const { data, error } = await sb().rpc("fiscalbr_stage_inbox_document", {
3393
+ p_note: toStagePayload(parsed, { unitId: options.unitId ?? null })
3394
+ });
3395
+ if (error) throw error;
3396
+ const res = data ?? {};
3397
+ return {
3398
+ documentId: String(res.document_id ?? ""),
3399
+ accessKey: String(res.access_key ?? parsed.accessKey),
3400
+ alreadyAccepted: Boolean(res.already_accepted),
3401
+ supplierPartyId: res.supplier_party_id ?? null,
3402
+ itemCount: Number(res.items ?? 0),
3403
+ matchedCount: Number(res.matched ?? 0),
3404
+ parsed
3405
+ };
3406
+ }
3407
+ async function saveNoteDecision(input) {
3408
+ const { data, error } = await sb().rpc("fiscalbr_save_inbox_decision", {
3409
+ p_document_id: input.documentId,
3410
+ p_items: input.items.map((i) => ({
3411
+ id: i.id,
3412
+ product_id: i.productId ?? null,
3413
+ entry_type: i.entryType ?? null,
3414
+ chart_account_id: i.chartAccountId ?? null,
3415
+ stock_unit_id: i.stockUnitId ?? null,
3416
+ conversion_factor: i.conversionFactor ?? 1,
3417
+ cost_mode: i.costMode ?? "novo",
3418
+ shortage_quantity: i.shortageQuantity ?? null,
3419
+ shortage_reason: i.shortageReason ?? null,
3420
+ shortage_note: i.shortageNote ?? null
3421
+ })),
3422
+ p_default_chart_account_id: input.defaultChartAccountId ?? null,
3423
+ p_default_location_id: input.defaultLocationId ?? null,
3424
+ p_duplicates: input.instalments ? input.instalments.map((d) => ({
3425
+ index: d.index,
3426
+ number: d.number ?? null,
3427
+ due_date: d.dueDate ?? null,
3428
+ value: d.amount,
3429
+ payment_type: d.paymentType ?? null,
3430
+ barcode: d.barcode ?? null,
3431
+ pix_key: d.pixKey ?? null,
3432
+ payment_method_id: d.paymentMethodId ?? null
3433
+ })) : null
3434
+ });
3435
+ if (error) throw error;
3436
+ const res = data ?? {};
3437
+ return {
3438
+ saved: Number(res.saved ?? 0),
3439
+ items: Number(res.items ?? 0),
3440
+ reconciled: Number(res.reconciled ?? 0)
3441
+ };
3442
+ }
3443
+ async function acceptNote(documentId, locationId) {
3444
+ const { data, error } = await sb().rpc("fiscalbr_accept_inbox_document", {
3445
+ p_document_id: documentId,
3446
+ p_location_id: locationId ?? null
3447
+ });
3448
+ if (error) throw error;
3449
+ const res = data ?? {};
3450
+ return {
3451
+ documentId: String(res.document_id ?? documentId),
3452
+ alreadyAccepted: Boolean(res.already_accepted),
3453
+ invoiceId: res.invoice_id ?? null,
3454
+ invoiceTotal: Number(res.invoice_total ?? 0),
3455
+ instalments: Number(res.installments ?? 0),
3456
+ operationId: res.operation_id ?? null,
3457
+ stockLines: Number(res.stock_lines ?? 0)
3458
+ };
3459
+ }
3460
+ async function rejectNote(documentId, reason) {
3461
+ const { error } = await sb().rpc("fiscalbr_reject_inbox_document", {
3462
+ p_document_id: documentId,
3463
+ p_reason: reason ?? null
3464
+ });
3465
+ if (error) throw error;
3466
+ }
3467
+ async function searchProducts(term, limit = 20) {
3468
+ let query = sb().from("products").select("id, name, sku, unit, cost, kind").eq("is_active", true).limit(limit);
3469
+ if (term.trim()) query = query.or(`name.ilike.%${term.trim()}%,sku.ilike.%${term.trim()}%`);
3470
+ const { data, error } = await query.order("name");
3471
+ if (error) throw error;
3472
+ return (data ?? []).map((p) => ({
3473
+ id: String(p.id),
3474
+ name: p.name ?? "",
3475
+ sku: p.sku ?? null,
3476
+ unit: p.unit ?? null,
3477
+ cost: p.cost == null ? null : Number(p.cost),
3478
+ kind: p.kind ?? null
3479
+ }));
3480
+ }
3481
+ async function listMeasurementUnits() {
3482
+ const { data, error } = await sb().from("plg_inventory_measurement_units").select("id, name, abbreviation, is_base").eq("is_active", true).order("name");
3483
+ if (error) throw error;
3484
+ return (data ?? []).map((u) => ({
3485
+ id: String(u.id),
3486
+ name: u.name ?? "",
3487
+ abbreviation: u.abbreviation ?? "",
3488
+ isBase: Boolean(u.is_base)
3489
+ }));
3490
+ }
3491
+ async function listChartAccounts() {
3492
+ const { data, error } = await sb().from("plg_financial_chart_of_accounts").select("id, code, name, kind, is_active").order("code");
3493
+ if (error) throw error;
3494
+ return (data ?? []).filter((a) => a.is_active !== false).map((a) => ({
3495
+ id: String(a.id),
3496
+ code: a.code ?? "",
3497
+ name: a.name ?? "",
3498
+ kind: a.kind ?? null
3499
+ }));
3500
+ }
3501
+ async function listStockLocations() {
3502
+ const { data, error } = await sb().from("plg_inventory_stock_locations").select("id, name, unit_id, is_active").order("name");
3503
+ if (error) throw error;
3504
+ return (data ?? []).filter((l) => l.is_active !== false).map((l) => ({ id: String(l.id), name: l.name ?? "", unitId: l.unit_id ?? null }));
3505
+ }
3506
+ async function listSupplierMap(supplierCnpj) {
3507
+ if (!supplierCnpj) return [];
3508
+ const { data, error } = await sb().from("plg_fiscalbr_supplier_product_map").select("supplier_product_code, product_id, hit_count, last_used_at").eq("supplier_cnpj", supplierCnpj);
3509
+ if (error) throw error;
3510
+ return (data ?? []).map((m) => ({
3511
+ supplierProductCode: m.supplier_product_code ?? "",
3512
+ productId: String(m.product_id),
3513
+ hitCount: Number(m.hit_count ?? 0),
3514
+ lastUsedAt: m.last_used_at ?? null
3515
+ }));
3516
+ }
3517
+ var money = (value) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(value || 0);
3518
+ var day = (iso) => iso ? iso.slice(0, 10).split("-").reverse().join("/") : "\u2014";
3519
+ var cnpj = (digits2) => {
3520
+ const d = (digits2 ?? "").replace(/\D/g, "");
3521
+ if (d.length !== 14) return digits2 ?? "\u2014";
3522
+ return `${d.slice(0, 2)}.${d.slice(2, 5)}.${d.slice(5, 8)}/${d.slice(8, 12)}-${d.slice(12)}`;
3523
+ };
3524
+ function InboundNotesView({ onOpenNote, onOpenManifest }) {
3525
+ const [notes, setNotes] = useState([]);
3526
+ const [loading, setLoading] = useState(true);
3527
+ const [filter, setFilter] = useState("all");
3528
+ const [busy, setBusy] = useState(false);
3529
+ const [report, setReport] = useState(null);
3530
+ const [dragging, setDragging] = useState(false);
3531
+ const fileInput = useRef(null);
3532
+ const reload = useCallback(async () => {
3533
+ setLoading(true);
3534
+ try {
3535
+ setNotes(await listInboxNotes({ filter }));
3536
+ } finally {
3537
+ setLoading(false);
3538
+ }
3539
+ }, [filter]);
3540
+ useEffect(() => {
3541
+ void reload();
3542
+ }, [reload]);
3543
+ const ingest = useCallback(async (files) => {
3544
+ const xmls = files.filter((f) => /\.xml$/i.test(f.name));
3545
+ if (xmls.length === 0) {
3546
+ setReport({ ok: [], failed: [{ file: "\u2014", message: "Nenhum arquivo .xml no que foi solto." }] });
3547
+ return;
3548
+ }
3549
+ setBusy(true);
3550
+ const next = { ok: [], failed: [] };
3551
+ for (const file of xmls) {
3552
+ try {
3553
+ const res = await stageNoteFromXml(await file.text());
3554
+ next.ok.push({
3555
+ file: file.name,
3556
+ number: res.parsed.nfeNumber,
3557
+ items: res.itemCount,
3558
+ matched: res.matchedCount,
3559
+ repeated: res.alreadyAccepted
3560
+ });
3561
+ } catch (error) {
3562
+ next.failed.push({ file: file.name, message: error instanceof Error ? error.message : String(error) });
3563
+ }
3564
+ }
3565
+ setBusy(false);
3566
+ setReport(next);
3567
+ await reload();
3568
+ }, [reload]);
3569
+ const remove = useCallback(async (note) => {
3570
+ await rejectNote(note.id, "Recusada na lista de entrada");
3571
+ await reload();
3572
+ }, [reload]);
3573
+ const filters = useMemo(() => [{
3574
+ id: "estado",
3575
+ label: "Estado",
3576
+ allLabel: "Todos",
3577
+ value: filter === "all" ? void 0 : filter,
3578
+ onChange: (value) => setFilter(value ?? "all"),
3579
+ options: [
3580
+ { value: "pending", label: "Estoque pendente" },
3581
+ { value: "stocked", label: "Estocado" },
3582
+ { value: "to_manifest", label: "A manifestar" },
3583
+ { value: "rejected", label: "Recusada" }
3584
+ ]
3585
+ }], [filter]);
3586
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
3587
+ /* @__PURE__ */ jsxs(PageHeaderActions, { children: [
3588
+ onOpenManifest && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onOpenManifest, children: [
3589
+ /* @__PURE__ */ jsx(Inbox, { className: "h-4 w-4 mr-1.5" }),
3590
+ "Manifesto de Notas"
3591
+ ] }),
3592
+ /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => fileInput.current?.click(), disabled: busy, children: [
3593
+ 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" }),
3594
+ "Importar XML"
3595
+ ] })
3596
+ ] }),
3597
+ /* @__PURE__ */ jsxs(
3598
+ Card,
3599
+ {
3600
+ className: `border-dashed p-6 text-center transition-colors ${dragging ? "border-primary bg-primary/5" : ""}`,
3601
+ onDragOver: (e) => {
3602
+ e.preventDefault();
3603
+ setDragging(true);
3604
+ },
3605
+ onDragLeave: () => setDragging(false),
3606
+ onDrop: (e) => {
3607
+ e.preventDefault();
3608
+ setDragging(false);
3609
+ void ingest(Array.from(e.dataTransfer.files));
3610
+ },
3611
+ children: [
3612
+ /* @__PURE__ */ jsx(UploadCloud, { className: "mx-auto h-8 w-8 text-muted-foreground", "aria-hidden": true }),
3613
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm font-medium", children: "Arraste arquivos XML aqui ou clique para selecionar" }),
3614
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Suporta m\xFAltiplos arquivos XML de NF-e. O XML do fornecedor d\xE1 entrada no estoque e cria a conta a pagar." }),
3615
+ /* @__PURE__ */ jsx(
3616
+ "input",
3617
+ {
3618
+ ref: fileInput,
3619
+ type: "file",
3620
+ accept: ".xml,text/xml,application/xml",
3621
+ multiple: true,
3622
+ className: "hidden",
3623
+ onChange: (e) => {
3624
+ void ingest(Array.from(e.target.files ?? []));
3625
+ e.target.value = "";
3626
+ }
3627
+ }
3628
+ )
3629
+ ]
3630
+ }
3631
+ ),
3632
+ report && /* @__PURE__ */ jsxs(Card, { className: "p-4 space-y-2", children: [
3633
+ report.ok.map((r) => /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
3634
+ /* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
3635
+ "NF-e ",
3636
+ r.number
3637
+ ] }),
3638
+ " ",
3639
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: r.repeated ? "\u2014 j\xE1 tinha sido aceita, nada mudou" : `\u2014 ${r.items} ${r.items === 1 ? "item" : "itens"}, ${r.matched} j\xE1 ${r.matched === 1 ? "vinculado" : "vinculados"}` })
3640
+ ] }, r.file)),
3641
+ report.failed.map((r) => /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive", children: [
3642
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: r.file }),
3643
+ " \u2014 ",
3644
+ r.message
3645
+ ] }, r.file)),
3646
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => setReport(null), children: "Fechar" })
3647
+ ] }),
3648
+ /* @__PURE__ */ jsx(ListFilterBar, { filters, onClear: () => setFilter("all"), activeCount: filter === "all" ? 0 : 1 }),
3649
+ loading ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-14 w-full" }, i)) }) : notes.length === 0 ? /* @__PURE__ */ jsxs(Card, { className: "p-10 text-center", children: [
3650
+ /* @__PURE__ */ jsx(Inbox, { className: "mx-auto h-8 w-8 text-muted-foreground", "aria-hidden": true }),
3651
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm font-medium", children: "Nenhuma nota de entrada" }),
3652
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "As notas chegam da SEFAZ pelo Manifesto, ou voc\xEA arrasta o XML aqui." })
3653
+ ] }) : /* @__PURE__ */ jsx(Card, { className: "overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
3654
+ /* @__PURE__ */ jsx("thead", { className: "border-b bg-muted/40 text-xs text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
3655
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children: "Emiss\xE3o" }),
3656
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children: "N\xBA" }),
3657
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children: "Fornecedor" }),
3658
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: "Itens" }),
3659
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-center font-medium", children: "Concilia\xE7\xE3o" }),
3660
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: "Valor" }),
3661
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-center font-medium", children: "Estoque" }),
3662
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: "A\xE7\xF5es" })
3663
+ ] }) }),
3664
+ /* @__PURE__ */ jsx("tbody", { children: notes.map((note) => /* @__PURE__ */ jsxs("tr", { className: "border-b last:border-0 hover:bg-muted/30", children: [
3665
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 tabular-nums text-muted-foreground", children: day(note.issueDate) }),
3666
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 tabular-nums font-medium", children: note.number }),
3667
+ /* @__PURE__ */ jsxs("td", { className: "px-3 py-2", children: [
3668
+ /* @__PURE__ */ jsx("p", { className: "font-medium leading-tight", children: note.supplierName || "\u2014" }),
3669
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground tabular-nums", children: cnpj(note.supplierCnpj) })
3670
+ ] }),
3671
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-right tabular-nums text-muted-foreground", children: note.itemCount }),
3672
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-center", children: /* @__PURE__ */ jsx(ReconcileBadge, { done: note.reconciledCount, total: note.itemCount }) }),
3673
+ /* @__PURE__ */ jsxs("td", { className: "px-3 py-2 text-right tabular-nums", children: [
3674
+ money(note.total),
3675
+ note.instalmentCount > 0 && Math.abs(note.instalmentTotal - note.total) > 0.01 && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-warning-soft-foreground", children: [
3676
+ "parcelas: ",
3677
+ money(note.instalmentTotal)
3678
+ ] })
3679
+ ] }),
3680
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-center", children: /* @__PURE__ */ jsx(StockBadge, { stockedAt: note.stockedAt, status: note.status }) }),
3681
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1", children: [
3682
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: note.stockedAt ? "ghost" : "outline", onClick: () => onOpenNote(note.id), children: note.stockedAt ? "Ver" : "Conciliar" }),
3683
+ !note.stockedAt && !note.invoiceId && /* @__PURE__ */ jsx(
3684
+ Button,
3685
+ {
3686
+ size: "sm",
3687
+ variant: "ghost",
3688
+ "aria-label": `Recusar a nota ${note.number}`,
3689
+ onClick: () => {
3690
+ void remove(note);
3691
+ },
3692
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
3693
+ }
3694
+ )
3695
+ ] }) })
3696
+ ] }, note.id)) })
3697
+ ] }) }) })
3698
+ ] });
3699
+ }
3700
+ function ReconcileBadge({ done, total }) {
3701
+ const full = total > 0 && done === total;
3702
+ return /* @__PURE__ */ jsxs(
3703
+ "span",
3704
+ {
3705
+ className: `inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium tabular-nums ${full ? "bg-success-soft text-success-soft-foreground" : "bg-muted text-muted-foreground"}`,
3706
+ children: [
3707
+ done,
3708
+ "/",
3709
+ total
3710
+ ]
3711
+ }
3712
+ );
3713
+ }
3714
+ function StockBadge({ stockedAt, status }) {
3715
+ if (status === "rejected") {
3716
+ return /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground line-through", children: "Recusada" });
3717
+ }
3718
+ return stockedAt ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center rounded-full bg-success-soft px-2 py-0.5 text-[11px] font-medium text-success-soft-foreground", children: [
3719
+ /* @__PURE__ */ jsx(FileDown, { className: "mr-1 h-3 w-3", "aria-hidden": true }),
3720
+ "Estocado"
3721
+ ] }) : /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-warning-soft px-2 py-0.5 text-[11px] font-medium text-warning-soft-foreground", children: "Pendente" });
3722
+ }
3723
+
3724
+ // src/types.ts
3725
+ var STOCK_BEARING_ENTRY_TYPES = ["insumo", "revenda", "patrimonio"];
3726
+ var isStockBearing = (type) => STOCK_BEARING_ENTRY_TYPES.includes(type);
3727
+ var money2 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(v || 0);
3728
+ var qty = (v) => new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 4 }).format(v || 0);
3729
+ var day2 = (iso) => iso ? iso.slice(0, 10).split("-").reverse().join("/") : "\u2014";
3730
+ var ENTRY_TYPES = [
3731
+ { value: "insumo", label: "Insumo" },
3732
+ { value: "revenda", label: "Produto Revenda" },
3733
+ { value: "patrimonio", label: "Patrim\xF4nio" },
3734
+ { value: "despesa", label: "Despesa", hint: "n\xE3o vira estoque" },
3735
+ { value: "servico", label: "Servi\xE7o", hint: "n\xE3o vira estoque" }
3736
+ ];
3737
+ var localSearch = (options) => (query) => {
3738
+ const q = query.trim().toLowerCase();
3739
+ return (q ? options.filter((o) => o.label.toLowerCase().includes(q)) : options).slice(0, 40);
3740
+ };
3741
+ var labelOf = (options, id) => options.find((o) => o.id === id)?.label;
3742
+ var draftOf = (item) => ({
3743
+ productId: item.productId,
3744
+ productLabel: "",
3745
+ entryType: item.entryType ?? null,
3746
+ chartAccountId: item.chartAccountId,
3747
+ stockUnitId: item.stockUnitId,
3748
+ conversionFactor: String(item.conversionFactor ?? 1),
3749
+ costMode: item.costMode === "media" ? "media" : "novo",
3750
+ shortageQuantity: item.shortageQuantity == null ? "" : String(item.shortageQuantity)
3751
+ });
3752
+ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3753
+ const [note, setNote] = useState(null);
3754
+ const [loading, setLoading] = useState(true);
3755
+ const [drafts, setDrafts] = useState({});
3756
+ const [instalments, setInstalments] = useState([]);
3757
+ const [defaultAccount, setDefaultAccount] = useState(null);
3758
+ const [location, setLocation] = useState(null);
3759
+ const [units, setUnits] = useState([]);
3760
+ const [accounts, setAccounts] = useState([]);
3761
+ const [locations, setLocations] = useState([]);
3762
+ const [saving, setSaving] = useState(false);
3763
+ const [accepting, setAccepting] = useState(false);
3764
+ const [error, setError] = useState(null);
3765
+ const [accepted, setAccepted] = useState(null);
3766
+ const load = useCallback(async () => {
3767
+ setLoading(true);
3768
+ try {
3769
+ const detail = await getInboxNote(noteId);
3770
+ setNote(detail);
3771
+ const [u, a, l] = await Promise.all([
3772
+ listMeasurementUnits().catch(() => []),
3773
+ listChartAccounts().catch(() => []),
3774
+ listStockLocations().catch(() => [])
3775
+ ]);
3776
+ setUnits(u);
3777
+ setAccounts(a);
3778
+ setLocations(l);
3779
+ if (detail) {
3780
+ setDrafts(Object.fromEntries(detail.items.map((i) => [i.id, draftOf(i)])));
3781
+ setInstalments(detail.instalments.map((d) => ({
3782
+ index: d.index,
3783
+ number: d.number,
3784
+ dueDate: d.dueDate,
3785
+ amount: d.amount,
3786
+ paymentType: d.paymentType,
3787
+ barcode: d.barcode,
3788
+ pixKey: d.pixKey
3789
+ })));
3790
+ setDefaultAccount(detail.defaultChartAccountId);
3791
+ setLocation(detail.defaultLocationId ?? l[0]?.id ?? null);
3792
+ }
3793
+ } catch (e) {
3794
+ setError(e instanceof Error ? e.message : String(e));
3795
+ } finally {
3796
+ setLoading(false);
3797
+ }
3798
+ }, [noteId]);
3799
+ useEffect(() => {
3800
+ void load();
3801
+ }, [load]);
3802
+ const patch = (id, change) => setDrafts((prev) => ({ ...prev, [id]: { ...prev[id], ...change } }));
3803
+ const accountOptions = useMemo(
3804
+ () => accounts.map((a) => ({ id: a.id, label: `${a.code} \xB7 ${a.name}` })),
3805
+ [accounts]
3806
+ );
3807
+ const unitOptions = useMemo(
3808
+ () => units.map((u) => ({ id: u.id, label: u.abbreviation, subtitle: u.name })),
3809
+ [units]
3810
+ );
3811
+ const locationOptions = useMemo(
3812
+ () => locations.map((l) => ({ id: l.id, label: l.name })),
3813
+ [locations]
3814
+ );
3815
+ const decisions = useMemo(() => {
3816
+ if (!note) return [];
3817
+ return note.items.map((item) => {
3818
+ const d = drafts[item.id] ?? draftOf(item);
3819
+ return {
3820
+ id: item.id,
3821
+ productId: isStockBearing(d.entryType) ? d.productId : null,
3822
+ entryType: d.entryType,
3823
+ // A herança que a legenda do V1 promete: «itens sem plano herdarão este».
3824
+ chartAccountId: d.chartAccountId ?? defaultAccount,
3825
+ stockUnitId: d.stockUnitId,
3826
+ conversionFactor: Number(d.conversionFactor.replace(",", ".")) || 1,
3827
+ costMode: d.costMode,
3828
+ shortageQuantity: d.shortageQuantity === "" ? null : Number(d.shortageQuantity.replace(",", "."))
3829
+ };
3830
+ });
3831
+ }, [note, drafts, defaultAccount]);
3832
+ const stockLines = decisions.filter((d) => isStockBearing(d.entryType));
3833
+ const missingProduct = stockLines.filter((d) => !d.productId).length;
3834
+ const missingType = decisions.filter((d) => !d.entryType).length;
3835
+ const instalmentSum = instalments.reduce((acc, d) => acc + (Number(d.amount) || 0), 0);
3836
+ const readOnly = Boolean(note?.stockedAt || note?.invoiceId);
3837
+ const save = async () => {
3838
+ if (!note) return;
3839
+ setSaving(true);
3840
+ setError(null);
3841
+ try {
3842
+ await saveNoteDecision({
3843
+ documentId: note.id,
3844
+ items: decisions,
3845
+ defaultChartAccountId: defaultAccount,
3846
+ defaultLocationId: location,
3847
+ instalments
3848
+ });
3849
+ await load();
3850
+ } catch (e) {
3851
+ setError(e instanceof Error ? e.message : String(e));
3852
+ } finally {
3853
+ setSaving(false);
3854
+ }
3855
+ };
3856
+ const accept = async () => {
3857
+ if (!note) return;
3858
+ setAccepting(true);
3859
+ setError(null);
3860
+ try {
3861
+ await saveNoteDecision({
3862
+ documentId: note.id,
3863
+ items: decisions,
3864
+ defaultChartAccountId: defaultAccount,
3865
+ defaultLocationId: location,
3866
+ instalments
3867
+ });
3868
+ setAccepted(await acceptNote(note.id, location));
3869
+ await load();
3870
+ } catch (e) {
3871
+ setError(e instanceof Error ? e.message : String(e));
3872
+ } finally {
3873
+ setAccepting(false);
3874
+ }
3875
+ };
3876
+ if (loading) {
3877
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
3878
+ /* @__PURE__ */ jsx(SubpageHeader, { title: "Concilia\xE7\xE3o", onBack, parentLabel: "Entrada de Notas" }),
3879
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" }),
3880
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-48 w-full" })
3881
+ ] });
3882
+ }
3883
+ if (!note) {
3884
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
3885
+ /* @__PURE__ */ jsx(SubpageHeader, { title: "Concilia\xE7\xE3o", onBack, parentLabel: "Entrada de Notas" }),
3886
+ /* @__PURE__ */ jsx(Card, { className: "p-8 text-center text-sm", children: error ? /* @__PURE__ */ jsxs("span", { className: "text-destructive", children: [
3887
+ "N\xE3o consegui carregar a nota: ",
3888
+ error
3889
+ ] }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Nota n\xE3o encontrada." }) })
3890
+ ] });
3891
+ }
3892
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 pb-24", children: [
3893
+ /* @__PURE__ */ jsx(
3894
+ SubpageHeader,
3895
+ {
3896
+ title: `Concilia\xE7\xE3o \xB7 NF-e ${note.number}`,
3897
+ onBack,
3898
+ parentLabel: "Entrada de Notas"
3899
+ }
3900
+ ),
3901
+ accepted && /* @__PURE__ */ jsxs(Card, { className: "border-success bg-success-soft p-4", children: [
3902
+ /* @__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"}.` }),
3903
+ accepted.invoiceId && onOpenInvoice && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "mt-2", onClick: () => onOpenInvoice(accepted.invoiceId), children: "Abrir a conta a pagar" })
3904
+ ] }),
3905
+ /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
3906
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Situa\xE7\xE3o fiscal" }),
3907
+ /* @__PURE__ */ jsxs("dl", { className: "mt-3 grid gap-x-6 gap-y-2 text-sm sm:grid-cols-2 lg:grid-cols-3", children: [
3908
+ /* @__PURE__ */ jsx(Field, { label: "Fornecedor", value: note.supplierName || "\u2014" }),
3909
+ /* @__PURE__ */ jsx(Field, { label: "CNPJ", value: note.supplierCnpj ?? "\u2014", mono: true }),
3910
+ /* @__PURE__ */ jsx(Field, { label: "Natureza da opera\xE7\xE3o", value: note.natureOperation ?? "\u2014" }),
3911
+ /* @__PURE__ */ jsx(
3912
+ Field,
3913
+ {
3914
+ label: "Manifesta\xE7\xE3o",
3915
+ value: `${note.manifestationStatus ?? "n\xE3o manifestada"}${note.manifestationAttempts ? ` \xB7 ${note.manifestationAttempts} tentativa(s)` : ""}`
3916
+ }
3917
+ ),
3918
+ /* @__PURE__ */ jsx(Field, { label: "Manifestada em", value: day2(note.manifestedAt) }),
3919
+ /* @__PURE__ */ jsx(Field, { label: "Recebida em", value: day2(note.receivedAt) })
3920
+ ] }),
3921
+ !note.supplierPartyId && /* @__PURE__ */ jsx("div", { className: "mt-3 rounded-md border border-warning bg-warning-soft px-3 py-2", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-warning-soft-foreground", children: [
3922
+ "Este fornecedor n\xE3o est\xE1 no cadastro de pessoas, ent\xE3o o t\xEDtulo a pagar vai nascer",
3923
+ " ",
3924
+ /* @__PURE__ */ jsx("strong", { children: "sem contraparte" }),
3925
+ ". Cadastre o CNPJ antes de aceitar para a conta saber a quem se deve."
3926
+ ] }) }),
3927
+ /bonific|brinde|doacao|doação/i.test(note.natureOperation ?? "") && /* @__PURE__ */ jsx("div", { className: "mt-3 rounded-md border border-dashed border-input px-3 py-2", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
3928
+ /* @__PURE__ */ jsx(Gift, { className: "mr-1 inline h-3.5 w-3.5", "aria-hidden": true }),
3929
+ "A natureza desta nota \xE9 ",
3930
+ /* @__PURE__ */ jsx("strong", { children: "bonifica\xE7\xE3o" }),
3931
+ ". Ela deveria entrar no estoque",
3932
+ " ",
3933
+ /* @__PURE__ */ jsx("strong", { children: "sem" }),
3934
+ " abrir conta a pagar \u2014 hoje ainda abre.",
3935
+ " ",
3936
+ /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-1.5 py-0.5 text-[10px]", children: "Em breve" })
3937
+ ] }) }),
3938
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-2", children: [
3939
+ /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => {
3940
+ void load();
3941
+ }, children: [
3942
+ /* @__PURE__ */ jsx(RefreshCw, { className: "mr-1.5 h-4 w-4" }),
3943
+ "Atualizar status"
3944
+ ] }),
3945
+ /* @__PURE__ */ jsx(
3946
+ EmBreve,
3947
+ {
3948
+ label: "Cadastrar fornecedor",
3949
+ o_que: "Criar a pessoa do fornecedor aqui, sem sair da nota \u2014 \xE9 o que preenche a contraparte do t\xEDtulo",
3950
+ quantas: "a Laissa cadastrou 37 fornecedores no V1 enquanto importava"
3951
+ }
3952
+ )
3953
+ ] })
3954
+ ] }),
3955
+ /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
3956
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Padr\xF5es desta nota" }),
3957
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 grid gap-4 sm:grid-cols-2", children: [
3958
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
3959
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Plano de contas padr\xE3o" }),
3960
+ /* @__PURE__ */ jsx(
3961
+ SearchSelect,
3962
+ {
3963
+ onSearch: localSearch(accountOptions),
3964
+ value: defaultAccount ?? void 0,
3965
+ displayValue: labelOf(accountOptions, defaultAccount),
3966
+ onChange: (id) => setDefaultAccount(id || null),
3967
+ minChars: 0,
3968
+ placeholder: "Escolher plano de contas",
3969
+ noResultsLabel: "Nenhum plano de contas",
3970
+ searchingLabel: "Buscando\u2026",
3971
+ disabled: readOnly
3972
+ }
3973
+ ),
3974
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block text-[11px] text-muted-foreground", children: "Itens sem plano herdar\xE3o este." })
3975
+ ] }),
3976
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
3977
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Local de estoque" }),
3978
+ /* @__PURE__ */ jsx(
3979
+ SearchSelect,
3980
+ {
3981
+ onSearch: localSearch(locationOptions),
3982
+ value: location ?? void 0,
3983
+ displayValue: labelOf(locationOptions, location),
3984
+ onChange: (id) => setLocation(id || null),
3985
+ minChars: 0,
3986
+ placeholder: "Escolher local",
3987
+ noResultsLabel: "Nenhum local de estoque",
3988
+ searchingLabel: "Buscando\u2026",
3989
+ disabled: readOnly
3990
+ }
3991
+ ),
3992
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block text-[11px] text-muted-foreground", children: "Onde as linhas de estoque desta nota entram." })
3993
+ ] })
3994
+ ] })
3995
+ ] }),
3996
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
3997
+ /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
3998
+ "Itens ",
3999
+ /* @__PURE__ */ jsxs("span", { className: "font-normal text-muted-foreground", children: [
4000
+ "(",
4001
+ note.items.length,
4002
+ ")"
4003
+ ] })
4004
+ ] }),
4005
+ note.items.map((item) => {
4006
+ const d = drafts[item.id] ?? draftOf(item);
4007
+ const factor = Number(d.conversionFactor.replace(",", ".")) || 1;
4008
+ const shortage = d.shortageQuantity === "" ? 0 : Number(d.shortageQuantity.replace(",", ".")) || 0;
4009
+ const stockQty = item.quantity * factor - shortage;
4010
+ const unitCost = factor > 0 ? item.unitPrice / factor : item.unitPrice;
4011
+ const bearing = isStockBearing(d.entryType);
4012
+ return /* @__PURE__ */ jsx(Card, { className: "p-4", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
4013
+ /* @__PURE__ */ jsxs("div", { children: [
4014
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Item na nota" }),
4015
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 font-medium leading-tight", children: [
4016
+ item.itemNumber,
4017
+ ". ",
4018
+ item.description
4019
+ ] }),
4020
+ /* @__PURE__ */ jsxs("dl", { className: "mt-2 grid grid-cols-2 gap-x-4 gap-y-1 text-xs sm:grid-cols-4", children: [
4021
+ /* @__PURE__ */ jsx(Field, { small: true, label: "Qtd", value: `${qty(item.quantity)} ${item.unit ?? ""}` }),
4022
+ /* @__PURE__ */ jsx(Field, { small: true, label: "Vl. unit.", value: money2(item.unitPrice) }),
4023
+ /* @__PURE__ */ jsx(Field, { small: true, label: "Total", value: money2(item.total) }),
4024
+ /* @__PURE__ */ jsx(Field, { small: true, label: "C\xF3d", value: item.supplierProductCode || "\u2014", mono: true }),
4025
+ /* @__PURE__ */ jsx(Field, { small: true, label: "EAN", value: item.eanBarcode ?? "\u2014", mono: true }),
4026
+ /* @__PURE__ */ jsx(Field, { small: true, label: "NCM", value: item.ncm ?? "\u2014", mono: true }),
4027
+ /* @__PURE__ */ jsx(Field, { small: true, label: "CFOP", value: item.cfop ?? "\u2014", mono: true }),
4028
+ /* @__PURE__ */ jsx(Field, { small: true, label: "CST/CSOSN", value: item.icmsCst ?? item.icmsCsosn ?? "\u2014", mono: true })
4029
+ ] })
4030
+ ] }),
4031
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3 lg:border-l lg:pl-4", children: [
4032
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Item no estoque" }),
4033
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4034
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Tipo do registro" }),
4035
+ /* @__PURE__ */ jsx("div", { className: "mt-1 flex flex-wrap gap-1", children: ENTRY_TYPES.map((t) => /* @__PURE__ */ jsxs(
4036
+ "button",
4037
+ {
4038
+ type: "button",
4039
+ disabled: readOnly,
4040
+ onClick: () => patch(item.id, { entryType: t.value }),
4041
+ className: `rounded-full border px-2.5 py-1 text-[11px] transition-colors disabled:opacity-60 ${d.entryType === t.value ? "border-primary bg-primary text-primary-foreground" : "border-input hover:bg-muted"}`,
4042
+ children: [
4043
+ t.label,
4044
+ t.hint && /* @__PURE__ */ jsxs("span", { className: "ml-1 opacity-70", children: [
4045
+ "\xB7 ",
4046
+ t.hint
4047
+ ] })
4048
+ ]
4049
+ },
4050
+ t.value
4051
+ )) })
4052
+ ] }),
4053
+ bearing && /* @__PURE__ */ jsxs(Fragment, { children: [
4054
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4055
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Produto vinculado" }),
4056
+ /* @__PURE__ */ jsx(
4057
+ SearchSelect,
4058
+ {
4059
+ value: d.productId ?? void 0,
4060
+ displayValue: d.productLabel || void 0,
4061
+ onChange: (id, option) => patch(item.id, { productId: id || null, productLabel: option?.label ?? "" }),
4062
+ onSearch: async (term) => {
4063
+ const found = await searchProducts(term || item.description.slice(0, 24));
4064
+ return found.map((prod) => ({
4065
+ id: prod.id,
4066
+ label: prod.name,
4067
+ subtitle: prod.sku ?? void 0
4068
+ }));
4069
+ },
4070
+ minChars: 0,
4071
+ placeholder: "Buscar produto do cat\xE1logo",
4072
+ noResultsLabel: "Nenhum produto encontrado",
4073
+ searchingLabel: "Buscando\u2026",
4074
+ disabled: readOnly
4075
+ }
4076
+ )
4077
+ ] }),
4078
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
4079
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4080
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Fator de convers\xE3o" }),
4081
+ /* @__PURE__ */ jsx(
4082
+ Input,
4083
+ {
4084
+ value: d.conversionFactor,
4085
+ onChange: (e) => patch(item.id, { conversionFactor: e.target.value }),
4086
+ inputMode: "decimal",
4087
+ disabled: readOnly
4088
+ }
4089
+ )
4090
+ ] }),
4091
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4092
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Unidade de estoque" }),
4093
+ /* @__PURE__ */ jsx(
4094
+ SearchSelect,
4095
+ {
4096
+ onSearch: localSearch(unitOptions),
4097
+ value: d.stockUnitId ?? void 0,
4098
+ displayValue: labelOf(unitOptions, d.stockUnitId),
4099
+ onChange: (id) => patch(item.id, { stockUnitId: id || null }),
4100
+ minChars: 0,
4101
+ placeholder: "Unidade",
4102
+ noResultsLabel: "Nenhuma unidade",
4103
+ searchingLabel: "Buscando\u2026",
4104
+ disabled: readOnly
4105
+ }
4106
+ )
4107
+ ] })
4108
+ ] }),
4109
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md bg-muted/50 px-3 py-2", children: [
4110
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [
4111
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Qtd estoque (calc.)" }),
4112
+ /* @__PURE__ */ jsx("span", { className: "font-semibold tabular-nums", children: qty(stockQty) })
4113
+ ] }),
4114
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [
4115
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Novo custo unit\xE1rio" }),
4116
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(unitCost) })
4117
+ ] })
4118
+ ] }),
4119
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
4120
+ /* @__PURE__ */ jsx(
4121
+ EmBreve,
4122
+ {
4123
+ label: "Criar produto daqui",
4124
+ o_que: "Cadastrar o produto a partir desta linha da nota, com nome, c\xF3digo e unidade j\xE1 preenchidos",
4125
+ quantas: "ela criou 33 produtos no V1 no meio de importa\xE7\xF5es"
4126
+ }
4127
+ ),
4128
+ /* @__PURE__ */ jsx(
4129
+ EmBreve,
4130
+ {
4131
+ label: "Lote e validade",
4132
+ o_que: "Informar lote e vencimento desta entrada \u2014 a coluna j\xE1 existe no movimento de estoque",
4133
+ quantas: "usado 5 vezes no V1; numa churrascaria \xE9 validade de carne"
4134
+ }
4135
+ )
4136
+ ] }),
4137
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
4138
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4139
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Modo de custo" }),
4140
+ /* @__PURE__ */ jsx("div", { className: "mt-1 flex gap-1", children: ["novo", "media"].map((mode) => /* @__PURE__ */ jsx(
4141
+ "button",
4142
+ {
4143
+ type: "button",
4144
+ disabled: readOnly,
4145
+ onClick: () => patch(item.id, { costMode: mode }),
4146
+ className: `flex-1 rounded-md border px-2 py-1 text-[11px] disabled:opacity-60 ${d.costMode === mode ? "border-primary bg-primary/10" : "border-input hover:bg-muted"}`,
4147
+ children: mode === "novo" ? "Novo" : "M\xE9dia"
4148
+ },
4149
+ mode
4150
+ )) })
4151
+ ] }),
4152
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4153
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Falta (qtd)" }),
4154
+ /* @__PURE__ */ jsx(
4155
+ Input,
4156
+ {
4157
+ value: d.shortageQuantity,
4158
+ onChange: (e) => patch(item.id, { shortageQuantity: e.target.value }),
4159
+ inputMode: "decimal",
4160
+ placeholder: "0",
4161
+ disabled: readOnly
4162
+ }
4163
+ )
4164
+ ] })
4165
+ ] })
4166
+ ] }),
4167
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4168
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Plano de contas" }),
4169
+ /* @__PURE__ */ jsx(
4170
+ SearchSelect,
4171
+ {
4172
+ onSearch: localSearch(accountOptions),
4173
+ value: d.chartAccountId ?? defaultAccount ?? void 0,
4174
+ displayValue: labelOf(accountOptions, d.chartAccountId ?? defaultAccount),
4175
+ onChange: (id) => patch(item.id, { chartAccountId: id || null }),
4176
+ minChars: 0,
4177
+ placeholder: "Herda o padr\xE3o da nota",
4178
+ noResultsLabel: "Nenhum plano de contas",
4179
+ searchingLabel: "Buscando\u2026",
4180
+ disabled: readOnly
4181
+ }
4182
+ )
4183
+ ] }),
4184
+ item.stockMovementId && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-success-soft-foreground", children: [
4185
+ /* @__PURE__ */ jsx(Check, { className: "mr-1 inline h-3 w-3", "aria-hidden": true }),
4186
+ "Esta linha j\xE1 deu entrada no estoque."
4187
+ ] })
4188
+ ] })
4189
+ ] }) }, item.id);
4190
+ })
4191
+ ] }),
4192
+ /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
4193
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between", children: [
4194
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Dados de pagamento" }),
4195
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
4196
+ instalments.length,
4197
+ " ",
4198
+ instalments.length === 1 ? "parcela" : "parcelas",
4199
+ " \xB7",
4200
+ " ",
4201
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(instalmentSum) })
4202
+ ] })
4203
+ ] }),
4204
+ note.declaredPayments.length > 0 && /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [
4205
+ "A nota declarou: ",
4206
+ note.declaredPayments.map((p) => `${p.label ?? p.code} ${money2(p.value)}`).join(" \xB7 ")
4207
+ ] }),
4208
+ /* @__PURE__ */ jsx("div", { className: "mt-3 space-y-2", children: instalments.map((inst, i) => /* @__PURE__ */ jsxs("div", { className: "grid items-end gap-2 sm:grid-cols-[2rem_1fr_1fr_1fr_2rem]", children: [
4209
+ /* @__PURE__ */ jsx("span", { className: "pb-2 text-xs tabular-nums text-muted-foreground", children: inst.index }),
4210
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4211
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "Vencimento" }),
4212
+ readOnly ? /* @__PURE__ */ jsx("p", { className: "py-2 text-sm tabular-nums", children: day2(inst.dueDate) }) : /* @__PURE__ */ jsx(
4213
+ DatePicker,
4214
+ {
4215
+ value: inst.dueDate ?? "",
4216
+ onChange: (v) => setInstalments((prev) => prev.map((p, j) => j === i ? { ...p, dueDate: v || null } : p))
4217
+ }
4218
+ )
4219
+ ] }),
4220
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4221
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "Valor" }),
4222
+ /* @__PURE__ */ jsx(
4223
+ Input,
4224
+ {
4225
+ value: String(inst.amount),
4226
+ inputMode: "decimal",
4227
+ disabled: readOnly,
4228
+ onChange: (e) => setInstalments((prev) => prev.map((p, j) => j === i ? { ...p, amount: Number(e.target.value.replace(",", ".")) || 0 } : p))
4229
+ }
4230
+ )
4231
+ ] }),
4232
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
4233
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "C\xF3d. barras / PIX" }),
4234
+ /* @__PURE__ */ jsx(
4235
+ Input,
4236
+ {
4237
+ value: inst.barcode ?? inst.pixKey ?? "",
4238
+ disabled: readOnly,
4239
+ onChange: (e) => setInstalments((prev) => prev.map((p, j) => j === i ? { ...p, barcode: e.target.value } : p))
4240
+ }
4241
+ )
4242
+ ] }),
4243
+ !readOnly && /* @__PURE__ */ jsx(
4244
+ Button,
4245
+ {
4246
+ variant: "ghost",
4247
+ size: "sm",
4248
+ "aria-label": `Remover a parcela ${inst.index}`,
4249
+ onClick: () => setInstalments((prev) => prev.filter((_, j) => j !== i)),
4250
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
4251
+ }
4252
+ )
4253
+ ] }, i)) }),
4254
+ !readOnly && /* @__PURE__ */ jsxs(
4255
+ Button,
4256
+ {
4257
+ variant: "outline",
4258
+ size: "sm",
4259
+ className: "mt-3",
4260
+ onClick: () => setInstalments((prev) => [...prev, {
4261
+ index: prev.length + 1,
4262
+ number: null,
4263
+ dueDate: note.issueDate,
4264
+ amount: 0,
4265
+ paymentType: "boleto"
4266
+ }]),
4267
+ children: [
4268
+ /* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-4 w-4" }),
4269
+ "Adicionar parcela"
4270
+ ]
4271
+ }
4272
+ ),
4273
+ Math.abs(instalmentSum - note.total) > 0.01 && /* @__PURE__ */ jsx(Hint, { className: "mt-3", children: /* @__PURE__ */ jsxs("span", { children: [
4274
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "mr-1 inline h-3.5 w-3.5", "aria-hidden": true }),
4275
+ "A nota diz ",
4276
+ money2(note.total),
4277
+ " e as parcelas somam ",
4278
+ money2(instalmentSum),
4279
+ ". O t\xEDtulo vai nascer com ",
4280
+ /* @__PURE__ */ jsx("strong", { children: money2(instalmentSum) }),
4281
+ " \u2014 \xE9 a soma das parcelas que define o saldo a pagar."
4282
+ ] }) })
4283
+ ] }),
4284
+ /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
4285
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Anexos da conta" }),
4286
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [
4287
+ "Boleto, comprovante, foto da nota. No sistema antigo, anexar o boleto tamb\xE9m",
4288
+ " ",
4289
+ /* @__PURE__ */ jsx("strong", { children: "preenchia as parcelas sozinho" }),
4290
+ " \u2014 \xE9 a automa\xE7\xE3o mais \xFAtil e a mais invis\xEDvel de l\xE1."
4291
+ ] }),
4292
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap gap-2", children: [
4293
+ /* @__PURE__ */ jsx(
4294
+ EmBreve,
4295
+ {
4296
+ label: "Anexar boleto",
4297
+ o_que: "Guardar o arquivo no t\xEDtulo que esta nota cria",
4298
+ quantas: "25 anexos no V1, o \xFAltimo hoje"
4299
+ }
4300
+ ),
4301
+ /* @__PURE__ */ jsx(
4302
+ EmBreve,
4303
+ {
4304
+ label: "Ler o c\xF3digo de barras do boleto",
4305
+ o_que: "Preencher vencimento e valor da parcela a partir do boleto anexado",
4306
+ quantas: "automa\xE7\xE3o do V1 que ningu\xE9m documentou"
4307
+ }
4308
+ )
4309
+ ] })
4310
+ ] }),
4311
+ error && /* @__PURE__ */ jsx(Card, { className: "border-destructive bg-destructive/5 p-4", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }) }),
4312
+ !readOnly && /* @__PURE__ */ jsxs("div", { className: "sticky bottom-0 -mx-4 flex items-center justify-between gap-3 border-t bg-card/95 px-4 py-3 pr-16 backdrop-blur", children: [
4313
+ /* @__PURE__ */ jsxs(
4314
+ Button,
4315
+ {
4316
+ variant: "ghost",
4317
+ onClick: async () => {
4318
+ await rejectNote(note.id);
4319
+ onBack();
4320
+ },
4321
+ disabled: saving || accepting,
4322
+ children: [
4323
+ /* @__PURE__ */ jsx(X, { className: "mr-1.5 h-4 w-4" }),
4324
+ "Rejeitar"
4325
+ ]
4326
+ }
4327
+ ),
4328
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
4329
+ (missingType > 0 || missingProduct > 0) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
4330
+ missingType > 0 && `${missingType} sem tipo`,
4331
+ missingType > 0 && missingProduct > 0 && " \xB7 ",
4332
+ missingProduct > 0 && `${missingProduct} sem produto`
4333
+ ] }),
4334
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => {
4335
+ void save();
4336
+ }, disabled: saving || accepting, children: [
4337
+ saving && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
4338
+ "Salvar concilia\xE7\xE3o"
4339
+ ] }),
4340
+ /* @__PURE__ */ jsxs(
4341
+ Button,
4342
+ {
4343
+ onClick: () => {
4344
+ void accept();
4345
+ },
4346
+ disabled: accepting || saving || missingType > 0 || missingProduct > 0,
4347
+ children: [
4348
+ 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" }),
4349
+ "Aceitar"
4350
+ ]
4351
+ }
4352
+ )
4353
+ ] })
4354
+ ] })
4355
+ ] });
4356
+ }
4357
+ function Field({ label, value, mono, small }) {
4358
+ return /* @__PURE__ */ jsxs("div", { children: [
4359
+ /* @__PURE__ */ jsx("dt", { className: `text-muted-foreground ${small ? "text-[10px]" : "text-xs"}`, children: label }),
4360
+ /* @__PURE__ */ jsx("dd", { className: `${mono ? "tabular-nums" : ""} ${small ? "text-xs" : "text-sm"}`, children: value })
4361
+ ] });
4362
+ }
4363
+ var money3 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(v || 0);
4364
+ var day3 = (iso) => iso ? iso.slice(0, 10).split("-").reverse().join("/") : "\u2014";
4365
+ var MANIFEST_TONE = {
4366
+ confirmada: "bg-success-soft text-success-soft-foreground",
4367
+ processando: "bg-info-soft text-info-soft-foreground",
4368
+ erro: "bg-destructive/10 text-destructive"
4369
+ };
4370
+ function ManifestView({ onOpenNote }) {
4371
+ const [notes, setNotes] = useState([]);
4372
+ const [loading, setLoading] = useState(true);
4373
+ const [tab, setTab] = useState("pendentes");
4374
+ const reload = useCallback(async () => {
4375
+ setLoading(true);
4376
+ try {
4377
+ setNotes(await listInboxNotes({ filter: "all", limit: 300 }));
4378
+ } finally {
4379
+ setLoading(false);
4380
+ }
4381
+ }, []);
4382
+ useEffect(() => {
4383
+ void reload();
4384
+ }, [reload]);
4385
+ const shown = useMemo(() => notes.filter((n) => {
4386
+ const status = n.manifestationStatus ?? "";
4387
+ if (tab === "todas") return true;
4388
+ if (tab === "confirmadas") return status === "confirmada";
4389
+ if (tab === "erro") return status === "erro";
4390
+ return status !== "confirmada";
4391
+ }), [notes, tab]);
4392
+ const pending = notes.filter((n) => (n.manifestationStatus ?? "") !== "confirmada").length;
4393
+ const filters = useMemo(() => [{
4394
+ id: "manifestacao",
4395
+ label: "Manifesta\xE7\xE3o",
4396
+ allLabel: "Todas",
4397
+ value: tab === "todas" ? void 0 : tab,
4398
+ onChange: (v) => setTab(v ?? "todas"),
4399
+ options: [
4400
+ { value: "pendentes", label: "Pendentes" },
4401
+ { value: "confirmadas", label: "Confirmadas" },
4402
+ { value: "erro", label: "Com erro" }
4403
+ ]
4404
+ }], [tab]);
4405
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
4406
+ /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
4407
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
4408
+ /* @__PURE__ */ jsx(ShieldCheck, { className: "mt-0.5 h-5 w-5 text-muted-foreground", "aria-hidden": true }),
4409
+ /* @__PURE__ */ jsxs("div", { children: [
4410
+ /* @__PURE__ */ jsxs("p", { className: "text-sm font-medium", children: [
4411
+ pending,
4412
+ " ",
4413
+ pending === 1 ? "nota aguardando" : "notas aguardando",
4414
+ " manifesta\xE7\xE3o \xE0 SEFAZ"
4415
+ ] }),
4416
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Manifestar responde \xE0 SEFAZ e tem prazo. Dar entrada responde ao seu estoque e ao financeiro \u2014 s\xE3o duas filas." })
4417
+ ] })
4418
+ ] }),
4419
+ /* @__PURE__ */ jsx(Hint, { className: "mt-3", children: /* @__PURE__ */ jsx("span", { children: "A manifesta\xE7\xE3o continua sendo feita no sistema antigo por enquanto. Aqui voc\xEA l\xEA a fila e o que a SEFAZ respondeu; a entrada no estoque e a conta a pagar j\xE1 s\xE3o feitas nesta tela nova." }) }),
4420
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap gap-2", children: [
4421
+ /* @__PURE__ */ jsx(EmBreve, { label: "Aceitar / Recusar", o_que: "Responder \xE0 SEFAZ \u2014 \xE9 obriga\xE7\xE3o com prazo. Hoje a manifesta\xE7\xE3o \xE9 feita no sistema antigo" }),
4422
+ /* @__PURE__ */ jsx(EmBreve, { label: "Puxar por chave", o_que: "Buscar uma NF-e espec\xEDfica pela chave de acesso de 44 d\xEDgitos" }),
4423
+ /* @__PURE__ */ jsx(EmBreve, { label: "Sincronizar agora", o_que: "Pedir \xE0 SEFAZ o que chegou desde a \xFAltima varredura" }),
4424
+ /* @__PURE__ */ jsx(EmBreve, { label: "Resincronizar tudo", o_que: "Refazer a varredura inteira do per\xEDodo" }),
4425
+ /* @__PURE__ */ jsx(EmBreve, { label: "Diagnosticar", o_que: "Por que uma nota n\xE3o chegou: certificado, credencial ou cursor parado" })
4426
+ ] })
4427
+ ] }),
4428
+ /* @__PURE__ */ jsx(ListFilterBar, { filters, onClear: () => setTab("todas"), activeCount: tab === "todas" ? 0 : 1 }),
4429
+ loading ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full" }, i)) }) : shown.length === 0 ? /* @__PURE__ */ jsxs(Card, { className: "p-10 text-center", children: [
4430
+ /* @__PURE__ */ jsx(FileText, { className: "mx-auto h-8 w-8 text-muted-foreground", "aria-hidden": true }),
4431
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm font-medium", children: "Nenhuma nota nesta fila" })
4432
+ ] }) : /* @__PURE__ */ jsx(Card, { className: "overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
4433
+ /* @__PURE__ */ jsx("thead", { className: "border-b bg-muted/40 text-xs text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
4434
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children: "Emiss\xE3o" }),
4435
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children: "N\xBA" }),
4436
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children: "Fornecedor" }),
4437
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: "Valor" }),
4438
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: "Parcelas" }),
4439
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-center font-medium", children: "Manifesta\xE7\xE3o" }),
4440
+ /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: "A\xE7\xF5es" })
4441
+ ] }) }),
4442
+ /* @__PURE__ */ jsx("tbody", { children: shown.map((note) => /* @__PURE__ */ jsxs("tr", { className: "border-b last:border-0 hover:bg-muted/30", children: [
4443
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 tabular-nums text-muted-foreground", children: day3(note.issueDate) }),
4444
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 tabular-nums font-medium", children: note.number }),
4445
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: note.supplierName || "\u2014" }),
4446
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-right tabular-nums", children: money3(note.total) }),
4447
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-right tabular-nums text-muted-foreground", children: note.instalmentCount }),
4448
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-center", children: /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${MANIFEST_TONE[note.manifestationStatus ?? ""] ?? "bg-muted text-muted-foreground"}`, children: note.manifestationStatus ?? "n\xE3o manifestada" }) }),
4449
+ /* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [
4450
+ /* @__PURE__ */ jsx(EmBreve, { label: "XML", o_que: "Ver o XML bruto autorizado desta nota" }),
4451
+ /* @__PURE__ */ jsx(EmBreve, { label: "PDF", o_que: "Abrir o DANFE desta nota" }),
4452
+ /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: () => onOpenNote(note.id), children: [
4453
+ "Abrir concilia\xE7\xE3o",
4454
+ /* @__PURE__ */ jsx(ExternalLink, { className: "ml-1.5 h-3.5 w-3.5" })
4455
+ ] })
4456
+ ] }) })
4457
+ ] }, note.id)) })
4458
+ ] }) }) })
4459
+ ] });
4460
+ }
4461
+ var HASH_PREFIX = "#/inbound-notes";
4462
+ function viewFromLocation() {
4463
+ if (typeof window === "undefined") return { kind: "list" };
4464
+ const hash = window.location.hash;
4465
+ const note = hash.match(/\/inbound-notes\/note\/([0-9a-f-]{36})/i);
4466
+ if (note) return { kind: "note", id: note[1] };
4467
+ if (/\/inbound-notes\/manifest/.test(hash)) return { kind: "manifest" };
4468
+ return { kind: "list" };
4469
+ }
4470
+ var pathOf = (view) => view.kind === "note" ? `${HASH_PREFIX}/note/${view.id}` : view.kind === "manifest" ? `${HASH_PREFIX}/manifest` : HASH_PREFIX;
4471
+ function InboundNotesPage({ payableRoute } = {}) {
4472
+ const [view, setView] = useState(viewFromLocation);
4473
+ const go = useCallback((next) => {
4474
+ setView(next);
4475
+ if (typeof window !== "undefined") {
4476
+ window.history.replaceState(null, "", pathOf(next));
4477
+ }
4478
+ }, []);
4479
+ useEffect(() => {
4480
+ if (typeof window === "undefined") return;
4481
+ const onPop = () => setView(viewFromLocation());
4482
+ window.addEventListener("popstate", onPop);
4483
+ window.addEventListener("hashchange", onPop);
4484
+ return () => {
4485
+ window.removeEventListener("popstate", onPop);
4486
+ window.removeEventListener("hashchange", onPop);
4487
+ };
4488
+ }, []);
4489
+ if (view.kind === "note") {
4490
+ return /* @__PURE__ */ jsx(
4491
+ NoteReconcileView,
4492
+ {
4493
+ noteId: view.id,
4494
+ onBack: () => go({ kind: "list" }),
4495
+ onOpenInvoice: payableRoute ? (invoiceId) => {
4496
+ window.location.hash = payableRoute(invoiceId);
4497
+ } : void 0
4498
+ }
4499
+ );
4500
+ }
4501
+ if (view.kind === "manifest") {
4502
+ return /* @__PURE__ */ jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsx(ManifestView, { onOpenNote: (id) => go({ kind: "note", id }) }) });
4503
+ }
4504
+ return /* @__PURE__ */ jsx(
4505
+ InboundNotesView,
4506
+ {
4507
+ onOpenNote: (id) => go({ kind: "note", id }),
4508
+ onOpenManifest: () => go({ kind: "manifest" })
4509
+ }
4510
+ );
4511
+ }
4512
+ function makeInboundNotesRoute(options = {}) {
4513
+ return function InboundNotesRoute() {
4514
+ return /* @__PURE__ */ jsx(InboundNotesPage, { ...options });
4515
+ };
4516
+ }
4517
+
1932
4518
  // src/lib/financial-provider.ts
1933
4519
  function fiscalReader(provider) {
1934
4520
  if (!provider.getNotaById) throw new Error("PlugNotas requires fiscal document reads from Financial.");
@@ -2020,6 +4606,84 @@ function createPlugNotasFinancialProvider(options) {
2020
4606
  }
2021
4607
  });
2022
4608
  }
4609
+ function TaxGroupsView({ onBack } = {}) {
4610
+ return /* @__PURE__ */ jsx(
4611
+ ComingSoon,
4612
+ {
4613
+ title: "Grupos Fiscais",
4614
+ summary: "Perfis tribut\xE1rios reutiliz\xE1veis \u2014 CFOP, CSOSN/CST, PIS, COFINS, ICMS \u2014 que os produtos usam na emiss\xE3o.",
4615
+ v1Path: "Estoque \u2192 Notas Fiscais \u2192 Grupos Fiscais",
4616
+ measured: "Vazio no V1 da Artorius (\xABNenhum grupo fiscal cadastrado\xBB), coerente com eles n\xE3o emitirem nota. O Pertinho do C\xE9u e o Espa\xE7o Facial t\xEAm 11 entre os dois.",
4617
+ missing: [
4618
+ "Trazer o cadastro para dentro do grupo: a tabela plg_fiscalbr_tax_groups j\xE1 existe e j\xE1 recebeu os 11 grupos do V1",
4619
+ "Hoje ele s\xF3 aparece na lista gen\xE9rica de cadastros, longe de onde a nota \xE9 usada",
4620
+ "S\xF3 faz sentido junto com Notas Emitidas \u2014 grupo fiscal \xE9 insumo de emiss\xE3o"
4621
+ ],
4622
+ onBack,
4623
+ backTo: "Notas Fiscais"
4624
+ }
4625
+ );
4626
+ }
4627
+ function IssuedNotesView({ onBack } = {}) {
4628
+ return /* @__PURE__ */ jsx(
4629
+ ComingSoon,
4630
+ {
4631
+ title: "Notas Emitidas",
4632
+ summary: "As NF-e, NFS-e e NFC-e que a casa emitiu, com o estado de cada uma na SEFAZ \u2014 autorizada, processando, rejeitada ou cancelada \u2014 e a comanda ou fatura que deu origem a ela.",
4633
+ v1Path: "Estoque \u2192 Notas Fiscais \u2192 Notas Emitidas",
4634
+ measured: "A Artorius n\xE3o emite nota hoje: em 80 documentos recebidos, zero emitidos. Quem emite \xE9 o Pertinho do C\xE9u e o Espa\xE7o Facial.",
4635
+ missing: [
4636
+ "Ligar o emissor da PlugNotas a este CNPJ (o certificado A1 j\xE1 est\xE1 l\xE1 desde maio; falta a credencial chegar neste ambiente)",
4637
+ "A lista com o estado na SEFAZ e o rastro de tentativas \u2014 as tabelas j\xE1 existem (plg_fiscalbr_nfce_jobs, _events)",
4638
+ "Abrir o XML autorizado e o DANFE de cada nota",
4639
+ "Cancelar uma nota emitida, com justificativa"
4640
+ ],
4641
+ onBack,
4642
+ backTo: "Notas Fiscais"
4643
+ }
4644
+ );
4645
+ }
4646
+ function ReturnedNotesView({ onBack } = {}) {
4647
+ return /* @__PURE__ */ jsx(
4648
+ ComingSoon,
4649
+ {
4650
+ title: "Notas Devolvidas",
4651
+ summary: "As notas fiscais de devolu\xE7\xE3o: o que voltou para o fornecedor, e o que isso desfaz no estoque e na conta a pagar.",
4652
+ v1Path: "Estoque \u2192 Notas Fiscais \u2192 Notas Devolvidas",
4653
+ v1AlsoMissing: true,
4654
+ missing: [
4655
+ "Decidir o gesto: devolver a nota INTEIRA ou s\xF3 algumas linhas dela",
4656
+ "A devolu\xE7\xE3o ESTORNA o estoque que a entrada criou \u2014 e o movimento de estorno j\xE1 existe no V2 (reverses_movement_id)",
4657
+ "E abate o t\xEDtulo a pagar, ou gera um cr\xE9dito com o fornecedor. \xC9 decis\xE3o de produto, n\xE3o de c\xF3digo",
4658
+ "Emitir a NF-e de devolu\xE7\xE3o exige emissor ligado \u2014 mesma depend\xEAncia de Notas Emitidas"
4659
+ ],
4660
+ onBack,
4661
+ backTo: "Notas Fiscais"
4662
+ }
4663
+ );
4664
+ }
4665
+ function inboundNotesInventoryView(options = {}) {
4666
+ return {
4667
+ id: "inbound-notes",
4668
+ label: "Notas Fiscais",
4669
+ icon: "FileText",
4670
+ render: () => /* @__PURE__ */ jsx(InboundNotesPage, { ...options }),
4671
+ // A ordem é a do V1: Grupos Fiscais vem antes das duas de emissão, que é
4672
+ // onde ele é usado.
4673
+ //
4674
+ // Bonificação NÃO é aba. Ela chegou aqui por engano meu: no V1 não existe
4675
+ // esse menu — a bonificação é uma NATUREZA DE OPERAÇÃO que vem escrita na
4676
+ // nota (57 delas, em 11 grafias), e o que ela muda é o comportamento da
4677
+ // entrada: a mercadoria soma ao estoque e NÃO abre conta a pagar. O aviso
4678
+ // continua onde serve, na conciliação da nota. Uma tela vazia para um
4679
+ // comportamento é um menu que promete o que não vai entregar.
4680
+ children: [
4681
+ { id: "tax-groups", label: "Grupos Fiscais", render: () => /* @__PURE__ */ jsx(TaxGroupsView, {}) },
4682
+ { id: "issued-notes", label: "Notas Emitidas", render: () => /* @__PURE__ */ jsx(IssuedNotesView, {}) },
4683
+ { id: "returned-notes", label: "Notas Devolvidas", render: () => /* @__PURE__ */ jsx(ReturnedNotesView, {}) }
4684
+ ]
4685
+ };
4686
+ }
2023
4687
 
2024
4688
  // src/index.ts
2025
4689
  function createPlugNotasPlugin(options = {}) {
@@ -2032,8 +4696,28 @@ function createPlugNotasPlugin(options = {}) {
2032
4696
  scope: "addon",
2033
4697
  defaultEnabled: true,
2034
4698
  dependencies: ["financial"],
4699
+ // SEM linha no rail, de propósito.
4700
+ //
4701
+ // Entrada de Notas é assunto de ESTOQUE, não assunto do menu: no V1 o caminho
4702
+ // é `Estoque → Notas Fiscais → Entrada de Notas`. Montei como linha solta na
4703
+ // primeira tentativa e estava errado — quem vem do V1 procura dentro do
4704
+ // Estoque e não acha. O app entrega a tela ao inventário por
4705
+ // `createInventoryPlugin({ extraViews })`, e ela aparece como aba irmã de
4706
+ // Produtos, Movimentações e Contagens.
4707
+ //
4708
+ // A rota própria FICA: ela é o deep link (`#/inbound-notes`), e é por onde o
4709
+ // agente e um link de e-mail chegam sem passar pela faixa de abas.
2035
4710
  navigation: [],
2036
- routes: [],
4711
+ routes: [
4712
+ {
4713
+ path: "/inbound-notes",
4714
+ // O registro de rotas monta componentes sem prop. A opção do host entra
4715
+ // por fechamento, e a fábrica mora no `.tsx` para este manifesto seguir
4716
+ // sendo TypeScript sem JSX.
4717
+ component: makeInboundNotesRoute({ payableRoute: options.payableRoute }),
4718
+ permission: { feature: "financial", action: "read" }
4719
+ }
4720
+ ],
2037
4721
  // O grupo fiscal é CADASTRO, e por isso entra como registry e não como
2038
4722
  // rota própria: ele aparece onde os outros cadastros do produto aparecem.
2039
4723
  registries: fiscalRegistries,
@@ -2108,6 +4792,6 @@ function createPlugNotasPlugin(options = {}) {
2108
4792
  });
2109
4793
  }
2110
4794
 
2111
- export { PLUGNOTAS_CONNECTOR_ID, PLUGNOTAS_FIELDS, PLUGNOTAS_SETTINGS, createPlugNotasConnector, createPlugNotasFinancialProvider, createPlugNotasPlugin, isValidCnpj, markPlugNotasEmitterReady };
4795
+ 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 };
2112
4796
  //# sourceMappingURL=index.js.map
2113
4797
  //# sourceMappingURL=index.js.map