diff --git a/app/db/normalizations.py b/app/db/normalizations.py index 8965fef..bcc6a75 100644 --- a/app/db/normalizations.py +++ b/app/db/normalizations.py @@ -1,9 +1,10 @@ import logging +from datetime import date from config import load_config import textwrap -from typing import Dict, Any +from typing import Dict, Any, Optional from decimal import Decimal, ROUND_HALF_UP -from utils import limpiar_cadena, normalizar_telefono_con_plus, corregir_y_validar_email, normalizar_url_para_insert, map_tax_code, cents, money_round, tax_fraction_from_code +from 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 @@ -66,7 +67,8 @@ def normalize_header_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]: "factuges_id": int(fd['ID_FACTURA']), "reference": str(fd['REFERENCIA']), - "invoice_date": str(fd['FECHA_FACTURA']), + # 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="…"), @@ -90,19 +92,51 @@ def normalize_header_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]: def normalize_details_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]: """ - Normaliza campos del registro de origen 'fd' (factura). + 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) + - tax_amount: escala 2 (cents), calculado con redondeo a 2 decimales """ config = load_config() tax_code = map_tax_code(str(fd.get("DES_TIPO_IVA"))) - disc_pct = fd.get('DESCUENTO_DET') - # Calcular cuota de IVA de la línea - frac = tax_fraction_from_code(tax_code) # 0.21, 0.10, 0… - # Ttotal_det ya incluye descuento - line_base = Decimal(str(fd.get('IMPORTE_TOTAL_DET') or 0)) - tax_amount = int( - (money_round(line_base * frac, 2)*100).to_integral_value()) + frac: Optional[Decimal] = tax_fraction_from_code( + tax_code) # p.ej. Decimal('0.21') o Decimal('0') + + # --- 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 frac is None: + tax_amount = 0 + else: + base = Decimal(str(total_det or 0)) + tax_amount = cents(money_round(base * frac, 2)) # Se calcula en el objeto de negocio de nuevo, comprobar si coincide # item_discount_amount = ( @@ -110,12 +144,12 @@ def normalize_details_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]: return { 'tax_code': tax_code, - 'position': int(fd['POSICION']), - 'description': rtf_a_texto_plano(str(fd['CONCEPTO'])), - 'quantity_value': None if fd['CANTIDAD'] is None else int(Decimal(str(fd['CANTIDAD'] or 0)) * 100), - 'unit_value': None if fd['IMPORTE_UNIDAD'] is None else int(Decimal(str(fd['IMPORTE_UNIDAD'] or 0)) * 10000), + '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': None if disc_pct in (None, 0) else int(Decimal(str(disc_pct)) * 100), - 'total_value': cents(fd.get('IMPORTE_TOTAL_DET')), + 'discount_percentage_value': discount_percentage_value, + 'total_value': total_value, 'tax_amount': tax_amount } diff --git a/app/db/sql_sentences.py b/app/db/sql_sentences.py index 957fe81..3cbe055 100644 --- a/app/db/sql_sentences.py +++ b/app/db/sql_sentences.py @@ -140,14 +140,13 @@ LIMPIAR_FACTUGES_LINK = ( # OPCION A SACAMOS EL RESUMEN DE LA TAXES DE LA CABECERA consulta_sql_customer_invoices_issue = ( f"SELECT ci.id, ci.series, ci.invoice_number, ci.invoice_date, ci.description, ci.customer_tin, ci.customer_name, ci.total_amount_value, ci.total_amount_scale, ci.reference, " - f"cit.taxable_amount_scale, cit.taxes_amount_scale, cit.tax_code, sum(cit.taxable_amount_value) as taxable_amount_value, sum(cit.taxes_amount_value) as taxes_amount_value, " + f"cit.taxable_amount_scale, cit.taxes_amount_scale, cit.tax_code, cit.taxable_amount_value, cit.taxes_amount_value, " f"vr.id as vrId, vr.uuid, vr.estado " f"FROM customer_invoices as ci " f"LEFT JOIN customer_invoice_taxes cit on (ci.id = cit.invoice_id) " f"LEFT JOIN verifactu_records vr on (ci.id = vr.invoice_id) " f"WHERE (ci.is_proforma = 0) AND (ci.status= 'issued') " f"AND (vr.estado <> 'Correcto') " - f"group by 1,2,3,4,5,6,7,8,9,10,11 " f"order by reference" ) diff --git a/app/db/sync_invoices.py b/app/db/sync_invoices.py index 05c2416..3be79b0 100644 --- a/app/db/sync_invoices.py +++ b/app/db/sync_invoices.py @@ -241,7 +241,7 @@ def insert_invoice_header(cur: str, cf: Dict[str, Any], hif: Dict[str, Any], cus invoice_id = str(uuid7()) logging.info("Inserting invoice %s %s %s %s", - invoice_id, hif.get('reference'), hif.get('invoice_date'), config['CTE_STATUS_INVOICE']) + invoice_id, hif.get('reference'), hif.get('invoice_date'), hif.get('operation_date'), config['CTE_STATUS_INVOICE']) cur.execute( SQL.INSERT_INVOICE, ( diff --git a/app/db/sync_invoices_verifactu.py b/app/db/sync_invoices_verifactu.py index 619efd4..19fe09c 100644 --- a/app/db/sync_invoices_verifactu.py +++ b/app/db/sync_invoices_verifactu.py @@ -108,8 +108,9 @@ def procesar_factura_verifactu( respuesta = crear_factura(factura, config) if respuesta.get("status") == 200 and respuesta.get("ok"): data = respuesta.get("data") + qr_verifactu = f"data:image/png;base64,{data.get('qr', '')}" cursor_mysql.execute(SQL.update_verifactu_records_with_invoiceId, (data.get("estado"), data.get( - "uuid"), data.get("url"), data.get("qr"), factura.get("id"))) + "uuid"), data.get("url"), qr_verifactu, factura.get("id"))) logging.info( f">>> Factura {factura.get("reference")} registrada en Verifactu") return True diff --git a/app/utils/__init__.py b/app/utils/__init__.py index 4b050cf..c591e2b 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -5,7 +5,7 @@ from .send_orders_mail import send_orders_mail from .text_converter import text_converter, limpiar_cadena from .send_rest_api import validar_nif, estado_factura, crear_factura from .tax_catalog_helper import TaxCatalog, get_default_tax_catalog, map_tax_code, calc_item_tax_amount, tax_fraction_from_code -from .importes_helper import unscale_to_str, unscale_to_decimal, cents, money_round +from .importes_helper import unscale_to_str, unscale_to_decimal, cents, cents4, money_round from .telefonos_helper import normalizar_telefono_con_plus from .mails_helper import corregir_y_validar_email from .websites_helper import normalizar_url_para_insert diff --git a/app/utils/importes_helper.py b/app/utils/importes_helper.py index e678a88..0183097 100644 --- a/app/utils/importes_helper.py +++ b/app/utils/importes_helper.py @@ -2,6 +2,12 @@ from decimal import Decimal, ROUND_HALF_UP from typing import Any, Optional +def cents4(value: Optional[Decimal | float | int]) -> int: + """Convierte a centésimas (valor * 10000). Soporta None.""" + v = Decimal(str(value or 0)) + return int((v * 10000).to_integral_value(rounding=ROUND_HALF_UP)) + + def cents(value: Optional[Decimal | float | int]) -> int: """Convierte a centésimas (valor * 100). Soporta None.""" v = Decimal(str(value or 0))