From 143bfded293140c6e18d044b6441fb2006fbef5f Mon Sep 17 00:00:00 2001 From: hreyes Date: Thu, 30 Apr 2026 16:26:14 -0600 Subject: [PATCH] feature/reporte-bak --- .../a76/factura_cove/report_service.py | 262 ++++- .../api/v1/modules/a76/factura_cove/routes.py | 6 +- .../v1/modules/a76/factura_cove/service.py | 127 ++- .../api/v1/modules/a76/factura_cove/tasks.py | 31 +- .../a76/factura_cove/templates/FooterCOVE.png | Bin 0 -> 8903 bytes .../a76/factura_cove/templates/HeaderCOVE.png | Bin 0 -> 18294 bytes .../factura_cove/templates/acuse_cove.html | 912 ++++++++++-------- .../src/lib/api/dashboard/a76/invoices.ts | 3 +- .../routes/dashboard/invoices/+page.svelte | 3 +- 9 files changed, 851 insertions(+), 493 deletions(-) create mode 100644 backend/api/v1/modules/a76/factura_cove/templates/FooterCOVE.png create mode 100644 backend/api/v1/modules/a76/factura_cove/templates/HeaderCOVE.png diff --git a/backend/api/v1/modules/a76/factura_cove/report_service.py b/backend/api/v1/modules/a76/factura_cove/report_service.py index 18042343..f00188df 100644 --- a/backend/api/v1/modules/a76/factura_cove/report_service.py +++ b/backend/api/v1/modules/a76/factura_cove/report_service.py @@ -22,9 +22,11 @@ Secciones del reporte (derivadas del Report REPORT legacy en Clarion): from __future__ import annotations +import base64 import logging import os import shutil +import tempfile from dataclasses import dataclass, field from decimal import Decimal from pathlib import Path @@ -101,25 +103,35 @@ class CoveAcuseContext: Contexto completo para el template acuse_cove.html. Mapeo de campos Clarion → atributos de este dataclass: - Loc:tipoOperacion → tipo_operacion - Loc:numeroFacturaOriginal → numero_factura - Loc:tipoFigura → tipo_figura - Loc:fechaExpedicion → fecha_expedicion - Loc:ObservacionesCove → observaciones - Loc:rfcConsulta → rfc_consulta - Loc:patenteAduanal → patente_aduanal - Emisor.* → emisor (CovePersonaView) - Destinatario.* → destinatario (CovePersonaView) - DetallePartidasFactura.* → items (lista de CoveItemView) + Loc:tipoOperacion → tipo_operacion + Loc:numeroFacturaOriginal → numero_factura_relacionada + Loc:numeroFactura → numero_factura + Loc:tipoFigura → tipo_figura + Loc:fechaExpedicion → fecha_expedicion + Loc:ObservacionesCove → observaciones + Loc:rfcConsulta → rfc_consulta + Loc:NombreRazonSocial_Consulta → nombre_razon_social_consulta + Loc:patenteAduanal → patente_aduanal + Loc:subdivision → subdivision + Loc:certificadoOrigen → certificado_origen + Loc:exportadorAutorizado → exportador_autorizado + Emisor.* → emisor (CovePersonaView) + Destinatario.* → destinatario (CovePersonaView) + DetallePartidasFactura.* → items (lista de CoveItemView) """ # --- Sección "Datos" (cabecera) --- tipo_operacion: str = "" + numero_factura_relacionada: str = "" numero_factura: str = "" tipo_figura: str = "" fecha_expedicion: str = "" observaciones: str = "" rfc_consulta: str = "" + nombre_razon_social_consulta: str = "" patente_aduanal: str = "" + subdivision: str = "" + certificado_origen: str = "" + exportador_autorizado: str = "" edocument: str = "" vucem_operation_num: str = "" # --- Personas --- @@ -217,35 +229,53 @@ def _build_persona_from_xml(root: ET.Element, role_candidates: list[str]) -> Cov Intenta construir una CovePersonaView desde un bloque o (o etiquetas equivalentes) en el XML de COVE. - Este mapeo es heurístico y se basa en los nombres de campos del manual COVE - y del código legacy (Loc:Emisor_*, Loc:Des_*). + Estrategia de búsqueda: + - Identidad/nombre: _find_child_text (hijos directos) para evitar que campos + de otros bloques hermanos contaminen el resultado. + - Domicilio: _find_first_text dentro del elemento de persona, porque el XML + de VUCEM puede anidar las direcciones en un sub-bloque . """ - role_elems: list[ET.Element] = [] wanted_roles = {r.lower() for r in role_candidates} + persona_elem: Optional[ET.Element] = None for node in root.iter(): if _strip_tag(node.tag) in wanted_roles: - role_elems.append(node) + persona_elem = node break - if not role_elems: + if persona_elem is None: return CovePersonaView() - e = role_elems[0] + e = persona_elem return CovePersonaView( - tipo_identificador=_find_child_text(e, ["tipoIdentificador", "tipo_identificador"]), - identificacion=_find_child_text(e, ["identificador", "taxId", "rfc", "sinTaxId"]), - nombre=_find_child_text(e, ["nombre", "razonSocial", "razon_social"]), + # Campos de identificación — busca solo hijos directos para no mezclar + # datos entre emisor y destinatario cuando ambos están en el mismo árbol. + tipo_identificador=_find_child_text(e, [ + "tipoIdentificador", "tipo_identificador", + ]), + identificacion=_find_child_text(e, [ + # El campo del schema Pydantic se serializa como "identificacion" + "identificacion", + # Variantes legacy / VUCEM camelCase + "identificador", "taxId", "tax_id", "rfc", "sinTaxId", "sin_tax_id", + ]), + nombre=_find_child_text(e, [ + "nombre", "razonSocial", "razon_social", "name", + ]), apellido_paterno=_find_child_text(e, ["apellidoPaterno", "apellido_paterno"]), apellido_materno=_find_child_text(e, ["apellidoMaterno", "apellido_materno"]), - calle=_find_child_text(e, ["calle"]), - numero_exterior=_find_child_text(e, ["numeroExterior", "numero_exterior"]), - numero_interior=_find_child_text(e, ["numeroInterior", "numero_interior"]), - colonia=_find_child_text(e, ["colonia"]), - codigo_postal=_find_child_text(e, ["codigoPostal", "codigo_postal"]), - localidad=_find_child_text(e, ["localidad"]), - municipio=_find_child_text(e, ["municipio"]), - entidad_federativa=_find_child_text(e, ["entidadFederativa", "entidad_federativa", "estado"]), - pais=_find_child_text(e, ["pais"]), + # Campos de domicilio — búsqueda recursiva dentro de la persona para + # manejar el wrapper que usa el esquema VUCEM. + calle=_find_first_text(e, ["calle", "street"]), + numero_exterior=_find_first_text(e, ["numeroExterior", "numero_exterior", "numExterior"]), + numero_interior=_find_first_text(e, ["numeroInterior", "numero_interior", "numInterior"]), + colonia=_find_first_text(e, ["colonia", "neighborhood"]), + codigo_postal=_find_first_text(e, ["codigoPostal", "codigo_postal", "cp", "zipCode", "zip_code"]), + localidad=_find_first_text(e, ["localidad", "ciudad", "city"]), + municipio=_find_first_text(e, ["municipio", "delegacion"]), + entidad_federativa=_find_first_text(e, [ + "entidadFederativa", "entidad_federativa", "estado", "state", + ]), + pais=_find_first_text(e, ["pais", "country", "pais_code", "paisCode"]), ) @@ -289,7 +319,9 @@ def _build_items_from_xml(root: ET.Element) -> list[CoveItemView]: # Descripciones específicas (marca/modelo/serie/submodelo) desc_especificas: list[CoveDescEspecificaView] = [] for de in node.iter(): - if _strip_tag(de.tag) not in {"descripcionEspecifica", "descripcion_especifica"}: + # _strip_tag ya devuelve minúsculas, así que los candidatos deben + # estar también en minúsculas para que el `in` funcione. + if _strip_tag(de.tag) not in {"descripcionespecifica", "descripcion_especifica"}: continue desc_especificas.append( CoveDescEspecificaView( @@ -337,6 +369,35 @@ class CoveAcuseReportService: ) self._tpl = self.jinja_env.get_template("acuse_cove.html") + # ------------------------------------------------------------------ + # Helpers de imagen + # ------------------------------------------------------------------ + + def _image_data_uri(self, filename: str) -> str: + """Carga una imagen del directorio de templates y la devuelve como data URI base64. + + Detecta el tipo real desde magic bytes en lugar de confiar en la extensión, + porque las imágenes del template son JPEGs renombrados como .png. + """ + img_path = self.template_dir / filename + try: + with open(img_path, "rb") as fh: + raw = fh.read() + data = base64.b64encode(raw).decode("ascii") + if raw[:2] == b"\xff\xd8": + mime = "image/jpeg" + elif raw[:8] == b"\x89PNG\r\n\x1a\n": + mime = "image/png" + elif raw[:6] in (b"GIF87a", b"GIF89a"): + mime = "image/gif" + else: + ext = img_path.suffix.lstrip(".").lower() + mime = f"image/{ext}" + return f"data:{mime};base64,{data}" + except Exception: + logger.warning("No se pudo cargar imagen del template: %s", img_path) + return "" + # ------------------------------------------------------------------ # wkhtmltopdf config (mismo helper que DodaReportPdfService) # ------------------------------------------------------------------ @@ -468,13 +529,48 @@ class CoveAcuseReportService: return None # Cabecera / datos generales - tipo_operacion = _find_first_text(cove_node, ["tipoOperacion", "tipo_operacion"]) - numero_factura = _find_first_text(cove_node, ["numeroFactura", "numero_factura", "factura"]) - tipo_figura = _find_first_text(cove_node, ["tipoFigura", "tipo_figura"]) - fecha_expedicion = _find_first_text(cove_node, ["fechaExpedicion", "fecha_expedicion"]) - observaciones = _find_first_text(cove_node, ["observaciones", "observacionesCove", "observaciones_cove"]) - rfc_consulta = _find_first_text(cove_node, ["rfcConsulta", "rfc_consulta"]) - patente_aduanal = _find_first_text(cove_node, ["patenteAduanal", "patente_aduanal"]) + # Cada lista incluye: camelCase VUCEM, snake_case del schema Pydantic, + # y variantes alternativas observadas en XMLs de VUCEM. + tipo_operacion = _find_first_text(cove_node, [ + "tipoOperacion", "tipo_operacion", + ]) + numero_factura_relacionada = _find_first_text(cove_node, [ + "numeroFacturaOriginal", "numero_factura_original", + "relacionFactura", "relacionFacturas", "numero_factura_relacionada", + "facturaRelacionada", + ]) + numero_factura = _find_first_text(cove_node, [ + "numeroFactura", "numero_factura", "factura", "invoice_number", + ]) + tipo_figura = _find_first_text(cove_node, [ + "tipoFigura", "tipo_figura", + ]) + fecha_expedicion = _find_first_text(cove_node, [ + "fechaExpedicion", "fecha_expedicion", "fechaEmision", "fecha_emision", + ]) + observaciones = _find_first_text(cove_node, [ + "observaciones", "observacionesCove", "observaciones_cove", + ]) + rfc_consulta = _find_first_text(cove_node, [ + "rfcConsulta", "rfc_consulta", "rfcDeConsulta", + ]) + nombre_razon_social_consulta = _find_first_text(cove_node, [ + "nombreRazonSocialConsulta", "nombre_razon_social_consulta", + "nombreConsulta", "razonSocialConsulta", + ]) + patente_aduanal = _find_first_text(cove_node, [ + "patenteAduanal", "patente_aduanal", "patente", + ]) + subdivision = _find_first_text(cove_node, [ + "subdivision", "tieneSubdivision", "tiene_subdivision", + ]) + certificado_origen = _find_first_text(cove_node, [ + "certificadoOrigen", "certificado_origen", + ]) + exportador_autorizado = _find_first_text(cove_node, [ + "exportadorAutorizado", "exportador_autorizado", + "numExportadorAutorizado", "numero_exportador_autorizado", + ]) if not invoice_specific and not numero_factura: logger.info( @@ -492,12 +588,17 @@ class CoveAcuseReportService: return CoveAcuseContext( tipo_operacion=tipo_operacion or "", + numero_factura_relacionada=numero_factura_relacionada or "", numero_factura=numero_factura or "", tipo_figura=tipo_figura or "", fecha_expedicion=fecha_expedicion or "", observaciones=observaciones or "", rfc_consulta=rfc_consulta or "", + nombre_razon_social_consulta=nombre_razon_social_consulta or "", patente_aduanal=patente_aduanal or "", + subdivision=subdivision or "", + certificado_origen=certificado_origen or "", + exportador_autorizado=exportador_autorizado or "", edocument=edoc_final, vucem_operation_num=(getattr(compliance, "vucem_operation_num", None) or "") if compliance else "", emisor=emisor_view, @@ -576,36 +677,97 @@ class CoveAcuseReportService: # Renderizado PDF # ------------------------------------------------------------------ + # Dimensiones de las imágenes de cabecera/pie (JPEG 1024px de ancho): + # HeaderCOVE: 149px tall → ~31mm a ancho Letter (215.9mm) + # FooterCOVE: 126px tall → ~27mm a ancho Letter + _HEADER_MARGIN_MM = "34" # 31mm imagen + 3mm buffer + _FOOTER_MARGIN_MM = "29" # 27mm imagen + 2mm buffer + + @staticmethod + def _make_page_frame_html(img_data_uri: str) -> str: + """Genera el HTML mínimo para usar como header/footer de wkhtmltopdf. + + Usa resets explícitos en html/body y overflow:hidden para evitar que + wkhtmltopdf añada scrollbars al viewport del header/footer, lo que + reduciría el ancho efectivo y cortaría la imagen. + """ + return ( + "" + "" + "" + f"" + "" + ) + def render_pdf_bytes(self, context: CoveAcuseContext) -> bytes: + header_uri = self._image_data_uri("HeaderCOVE.png") + footer_uri = self._image_data_uri("FooterCOVE.png") + html = self._tpl.render( + header_img=header_uri, + footer_img=footer_uri, tipo_operacion=context.tipo_operacion, + numero_factura_relacionada=context.numero_factura_relacionada, numero_factura=context.numero_factura, tipo_figura=context.tipo_figura, fecha_expedicion=context.fecha_expedicion, observaciones=context.observaciones, rfc_consulta=context.rfc_consulta, + nombre_razon_social_consulta=context.nombre_razon_social_consulta, patente_aduanal=context.patente_aduanal, + subdivision=context.subdivision, + certificado_origen=context.certificado_origen, + exportador_autorizado=context.exportador_autorizado, edocument=context.edocument, vucem_operation_num=context.vucem_operation_num, emisor=context.emisor, destinatario=context.destinatario, items=context.items, ) - options = { - "page-size": "Letter", - "encoding": "UTF-8", - "margin-top": "5mm", - "margin-bottom": "5mm", - "margin-left": "8mm", - "margin-right": "8mm", - "enable-local-file-access": "", - } - return pdfkit.from_string( - html, - False, - options=options, - configuration=self._get_wkhtmltopdf_config(), + + # Escribir los HTML de cabecera y pie en archivos temporales. + # wkhtmltopdf los renderiza en el área de margen de CADA página. + header_tmp = tempfile.NamedTemporaryFile( + suffix=".html", delete=False, mode="w", encoding="utf-8" ) + header_tmp.write(self._make_page_frame_html(header_uri)) + header_tmp.close() + + footer_tmp = tempfile.NamedTemporaryFile( + suffix=".html", delete=False, mode="w", encoding="utf-8" + ) + footer_tmp.write(self._make_page_frame_html(footer_uri)) + footer_tmp.close() + + try: + options = { + "page-size": "Letter", + "encoding": "UTF-8", + "margin-top": self._HEADER_MARGIN_MM, + "margin-bottom": self._FOOTER_MARGIN_MM, + "margin-left": "0", + "margin-right": "0", + "header-html": header_tmp.name, + "footer-html": footer_tmp.name, + "header-spacing": "0", + "footer-spacing": "0", + "print-media-type": "", + "enable-local-file-access": "", + } + return pdfkit.from_string( + html, + False, + options=options, + configuration=self._get_wkhtmltopdf_config(), + ) + finally: + os.unlink(header_tmp.name) + os.unlink(footer_tmp.name) def build_pdf( self, diff --git a/backend/api/v1/modules/a76/factura_cove/routes.py b/backend/api/v1/modules/a76/factura_cove/routes.py index 10984977..969feaa0 100644 --- a/backend/api/v1/modules/a76/factura_cove/routes.py +++ b/backend/api/v1/modules/a76/factura_cove/routes.py @@ -170,6 +170,7 @@ def check_cove_eligibility( def get_cove_acuse_pdf( invoice_id: int, company_id: int = Query(..., description="Company ID"), + force_refresh: bool = Query(False, description="Forzar regeneración del PDF ignorando la caché S3"), db: Session = Depends(get_core_db), current_user: Dict[str, Any] = Depends(get_current_user), ): @@ -180,6 +181,7 @@ def get_cove_acuse_pdf( en la configuración VU (agente / empresa). - Renderiza el PDF con Jinja2 + pdfkit/wkhtmltopdf (mismo patrón que DODA). - Cachea el PDF en S3 con clave estable; en la siguiente llamada lo reutiliza. + - Usa ``force_refresh=true`` para saltar la caché y regenerar el PDF. - Devuelve StreamingResponse con Content-Type application/pdf. Si aún no existe un XML de COVE asociado (consulta a VU no realizada), @@ -205,8 +207,8 @@ def get_cove_acuse_pdf( pdf_key = cove_acuse_pdf_key(tenant_id, company_id, invoice_id) filename = f"acuse_cove_{invoice.invoice_number or invoice_id}.pdf" - # ── Caché S3: reutilizar si el PDF ya existe ──────────────── - if settings.use_s3_object_storage and storage_s3.object_exists(pdf_key): + # ── Caché S3: reutilizar si el PDF ya existe y no se fuerza regeneración ─ + if not force_refresh and settings.use_s3_object_storage and storage_s3.object_exists(pdf_key): try: cached_pdf = storage_s3.get_object_bytes(pdf_key) if cached_pdf: diff --git a/backend/api/v1/modules/a76/factura_cove/service.py b/backend/api/v1/modules/a76/factura_cove/service.py index 5e20ed3f..73d6ddb6 100644 --- a/backend/api/v1/modules/a76/factura_cove/service.py +++ b/backend/api/v1/modules/a76/factura_cove/service.py @@ -257,14 +257,14 @@ class FacturaCoveDomainService: if errors.has_errors(): return None + # rfc_usuario_vu = RFC del importador/empresa registrada en VUCEM (PMI, la empresa + # cuyo FIEL se usa para autenticar). En el sistema legado viene de query_tax_id. rfc_usuario_vu = ( (getattr(vu, "query_tax_id", None) or "").strip() if vu else "" ) or ( (getattr(company_vu, "query_rfc", None) or "").strip() if company_vu else "" ) - # Clave/token del webservice: usar el valor de VU si existe, o una - # clave fija de pruebas mientras se termina la configuración real. return ConfiguracionVU( rfc_usuario_vu=rfc_usuario_vu, clave_webservice=clave_webservice, @@ -273,6 +273,42 @@ class FacturaCoveDomainService: clave_fiel=clave_fiel_value, ) + def _normalize_country_to_m3(self, raw_country: str) -> str: + """ + Normaliza el código de país al m3_key del catálogo (GPaises / public.countries). + + VUCEM requiere el código m3_key de 3 caracteres. La DB puede almacenar + códigos de 2 letras (mex_key / ame_key) o el m3_key directamente. + La búsqueda se hace en orden: + 1. Coincidencia exacta en m3_key (3 chars). + 2. Coincidencia en mex_key o ame_key (2 chars). + 3. Fallback: el valor tal cual, truncado a 3 chars. + """ + if not raw_country: + return "" + code = raw_country.strip().upper() + try: + from api.v1.modules.public.reference_data.countries.models import Country + if len(code) == 3: + country = self.db.query(Country).filter(Country.m3_key == code).first() + if country: + return country.m3_key + if len(code) == 2: + country = ( + self.db.query(Country) + .filter((Country.mex_key == code) | (Country.ame_key == code)) + .first() + ) + if country: + return country.m3_key + # For any length that didn't match, try m3_key with a 3-char prefix + country = self.db.query(Country).filter(Country.m3_key == code[:3]).first() + if country: + return country.m3_key + except Exception: + pass + return code[:3] + def _clientprovider_to_persona(self, cp: ClientProvider) -> PersonaCove: """ Construye una PersonaCove a partir de un ClientProvider + su dirección. @@ -284,9 +320,8 @@ class FacturaCoveDomainService: tipo_identificador = "0" if tipo_nat == "E" else "1" identificacion = (cp.rfc or "").strip().upper() - # País: normalizar a código de 3 caracteres (ISO o catálogo VU). raw_country = (addr.country or "") if addr and getattr(addr, "country", None) else "" - country_code = raw_country.strip().upper()[:3] if raw_country else "" + country_code = self._normalize_country_to_m3(raw_country) return PersonaCove( tipo_identificador=tipo_identificador, @@ -330,9 +365,8 @@ class FacturaCoveDomainService: if addr is None and company.addresses: addr = company.addresses[0] - # País: normalizar a código de 3 caracteres (ISO o catálogo VU). raw_country = (addr.country or "") if addr and getattr(addr, "country", None) else "" - country_code = raw_country.strip().upper()[:3] if raw_country else "" + country_code = self._normalize_country_to_m3(raw_country) return PersonaCove( tipo_identificador="1", # Empresa mexicana por defecto @@ -392,6 +426,24 @@ class FacturaCoveDomainService: ) if provider: emisor_persona = self._clientprovider_to_persona(provider) + if not emisor_persona.calle: + errors.add_error( + field="emisor.calle", + message="El proveedor/exportador no tiene calle configurada (requerida por VUCEM)", + solution=[ + "Completa la dirección del proveedor/exportador con la calle en el catálogo de clientes y proveedores." + ], + code="EMISOR_CALLE_MISSING", + ) + if not emisor_persona.pais: + errors.add_error( + field="emisor.pais", + message="El proveedor/exportador no tiene país configurado (requerido por VUCEM)", + solution=[ + "Configura el código de país en la dirección del proveedor/exportador usando el catálogo de países." + ], + code="EMISOR_PAIS_MISSING", + ) else: errors.add_error( field="emisor", @@ -460,6 +512,25 @@ class FacturaCoveDomainService: code="DESTINATARIO_NOT_FOUND", ) + if destinatario_persona and not destinatario_persona.calle: + errors.add_error( + field="destinatario.calle", + message="El destinatario/importador no tiene calle configurada (requerida por VUCEM)", + solution=[ + "Completa la dirección del destinatario con la calle en el catálogo de clientes/proveedores o en los datos de la empresa." + ], + code="DESTINATARIO_CALLE_MISSING", + ) + if destinatario_persona and not destinatario_persona.pais: + errors.add_error( + field="destinatario.pais", + message="El destinatario/importador no tiene país configurado (requerido por VUCEM)", + solution=[ + "Configura el código de país en la dirección del destinatario usando el catálogo de países." + ], + code="DESTINATARIO_PAIS_MISSING", + ) + return emisor_persona, destinatario_persona def _build_mercancias(self, ctx: InvoiceContext, errors: ErrorCollector) -> list[MercanciaCove]: @@ -731,14 +802,46 @@ class FacturaCoveDomainService: correo_destino = (recipient_email or (ctx.vu.vu_email if ctx.vu else None) or "").strip() or None + # rfc_consulta = RFC del agente aduanal / representante que opera en VUCEM. + # VUCEM exige que sea distinto del RFC del que registra (rfc_usuario_vu / FIEL). + # En el sistema legado viene del campo "Usuario Web Service VU" (web_service_user), + # que contiene el RFC personal del agente (p. ej. MABL620809BY7). + # Fallback: tax_id del broker → identificación del destinatario. + rfc_consulta_value = ( + (ctx.vu.web_service_user or "").strip().upper() if ctx.vu and ctx.vu.web_service_user else "" + ) or ( + (ctx.broker.tax_id or "").strip().upper() if ctx.broker and ctx.broker.tax_id else "" + ) or ( + (destinatario.identificacion or "").strip().upper() if destinatario else "" + ) + + if ( + rfc_consulta_value + and configuracion_vu + and rfc_consulta_value == (configuracion_vu.rfc_usuario_vu or "").strip().upper() + ): + raise ValidationException( + "Conflicto de RFC en COVE", + errors=[ + { + "field": "rfc_consulta", + "message": ( + f"El RFC de consulta ({rfc_consulta_value}) es igual al RFC del usuario VU " + f"({configuracion_vu.rfc_usuario_vu}). VUCEM no permite que ambos sean iguales." + ), + "solution": [ + "Captura el RFC del agente aduanal o representante en el campo " + "'Usuario Web Service VU' (web_service_user) de la configuración VU. " + "Ese RFC debe ser distinto del RFC del importador que aparece en 'R.F.C de consulta' (query_tax_id)." + ], + "code": "RFC_CONSULTA_EQUALS_RFC_USUARIO_VU", + } + ], + ) + return FacturaCoveRequest( configuracion_vu=configuracion_vu, - # El RFC de consulta NO debe ser igual al RFC del que registra el comprobante. - # Usamos como RFC de consulta el RFC del agente aduanal (customs broker), - # y dejamos que configuracion_vu.rfc_usuario_vu represente al contribuyente. - rfc_consulta=( - (ctx.broker.tax_id or "").strip().upper() if ctx.broker and ctx.broker.tax_id else "" - ), + rfc_consulta=rfc_consulta_value, tipo_figura=tipo_figura, numero_factura=numero_factura, tipo_operacion=tipo_operacion, diff --git a/backend/api/v1/modules/a76/factura_cove/tasks.py b/backend/api/v1/modules/a76/factura_cove/tasks.py index c5e42129..7d7a65f6 100644 --- a/backend/api/v1/modules/a76/factura_cove/tasks.py +++ b/backend/api/v1/modules/a76/factura_cove/tasks.py @@ -33,6 +33,27 @@ def _strip_ns(tag: str) -> str: return tag.strip().lower() +def _extract_edocument_from_xml(xml_bytes: bytes) -> str | None: + """ + Extrae el número de e-document (COVE) de un XML de VUCEM. + + Busca los tags más comunes que VUCEM utiliza para el identificador + del documento COVE: , , , . + """ + try: + root = ET.fromstring(xml_bytes) + except Exception: + return None + + for node in root.iter(): + tag = _strip_ns(node.tag) + if tag in {"edocumento", "e-document", "edocument", "cove", "numerocove", "numero_cove"}: + value = (node.text or "").strip() + if value: + return value + return None + + def _parse_consulta_respuesta(b64_str: str) -> tuple[bool, str | None, list[str]]: """ Parsea el XML de la respuesta de VUCEM a la consulta de estado (campo @@ -40,7 +61,7 @@ def _parse_consulta_respuesta(b64_str: str) -> tuple[bool, str | None, list[str] Devuelve una tupla (tiene_error, edocumento, mensajes_error) donde: - tiene_error: True si VUCEM reportó true - - edocumento: el número de COVE asignado por VUCEM () si no hay error + - edocumento: el número de COVE asignado por VUCEM () si no hay error - mensajes_error: lista de textos de error si tiene_error es True """ tiene_error = False @@ -63,14 +84,16 @@ def _parse_consulta_respuesta(b64_str: str) -> tuple[bool, str | None, list[str] if tag == "contieneerror": tiene_error = (node.text or "").strip().lower() == "true" - elif tag == "edocumento": + elif tag in {"edocument", "edocumento"}: + # VUCEM usa (sin 'o') en la respuesta de consulta value = (node.text or "").strip() if value: edocumento = value - elif tag == "mensaje": + elif tag in {"mensaje", "leyenda", "descripcionerror", "mensajeerror"}: + # VUCEM puede usar como mensaje general o / para errores text = (node.text or "").strip() - if text: + if text and tiene_error: mensajes_error.append(text) return tiene_error, edocumento, mensajes_error diff --git a/backend/api/v1/modules/a76/factura_cove/templates/FooterCOVE.png b/backend/api/v1/modules/a76/factura_cove/templates/FooterCOVE.png new file mode 100644 index 0000000000000000000000000000000000000000..e2d614c125bb5006ba7d7318589fc1705284641c GIT binary patch literal 8903 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 - - Acuse de COVE - + /* Configuración de impresión */ + @media print { + body { + background-color: white; + padding: 0; + margin: 0; + } + .hoja-carta { + box-shadow: none; + width: 100%; + margin: 0; + /* Top ajustado: el header-html de wkhtmltopdf ya ocupa su margen superior. + Los 5mm aquí son solo el respiro entre el borde del área de contenido + y la primera tabla. Lados: 8mm para alinearse visualmente con la imagen. */ + padding: 5mm 8mm 8mm; + } + .titulo-principal { margin-top: 0; margin-bottom: 1px; } + .browser-only { display: none; } + .cove-table th { background-color: #C0C0C0 !important; } + .cove-badge { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + background-color: #1a3d6e !important; + color: #ffffff !important; + } + } + - - +
- -
Datos
+ +
+ Encabezado COVE +
-
Datos del comprobante
-
-
-
Tipo de operación
-
{{ tipo_operacion }}
-
-
-
No. de factura
-
{{ numero_factura }}
-
-
-
Tipo de figura
-
{{ tipo_figura }}
-
-
-
Fecha de expedición
-
{{ fecha_expedicion }}
-
-
- - -
Observaciones
-
{{ observaciones or '' }}
- - -
RFC con permiso de consulta
-
-
-
RFC de Consulta
-
{{ rfc_consulta }}
-
-
-
Número de autorización / Patente aduanal
-
{{ patente_aduanal }}
-
-
- -
- - -
Datos de la factura
- - -
- Datos generales del proveedor -
-
-
-
Tipo de identificador
-
{{ emisor.tipo_identificador }}
-
-
-
Tax ID / Sin Tax / RFC / CURP
-
{{ emisor.identificacion }}
-
-
-
Nombre o Razón Social
-
{{ emisor.nombre }}
-
-
-
Apellido paterno
-
{{ emisor.apellido_paterno }}
-
-
-
-
-
Apellido materno
-
{{ emisor.apellido_materno }}
-
-
-
Certificado de origen
-
-
-
-
No. exportador autorizado
-
-
-
- -
- Domicilio del proveedor -
-
-
-
Calle
-
{{ emisor.calle }}
-
-
-
No. exterior
-
{{ emisor.numero_exterior }}
-
-
-
No. interior
-
{{ emisor.numero_interior }}
-
-
-
Código postal
-
{{ emisor.codigo_postal }}
-
-
-
-
-
Colonia
-
{{ emisor.colonia }}
-
-
-
Localidad
-
{{ emisor.localidad }}
-
-
-
Municipio
-
{{ emisor.municipio }}
-
-
-
Entidad federativa
-
{{ emisor.entidad_federativa }}
-
-
-
-
-
País
-
{{ emisor.pais }}
-
-
-
- -
- - -
- Datos generales del destinatario -
-
-
-
Tipo de identificador
-
{{ destinatario.tipo_identificador }}
-
-
-
Tax ID / Sin Tax / RFC / CURP
-
{{ destinatario.identificacion }}
-
-
-
Nombre o Razón Social
-
{{ destinatario.nombre }}
-
-
-
Apellido paterno
-
{{ destinatario.apellido_paterno }}
-
-
-
-
-
Apellido materno
-
{{ destinatario.apellido_materno }}
-
-
-
- -
- Domicilio del destinatario -
-
-
-
Calle
-
{{ destinatario.calle }}
-
-
-
No. exterior
-
{{ destinatario.numero_exterior }}
-
-
-
No. interior
-
{{ destinatario.numero_interior }}
-
-
-
Código postal
-
{{ destinatario.codigo_postal }}
-
-
-
-
-
Colonia
-
{{ destinatario.colonia }}
-
-
-
Localidad
-
{{ destinatario.localidad }}
-
-
-
Municipio
-
{{ destinatario.municipio }}
-
-
-
Entidad federativa
-
{{ destinatario.entidad_federativa }}
-
-
-
-
-
País
-
{{ destinatario.pais }}
-
-
-
- -
- - -
Datos de la mercancía
- - {% if items %} - - - - - - - - - - - - - - {% for item in items %} - - - - - - - - - - {% if item.descripciones_especificas %} - - - - {% endif %} - {% endfor %} - -
Descripción genérica de la mercancíaClave UMCCantidad UMCTipo monedaValor unitarioValor totalValor total en dólares
{{ item.descripcion_generica }}{{ item.clave_unidad_medida }}{{ item.cantidad }}{{ item.tipo_moneda }}$ {{ item.valor_unitario }}$ {{ item.valor_total }}$ {{ item.valor_dolares }}
- + +
- - - - + + {% if edocument %} + + {% endif %} + +
MarcaModeloSerieSubmodelo +
Datos del Acuse de Valor:
+
COMPROBANTE DE VALOR ELECTRÓNICO
+
+ {{ edocument }} +
+ + + + + + + + + + + + + + + + + + +
Tipo de operaciónRelación de facturasNo. de factura
{{ tipo_operacion }}{{ numero_factura_relacionada }}{{ numero_factura }}
+ + + + + + + + + + + + + + +
Tipo de figuraFecha Exp.
{{ tipo_figura }}{{ fecha_expedicion }}
+ + + + + + + + + + + +
Observaciones
{{ observaciones }}
+ +
RFC Con permiso de consulta
+ + + + + + + + + + + + + +
RFC de ConsultaNombre o Razón Social
{{ rfc_consulta }}{{ nombre_razon_social_consulta }}
+ +
Número de patente aduanal
+ + + + + + + + + + +
Número de autorización aduanal
{{ patente_aduanal }}
+ +
Datos de la factura
+ + + + + + + + + + + + + + + + +
SubdivisiónCertificado de origenNo. de exportador autorizado
{{ subdivision }}{{ certificado_origen }}{{ exportador_autorizado }}
+ + +
Datos generales del proveedor
+ + + + + + + + + + + + + +
Tipo de identificadorTax ID/Sin Tax/RFC/CURP
{{ emisor.tipo_identificador }}{{ emisor.identificacion }}
+ + + + + + + + + + + + + + + + + +
Nombre(s) o Razón SocialApellido paternoApellido materno
{{ emisor.nombre }}{{ emisor.apellido_paterno }}{{ emisor.apellido_materno }}
+ +
Domicilio del proveedor
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CalleNo. exteriorNo. interiorCódigo postal
{{ emisor.calle }}{{ emisor.numero_exterior }}{{ emisor.numero_interior }}{{ emisor.codigo_postal }}
ColoniaLocalidad
{{ emisor.colonia }}{{ emisor.localidad }}
Entidad federativaMunicipio
{{ emisor.entidad_federativa }}{{ emisor.municipio }}
País
{{ emisor.pais }}
+ + +
Datos generales del destinatario
+ + + + + + + + + + + + + +
Tipo de IdentificadorTax ID/Sin Tax/RFC/CURP
{{ destinatario.tipo_identificador }}{{ destinatario.identificacion }}
+ + + + + + + + + + + + + + + + + +
Nombre(s) o Razón SocialApellido paternoApellido materno
{{ destinatario.nombre }}{{ destinatario.apellido_paterno }}{{ destinatario.apellido_materno }}
+ +
Domicilio del destinatario
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CalleNo. exteriorNo. interiorCódigo postal
{{ destinatario.calle }}{{ destinatario.numero_exterior }}{{ destinatario.numero_interior }}{{ destinatario.codigo_postal }}
ColoniaLocalidad
{{ destinatario.colonia }}{{ destinatario.localidad }}
Entidad federativaMunicipio
{{ destinatario.entidad_federativa }}{{ destinatario.municipio }}
País
{{ destinatario.pais }}
+ + + {% if items %} + {% for item in items %} +
Datos de la mercancía
+ + + + + + + + + + + + + + + + +
Descripción genérica de la mercancíaClave UMCCantidad UMC
{{ item.descripcion_generica }}{{ item.clave_unidad_medida }}{{ item.cantidad }}
+ + + + + + + + + + + + + + + + + + + + +
Tipo monedaValor unitarioValor totalValor total en dólares
{{ item.tipo_moneda }}$ {{ item.valor_unitario }}$ {{ item.valor_total }}$ {{ item.valor_dolares }}
+ + {% if item.descripciones_especificas %} +
Descripción de la mercancía
+ + + + + + + + + + + + {% for desc in item.descripciones_especificas %} - - - - + + + + {% endfor %} -
MarcaModeloSerieSubmodelo
{{ desc.marca }}{{ desc.modelo }}{{ desc.numero_serie }}{{ desc.submodelo }}{{ desc.marca }}{{ desc.modelo }}{{ desc.numero_serie }}{{ desc.submodelo }}
-
- {% else %} -
- Sin partidas registradas. -
- {% endif %} + + {% endif %} - - + {% endfor %} + {% else %} + + + + +
Sin partidas registradas.
+ {% endif %} + + +
+ Pie de página COVE +
+ +
- + \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 73f25a95..e4d46239 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -586,8 +586,9 @@ export const invoicesApi = { }>(`/v1/a76/factura-cove/invoices/${invoiceId}/cove/eligibility?${params.toString()}`); }, - getCoveReceipt: (invoiceId: number, companyId: number): Promise => { + getCoveReceipt: (invoiceId: number, companyId: number, forceRefresh = false): Promise => { const params = new URLSearchParams({ company_id: companyId.toString() }); + if (forceRefresh) params.set('force_refresh', 'true'); return api.getBlob( `/v1/a76/factura-cove/invoices/${invoiceId}/cove/acuse?${params.toString()}` ); diff --git a/frontend/src/routes/dashboard/invoices/+page.svelte b/frontend/src/routes/dashboard/invoices/+page.svelte index 6d58da6c..b4b11a2b 100644 --- a/frontend/src/routes/dashboard/invoices/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/+page.svelte @@ -1332,7 +1332,8 @@ try { const blob = await invoicesApi.getCoveReceipt( selectedInvoice.id, - companyStore.activeCompany.id + companyStore.activeCompany.id, + true ); const url = URL.createObjectURL(blob); const a = document.createElement('a');