feat(fin,crm): catálogo c_UsoCFDI y claves fiscales del receptor
Cierra las decisiones pendientes 1 y 5. Agrega sat.cfdi_uses con su endpoint de solo lectura y amarra la ficha del cliente a los catálogos del SAT con crm.accounts.tax_regime_id y cfdi_use_id. Las columnas de texto libre tax_regime y cfdi_use se conservan intactas: la migración hace un backfill conservador que solo resuelve lo inequívoco (la clave del catálogo, o la descripción exacta sin distinguir mayúsculas ni espacios) y deja en NULL lo que no case, porque deducir el régimen de un receptor a partir de texto libre provoca CFDI rechazados. La UI muestra el texto anterior junto al selector para que el usuario elija la clave que corresponde. El selector de régimen se acota al tipo de persona de la cuenta, y el service valida ambas claves contra el catálogo. sync_catalogs ahora omite los catálogos cuya tabla todavía no existe: al correr el historial desde cero, la migración anterior la invoca antes de que se creen los catálogos agregados después. Las claves de c_UsoCFDI quedan pendientes de validación con el área Fiscal antes de producción, igual que el subset de c_ClaveProdServ; no se cargaron las banderas de persona física/moral ni la compatibilidad por régimen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Catálogo c_UsoCFDI y claves fiscales del receptor en crm.accounts.
|
||||
|
||||
Cierra las decisiones pendientes 1 y 5 del ticket de catálogos SAT: agrega
|
||||
``sat.cfdi_uses`` y amarra el régimen fiscal y el uso de CFDI de la cuenta a los
|
||||
catálogos, conservando las columnas de texto libre que ya existían.
|
||||
|
||||
Revision ID: f7a8b9c0d1e2
|
||||
Revises: e6f7a8b9c0d1
|
||||
Create Date: 2026-08-07 00:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from api.v1.modules.fin.catalogs.seed_data import sync_catalogs
|
||||
|
||||
revision: str = "f7a8b9c0d1e2"
|
||||
down_revision: Union[str, None] = "e6f7a8b9c0d1"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ---------- sat.cfdi_uses ----------
|
||||
op.create_table(
|
||||
"cfdi_uses",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("code", sa.String(length=4), nullable=False),
|
||||
sa.Column("description", sa.String(length=500), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
|
||||
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.PrimaryKeyConstraint("id"),
|
||||
schema="sat",
|
||||
)
|
||||
op.create_index("ix_sat_cfdi_uses_id", "cfdi_uses", ["id"], schema="sat")
|
||||
op.create_index("ix_sat_cfdi_uses_code", "cfdi_uses", ["code"], unique=True, schema="sat")
|
||||
|
||||
# sync_catalogs es idempotente: siembra c_UsoCFDI y deja intactos los catálogos
|
||||
# que ya sembró la migración anterior.
|
||||
sync_catalogs(op.get_bind())
|
||||
|
||||
# ---------- crm.accounts: claves fiscales del receptor ----------
|
||||
# Nullables: las cuentas existentes solo tienen el texto libre.
|
||||
op.add_column("accounts", sa.Column("tax_regime_id", sa.Integer(), nullable=True), schema="crm")
|
||||
op.add_column("accounts", sa.Column("cfdi_use_id", sa.Integer(), nullable=True), schema="crm")
|
||||
op.create_foreign_key(
|
||||
"fk_crm_accounts_tax_regime_id", "accounts", "tax_regimes",
|
||||
["tax_regime_id"], ["id"], source_schema="crm", referent_schema="sat",
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_crm_accounts_cfdi_use_id", "accounts", "cfdi_uses",
|
||||
["cfdi_use_id"], ["id"], source_schema="crm", referent_schema="sat",
|
||||
)
|
||||
|
||||
# Backfill conservador: solo resuelve lo inequívoco. Se compara el texto libre
|
||||
# contra la clave del catálogo (p. ej. "601", "G03") y contra la descripción
|
||||
# exacta, sin distinguir mayúsculas ni espacios sobrantes. Lo que no case así se
|
||||
# queda en NULL para que lo revise el usuario: adivinar el régimen de un receptor
|
||||
# a partir de texto libre provoca CFDI rechazados.
|
||||
for column, catalog in [("tax_regime", "tax_regimes"), ("cfdi_use", "cfdi_uses")]:
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE crm.accounts AS a
|
||||
SET {column}_id = c.id
|
||||
FROM sat.{catalog} AS c
|
||||
WHERE a.{column}_id IS NULL
|
||||
AND a.{column} IS NOT NULL
|
||||
AND (
|
||||
upper(btrim(a.{column})) = upper(c.code)
|
||||
OR upper(btrim(a.{column})) = upper(c.description)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint("fk_crm_accounts_cfdi_use_id", "accounts", schema="crm", type_="foreignkey")
|
||||
op.drop_constraint("fk_crm_accounts_tax_regime_id", "accounts", schema="crm", type_="foreignkey")
|
||||
op.drop_column("accounts", "cfdi_use_id", schema="crm")
|
||||
op.drop_column("accounts", "tax_regime_id", schema="crm")
|
||||
op.drop_table("cfdi_uses", schema="sat")
|
||||
@@ -25,6 +25,9 @@ class AccountBase(BaseModel):
|
||||
# Fiscal
|
||||
tax_regime: str | None = Field(None, max_length=120)
|
||||
cfdi_use: str | None = Field(None, max_length=60)
|
||||
# Claves contra los catálogos del SAT; sustituyen al texto libre de arriba al timbrar.
|
||||
tax_regime_id: int | None = Field(None, description="c_RegimenFiscal del receptor")
|
||||
cfdi_use_id: int | None = Field(None, description="c_UsoCFDI del receptor")
|
||||
payment_method: str | None = Field(None, max_length=60)
|
||||
payment_form: str | None = Field(None, max_length=60)
|
||||
currency: str | None = Field(None, max_length=3)
|
||||
@@ -65,6 +68,8 @@ class AccountUpdate(BaseModel):
|
||||
website: str | None = Field(None, max_length=255)
|
||||
tax_regime: str | None = Field(None, max_length=120)
|
||||
cfdi_use: str | None = Field(None, max_length=60)
|
||||
tax_regime_id: int | None = None
|
||||
cfdi_use_id: int | None = None
|
||||
payment_method: str | None = Field(None, max_length=60)
|
||||
payment_form: str | None = Field(None, max_length=60)
|
||||
currency: str | None = Field(None, max_length=3)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Integer, Numeric, String, Text, text
|
||||
from sqlalchemy import ForeignKey, Integer, Numeric, String, Text, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from api.v1.modules.fin.catalogs.models import CfdiUse, TaxRegime # noqa: F401 (resuelve las FK)
|
||||
from core.database import Base
|
||||
|
||||
|
||||
@@ -47,8 +48,17 @@ class Account(Base, TenantScopedMixin, TimestampMixin):
|
||||
website: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
|
||||
# ----- Información fiscal -----
|
||||
# Régimen fiscal y uso de CFDI en texto libre: se conservan como capturó el usuario
|
||||
# para no perder lo ya registrado, pero lo que vale al timbrar son las FK de abajo.
|
||||
tax_regime: Mapped[str | None] = mapped_column(String(120), nullable=True) # régimen fiscal
|
||||
cfdi_use: Mapped[str | None] = mapped_column(String(60), nullable=True) # uso de CFDI
|
||||
# Claves del receptor contra los catálogos del SAT (c_RegimenFiscal y c_UsoCFDI).
|
||||
tax_regime_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("sat.tax_regimes.id"), nullable=True
|
||||
)
|
||||
cfdi_use_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("sat.cfdi_uses.id"), nullable=True
|
||||
)
|
||||
payment_method: Mapped[str | None] = mapped_column(String(60), nullable=True) # método de pago
|
||||
payment_form: Mapped[str | None] = mapped_column(String(60), nullable=True) # forma de pago
|
||||
currency: Mapped[str | None] = mapped_column(String(3), nullable=True) # moneda
|
||||
|
||||
@@ -3,10 +3,24 @@ from datetime import datetime, timezone
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from api.v1.modules.fin.catalogs.models import CfdiUse, TaxRegime
|
||||
|
||||
from .dto import AccountCreate, AccountUpdate
|
||||
from .models import Account
|
||||
|
||||
|
||||
def _validate_sat_refs(db: Session, data: dict) -> None:
|
||||
"""Verifica las claves del SAT del receptor antes de guardar la cuenta."""
|
||||
for field, model, msg in [
|
||||
("tax_regime_id", TaxRegime, "El régimen fiscal indicado no existe en el catálogo del SAT"),
|
||||
("cfdi_use_id", CfdiUse, "El uso de CFDI indicado no existe en el catálogo del SAT"),
|
||||
]:
|
||||
value = data.get(field)
|
||||
if field in data and value is not None:
|
||||
if db.query(model.id).filter(model.id == value).first() is None:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=msg)
|
||||
|
||||
|
||||
def get_accounts(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
@@ -53,8 +67,10 @@ def get_account(db: Session, account_id: int, tenant_id: int, company_id: int) -
|
||||
def create_account(
|
||||
db: Session, payload: AccountCreate, tenant_id: int, company_id: int, user_id: str | None = None
|
||||
) -> Account:
|
||||
data = payload.model_dump()
|
||||
_validate_sat_refs(db, data)
|
||||
account = Account(
|
||||
**payload.model_dump(),
|
||||
**data,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
created_by=user_id,
|
||||
@@ -75,7 +91,9 @@ def update_account(
|
||||
user_id: str | None = None,
|
||||
) -> Account:
|
||||
account = get_account(db, account_id, tenant_id, company_id)
|
||||
for field, value in payload.model_dump(exclude_unset=True).items():
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
_validate_sat_refs(db, data)
|
||||
for field, value in data.items():
|
||||
setattr(account, field, value)
|
||||
account.updated_by = user_id
|
||||
db.commit()
|
||||
|
||||
@@ -55,3 +55,7 @@ class PaymentMethodResponse(SatCatalogItem):
|
||||
|
||||
class TaxObjectResponse(SatCatalogItem):
|
||||
"""``c_ObjetoImp``."""
|
||||
|
||||
|
||||
class CfdiUseResponse(SatCatalogItem):
|
||||
"""``c_UsoCFDI``."""
|
||||
|
||||
@@ -128,3 +128,16 @@ class TaxObject(Base, SatCatalogMixin):
|
||||
__table_args__ = {"schema": "sat"}
|
||||
|
||||
code: Mapped[str] = mapped_column(String(2), nullable=False, unique=True, index=True)
|
||||
|
||||
|
||||
class CfdiUse(Base, SatCatalogMixin):
|
||||
"""``c_UsoCFDI`` — uso que el receptor le dará al comprobante.
|
||||
|
||||
Lo declara el receptor, no el emisor, y el SAT lo valida contra su régimen
|
||||
fiscal: por eso vive en la ficha del cliente (``crm.accounts.cfdi_use_id``).
|
||||
"""
|
||||
|
||||
__tablename__ = "cfdi_uses"
|
||||
__table_args__ = {"schema": "sat"}
|
||||
|
||||
code: Mapped[str] = mapped_column(String(4), nullable=False, unique=True, index=True)
|
||||
|
||||
@@ -18,6 +18,7 @@ from core.security import get_current_user
|
||||
|
||||
from . import service
|
||||
from .dto import (
|
||||
CfdiUseResponse,
|
||||
PaymentFormResponse,
|
||||
PaymentMethodResponse,
|
||||
ProductServiceResponse,
|
||||
@@ -124,3 +125,14 @@ def list_tax_objects(
|
||||
):
|
||||
"""``c_ObjetoImp`` — si la partida es objeto de impuesto."""
|
||||
return service.get_tax_objects(db, search, active_only)
|
||||
|
||||
|
||||
@router.get("/catalogs/cfdi-uses", response_model=list[CfdiUseResponse])
|
||||
def list_cfdi_uses(
|
||||
search: str | None = _SEARCH,
|
||||
active_only: bool = _ACTIVE_ONLY,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""``c_UsoCFDI`` — uso que el receptor le dará al comprobante."""
|
||||
return service.get_cfdi_uses(db, search, active_only)
|
||||
|
||||
@@ -49,6 +49,7 @@ products_services_table = _catalog_table("products_services")
|
||||
voucher_types_table = _catalog_table("voucher_types")
|
||||
payment_methods_table = _catalog_table("payment_methods")
|
||||
tax_objects_table = _catalog_table("tax_objects")
|
||||
cfdi_uses_table = _catalog_table("cfdi_uses")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -234,6 +235,48 @@ PRODUCTS_SERVICES: list[dict] = [
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# c_UsoCFDI
|
||||
# ---------------------------------------------------------------------------
|
||||
# Catálogo del uso que el receptor da al comprobante. Se siembran clave y
|
||||
# descripción; **no** se cargan las banderas de persona física/moral ni la
|
||||
# compatibilidad por régimen fiscal, porque esa matriz cambia entre versiones del
|
||||
# catálogo y equivocarla provoca rechazos al timbrar.
|
||||
#
|
||||
# Pendiente validación con área Fiscal antes de producción, igual que el subset de
|
||||
# c_ClaveProdServ.
|
||||
|
||||
CFDI_USES: list[dict] = [
|
||||
{"code": code, "description": description, "is_active": True}
|
||||
for code, description in [
|
||||
("G01", "Adquisición de mercancías"),
|
||||
("G02", "Devoluciones, descuentos o bonificaciones"),
|
||||
("G03", "Gastos en general"),
|
||||
("I01", "Construcciones"),
|
||||
("I02", "Mobiliario y equipo de oficina por inversiones"),
|
||||
("I03", "Equipo de transporte"),
|
||||
("I04", "Equipo de cómputo y accesorios"),
|
||||
("I05", "Dados, troqueles, moldes, matrices y herramental"),
|
||||
("I06", "Comunicaciones telefónicas"),
|
||||
("I07", "Comunicaciones satelitales"),
|
||||
("I08", "Otra maquinaria y equipo"),
|
||||
("D01", "Honorarios médicos, dentales y gastos hospitalarios"),
|
||||
("D02", "Gastos médicos por incapacidad o discapacidad"),
|
||||
("D03", "Gastos funerales"),
|
||||
("D04", "Donativos"),
|
||||
("D05", "Intereses reales efectivamente pagados por créditos hipotecarios (casa habitación)"),
|
||||
("D06", "Aportaciones voluntarias al SAR"),
|
||||
("D07", "Primas por seguros de gastos médicos"),
|
||||
("D08", "Gastos de transportación escolar obligatoria"),
|
||||
("D09", "Depósitos en cuentas para el ahorro, primas que tengan como base planes de pensiones"),
|
||||
("D10", "Pagos por servicios educativos (colegiaturas)"),
|
||||
("S01", "Sin efectos fiscales"),
|
||||
("CP01", "Pagos"),
|
||||
("CN01", "Nómina"),
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
# Orden estable de sincronización: (tabla, filas).
|
||||
CATALOGS: list[tuple[sa.Table, list[dict]]] = [
|
||||
(tax_regimes_table, TAX_REGIMES),
|
||||
@@ -244,6 +287,7 @@ CATALOGS: list[tuple[sa.Table, list[dict]]] = [
|
||||
(voucher_types_table, VOUCHER_TYPES),
|
||||
(payment_methods_table, PAYMENT_METHODS),
|
||||
(tax_objects_table, TAX_OBJECTS),
|
||||
(cfdi_uses_table, CFDI_USES),
|
||||
]
|
||||
|
||||
|
||||
@@ -257,11 +301,24 @@ def sync_catalogs(connection) -> dict[str, int]:
|
||||
Devuelve un resumen ``{"sat.tabla": filas_insertadas}`` útil para la bitácora de
|
||||
la migración.
|
||||
|
||||
Los catálogos cuya tabla todavía no existe se omiten: al correr el historial de
|
||||
migraciones desde cero, una migración antigua invoca esta misma función cuando los
|
||||
catálogos agregados después aún no se han creado. Cada uno se siembra en la
|
||||
migración que lo crea.
|
||||
|
||||
Se usa contra el ``connection`` que da ``op.get_bind()`` en Alembic, o contra la
|
||||
conexión de una sesión en pruebas.
|
||||
"""
|
||||
inspector = sa.inspect(connection)
|
||||
# La inspección no aplica el schema_translate_map (las pruebas mapean sat -> None
|
||||
# sobre SQLite), así que se resuelve el schema efectivo a mano.
|
||||
schema_map = connection.get_execution_options().get("schema_translate_map") or {}
|
||||
|
||||
inserted: dict[str, int] = {}
|
||||
for table, rows in CATALOGS:
|
||||
effective_schema = schema_map.get(table.schema, table.schema)
|
||||
if not inspector.has_table(table.name, schema=effective_schema):
|
||||
continue
|
||||
key = f"sat.{table.name}"
|
||||
inserted[key] = 0
|
||||
for row in rows:
|
||||
|
||||
@@ -8,6 +8,7 @@ from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import (
|
||||
CfdiUse,
|
||||
PaymentForm,
|
||||
PaymentMethod,
|
||||
ProductService,
|
||||
@@ -99,3 +100,7 @@ def get_payment_methods(db: Session, search=None, active_only=True) -> list[Paym
|
||||
|
||||
def get_tax_objects(db: Session, search=None, active_only=True) -> list[TaxObject]:
|
||||
return search_catalog(db, TaxObject, search, active_only)
|
||||
|
||||
|
||||
def get_cfdi_uses(db: Session, search=None, active_only=True) -> list[CfdiUse]:
|
||||
return search_catalog(db, CfdiUse, search, active_only)
|
||||
|
||||
@@ -28,8 +28,11 @@ export interface Account {
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
/** Texto libre histórico; lo que vale al timbrar son las claves del SAT de abajo. */
|
||||
tax_regime: string | null;
|
||||
cfdi_use: string | null;
|
||||
tax_regime_id: number | null;
|
||||
cfdi_use_id: number | null;
|
||||
payment_method: string | null;
|
||||
payment_form: string | null;
|
||||
currency: string | null;
|
||||
|
||||
@@ -90,5 +90,7 @@ export const satCatalogsAPI = {
|
||||
paymentMethods: (companyId: number, params?: { search?: string; active_only?: boolean }) =>
|
||||
fetchCatalog<SatCatalogItem>('payment-methods', companyId, params),
|
||||
taxObjects: (companyId: number, params?: { search?: string; active_only?: boolean }) =>
|
||||
fetchCatalog<SatCatalogItem>('tax-objects', companyId, params)
|
||||
fetchCatalog<SatCatalogItem>('tax-objects', companyId, params),
|
||||
cfdiUses: (companyId: number, params?: { search?: string; active_only?: boolean }) =>
|
||||
fetchCatalog<SatCatalogItem>('cfdi-uses', companyId, params)
|
||||
};
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
<script lang="ts">
|
||||
import type { AccountInput } from '$lib/api/crm';
|
||||
import { satCatalogsAPI, type SatCatalogItem, type SatTaxRegime } from '$lib/api/fin';
|
||||
import {
|
||||
ACCOUNT_TYPES, ACCOUNT_STATUS, RECORD_TYPES, PERSON_TYPES,
|
||||
COMMERCIAL_CLASSIFICATION, CONTACT_METHODS
|
||||
} from '$lib/components/crm/format';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
// `form` es un objeto reactivo del padre; se mutan sus propiedades vía bind:value.
|
||||
let { form = $bindable(), tab }: { form: AccountInput; tab: string } = $props();
|
||||
// `companyId` solo se usa para consultar los catálogos del SAT del receptor.
|
||||
let { form = $bindable(), tab, companyId = null }: { form: AccountInput; tab: string; companyId?: number | null } = $props();
|
||||
|
||||
let taxRegimes = $state<SatTaxRegime[]>([]);
|
||||
let cfdiUses = $state<SatCatalogItem[]>([]);
|
||||
|
||||
// El régimen se acota al tipo de persona de la cuenta: una persona física no puede
|
||||
// declararse en el 601 y viceversa. Sin tipo de persona se ofrecen todos.
|
||||
const regimesForPersonType = $derived(
|
||||
form.person_type === 'fisica'
|
||||
? taxRegimes.filter((r) => r.applies_to_individual)
|
||||
: form.person_type === 'moral'
|
||||
? taxRegimes.filter((r) => r.applies_to_legal_entity)
|
||||
: taxRegimes
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
const cid = companyId;
|
||||
if (!cid || tab !== 'fiscal') return;
|
||||
void loadCatalogs(cid);
|
||||
});
|
||||
|
||||
async function loadCatalogs(cid: number) {
|
||||
try {
|
||||
[taxRegimes, cfdiUses] = await Promise.all([
|
||||
satCatalogsAPI.taxRegimes(cid),
|
||||
satCatalogsAPI.cfdiUses(cid)
|
||||
]);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : 'No se pudieron cargar los catálogos del SAT');
|
||||
}
|
||||
}
|
||||
|
||||
const inputCls =
|
||||
'rounded-md border bg-transparent px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
||||
@@ -35,8 +68,26 @@
|
||||
</div>
|
||||
{:else if tab === 'fiscal'}
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Régimen fiscal</span><input class={inputCls} bind:value={form.tax_regime} /></label>
|
||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Uso de CFDI</span><input class={inputCls} bind:value={form.cfdi_use} /></label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="font-medium">Régimen fiscal</span>
|
||||
<select class={inputCls} bind:value={form.tax_regime_id}>
|
||||
<option value={null}>Sin especificar</option>
|
||||
{#each regimesForPersonType as r (r.id)}<option value={r.id}>{r.code} — {r.description}</option>{/each}
|
||||
</select>
|
||||
{#if !form.tax_regime_id && form.tax_regime}
|
||||
<span class="text-xs text-muted-foreground">Capturado antes como texto: «{form.tax_regime}». Elige la clave del SAT que corresponde.</span>
|
||||
{/if}
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm">
|
||||
<span class="font-medium">Uso de CFDI</span>
|
||||
<select class={inputCls} bind:value={form.cfdi_use_id}>
|
||||
<option value={null}>Sin especificar</option>
|
||||
{#each cfdiUses as u (u.id)}<option value={u.id}>{u.code} — {u.description}</option>{/each}
|
||||
</select>
|
||||
{#if !form.cfdi_use_id && form.cfdi_use}
|
||||
<span class="text-xs text-muted-foreground">Capturado antes como texto: «{form.cfdi_use}». Elige la clave del SAT que corresponde.</span>
|
||||
{/if}
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Método de pago</span><input class={inputCls} bind:value={form.payment_method} /></label>
|
||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Forma de pago</span><input class={inputCls} bind:value={form.payment_form} /></label>
|
||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Moneda</span><input class={inputCls} maxlength="3" bind:value={form.currency} /></label>
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
</div>
|
||||
|
||||
{#if activeTab.kind === 'info'}
|
||||
<AccountFields bind:form tab={tab} />
|
||||
<AccountFields bind:form tab={tab} {companyId} />
|
||||
<div class="mt-6 flex justify-end border-t pt-4">
|
||||
<Button onclick={save} disabled={saving}>{saving ? 'Guardando…' : 'Guardar cambios'}</Button>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<AccountFields bind:form {tab} />
|
||||
<AccountFields bind:form {tab} {companyId} />
|
||||
|
||||
<div class="mt-6 flex justify-end gap-2 border-t pt-4">
|
||||
<Button variant="outline" href="/dashboard/crm/cuentas">Cancelar</Button>
|
||||
|
||||
Reference in New Issue
Block a user