@fayz-ai/plugin-fiscal-br 0.3.2 → 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 { 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";
@@ -1576,13 +1581,2939 @@ DROP INDEX IF EXISTS public.plg_fiscalbr_tax_groups_name_uidx;
1576
1581
  CREATE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_name_idx
1577
1582
  ON public.plg_fiscalbr_tax_groups USING btree (tenant_id, lower(name));
1578
1583
  `;
1584
+ var MIGRATION_005_A_NOTA_EMITIDA_E_UM_DOCUMENTO = `-- A nota emitida \xE9 um DOCUMENTO, e o documento j\xE1 tem arqu\xE9tipo.
1585
+ --
1586
+ -- O V1 do Pertinho do C\xE9u emitiu 496 notas (todas NFC-e: 440 conclu\xEDdas, 51
1587
+ -- rejeitadas, 5 com erro) e nenhuma delas tem onde morar no V2. O que existe
1588
+ -- hoje aqui \xE9 a caixa de ENTRADA (\`plg_fiscalbr_inbox_*\`, a nota que chega do
1589
+ -- fornecedor) e o JOB de sa\xEDda (\`plg_fiscalbr_nfce_jobs\`, a tentativa de
1590
+ -- emitir). Falta o resultado: a nota que a casa emitiu, com chave, protocolo e
1591
+ -- XML.
1592
+ --
1593
+ -- \u2500\u2500 Por que pendurar em \`public.documents\` e n\xE3o numa tabela solta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1594
+ --
1595
+ -- Porque a pergunta que o operador faz n\xE3o \xE9 "quais notas fiscais existem",
1596
+ -- \xE9 "o que foi emitido sobre ESTE pedido". Hoje a resposta est\xE1 espalhada:
1597
+ -- o anexo do t\xEDtulo est\xE1 em \`documents\`, o formul\xE1rio est\xE1 em \`documents\`, a
1598
+ -- foto do ponto est\xE1 em \`documents\` \u2014 e a nota fiscal ficaria de fora, sendo
1599
+ -- o documento mais documento de todos.
1600
+ --
1601
+ -- \`documents\` \xE9 o arqu\xE9tipo: \`kind\` diz a esp\xE9cie, \`subject_type\`/\`subject_id\`
1602
+ -- dizem sobre o qu\xEA, e o plugin pendura o que s\xF3 ele entende numa extens\xE3o 1:1
1603
+ -- pela chave \u2014 exatamente como \`plg_forms_documents\` j\xE1 faz. Esta migra\xE7\xE3o
1604
+ -- segue esse padr\xE3o, n\xE3o inventa outro.
1605
+ --
1606
+ -- \u2500\u2500 O que entra e o que N\xC3O entra \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1607
+ --
1608
+ -- ENTRA o artefato legal e a trilha:
1609
+ -- xml_content 113 de 496 preenchidos, 618 kB no total (5,6 kB cada).
1610
+ -- \xC9 a prova da emiss\xE3o e n\xE3o se regenera. Fica.
1611
+ -- os quatro payloads 108 kB para as 496 notas inteiras. \xC9 a conversa com a
1612
+ -- SEFAZ, e \xE9 ela que explica as 51 rejei\xE7\xF5es.
1613
+ -- pdf_storage_path 84 de 496. Vira \`documents.storage_path\` com
1614
+ -- \`storage_provider = 'v1-pending'\`, a mesma conven\xE7\xE3o
1615
+ -- que \`upsert_document\` usa: o REGISTRO chegou, os bytes
1616
+ -- ainda n\xE3o.
1617
+ --
1618
+ -- N\xC3O entra nada que o V2 j\xE1 saiba responder por conta pr\xF3pria, e a emiss\xE3o
1619
+ -- nova do V2 continua nascendo por \`plg_fiscalbr_nfce_jobs\` \u2014 este arquivo \xE9 o
1620
+ -- destino do resultado, n\xE3o um segundo caminho de emiss\xE3o.
1621
+ --
1622
+ -- \u2500\u2500 Os dois ponteiros da nota do V1, que n\xE3o s\xE3o o mesmo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1623
+ --
1624
+ -- fiscal_documents.order_id 496 de 496 \u2014 a COMANDA (\`table_orders\`)
1625
+ -- fiscal_documents.invoice_id 118 de 496 \u2014 a VENDA (\`invoices\`)
1626
+ --
1627
+ -- Os dois chegam em \`public.orders\` no V2, e \xE0s vezes na MESMA linha (a
1628
+ -- comanda que virou nota adota a venda). Guardar os dois na mesma coluna
1629
+ -- perderia justamente a distin\xE7\xE3o: \`order_id\` na extens\xE3o \xE9 a comanda; a venda
1630
+ -- vai para \`plg_fiscalbr_document_orders\`, que \xE9 N:N porque uma nota pode
1631
+ -- juntar v\xE1rias (511 v\xEDnculos para 496 notas).
1632
+ --
1633
+ -- Idempotente.
1634
+
1635
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1636
+ -- 1. A esp\xE9cie entra no vocabul\xE1rio
1637
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1638
+ -- \`migration.upsert_document\` RECUSA esp\xE9cie que n\xE3o esteja aqui, e faz certo:
1639
+ -- \xE9 o registro que impede \`kind\` de virar campo livre.
1640
+
1641
+ INSERT INTO public.document_kinds (kind, plugin, description, statuses, status)
1642
+ VALUES ('fiscal_document', 'fiscal_br',
1643
+ 'A nota fiscal que a casa EMITIU (NFC-e, NF-e, NFS-e), com chave, protocolo e XML',
1644
+ ARRAY['draft','processing','authorized','rejected','error','cancelled','substituted'],
1645
+ 'declared')
1646
+ ON CONFLICT (kind) DO UPDATE
1647
+ SET plugin = EXCLUDED.plugin,
1648
+ description = EXCLUDED.description,
1649
+ statuses = EXCLUDED.statuses,
1650
+ updated_at = now();
1651
+
1652
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1653
+ -- 2. A extens\xE3o: o que s\xF3 o fiscal entende
1654
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1655
+
1656
+ CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_documents (
1657
+ -- A chave \xC9 o documento. N\xE3o h\xE1 id pr\xF3prio: a nota e o documento s\xE3o a mesma
1658
+ -- coisa vista de dois lugares, e um segundo id s\xF3 criaria a chance de eles
1659
+ -- discordarem.
1660
+ document_id uuid PRIMARY KEY REFERENCES public.documents(id) ON DELETE CASCADE,
1661
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
1662
+ unit_id uuid,
1663
+ -- A comanda/pedido que originou a emiss\xE3o.
1664
+ order_id uuid REFERENCES public.orders(id) ON DELETE SET NULL,
1665
+ model text NOT NULL,
1666
+ environment text,
1667
+ status text NOT NULL,
1668
+ series text,
1669
+ number text,
1670
+ -- 44 d\xEDgitos. Sem CHECK de formato: uma nota REJEITADA n\xE3o tem chave, e uma
1671
+ -- chave que a SEFAZ devolveu torta \xE9 informa\xE7\xE3o, n\xE3o erro de digita\xE7\xE3o.
1672
+ access_key text,
1673
+ verification_code text,
1674
+ protocol text,
1675
+ provider text,
1676
+ provider_document_id text,
1677
+ provider_integration_id text,
1678
+ total_value numeric(14,2),
1679
+ iss_value numeric(14,2),
1680
+ recipient_snapshot jsonb,
1681
+ request_payload jsonb,
1682
+ response_payload jsonb,
1683
+ status_payload jsonb,
1684
+ rejection_payload jsonb,
1685
+ source_snapshot jsonb,
1686
+ -- O artefato legal. 5,6 kB por nota nas 113 que o t\xEAm; n\xE3o se regenera.
1687
+ xml text,
1688
+ authorized_at timestamptz,
1689
+ last_polled_at timestamptz,
1690
+ emitted_by_user_id uuid,
1691
+ cancelled_at timestamptz,
1692
+ cancel_reason text,
1693
+ cancelled_by_user_id uuid,
1694
+ -- A cadeia de substitui\xE7\xE3o. Auto-refer\xEAncia, ent\xE3o SET NULL: apagar a nota
1695
+ -- substitu\xEDda n\xE3o pode arrastar a que a substituiu.
1696
+ substitutes_document_id uuid REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE SET NULL,
1697
+ substituted_by_document_id uuid REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE SET NULL,
1698
+ observations text,
1699
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
1700
+ created_at timestamptz NOT NULL DEFAULT now(),
1701
+ updated_at timestamptz NOT NULL DEFAULT now()
1702
+ );
1703
+
1704
+ DO $$ BEGIN
1705
+ ALTER TABLE public.plg_fiscalbr_documents
1706
+ ADD CONSTRAINT plg_fiscalbr_documents_unit_fkey
1707
+ FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
1708
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
1709
+
1710
+ -- A chave de acesso \xE9 \xFAnica POR INQUILINO e s\xF3 quando existe: 56 das 496 notas
1711
+ -- do Pertinho n\xE3o t\xEAm chave nenhuma (rejeitadas e com erro), e um UNIQUE
1712
+ -- comum as empilharia todas num conflito s\xF3.
1713
+ CREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_documents_chave_idx
1714
+ ON public.plg_fiscalbr_documents (tenant_id, access_key) WHERE access_key IS NOT NULL;
1715
+ CREATE INDEX IF NOT EXISTS plg_fiscalbr_documents_order_idx
1716
+ ON public.plg_fiscalbr_documents (tenant_id, order_id);
1717
+ CREATE INDEX IF NOT EXISTS plg_fiscalbr_documents_status_idx
1718
+ ON public.plg_fiscalbr_documents (tenant_id, unit_id, status, authorized_at DESC);
1719
+
1720
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1721
+ -- 3. A trilha da emiss\xE3o
1722
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1723
+ -- 1.886 eventos para 496 notas \u2014 quase quatro por nota. \xC9 aqui que a rejei\xE7\xE3o
1724
+ -- diz o motivo, e \xE9 o \xFAnico lugar em que ela diz.
1725
+
1726
+ CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_document_events (
1727
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
1728
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
1729
+ unit_id uuid,
1730
+ document_id uuid NOT NULL REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE CASCADE,
1731
+ event_type text NOT NULL,
1732
+ status text,
1733
+ justification text,
1734
+ request_payload jsonb,
1735
+ response_payload jsonb,
1736
+ created_by_user_id uuid,
1737
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
1738
+ created_at timestamptz NOT NULL DEFAULT now()
1739
+ );
1740
+
1741
+ CREATE INDEX IF NOT EXISTS plg_fiscalbr_document_events_doc_idx
1742
+ ON public.plg_fiscalbr_document_events (document_id, created_at DESC);
1743
+
1744
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1745
+ -- 4. Quais vendas a nota cobre
1746
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1747
+
1748
+ CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_document_orders (
1749
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
1750
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
1751
+ document_id uuid NOT NULL REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE CASCADE,
1752
+ order_id uuid NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE,
1753
+ created_at timestamptz NOT NULL DEFAULT now(),
1754
+ UNIQUE (document_id, order_id)
1755
+ );
1756
+
1757
+ CREATE INDEX IF NOT EXISTS plg_fiscalbr_document_orders_order_idx
1758
+ ON public.plg_fiscalbr_document_orders (tenant_id, order_id);
1759
+
1760
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1761
+ -- 5. updated_at e a cerca
1762
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1763
+
1764
+ DO $$ BEGIN
1765
+ CREATE TRIGGER plg_fiscalbr_documents_updated_at BEFORE UPDATE
1766
+ ON public.plg_fiscalbr_documents FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
1767
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
1768
+
1769
+ -- \`app.current_tenant_id()\` e n\xE3o \`user_tenant_ids()\`, pela raz\xE3o que a 013 do
1770
+ -- plugin-tables teve de consertar em produ\xE7\xE3o: a segunda devolve a UNI\xC3O das
1771
+ -- contas do chamador e mostraria a nota fiscal de um cliente para outro.
1772
+ DO $$
1773
+ DECLARE t text;
1774
+ BEGIN
1775
+ FOREACH t IN ARRAY ARRAY['plg_fiscalbr_documents','plg_fiscalbr_document_events',
1776
+ 'plg_fiscalbr_document_orders'] LOOP
1777
+ EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
1778
+ EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_select', t);
1779
+ EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_insert', t);
1780
+ EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_update', t);
1781
+ EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_delete', t);
1782
+ EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT USING (tenant_id = (select app.current_tenant_id()))', t || '_select', t);
1783
+ EXECUTE format('CREATE POLICY %I ON public.%I FOR INSERT WITH CHECK (tenant_id = (select app.current_tenant_id()))', t || '_insert', t);
1784
+ EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE USING (tenant_id = (select app.current_tenant_id()))', t || '_update', t);
1785
+ EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE USING (tenant_id = (select app.current_tenant_id()))', t || '_delete', t);
1786
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);
1787
+ END LOOP;
1788
+ END $$;
1789
+ `;
1790
+ var MIGRATION_006_O_GRUPO_FISCAL_GANHA_O_RAIL = `-- O grupo fiscal ganha observa\xE7\xE3o e etiqueta.
1791
+ --
1792
+ -- A tabela existe desde a 002 e nunca teve TELA: o grupo fiscal de toda conta
1793
+ -- migrada estava no banco sem porta. Estas duas colunas s\xE3o o que falta para
1794
+ -- ele ter o mesmo rail de todo cadastro do produto \u2014 e a etiqueta tem uso
1795
+ -- imediato aqui, porque \xABSimples\xBB, \xABmonof\xE1sico\xBB e \xABsubstitui\xE7\xE3o tribut\xE1ria\xBB
1796
+ -- s\xE3o recortes que nenhum campo do grupo responde e que decidem qual grupo
1797
+ -- algu\xE9m procura.
1798
+ --
1799
+ -- Idempotente.
1800
+
1801
+ ALTER TABLE public.plg_fiscalbr_tax_groups
1802
+ ADD COLUMN IF NOT EXISTS notes text,
1803
+ ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[],
1804
+ ADD COLUMN IF NOT EXISTS unit_id uuid;
1805
+
1806
+ CREATE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_tags_gin
1807
+ ON public.plg_fiscalbr_tax_groups USING gin (tags);
1808
+ `;
1809
+ var MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL = `-- ---------------------------------------------------------------------------
1810
+ -- 007_a_rls_do_documento_passa_a_ser_legivel.sql
1811
+ --
1812
+ -- MESMO caso da 036 do plugin-tables, e a mesma medi\xE7\xE3o antes de escrever.
1813
+ --
1814
+ -- \`check-plugin-review\` reprovou estas tabelas com *\xABcria public.X com
1815
+ -- tenant_id e nunca liga row security\xBB*. Fui ao banco: **todas t\xEAm RLS ligada,
1816
+ -- com pol\xEDticas** \u2014 medido no cluster do Pertinho em 29-09:
1817
+ --
1818
+ -- plg_fiscalbr_documents ........ rls=t, 4 pol\xEDticas
1819
+ -- plg_fiscalbr_document_events .. rls=t, 4
1820
+ -- plg_fiscalbr_document_orders .. rls=t, 4
1821
+ --
1822
+ -- N\xE3o h\xE1 vazamento. O \`ENABLE\` dessas tabelas mora dentro de um
1823
+ -- \`DO $$ \u2026 EXECUTE format(...)\`, e o port\xE3o l\xEA o ARQUIVO, n\xE3o o banco \u2014 uma
1824
+ -- garantia que o revisor n\xE3o consegue ler \xE9 uma garantia que ningu\xE9m confere.
1825
+ --
1826
+ -- As migrations que as criam J\xC1 FORAM APLICADAS; reescrev\xEA-las trocaria o
1827
+ -- checksum, que \xE9 a deriva de ledger pela qual este reposit\xF3rio para o mundo.
1828
+ -- Ent\xE3o a legibilidade entra por arquivo NOVO \u2014 o port\xE3o pergunta ao SQL
1829
+ -- INTEIRO do plugin.
1830
+ --
1831
+ -- Ligar RLS j\xE1 ligada \xE9 no-op: isto n\xE3o muda comportamento, torna confer\xEDvel o
1832
+ -- que j\xE1 era verdade.
1833
+ --
1834
+ -- Idempotente e replay-safe.
1835
+ -- ---------------------------------------------------------------------------
1836
+
1837
+ ALTER TABLE public.plg_fiscalbr_documents ENABLE ROW LEVEL SECURITY;
1838
+ ALTER TABLE public.plg_fiscalbr_document_events ENABLE ROW LEVEL SECURITY;
1839
+ ALTER TABLE public.plg_fiscalbr_document_orders ENABLE ROW LEVEL SECURITY;
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
+ `;
1579
2834
  var MIGRATIONS = [
1580
2835
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
1581
2836
  { id: "001_preserve_terminal_results_and_recover_leases", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },
1582
2837
  { id: "002_a_nota_que_chega_tem_onde_esperar", sql: MIGRATION_002_A_NOTA_QUE_CHEGA_TEM_ONDE_ESPERAR },
1583
2838
  { id: "003_a_caixa_de_entrada_atravessa", sql: MIGRATION_003_A_CAIXA_DE_ENTRADA_ATRAVESSA },
1584
- { id: "004_o_grupo_fiscal_do_v1_repete_nome", sql: MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME }
2839
+ { id: "004_o_grupo_fiscal_do_v1_repete_nome", sql: MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME },
2840
+ { id: "005_a_nota_emitida_e_um_documento", sql: MIGRATION_005_A_NOTA_EMITIDA_E_UM_DOCUMENTO },
2841
+ { id: "006_o_grupo_fiscal_ganha_o_rail", sql: MIGRATION_006_O_GRUPO_FISCAL_GANHA_O_RAIL },
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 }
2846
+ ];
2847
+ var taxGroupEntity = {
2848
+ name: "Grupo fiscal",
2849
+ namePlural: "Grupos fiscais",
2850
+ icon: "ReceiptText",
2851
+ displayField: "name",
2852
+ defaultSort: "name",
2853
+ formColumns: 2,
2854
+ facets: [activeFacet({ active: "Ativos", inactive: "Inativos", all: "Todos" })],
2855
+ fieldGroups: [
2856
+ {
2857
+ id: "identity",
2858
+ label: "Identifica\xE7\xE3o",
2859
+ columns: 2,
2860
+ description: "O nome \xE9 como a casa chama o grupo (\xABBebida alco\xF3lica\xBB, \xABServi\xE7o isento\xBB) \u2014 o resto \xE9 o que a nota exige."
2861
+ },
2862
+ { id: "icms", label: "ICMS", columns: 3 },
2863
+ { id: "pis_cofins", label: "PIS e COFINS", columns: 2 },
2864
+ {
2865
+ id: "ibs_cbs",
2866
+ label: "IBS e CBS",
2867
+ columns: 2,
2868
+ description: "A reforma tribut\xE1ria. Em branco enquanto n\xE3o for exigido da sua opera\xE7\xE3o."
2869
+ },
2870
+ { ...RECORD_GROUP }
2871
+ ],
2872
+ fields: [
2873
+ { key: "name", label: "Nome", type: "text", required: true, showInTable: true, searchable: true, group: "identity", span: 2 },
2874
+ // O CFOP classifica a OPERAÇÃO (venda dentro do estado, fora, devolução) e
2875
+ // é o primeiro campo que o contador confere.
2876
+ { key: "cfop", label: "CFOP", type: "text", showInTable: true, searchable: true, group: "identity" },
2877
+ // CEST anda com a substituição tributária: só existe para quem está nela.
2878
+ { key: "cest", label: "CEST", type: "text", showInTable: false, group: "identity" },
2879
+ { key: "benefitCode", label: "C\xF3digo de benef\xEDcio", type: "text", showInTable: false, group: "identity" },
2880
+ // CST é do regime normal e CSOSN é do Simples — a casa usa UM dos dois, e
2881
+ // qual depende do regime dela, não deste grupo. Os dois ficam à vista de
2882
+ // propósito: esconder o que a conta não usa exigiria saber o regime aqui.
2883
+ { key: "cst", label: "CST", type: "text", showInTable: true, group: "icms" },
2884
+ { key: "csosn", label: "CSOSN (Simples)", type: "text", showInTable: false, group: "icms" },
2885
+ {
2886
+ key: "icmsOrigin",
2887
+ label: "Origem da mercadoria",
2888
+ type: "select",
2889
+ showInTable: false,
2890
+ group: "icms",
2891
+ options: [
2892
+ { value: "0", label: "0 \u2014 Nacional" },
2893
+ { value: "1", label: "1 \u2014 Estrangeira, importa\xE7\xE3o direta" },
2894
+ { value: "2", label: "2 \u2014 Estrangeira, mercado interno" },
2895
+ { value: "3", label: "3 \u2014 Nacional, conte\xFAdo de importa\xE7\xE3o > 40%" },
2896
+ { value: "4", label: "4 \u2014 Nacional, processos produtivos b\xE1sicos" },
2897
+ { value: "5", label: "5 \u2014 Nacional, conte\xFAdo de importa\xE7\xE3o \u2264 40%" },
2898
+ { value: "6", label: "6 \u2014 Estrangeira, sem similar nacional" },
2899
+ { value: "7", label: "7 \u2014 Estrangeira, mercado interno sem similar" },
2900
+ { value: "8", label: "8 \u2014 Nacional, conte\xFAdo de importa\xE7\xE3o > 70%" }
2901
+ ]
2902
+ },
2903
+ { key: "icmsRate", label: "Al\xEDquota ICMS (%)", type: "number", min: 0, max: 100, showInTable: true, group: "icms" },
2904
+ { key: "icmsBcMode", label: "Modalidade da base", type: "text", showInTable: false, group: "icms" },
2905
+ { key: "baseCalculationMode", label: "C\xE1lculo da base", type: "text", showInTable: false, group: "icms" },
2906
+ // O fundo de combate à pobreza é adicional ao ICMS em alguns estados.
2907
+ { key: "povertyFundRate", label: "Fundo de pobreza (%)", type: "number", min: 0, max: 100, showInTable: false, group: "icms" },
2908
+ { key: "pisSituation", label: "Situa\xE7\xE3o PIS", type: "text", showInTable: false, group: "pis_cofins" },
2909
+ { key: "pisRate", label: "Al\xEDquota PIS (%)", type: "number", min: 0, max: 100, showInTable: false, group: "pis_cofins" },
2910
+ { key: "cofinsSituation", label: "Situa\xE7\xE3o COFINS", type: "text", showInTable: false, group: "pis_cofins" },
2911
+ { key: "cofinsRate", label: "Al\xEDquota COFINS (%)", type: "number", min: 0, max: 100, showInTable: false, group: "pis_cofins" },
2912
+ { key: "ibsCbsSituation", label: "Situa\xE7\xE3o IBS/CBS", type: "text", showInTable: false, group: "ibs_cbs" },
2913
+ { key: "ibsCbsClassification", label: "Classifica\xE7\xE3o IBS/CBS", type: "text", showInTable: false, group: "ibs_cbs" },
2914
+ ...recordRailFields({ tagsTable: "plg_fiscalbr_tax_groups", activeLabel: "Ativo" })
2915
+ ],
2916
+ data: {
2917
+ table: "plg_fiscalbr_tax_groups",
2918
+ schema: "public",
2919
+ tenantScoped: true,
2920
+ searchColumns: ["name", "cfop"]
2921
+ }
2922
+ };
2923
+ var fiscalRegistries = [
2924
+ {
2925
+ id: "taxGroups",
2926
+ icon: "ReceiptText",
2927
+ entity: taxGroupEntity,
2928
+ description: "A tributa\xE7\xE3o que um produto herda: CFOP, ICMS, PIS/COFINS e, quando exigido, IBS/CBS.",
2929
+ agentHint: "Regras de imposto que um produto herda \u2014 N\xC3O \xE9 a nota emitida nem o imposto de uma venda espec\xEDfica."
2930
+ }
2931
+ ];
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" }
1585
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
+ }
1586
4517
 
1587
4518
  // src/lib/financial-provider.ts
1588
4519
  function fiscalReader(provider) {
@@ -1675,6 +4606,84 @@ function createPlugNotasFinancialProvider(options) {
1675
4606
  }
1676
4607
  });
1677
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
+ }
1678
4687
 
1679
4688
  // src/index.ts
1680
4689
  function createPlugNotasPlugin(options = {}) {
@@ -1687,8 +4696,31 @@ function createPlugNotasPlugin(options = {}) {
1687
4696
  scope: "addon",
1688
4697
  defaultEnabled: true,
1689
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.
1690
4710
  navigation: [],
1691
- 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
+ ],
4721
+ // O grupo fiscal é CADASTRO, e por isso entra como registry e não como
4722
+ // rota própria: ele aparece onde os outros cadastros do produto aparecem.
4723
+ registries: fiscalRegistries,
1692
4724
  connectors: [createPlugNotasConnector(options)],
1693
4725
  functions: [{
1694
4726
  slug: "plugnotas-bridge",
@@ -1760,6 +4792,6 @@ function createPlugNotasPlugin(options = {}) {
1760
4792
  });
1761
4793
  }
1762
4794
 
1763
- 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 };
1764
4796
  //# sourceMappingURL=index.js.map
1765
4797
  //# sourceMappingURL=index.js.map