panels-origin/db/core/changesets/018-schema-expenses-cost.sql
Alberto Martinez 41273dfb04 Gastos, almacén y control presupuestal
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Validación de endpoints y SPs

Se probaron todos los endpoints nuevos (gastos, almacén, control presupuestal, IAM) y sus RPCs asociados.

### Bugs corregidos en migraciones/SQL

1. **IAM grants en schema core** — `iam-004e` intentaba `GRANT` sobre `core` con rol `iam_owner` (sin permiso). Los grants cruzados se movieron a `core-027-iam-rpc-cross-grants.sql`.
2. **`_cost_settings` ambiguo** — columnas `budget_warn_pct` etc. colisionaban con `RETURNS TABLE` en PL/pgSQL, rompiendo `fn_expense_create` y control presupuestal. Corregido en `core-028-fix-cost-settings-ambiguous.sql`.

### Tests añadidos

- `api/cost_modules_test.ts` — CRUD RPC gastos, flujo almacén completo, control presupuestal, IAM permisos
- `scripts/crud-smoke-test.sh` — smoke HTTP de todos los endpoints nuevos

### Resultados

- `deno test cost_modules_test.ts` — 5/5 OK
- `./scripts/crud-smoke-test.sh` — todos los checks OK (gastos CRUD, IVA, cost-settings, almacén, transferencias, cost-control, IAM)
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-eeec3b5c-f789-43e8-a353-0755e4706377?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-eeec3b5c-f789-43e8-a353-0755e4706377&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
2026-09-04 04:29:02 +00:00

103 lines
5.1 KiB
SQL

--liquibase formatted sql
-- PANELS · core · gastos, IVA y configuración de control de costos
--changeset panel:core-018a-tenant-cost-settings endDelimiter:; splitStatements:true
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='tenant_cost_settings'
CREATE TABLE core.tenant_cost_settings (
tenant_id INTEGER PRIMARY KEY,
budget_warn_pct NUMERIC(5,2) NOT NULL DEFAULT 85,
budget_critical_pct NUMERIC(5,2) NOT NULL DEFAULT 100,
default_iva_rate NUMERIC(5,4) NOT NULL DEFAULT 0.16,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
--changeset panel:core-018b-expense-entries endDelimiter:; splitStatements:true
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='expense_entries'
CREATE TABLE core.expense_entries (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id INTEGER NOT NULL,
company_id BIGINT NOT NULL REFERENCES core.companies(id),
project_id BIGINT REFERENCES core.projects(id) ON DELETE SET NULL,
expense_date DATE NOT NULL,
source TEXT NOT NULL DEFAULT 'manual' CHECK (source IN ('manual', 'payroll', 'destajo')),
source_ref_id BIGINT,
description TEXT NOT NULL DEFAULT '',
supplier_name TEXT NOT NULL DEFAULT '',
supplier_rfc TEXT NOT NULL DEFAULT '',
budget_chapter_id BIGINT REFERENCES core.budget_chapters(id) ON DELETE SET NULL,
budget_item_id BIGINT REFERENCES core.budget_items(id) ON DELETE SET NULL,
quantity NUMERIC(14,4) NOT NULL DEFAULT 0,
unit TEXT NOT NULL DEFAULT '',
unit_price NUMERIC(14,4) NOT NULL DEFAULT 0,
subtotal NUMERIC(14,2) NOT NULL DEFAULT 0,
iva_rate NUMERIC(5,4) NOT NULL DEFAULT 0.16,
iva_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
total NUMERIC(14,2) NOT NULL DEFAULT 0,
tax_included BOOLEAN NOT NULL DEFAULT false,
is_deductible BOOLEAN NOT NULL DEFAULT true,
counts_toward_budget BOOLEAN NOT NULL DEFAULT true,
is_locked BOOLEAN NOT NULL DEFAULT false,
status TEXT NOT NULL DEFAULT 'confirmed' CHECK (status IN ('draft', 'confirmed', 'void')),
created_by_id INTEGER,
created_by_name TEXT NOT NULL DEFAULT '',
notes TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_core_expense_entries_tenant ON core.expense_entries(tenant_id);
CREATE INDEX idx_core_expense_entries_project_date ON core.expense_entries(project_id, expense_date);
CREATE INDEX idx_core_expense_entries_budget_item ON core.expense_entries(budget_item_id);
CREATE UNIQUE INDEX idx_core_expense_entries_source_ref
ON core.expense_entries(source, source_ref_id)
WHERE source_ref_id IS NOT NULL AND status <> 'void';
--changeset panel:core-018c-expense-attachments endDelimiter:; splitStatements:true
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='expense_attachments'
CREATE TABLE core.expense_attachments (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
expense_id BIGINT NOT NULL REFERENCES core.expense_entries(id) ON DELETE CASCADE,
original_name TEXT NOT NULL,
mime TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
sha256 TEXT NOT NULL,
iv TEXT NOT NULL,
storage_name TEXT NOT NULL,
uploaded_by_id INTEGER,
uploaded_by_name TEXT NOT NULL DEFAULT '',
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
--changeset panel:core-018d-iva-period-manual endDelimiter:; splitStatements:true
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='iva_period_entries'
CREATE TABLE core.iva_period_entries (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id INTEGER NOT NULL,
company_id BIGINT NOT NULL REFERENCES core.companies(id),
period_year INTEGER NOT NULL,
period_month INTEGER NOT NULL CHECK (period_month BETWEEN 1 AND 12),
iva_trasladado NUMERIC(14,2) NOT NULL DEFAULT 0,
notes TEXT NOT NULL DEFAULT '',
updated_by_name TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, company_id, period_year, period_month)
);
CREATE INDEX idx_core_iva_period_tenant ON core.iva_period_entries(tenant_id);
--changeset panel:core-018e-destajo-budget-item endDelimiter:; splitStatements:true
ALTER TABLE core.destajo_jobs
ADD COLUMN IF NOT EXISTS budget_item_id BIGINT REFERENCES core.budget_items(id) ON DELETE SET NULL;
--changeset panel:core-018f-warehouse-material-budget-links endDelimiter:; splitStatements:true
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='warehouse_material_budget_links'
CREATE TABLE core.warehouse_material_budget_links (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
material_id BIGINT NOT NULL,
budget_item_id BIGINT NOT NULL REFERENCES core.budget_items(id) ON DELETE CASCADE,
conversion_factor NUMERIC(14,6) NOT NULL DEFAULT 1,
UNIQUE (material_id, budget_item_id)
);