feat(crm): PDF de cotización (formato maestro) + marca por tenant + envío por correo
- Modelo crm.quote_settings (emisor, logo, color, prefijo, términos) por compañía
+ columna quotes.pdf_file_key + migración con down().
- Generador PDF (formato maestro: emisor+logo, cliente, carga/ruta, costos,
resumen, condiciones); logo incrustado como JPEG (Pillow).
- Endpoints: GET /quotes/{id}/pdf-url, POST /quotes/{id}/send-email (adjunta PDF),
GET/PUT /quote-settings, POST /quote-settings/logo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""crm quote_settings (marca por tenant) + quotes.pdf_file_key
|
||||
|
||||
Revision ID: a0b1c2d3e4f5
|
||||
Revises: f8a9b0c1d2e3
|
||||
Create Date: 2026-07-29 00:00:00.000000
|
||||
|
||||
PDF de cotización con formato maestro + branding por tenant + envío por correo.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "a0b1c2d3e4f5"
|
||||
down_revision: Union[str, None] = "f8a9b0c1d2e3"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
SCHEMA = "crm"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("quotes", sa.Column("pdf_file_key", sa.String(length=512), nullable=True), schema=SCHEMA)
|
||||
|
||||
op.create_table(
|
||||
"quote_settings",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("emitter_name", sa.String(length=255), nullable=True),
|
||||
sa.Column("emitter_rfc", sa.String(length=13), nullable=True),
|
||||
sa.Column("emitter_address", sa.Text(), nullable=True),
|
||||
sa.Column("emitter_phone", sa.String(length=60), nullable=True),
|
||||
sa.Column("emitter_email", sa.String(length=255), nullable=True),
|
||||
sa.Column("emitter_website", sa.String(length=255), nullable=True),
|
||||
sa.Column("logo_file_key", sa.String(length=512), nullable=True),
|
||||
sa.Column("accent_color", sa.String(length=9), nullable=True, server_default=sa.text("'#2f6bf0'")),
|
||||
sa.Column("quote_prefix", sa.String(length=12), nullable=True, server_default=sa.text("'COT'")),
|
||||
sa.Column("default_terms", sa.Text(), nullable=True),
|
||||
sa.Column("footer_note", sa.Text(), nullable=True),
|
||||
sa.Column("tenant_id", sa.Integer(), nullable=False),
|
||||
sa.Column("company_id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("deleted_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.ForeignKeyConstraint(["tenant_id"], ["core.tenants.id"]),
|
||||
schema=SCHEMA,
|
||||
)
|
||||
op.create_index("ix_crm_quote_settings_id", "quote_settings", ["id"], schema=SCHEMA)
|
||||
op.create_index("ix_crm_quote_settings_tenant_id", "quote_settings", ["tenant_id"], schema=SCHEMA)
|
||||
op.create_index("ix_crm_quote_settings_company_id", "quote_settings", ["company_id"], schema=SCHEMA)
|
||||
# Una configuración por compañía
|
||||
op.create_index(
|
||||
"uq_crm_quote_settings_company", "quote_settings", ["tenant_id", "company_id"],
|
||||
unique=True, schema=SCHEMA, postgresql_where=sa.text("deleted_at IS NULL"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("quote_settings", schema=SCHEMA)
|
||||
op.drop_column("quotes", "pdf_file_key", schema=SCHEMA)
|
||||
Reference in New Issue
Block a user