import logging from datetime import date from app.config import load_config import textwrap from typing import Dict, Any, Optional from decimal import Decimal, ROUND_HALF_UP from app.utils import limpiar_cadena, normalizar_telefono_con_plus, corregir_y_validar_email, normalizar_url_para_insert, map_tax_code, cents, cents4, money_round, tax_fraction_from_code from striprtf.striprtf import rtf_to_text def rtf_a_texto_plano(rtf: str) -> str: """ Convierte RTF a texto plano usando striprtf. """ return rtf_to_text(rtf).strip() def normalize_customer_fields(fd: Dict[str, Any]) -> Dict[str, Any]: """ Normaliza campos del registro de origen 'fd' (factura). """ config = load_config() # >>> aquí usa tus helpers reales: def clean_phone(v): return normalizar_telefono_con_plus(v) def clean_web(v): return normalizar_url_para_insert(v) def clean_tin(v): return limpiar_cadena(v) email1_ok, email1 = corregir_y_validar_email(fd.get("EMAIL_1")) email2_ok, email2 = corregir_y_validar_email(fd.get("EMAIL_2")) return { "is_company": config["CTE_IS_COMPANY"], "tin": clean_tin(str(fd.get("NIF_CIF"))), "name": str(fd.get("NOMBRE")), "street": str(fd.get("CALLE")), "city": str(fd.get("POBLACION")), "province": str(fd.get("PROVINCIA")), "postal_code": str(fd.get("CODIGO_POSTAL")), "country": config['CTE_COUNTRY_CODE'], "language_code": config['CTE_LANGUAGE_CODE'], "phone_primary": clean_phone(fd.get("TELEFONO_1")), "phone_secondary": clean_phone(fd.get("TELEFONO_2")), "mobile_primary": clean_phone(fd.get("MOVIL_1")), "mobile_secondary": clean_phone(fd.get("MOVIL_2")), "email_primary": email1 if email1_ok else None, "email_secondary": email2 if email2_ok else None, "website": clean_web(str(fd.get("PAGINA_WEB"))) } def normalize_header_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]: """ Normaliza campos del registro de origen 'fd' (factura). # campos pendiente de revisar en un futuro # xxxxxxx = str(factura_detalle['ID_EMPRESA']) # xxxxxxx = str(factura_detalle['OBSERVACIONES']) """ config = load_config() return { "company_id": config['CTE_COMPANY_ID'], "is_proforma": config["CTE_IS_PROFORMA"], "status": config["CTE_STATUS_INVOICE"], "series": config['CTE_SERIE'], "factuges_id": int(fd['ID_FACTURA']), "reference": str(fd['REFERENCIA']), # Se asigna la fecha de la subida que es cuando se va a presentar a la AEAT "invoice_date": date.today().strftime("%Y-%m-%d"), "operation_date": str(fd['FECHA_FACTURA']), "description": textwrap.shorten( f"{str(fd['REFERENCIA'])} - {str(fd.get('NOMBRE')) or ''}", width=50, placeholder="…"), # siempre tendrán 2 decimales 'subtotal_amount_value': cents(fd.get('IMPORTE_NETO')), 'discount_amount_value': cents(fd.get('IMPORTE_DESCUENTO')), 'discount_percentage_val': int((Decimal(str(fd.get('DESCUENTO') or 0)) * 100).to_integral_value()) if fd.get('DESCUENTO') is not None else 0, 'taxable_amount_value': cents(fd.get('BASE_IMPONIBLE')), 'tax_code': map_tax_code(str(fd.get("DES_TIPO_IVA"))), 'taxes_amount_value': cents((fd.get('IMPORTE_IVA') or 0) + (fd.get('IMPORTE_RE') or 0)), 'total_amount_value': cents(fd.get('IMPORTE_TOTAL')), 'base': cents(fd.get('BASE_IMPONIBLE')), 'iva': cents(fd.get('IMPORTE_IVA')), 're': cents(fd.get('IMPORTE_RE')) } def normalize_details_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]: """ Normaliza campos de detalle de factura desde el registro de origen `fd`. Escalas: - quantity_value: escala 2 (cents) - unit_value: escala 4 (cents4) - discount_percentage_value: escala 2 (10 -> 1000) - total_value: escala 4 (cents4) - iva_amount: escala 2 (cents), calculado con redondeo a 2 decimales - """ config = load_config() iva_code = map_tax_code(str(fd.get("DES_TIPO_IVA"))) # Calcular cuota de IVA de la línea iva_frac: Optional[Decimal] = tax_fraction_from_code(iva_code) # p.ej. Decimal('0.21') o Decimal('0') iva_percentage_value = None if ( iva_frac is None or iva_frac == 0) else cents4(iva_frac) rec_code = None rec_percentage_value = None rec_amount = None # --- Campos base del origen --- cantidad = fd.get("CANTIDAD") importe_unidad = fd.get("IMPORTE_UNIDAD") # total de línea (ya con descuento) total_det = fd.get("IMPORTE_TOTAL_DET") concepto_rtf = fd.get("CONCEPTO") # --- cantidad y precio unitario --- quantity_value = None if cantidad is None else cents( cantidad) # escala 2 unit_value = None if importe_unidad is None else cents4( importe_unidad) # escala 4 # --- descuento % (escala 2) --- disc_raw = fd.get("DESCUENTO_DET") disc_pct: Optional[Decimal] = None if disc_raw is None else Decimal( str(disc_raw)) discount_percentage_value = None if ( disc_pct is None or disc_pct == 0) else cents(disc_pct) # --- total de línea (escala 4) --- total_value = cents4(total_det) # --- cuota (escala 2) --- # calculamos sobre el total en unidades monetarias con redondeo bancario a 2 decimales if iva_frac is None: iva_amount = 0 else: base = Decimal(str(total_det or 0)) iva_amount = cents(money_round(base * iva_frac, 2)) # Se calcula en el objeto de negocio de nuevo, comprobar si coincide # item_discount_amount = ( # (factura_detalle['IMPORTE_UNIDAD'] or 0)*((factura_detalle['DESCUENTO'] or 0)/100))*100 return { 'position': int(fd.get("POSICION") or 0), 'description': rtf_a_texto_plano(str(concepto_rtf or "")), 'quantity_value': quantity_value, 'unit_value': unit_value, 'disc_pct': disc_pct, 'discount_percentage_value': discount_percentage_value, 'total_value': total_value, 'iva_code': iva_code, 'iva_percentage_value': iva_percentage_value, 'iva_amount': iva_amount, 'rec_code': rec_code, 'rec_percentage_value': rec_percentage_value, 'rec_amount': rec_amount, }