En un restaurante con 300 cuentas diarias, en un comercio donde la mitad de los clientes paga en efectivo sin pedir factura, o en un e-commerce con órdenes de bajo monto que nadie factura individualmente, siempre hay una pregunta pendiente: ¿qué hacemos con todas esas ventas para el SAT?
La respuesta es la Factura Global. Y si tu sistema maneja este tipo de negocio, tarde o temprano necesitas automatizarla.
¿Qué es la Factura Global y cuándo es obligatoria?
La Factura Global es un CFDI que consolida en un solo documento todas las ventas del período donde el cliente no solicitó factura individual. No es opcional: si tienes ventas sin CFDI emitido, el SAT espera que las captures en una Factura Global.
Los casos más comunes donde aparece este requerimiento:
- Restaurantes y cafeterías con ventas en efectivo
- Tiendas de conveniencia, abarrotes o retail
- Gasolineras
- E-commerce donde el pago fue en OXXO o efectivo y el comprador nunca pidió factura
La lógica es simple: el SAT quiere que toda venta quede en algún CFDI. Si no fue en uno individual, va en la global del período.
Cómo es diferente a una factura normal
Una Factura Global no se ve tan distinta en el XML, pero tiene tres diferencias fundamentales que tu sistema debe manejar.
1. El receptor siempre es el mismo
No importa si vendiste a 500 personas distintas en el día. El receptor de la Factura Global es siempre el mismo RFC genérico del SAT:
<cfdi:Receptor
RFC="XAXX010101000"
Nombre="PUBLICO EN GENERAL"
DomicilioFiscalReceptor="64000"
RegimenFiscalReceptor="616"
UsoCFDI="S01"/>Estos valores son fijos. El Nombre debe ser PUBLICO EN GENERAL exactamente así, en mayúsculas. El RegimenFiscalReceptor es siempre 616 (Sin obligaciones fiscales) y el UsoCFDI es siempre S01 (Sin efectos fiscales). Si cambias cualquiera de estos, el PAC te lo rechaza con un 301.
Para ventas a extranjeros, el RFC cambia a XEXX010101000, pero el resto de los campos se mantiene igual.
2. Declara el período que consolida
Este es el elemento que no existe en ningún otro tipo de CFDI: el nodo InformacionGlobal. Es obligatorio cuando el receptor es XAXX010101000, y le dice al SAT qué período estás cerrando.
<cfdi:InformacionGlobal
Periodicidad="04"
Meses="08"
Año="2025"/>Los tres atributos son requeridos y deben ser coherentes entre sí. Las periodicidades disponibles son:
| Código | Periodicidad |
|---|---|
01 | Diaria |
02 | Semanal |
03 | Quincenal |
04 | Mensual |
05 | Bimestral |
El código de Meses del 01 al 12 aplica para periodicidades diaria, semanal, quincenal y mensual. Los códigos 13 al 18 son exclusivos de la periodicidad bimestral:
| Código | Bimestre |
|---|---|
13 | Enero–Febrero |
14 | Marzo–Abril |
15 | Mayo–Junio |
16 | Julio–Agosto |
17 | Septiembre–Octubre |
18 | Noviembre–Diciembre |
Un error de schema frecuente es mezclar un código bimestral (13–18) con Periodicidad="04" (mensual). El XSD lo rechaza.
3. Tiene plazos de cancelación distintos
La Factura Global no se puede cancelar en cualquier momento. El SAT la vincula al período fiscal que declara, y una vez que ese período cierra para efectos de declaración, la cancelación ya no es posible.
| Periodicidad | Límite de cancelación |
|---|---|
| Diaria | El mismo día o el día siguiente |
| Mensual | Antes del día 17 del mes siguiente |
| Bimestral | Antes de la fecha límite de la declaración bimestral |
Si intentas cancelar fuera de plazo, el SAT responde con el error CA211. Este no tiene workaround.
El flujo completo de automatización
Ahora sí, el código. El ciclo completo de una Factura Global automatizada tiene tres pasos: agrupar las ventas del período, generar el CFDI, y manejar el caso donde un cliente pide su factura individual después.
Paso 1: Identificar qué ventas van a la global
La Factura Global solo incluye ventas sin CFDI individual emitido. Tu sistema necesita llevar un registro de qué transacciones ya tienen CFDI y cuáles no:
def get_unfactured_sales(period_start: date, period_end: date) -> list[Sale]:
return db.query("""
SELECT * FROM sales
WHERE date BETWEEN :start AND :end
AND cfdi_uuid IS NULL
AND included_in_global IS FALSE
""", start=period_start, end=period_end)Paso 2: Generar el payload del CFDI
El SubTotal es la suma de todas las ventas netas del período. El Total incluye impuestos. La Fecha debe caer dentro del período declarado en InformacionGlobal.
def build_factura_global(sales: list[Sale], period: Period) -> dict:
subtotal = sum(s.amount_before_tax for s in sales)
iva = sum(s.iva_amount for s in sales)
return {
"Receptor": {
"RFC": "XAXX010101000",
"Nombre": "PUBLICO EN GENERAL",
"DomicilioFiscalReceptor": EMISOR_CP,
"RegimenFiscalReceptor": "616",
"UsoCFDI": "S01",
},
"InformacionGlobal": {
"Periodicidad": period.codigo,
"Meses": period.meses_codigo,
"Año": str(period.year),
},
"TipoDeComprobante": "I",
"MetodoPago": "PUE",
"FormaPago": "01", # efectivo; ajustar según la realidad del negocio
"Exportacion": "01",
"SubTotal": f"{subtotal:.2f}",
"Total": f"{(subtotal + iva):.2f}",
# ... Conceptos e Impuestos
}Después de timbrar, marca esas ventas como incluidas en la global:
def after_stamp(sales: list[Sale], global_uuid: str):
sale_ids = [s.id for s in sales]
db.execute("""
UPDATE sales
SET included_in_global = TRUE, global_cfdi_uuid = :uuid
WHERE id = ANY(:ids)
""", uuid=global_uuid, ids=sale_ids)Paso 3: El caso edge que complica todo
El cliente llega al día siguiente y pide su factura. Tú ya la incluiste en la Factura Global de ayer.
Esto pasa más seguido de lo que parece, y el flujo correcto no es intuitivo:
- Emites la factura individual al RFC del cliente
- Cancelas la Factura Global con motivo
01(error con relación), indicando el UUID de la factura individual comoFolioSustitucion - Emites una nueva Factura Global por el mismo período, excluyendo esa venta
def handle_late_invoice_request(sale_id: str, customer_rfc: str) -> dict:
# 1. Obtener la Factura Global que contiene esa venta
sale = db.get_sale(sale_id)
global_uuid = sale.global_cfdi_uuid
# 2. Emitir la factura individual primero
individual_uuid = stamp_individual_invoice(sale_id, customer_rfc)
# 3. Cancelar la Factura Global relacionándola con la factura individual
cancel_cfdi(
uuid=global_uuid,
motivo="01",
folio_sustitucion=individual_uuid
)
# 4. Re-emitir la Factura Global sin esa venta
period = get_period_from_global(global_uuid)
remaining_sales = get_period_sales_excluding(sale_id, period)
new_global_uuid = stamp_factura_global(remaining_sales, period)
return {
"individual_uuid": individual_uuid,
"new_global_uuid": new_global_uuid
}Dos cosas importantes en este flujo:
- El motivo
01exigeFolioSustitucion. Si no lo incluyes, el SAT responde conCA208. - Esto solo es posible mientras estés dentro del plazo de cancelación del período. Si ya pasó el día 17 del mes siguiente para una Factura Global mensual, el
CA211bloqueará la cancelación y tendrás que resolver con el cliente de otra forma.
Los errores que más aparecen
| Error | Cuándo ocurre |
|---|---|
301 | El nodo InformacionGlobal está presente pero el RFC receptor no es XAXX010101000, o viceversa |
| Schema validation fail | Código de Meses bimestral con Periodicidad mensual |
CA211 | Cancelación después del plazo del período fiscal |
CA208 | Cancelación con motivo 01 sin incluir el FolioSustitucion |
CA209 | Cancelación con motivo 02/03/04 incluyendo FolioSustitucion (no debe ir) |
Checklist antes de timbrar
- ☐ RFC receptor es exactamente
XAXX010101000para ventas nacionales - ☐
Nombredel receptor esPUBLICO EN GENERALen mayúsculas - ☐
RegimenFiscalReceptor=616,UsoCFDI=S01 - ☐ Nodo
InformacionGlobalpresente con los tres atributos obligatorios - ☐ El código de
Meseses coherente con laPeriodicidaddeclarada - ☐ La
Fechadel CFDI cae dentro del período que se está cerrando - ☐ El
SubTotalrefleja la suma real de todas las ventas incluidas - ☐ Las ventas incluidas están marcadas en tu base de datos antes de confirmar el timbrado
La Factura Global parece sencilla en concepto pero tiene varios puntos de falla en la implementación: los campos fijos del receptor que el SAT no negocia, la coherencia entre periodicidad y código de mes, y el flujo de cancelación cuando un cliente aparece tarde. Con el checklist y el manejo del caso edge cubiertos, tu integración aguanta los escenarios que normalmente causan problemas en producción.