feature/reporte-bak
This commit is contained in:
@@ -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 <emisor> o <destinatario>
|
||||
(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 <domicilio>.
|
||||
"""
|
||||
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 <domicilio> 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 (
|
||||
"<!DOCTYPE html><html><head><meta charset=\"UTF-8\">"
|
||||
"<style>"
|
||||
"*{margin:0;padding:0;box-sizing:border-box;}"
|
||||
"html,body{width:100%;overflow:hidden;}"
|
||||
"body{padding:0 8mm;}"
|
||||
"img{width:100%;display:block;}"
|
||||
"</style>"
|
||||
"</head><body>"
|
||||
f"<img src=\"{img_data_uri}\">"
|
||||
"</body></html>"
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: <eDocumento>, <e-document>, <edocument>, <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ó <contieneError>true</>
|
||||
- edocumento: el número de COVE asignado por VUCEM (<eDocumento>) si no hay error
|
||||
- edocumento: el número de COVE asignado por VUCEM (<eDocument>) 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 <eDocument> (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 <leyenda> como mensaje general o <mensaje>/<descripcionError> para errores
|
||||
text = (node.text or "").strip()
|
||||
if text:
|
||||
if text and tiene_error:
|
||||
mensajes_error.append(text)
|
||||
|
||||
return tiene_error, edocumento, mensajes_error
|
||||
|
||||
BIN
backend/api/v1/modules/a76/factura_cove/templates/FooterCOVE.png
Normal file
BIN
backend/api/v1/modules/a76/factura_cove/templates/FooterCOVE.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
BIN
backend/api/v1/modules/a76/factura_cove/templates/HeaderCOVE.png
Normal file
BIN
backend/api/v1/modules/a76/factura_cove/templates/HeaderCOVE.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -1,452 +1,518 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Acuse de COVE</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 8pt;
|
||||
color: #000;
|
||||
}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reporte COVE</title>
|
||||
<style>
|
||||
/* Resets básicos para simular una hoja impresa */
|
||||
body {
|
||||
background-color: #525659; /* Fondo oscuro típico de visores PDF */
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* ── Header / Footer ────────────────────────────────────────── */
|
||||
.page-header {
|
||||
background: #003366;
|
||||
color: #fff;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.page-header .title-main {
|
||||
font-size: 13pt;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.page-header .title-sub {
|
||||
font-size: 8pt;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.page-header .logo-area {
|
||||
text-align: right;
|
||||
font-size: 7pt;
|
||||
opacity: 0.8;
|
||||
}
|
||||
/* Contenedor principal que simula la hoja tamaño Carta (8.5 x 11 pulgadas) */
|
||||
.hoja-carta {
|
||||
width: 8.5in;
|
||||
min-height: 11in;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
padding: 0.3in 0.4in; /* Márgenes de impresión estándar */
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
background: #003366;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
margin-top: 6px;
|
||||
font-size: 7pt;
|
||||
text-align: center;
|
||||
}
|
||||
/* Tipografías y tamaños legacy exactos de Clarion */
|
||||
.titulo-seccion {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 13pt;
|
||||
font-weight: normal;
|
||||
color: black;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* ── Sección genérica ─────────────────────────────────────── */
|
||||
.section-title {
|
||||
font-size: 8.5pt;
|
||||
font-weight: bold;
|
||||
color: #003366;
|
||||
margin: 6px 0 2px;
|
||||
border-bottom: 1px solid #003366;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.titulo-principal {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 13pt;
|
||||
font-weight: normal;
|
||||
color: black;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* ── Grilla de campos ─────────────────────────────────────── */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
border: 1px solid #999;
|
||||
width: 100%;
|
||||
}
|
||||
.grid-2 { grid-template-columns: 1fr 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||||
.grid-4 { grid-template-columns: 1fr 1fr 1fr 1fr; }
|
||||
.grid-5 { grid-template-columns: 2fr 1fr 1fr 1fr 1fr; }
|
||||
/* Tablas rígidas para simular comandos BOX y LINE */
|
||||
.cove-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 2px; /* Pequeño gap entre cajas de Clarion */
|
||||
table-layout: fixed; /* Fuerza a respetar estrictamente los anchos del colgroup */
|
||||
}
|
||||
|
||||
.field-group {
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.field-group:last-child { border-right: none; }
|
||||
.cove-table th, .cove-table td {
|
||||
border: 1px solid black;
|
||||
text-align: left;
|
||||
vertical-align: top; /* Alineación arriba a la izquierda como en Clarion */
|
||||
padding-top: 1px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
padding-bottom: 1px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 6.5pt;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.field-value {
|
||||
font-size: 8pt;
|
||||
min-height: 12px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.cove-table th {
|
||||
/* FONT('Arial',10,,FONT:bold) y FILL(COLOR:Silver) */
|
||||
background-color: #C0C0C0 !important;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 10pt;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
height: 18px;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
/* ── Banda de encabezado de columna (fondo gris) ──────────── */
|
||||
.band-header {
|
||||
background: #c0c0c0;
|
||||
font-weight: bold;
|
||||
font-size: 7.5pt;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #999;
|
||||
border-bottom: none;
|
||||
}
|
||||
.cove-table td {
|
||||
/* FONT('Times New Roman',11) */
|
||||
font-family: "Times New Roman", Times, serif;
|
||||
font-size: 11pt;
|
||||
color: black;
|
||||
height: 20px; /* Altura mínima de la banda de datos */
|
||||
}
|
||||
|
||||
/* ── Observaciones ────────────────────────────────────────── */
|
||||
.obs-box {
|
||||
border: 1px solid #999;
|
||||
min-height: 28px;
|
||||
padding: 3px 5px;
|
||||
font-size: 8pt;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
/* Casos especiales de altura */
|
||||
.td-observaciones {
|
||||
min-height: 28pt;
|
||||
height: 28pt;
|
||||
}
|
||||
|
||||
/* ── Tabla de partidas ────────────────────────────────────── */
|
||||
table.partidas {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 7.5pt;
|
||||
margin-top: 2px;
|
||||
}
|
||||
table.partidas th {
|
||||
background: #c0c0c0;
|
||||
border: 1px solid #888;
|
||||
padding: 2px 4px;
|
||||
text-align: left;
|
||||
font-size: 7pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
table.partidas td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.partidas td.num { text-align: right; }
|
||||
table.partidas .sub-th {
|
||||
background: #ddd;
|
||||
font-size: 6.5pt;
|
||||
font-style: italic;
|
||||
}
|
||||
/* Imagen fluida dentro de sus contenedores */
|
||||
.img-header-footer {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── COVE badge ───────────────────────────────────────────── */
|
||||
.cove-badge {
|
||||
display: inline-block;
|
||||
background: #003366;
|
||||
color: #fff;
|
||||
font-size: 8pt;
|
||||
font-weight: bold;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.subtitulo {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 8pt;
|
||||
font-weight: normal;
|
||||
color: #555;
|
||||
margin-top: 0;
|
||||
margin-bottom: 3px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* ── Separador ────────────────────────────────────────────── */
|
||||
hr.sep { border: none; border-top: 1px solid #ccc; margin: 4px 0; }
|
||||
/* Badge con el número de COVE (ej. COVE24782HO65) */
|
||||
.cove-badge {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
background-color: #1a3d6e;
|
||||
padding: 3px 10px;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.5px;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
/* ── Datos principales fila ───────────────────────────────── */
|
||||
.main-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.main-row .field-group { flex: 1; }
|
||||
</style>
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ═══════════════════ HEADER ════════════════════════════ -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="title-main">COMPROBANTE DE VALOR ELECTRÓNICO (COVE)</div>
|
||||
<div class="title-sub">Acuse de Recepción — Ventanilla Única</div>
|
||||
</div>
|
||||
<div class="logo-area">
|
||||
{% if edocument %}
|
||||
<span class="cove-badge">E-Document: {{ edocument }}</span><br/>
|
||||
{% endif %}
|
||||
{% if vucem_operation_num %}
|
||||
<span>Op. VUCEM: {{ vucem_operation_num }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hoja-carta">
|
||||
|
||||
<!-- ═══════════════════ SECCIÓN: DATOS GENERALES ══════════ -->
|
||||
<div class="section-title">Datos</div>
|
||||
<!-- HEADER IMAGE (solo visible en el navegador; en PDF lo repite wkhtmltopdf via --header-html) -->
|
||||
<div class="browser-only">
|
||||
<img src="{{ header_img }}" alt="Encabezado COVE" class="img-header-footer" style="margin-bottom: 10px;">
|
||||
</div>
|
||||
|
||||
<div class="band-header">Datos del comprobante</div>
|
||||
<div class="grid grid-4">
|
||||
<div class="field-group">
|
||||
<div class="field-label">Tipo de operación</div>
|
||||
<div class="field-value">{{ tipo_operacion }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">No. de factura</div>
|
||||
<div class="field-value">{{ numero_factura }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Tipo de figura</div>
|
||||
<div class="field-value">{{ tipo_figura }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Fecha de expedición</div>
|
||||
<div class="field-value">{{ fecha_expedicion }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Observaciones ── -->
|
||||
<div class="band-header">Observaciones</div>
|
||||
<div class="obs-box">{{ observaciones or '' }}</div>
|
||||
|
||||
<!-- ── RFC consulta / Patente ── -->
|
||||
<div class="band-header">RFC con permiso de consulta</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="field-group">
|
||||
<div class="field-label">RFC de Consulta</div>
|
||||
<div class="field-value">{{ rfc_consulta }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Número de autorización / Patente aduanal</div>
|
||||
<div class="field-value">{{ patente_aduanal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="sep"/>
|
||||
|
||||
<!-- ═══════════════════ SECCIÓN: DATOS DE LA FACTURA ══════ -->
|
||||
<div class="section-title">Datos de la factura</div>
|
||||
|
||||
<!-- ── Proveedor / Emisor ── -->
|
||||
<div class="section-title" style="font-size:7.5pt; border-bottom:none; margin-top:4px;">
|
||||
Datos generales del proveedor
|
||||
</div>
|
||||
<div class="grid grid-4">
|
||||
<div class="field-group">
|
||||
<div class="field-label">Tipo de identificador</div>
|
||||
<div class="field-value">{{ emisor.tipo_identificador }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Tax ID / Sin Tax / RFC / CURP</div>
|
||||
<div class="field-value">{{ emisor.identificacion }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Nombre o Razón Social</div>
|
||||
<div class="field-value">{{ emisor.nombre }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Apellido paterno</div>
|
||||
<div class="field-value">{{ emisor.apellido_paterno }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-3">
|
||||
<div class="field-group">
|
||||
<div class="field-label">Apellido materno</div>
|
||||
<div class="field-value">{{ emisor.apellido_materno }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Certificado de origen</div>
|
||||
<div class="field-value"></div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">No. exportador autorizado</div>
|
||||
<div class="field-value"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title" style="font-size:7.5pt; border-bottom:none; margin-top:4px;">
|
||||
Domicilio del proveedor
|
||||
</div>
|
||||
<div class="grid grid-4">
|
||||
<div class="field-group">
|
||||
<div class="field-label">Calle</div>
|
||||
<div class="field-value">{{ emisor.calle }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">No. exterior</div>
|
||||
<div class="field-value">{{ emisor.numero_exterior }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">No. interior</div>
|
||||
<div class="field-value">{{ emisor.numero_interior }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Código postal</div>
|
||||
<div class="field-value">{{ emisor.codigo_postal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-4">
|
||||
<div class="field-group">
|
||||
<div class="field-label">Colonia</div>
|
||||
<div class="field-value">{{ emisor.colonia }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Localidad</div>
|
||||
<div class="field-value">{{ emisor.localidad }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Municipio</div>
|
||||
<div class="field-value">{{ emisor.municipio }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Entidad federativa</div>
|
||||
<div class="field-value">{{ emisor.entidad_federativa }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="field-group">
|
||||
<div class="field-label">País</div>
|
||||
<div class="field-value">{{ emisor.pais }}</div>
|
||||
</div>
|
||||
<div class="field-group"></div>
|
||||
</div>
|
||||
|
||||
<hr class="sep"/>
|
||||
|
||||
<!-- ── Destinatario ── -->
|
||||
<div class="section-title" style="font-size:7.5pt; border-bottom:none; margin-top:4px;">
|
||||
Datos generales del destinatario
|
||||
</div>
|
||||
<div class="grid grid-4">
|
||||
<div class="field-group">
|
||||
<div class="field-label">Tipo de identificador</div>
|
||||
<div class="field-value">{{ destinatario.tipo_identificador }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Tax ID / Sin Tax / RFC / CURP</div>
|
||||
<div class="field-value">{{ destinatario.identificacion }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Nombre o Razón Social</div>
|
||||
<div class="field-value">{{ destinatario.nombre }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Apellido paterno</div>
|
||||
<div class="field-value">{{ destinatario.apellido_paterno }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="field-group">
|
||||
<div class="field-label">Apellido materno</div>
|
||||
<div class="field-value">{{ destinatario.apellido_materno }}</div>
|
||||
</div>
|
||||
<div class="field-group"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title" style="font-size:7.5pt; border-bottom:none; margin-top:4px;">
|
||||
Domicilio del destinatario
|
||||
</div>
|
||||
<div class="grid grid-4">
|
||||
<div class="field-group">
|
||||
<div class="field-label">Calle</div>
|
||||
<div class="field-value">{{ destinatario.calle }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">No. exterior</div>
|
||||
<div class="field-value">{{ destinatario.numero_exterior }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">No. interior</div>
|
||||
<div class="field-value">{{ destinatario.numero_interior }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Código postal</div>
|
||||
<div class="field-value">{{ destinatario.codigo_postal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-4">
|
||||
<div class="field-group">
|
||||
<div class="field-label">Colonia</div>
|
||||
<div class="field-value">{{ destinatario.colonia }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Localidad</div>
|
||||
<div class="field-value">{{ destinatario.localidad }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Municipio</div>
|
||||
<div class="field-value">{{ destinatario.municipio }}</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field-label">Entidad federativa</div>
|
||||
<div class="field-value">{{ destinatario.entidad_federativa }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="field-group">
|
||||
<div class="field-label">País</div>
|
||||
<div class="field-value">{{ destinatario.pais }}</div>
|
||||
</div>
|
||||
<div class="field-group"></div>
|
||||
</div>
|
||||
|
||||
<hr class="sep"/>
|
||||
|
||||
<!-- ═══════════════════ SECCIÓN: PARTIDAS ══════════════════ -->
|
||||
<div class="section-title">Datos de la mercancía</div>
|
||||
|
||||
{% if items %}
|
||||
<table class="partidas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30%">Descripción genérica de la mercancía</th>
|
||||
<th style="width:8%">Clave UMC</th>
|
||||
<th style="width:8%">Cantidad UMC</th>
|
||||
<th style="width:8%">Tipo moneda</th>
|
||||
<th style="width:13%">Valor unitario</th>
|
||||
<th style="width:13%">Valor total</th>
|
||||
<th style="width:13%">Valor total en dólares</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.descripcion_generica }}</td>
|
||||
<td>{{ item.clave_unidad_medida }}</td>
|
||||
<td class="num">{{ item.cantidad }}</td>
|
||||
<td>{{ item.tipo_moneda }}</td>
|
||||
<td class="num">$ {{ item.valor_unitario }}</td>
|
||||
<td class="num">$ {{ item.valor_total }}</td>
|
||||
<td class="num">$ {{ item.valor_dolares }}</td>
|
||||
</tr>
|
||||
{% if item.descripciones_especificas %}
|
||||
<tr>
|
||||
<td colspan="7" style="padding: 0;">
|
||||
<table style="width:100%; border-collapse:collapse; font-size:7pt;">
|
||||
<!-- Bloque de título del acuse + badge con número COVE -->
|
||||
<table style="width:100%;border:none;border-collapse:collapse;margin-bottom:4px;">
|
||||
<tr>
|
||||
<th class="sub-th" style="border:1px solid #ccc; padding:2px 4px; width:25%">Marca</th>
|
||||
<th class="sub-th" style="border:1px solid #ccc; padding:2px 4px; width:25%">Modelo</th>
|
||||
<th class="sub-th" style="border:1px solid #ccc; padding:2px 4px; width:25%">Serie</th>
|
||||
<th class="sub-th" style="border:1px solid #ccc; padding:2px 4px; width:25%">Submodelo</th>
|
||||
<td style="border:none;padding:0;vertical-align:bottom;">
|
||||
<div class="titulo-principal">Datos del Acuse de Valor:</div>
|
||||
<div class="subtitulo">COMPROBANTE DE VALOR ELECTRÓNICO</div>
|
||||
</td>
|
||||
{% if edocument %}
|
||||
<td style="border:none;padding:0;vertical-align:middle;text-align:right;white-space:nowrap;width:1%;">
|
||||
<span class="cove-badge">{{ edocument }}</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ==========================================
|
||||
SECCIÓN 1: DATOS DE LA FACTURA
|
||||
=========================================== -->
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 33%;">
|
||||
<col style="width: 34%;">
|
||||
<col style="width: 33%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Tipo de operación</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Relación de facturas</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">No. de factura</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ tipo_operacion }}</td>
|
||||
<td>{{ numero_factura_relacionada }}</td>
|
||||
<td>{{ numero_factura }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 51%;">
|
||||
<col style="width: 49%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Tipo de figura</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Fecha Exp.</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ tipo_figura }}</td>
|
||||
<td>{{ fecha_expedicion }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 100%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Observaciones</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-observaciones">{{ observaciones }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="titulo-seccion">RFC Con permiso de consulta</div>
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 51%;">
|
||||
<col style="width: 49%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">RFC de Consulta</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Nombre o Razón Social</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ rfc_consulta }}</td>
|
||||
<td>{{ nombre_razon_social_consulta }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="titulo-seccion">Número de patente aduanal</div>
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 100%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Número de autorización aduanal</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ patente_aduanal }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="titulo-seccion">Datos de la factura</div>
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 33%;">
|
||||
<col style="width: 34%;">
|
||||
<col style="width: 33%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Subdivisión</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Certificado de origen</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">No. de exportador autorizado</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ subdivision }}</td>
|
||||
<td>{{ certificado_origen }}</td>
|
||||
<td>{{ exportador_autorizado }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ==========================================
|
||||
SECCIÓN 2: DATOS DEL PROVEEDOR
|
||||
=========================================== -->
|
||||
<div class="titulo-seccion">Datos generales del proveedor</div>
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 44%;">
|
||||
<col style="width: 56%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Tipo de identificador</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Tax ID/Sin Tax/RFC/CURP</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ emisor.tipo_identificador }}</td>
|
||||
<td>{{ emisor.identificacion }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 44%;">
|
||||
<col style="width: 28%;">
|
||||
<col style="width: 28%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Nombre(s) o Razón Social</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Apellido paterno</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Apellido materno</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ emisor.nombre }}</td>
|
||||
<td>{{ emisor.apellido_paterno }}</td>
|
||||
<td>{{ emisor.apellido_materno }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="titulo-seccion">Domicilio del proveedor</div>
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 51%;">
|
||||
<col style="width: 16%;">
|
||||
<col style="width: 19%;">
|
||||
<col style="width: 14%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Calle</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">No. exterior</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">No. interior</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Código postal</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ emisor.calle }}</td>
|
||||
<td>{{ emisor.numero_exterior }}</td>
|
||||
<td>{{ emisor.numero_interior }}</td>
|
||||
<td>{{ emisor.codigo_postal }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Colonia</th>
|
||||
<th colspan="3" bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Localidad</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ emisor.colonia }}</td>
|
||||
<td colspan="3">{{ emisor.localidad }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Entidad federativa</th>
|
||||
<th colspan="3" bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Municipio</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ emisor.entidad_federativa }}</td>
|
||||
<td colspan="3">{{ emisor.municipio }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" bgcolor="#C0C0C0" style="background-color: #C0C0C0;">País</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4">{{ emisor.pais }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ==========================================
|
||||
SECCIÓN 3: DATOS DEL DESTINATARIO
|
||||
=========================================== -->
|
||||
<div class="titulo-seccion">Datos generales del destinatario</div>
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 44%;">
|
||||
<col style="width: 56%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Tipo de Identificador</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Tax ID/Sin Tax/RFC/CURP</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ destinatario.tipo_identificador }}</td>
|
||||
<td>{{ destinatario.identificacion }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 44%;">
|
||||
<col style="width: 28%;">
|
||||
<col style="width: 28%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Nombre(s) o Razón Social</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Apellido paterno</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Apellido materno</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ destinatario.nombre }}</td>
|
||||
<td>{{ destinatario.apellido_paterno }}</td>
|
||||
<td>{{ destinatario.apellido_materno }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="titulo-seccion">Domicilio del destinatario</div>
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 51%;">
|
||||
<col style="width: 16%;">
|
||||
<col style="width: 19%;">
|
||||
<col style="width: 14%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Calle</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">No. exterior</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">No. interior</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Código postal</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ destinatario.calle }}</td>
|
||||
<td>{{ destinatario.numero_exterior }}</td>
|
||||
<td>{{ destinatario.numero_interior }}</td>
|
||||
<td>{{ destinatario.codigo_postal }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Colonia</th>
|
||||
<th colspan="3" bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Localidad</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ destinatario.colonia }}</td>
|
||||
<td colspan="3">{{ destinatario.localidad }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Entidad federativa</th>
|
||||
<th colspan="3" bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Municipio</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ destinatario.entidad_federativa }}</td>
|
||||
<td colspan="3">{{ destinatario.municipio }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" bgcolor="#C0C0C0" style="background-color: #C0C0C0;">País</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4">{{ destinatario.pais }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ==========================================
|
||||
SECCIÓN 4: PARTIDAS / MERCANCÍAS
|
||||
=========================================== -->
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
<div class="titulo-seccion" style="{% if not loop.first %}margin-top: 10px;{% endif %}">Datos de la mercancía</div>
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 58%;">
|
||||
<col style="width: 22%;">
|
||||
<col style="width: 20%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Descripción genérica de la mercancía</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Clave UMC</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Cantidad UMC</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ item.descripcion_generica }}</td>
|
||||
<td>{{ item.clave_unidad_medida }}</td>
|
||||
<td>{{ item.cantidad }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 28%;">
|
||||
<col style="width: 30%;">
|
||||
<col style="width: 22%;">
|
||||
<col style="width: 20%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Tipo moneda</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Valor unitario</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Valor total</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Valor total en dólares</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ item.tipo_moneda }}</td>
|
||||
<td>$ {{ item.valor_unitario }}</td>
|
||||
<td>$ {{ item.valor_total }}</td>
|
||||
<td>$ {{ item.valor_dolares }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if item.descripciones_especificas %}
|
||||
<div class="titulo-seccion" style="margin-top: 6px;">Descripción de la mercancía</div>
|
||||
<table class="cove-table">
|
||||
<colgroup>
|
||||
<col style="width: 28%;">
|
||||
<col style="width: 30%;">
|
||||
<col style="width: 22%;">
|
||||
<col style="width: 20%;">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Marca</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Modelo</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Serie</th>
|
||||
<th bgcolor="#C0C0C0" style="background-color: #C0C0C0;">Submodelo</th>
|
||||
</tr>
|
||||
{% for desc in item.descripciones_especificas %}
|
||||
<tr>
|
||||
<td style="border:1px solid #ccc; padding:2px 4px;">{{ desc.marca }}</td>
|
||||
<td style="border:1px solid #ccc; padding:2px 4px;">{{ desc.modelo }}</td>
|
||||
<td style="border:1px solid #ccc; padding:2px 4px;">{{ desc.numero_serie }}</td>
|
||||
<td style="border:1px solid #ccc; padding:2px 4px;">{{ desc.submodelo }}</td>
|
||||
<td>{{ desc.marca }}</td>
|
||||
<td>{{ desc.modelo }}</td>
|
||||
<td>{{ desc.numero_serie }}</td>
|
||||
<td>{{ desc.submodelo }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div style="padding:8px; border:1px solid #ccc; color:#666; font-style:italic;">
|
||||
Sin partidas registradas.
|
||||
</div>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<!-- ═══════════════════ FOOTER ════════════════════════════ -->
|
||||
<div class="page-footer">
|
||||
Comprobante de Valor Electrónico — Ventanilla Única — VUCEM
|
||||
{% if edocument %} | E-Document: {{ edocument }}{% endif %}
|
||||
{% if vucem_operation_num %} | Operación: {{ vucem_operation_num }}{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<table class="cove-table">
|
||||
<tr>
|
||||
<td style="font-style: italic; color: #555;">Sin partidas registradas.</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<!-- FOOTER IMAGE (solo visible en el navegador; en PDF lo repite wkhtmltopdf via --footer-html) -->
|
||||
<div class="browser-only">
|
||||
<img src="{{ footer_img }}" alt="Pie de página COVE" class="img-header-footer" style="margin-top: 25px;">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user