diff --git a/ddl/60-projects-timesheets.sql b/ddl/60-projects-timesheets.sql new file mode 100644 index 0000000..2e1bddf --- /dev/null +++ b/ddl/60-projects-timesheets.sql @@ -0,0 +1,381 @@ +-- ============================================================= +-- ARCHIVO: 60-projects-timesheets.sql +-- DESCRIPCION: Schema de proyectos con soporte para timesheets +-- VERSION: 1.0.0 +-- PROYECTO: ERP-Core V2 +-- FECHA: 2026-01-20 +-- DEPENDE DE: auth schema (tenants, users, companies) +-- ============================================================= + +-- ===================== +-- SCHEMA: projects +-- Schema para modulo de gestion de proyectos +-- ===================== +CREATE SCHEMA IF NOT EXISTS projects; + +-- ===================== +-- EXTENSIONES REQUERIDAS +-- ===================== +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ===================== +-- TIPOS ENUMERADOS +-- ===================== + +-- Estado del proyecto +DO $$ BEGIN + CREATE TYPE projects.project_status_enum AS ENUM ( + 'draft', -- Borrador + 'active', -- Activo + 'completed', -- Completado + 'cancelled', -- Cancelado + 'on_hold' -- En espera + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Privacidad del proyecto +DO $$ BEGIN + CREATE TYPE projects.project_privacy_enum AS ENUM ( + 'public', -- Publico (visible para todos) + 'private', -- Privado (solo miembros) + 'followers' -- Solo seguidores + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Estado de la tarea +DO $$ BEGIN + CREATE TYPE projects.task_status_enum AS ENUM ( + 'todo', -- Por hacer + 'in_progress', -- En progreso + 'review', -- En revision + 'done', -- Completada + 'cancelled' -- Cancelada + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Prioridad de la tarea +DO $$ BEGIN + CREATE TYPE projects.task_priority_enum AS ENUM ( + 'low', -- Baja + 'normal', -- Normal + 'high', -- Alta + 'urgent' -- Urgente + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Estado del timesheet +DO $$ BEGIN + CREATE TYPE projects.timesheet_status_enum AS ENUM ( + 'draft', -- Borrador + 'submitted', -- Enviado para aprobacion + 'approved', -- Aprobado + 'rejected' -- Rechazado + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Estado del milestone +DO $$ BEGIN + CREATE TYPE projects.milestone_status_enum AS ENUM ( + 'pending', -- Pendiente + 'completed', -- Completado + 'cancelled' -- Cancelado + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- ===================== +-- TABLA: projects +-- Proyectos +-- ===================== +CREATE TABLE IF NOT EXISTS projects.projects ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + + -- Multi-tenant + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + company_id UUID NOT NULL, + + -- Identificacion + name VARCHAR(255) NOT NULL, + code VARCHAR(50), + description TEXT, + + -- Responsables + manager_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, + partner_id UUID, -- Cliente asociado al proyecto + + -- Cuenta analitica (para integracion contable) + analytic_account_id UUID, + + -- Fechas + date_start DATE, + date_end DATE, + + -- Estado y configuracion + status projects.project_status_enum DEFAULT 'draft', + privacy projects.project_privacy_enum DEFAULT 'public', + allow_timesheets BOOLEAN DEFAULT TRUE, + color VARCHAR(20), + + -- Audit columns + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_by UUID REFERENCES auth.users(id), + deleted_at TIMESTAMPTZ, + deleted_by UUID REFERENCES auth.users(id), + + -- Constraint de unicidad por codigo dentro de company + UNIQUE(company_id, code) +); + +-- Indices para projects +CREATE INDEX IF NOT EXISTS idx_projects_tenant ON projects.projects(tenant_id); +CREATE INDEX IF NOT EXISTS idx_projects_company ON projects.projects(company_id); +CREATE INDEX IF NOT EXISTS idx_projects_manager ON projects.projects(manager_id); +CREATE INDEX IF NOT EXISTS idx_projects_partner ON projects.projects(partner_id); +CREATE INDEX IF NOT EXISTS idx_projects_status ON projects.projects(status); +CREATE INDEX IF NOT EXISTS idx_projects_active ON projects.projects(tenant_id) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_projects_code ON projects.projects(code); + +-- ===================== +-- TABLA: project_stages +-- Etapas/columnas del tablero Kanban +-- ===================== +CREATE TABLE IF NOT EXISTS projects.project_stages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + + -- Multi-tenant + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + -- Puede ser global o por proyecto + project_id UUID REFERENCES projects.projects(id) ON DELETE CASCADE, + + -- Identificacion + name VARCHAR(100) NOT NULL, + sequence INT DEFAULT 0, + + -- Configuracion + fold BOOLEAN DEFAULT FALSE, -- Columna plegada por defecto + is_closed BOOLEAN DEFAULT FALSE, -- Indica etapa de cierre + + -- Audit columns + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Indices para project_stages +CREATE INDEX IF NOT EXISTS idx_project_stages_tenant ON projects.project_stages(tenant_id); +CREATE INDEX IF NOT EXISTS idx_project_stages_project ON projects.project_stages(project_id); +CREATE INDEX IF NOT EXISTS idx_project_stages_sequence ON projects.project_stages(sequence); + +-- ===================== +-- TABLA: tasks +-- Tareas del proyecto +-- ===================== +CREATE TABLE IF NOT EXISTS projects.tasks ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + + -- Multi-tenant + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + -- Relaciones + project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE, + stage_id UUID REFERENCES projects.project_stages(id) ON DELETE SET NULL, + parent_id UUID REFERENCES projects.tasks(id) ON DELETE SET NULL, + + -- Identificacion + name VARCHAR(255) NOT NULL, + description TEXT, + + -- Asignacion + assigned_to UUID REFERENCES auth.users(id) ON DELETE SET NULL, + + -- Fechas + date_deadline DATE, + + -- Estimacion y seguimiento + estimated_hours DECIMAL(10,2) DEFAULT 0, + + -- Estado y prioridad + priority projects.task_priority_enum DEFAULT 'normal', + status projects.task_status_enum DEFAULT 'todo', + + -- Ordenamiento + sequence INT DEFAULT 0, + color VARCHAR(20), + + -- Audit columns + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_by UUID REFERENCES auth.users(id), + deleted_at TIMESTAMPTZ, + deleted_by UUID REFERENCES auth.users(id) +); + +-- Indices para tasks +CREATE INDEX IF NOT EXISTS idx_tasks_tenant ON projects.tasks(tenant_id); +CREATE INDEX IF NOT EXISTS idx_tasks_project ON projects.tasks(project_id); +CREATE INDEX IF NOT EXISTS idx_tasks_stage ON projects.tasks(stage_id); +CREATE INDEX IF NOT EXISTS idx_tasks_parent ON projects.tasks(parent_id); +CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON projects.tasks(assigned_to); +CREATE INDEX IF NOT EXISTS idx_tasks_status ON projects.tasks(status); +CREATE INDEX IF NOT EXISTS idx_tasks_priority ON projects.tasks(priority); +CREATE INDEX IF NOT EXISTS idx_tasks_deadline ON projects.tasks(date_deadline); +CREATE INDEX IF NOT EXISTS idx_tasks_active ON projects.tasks(tenant_id, project_id) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_tasks_sequence ON projects.tasks(project_id, sequence); + +-- ===================== +-- TABLA: milestones +-- Hitos del proyecto +-- ===================== +CREATE TABLE IF NOT EXISTS projects.milestones ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + + -- Multi-tenant + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + -- Relacion + project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE, + + -- Identificacion + name VARCHAR(255) NOT NULL, + description TEXT, + + -- Fecha objetivo + date_deadline DATE, + + -- Estado + status projects.milestone_status_enum DEFAULT 'pending', + + -- Audit columns + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_by UUID REFERENCES auth.users(id) +); + +-- Indices para milestones +CREATE INDEX IF NOT EXISTS idx_milestones_tenant ON projects.milestones(tenant_id); +CREATE INDEX IF NOT EXISTS idx_milestones_project ON projects.milestones(project_id); +CREATE INDEX IF NOT EXISTS idx_milestones_status ON projects.milestones(status); +CREATE INDEX IF NOT EXISTS idx_milestones_deadline ON projects.milestones(date_deadline); + +-- ===================== +-- TABLA: timesheets +-- Registro de horas trabajadas (estilo Odoo) +-- ===================== +CREATE TABLE IF NOT EXISTS projects.timesheets ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + + -- Multi-tenant + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + company_id UUID NOT NULL, + + -- Relaciones + project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE, + task_id UUID REFERENCES projects.tasks(id) ON DELETE SET NULL, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + + -- Datos del registro + date DATE NOT NULL, + hours DECIMAL(5,2) NOT NULL CHECK (hours >= 0 AND hours <= 24), + description TEXT, + + -- Facturacion + billable BOOLEAN DEFAULT TRUE, + invoiced BOOLEAN DEFAULT FALSE, + invoice_id UUID, -- FK a factura cuando se facture + + -- Aprobacion + status projects.timesheet_status_enum DEFAULT 'draft', + approved_by UUID REFERENCES auth.users(id), + approved_at TIMESTAMPTZ, + + -- Audit columns + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_by UUID REFERENCES auth.users(id) +); + +-- Indices para timesheets +CREATE INDEX IF NOT EXISTS idx_timesheets_tenant ON projects.timesheets(tenant_id); +CREATE INDEX IF NOT EXISTS idx_timesheets_company ON projects.timesheets(company_id); +CREATE INDEX IF NOT EXISTS idx_timesheets_project ON projects.timesheets(project_id); +CREATE INDEX IF NOT EXISTS idx_timesheets_task ON projects.timesheets(task_id); +CREATE INDEX IF NOT EXISTS idx_timesheets_user ON projects.timesheets(user_id); +CREATE INDEX IF NOT EXISTS idx_timesheets_user_date ON projects.timesheets(user_id, date); +CREATE INDEX IF NOT EXISTS idx_timesheets_date ON projects.timesheets(date); +CREATE INDEX IF NOT EXISTS idx_timesheets_status ON projects.timesheets(status); +CREATE INDEX IF NOT EXISTS idx_timesheets_billable ON projects.timesheets(billable) WHERE billable = TRUE; +CREATE INDEX IF NOT EXISTS idx_timesheets_not_invoiced ON projects.timesheets(project_id, invoiced) WHERE invoiced = FALSE; +CREATE INDEX IF NOT EXISTS idx_timesheets_invoice ON projects.timesheets(invoice_id); + +-- ===================== +-- TABLA: project_members +-- Miembros del equipo del proyecto +-- ===================== +CREATE TABLE IF NOT EXISTS projects.project_members ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + + -- Multi-tenant + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + -- Relaciones + project_id UUID NOT NULL REFERENCES projects.projects(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + + -- Rol en el proyecto + role VARCHAR(50) DEFAULT 'member', -- member, contributor, viewer + + -- Audit columns + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + + -- Un usuario solo puede ser miembro una vez por proyecto + UNIQUE(project_id, user_id) +); + +-- Indices para project_members +CREATE INDEX IF NOT EXISTS idx_project_members_tenant ON projects.project_members(tenant_id); +CREATE INDEX IF NOT EXISTS idx_project_members_project ON projects.project_members(project_id); +CREATE INDEX IF NOT EXISTS idx_project_members_user ON projects.project_members(user_id); + +-- ===================== +-- COMENTARIOS +-- ===================== +COMMENT ON SCHEMA projects IS 'Schema para gestion de proyectos: proyectos, tareas, timesheets, milestones'; + +COMMENT ON TABLE projects.projects IS 'Proyectos con seguimiento de tareas y timesheets'; +COMMENT ON COLUMN projects.projects.allow_timesheets IS 'TRUE si el proyecto permite registro de horas'; +COMMENT ON COLUMN projects.projects.analytic_account_id IS 'Cuenta analitica para integracion contable'; + +COMMENT ON TABLE projects.project_stages IS 'Etapas del tablero Kanban (columnas)'; +COMMENT ON COLUMN projects.project_stages.fold IS 'TRUE si la columna debe mostrarse plegada'; +COMMENT ON COLUMN projects.project_stages.is_closed IS 'TRUE si representa una etapa de cierre/completado'; + +COMMENT ON TABLE projects.tasks IS 'Tareas asignables a proyectos'; +COMMENT ON COLUMN projects.tasks.estimated_hours IS 'Horas estimadas para completar la tarea'; +COMMENT ON COLUMN projects.tasks.parent_id IS 'Referencia a tarea padre para subtareas'; + +COMMENT ON TABLE projects.milestones IS 'Hitos importantes del proyecto'; + +COMMENT ON TABLE projects.timesheets IS 'Registro de horas trabajadas estilo Odoo'; +COMMENT ON COLUMN projects.timesheets.hours IS 'Horas trabajadas (0-24 por dia)'; +COMMENT ON COLUMN projects.timesheets.billable IS 'TRUE si las horas son facturables al cliente'; +COMMENT ON COLUMN projects.timesheets.invoiced IS 'TRUE si ya fue incluido en una factura'; +COMMENT ON COLUMN projects.timesheets.status IS 'Flujo de aprobacion: draft -> submitted -> approved/rejected'; + +COMMENT ON TABLE projects.project_members IS 'Miembros del equipo del proyecto'; +COMMENT ON COLUMN projects.project_members.role IS 'Rol: member (puede editar), contributor (puede agregar), viewer (solo lectura)';