From 9a34e385576d9492f47c13bf5c90429a9606ed76 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 20 Nov 2025 19:51:03 +0100 Subject: [PATCH] . --- .env.development | 6 - .env.production | 6 - app/config/settings.py | 32 +++--- app/db/normalizations.py | 121 ++++++++++++++++++++ app/db/sql_sentences.py | 97 ++++++++++++---- app/db/sync_invoices.py | 176 +++++++----------------------- app/db/sync_invoices_verifactu.py | 60 ++-------- app/main.py | 2 +- app/utils/tax_catalog_helper.py | 2 - 9 files changed, 261 insertions(+), 241 deletions(-) create mode 100644 app/db/normalizations.py diff --git a/.env.development b/.env.development index 0a41414..2d69569 100644 --- a/.env.development +++ b/.env.development @@ -30,12 +30,6 @@ DEV_UECKO_MYSQL_DATABASE = uecko_erp_sync DEV_UECKO_MYSQL_USER = rodax DEV_UECKO_MYSQL_PASSWORD = rodax -UECKO_DEFAULT_IVA = 2100 -UECKO_DEFAULT_CURRENCY_CODE = EUR -UECKO_DEFAULT_VALIDEZ = "30 días" -UECKO_DEFAULT_LOPD = "" -UECKO_DEFAULT_NOTAS = "" -UECKO_DEFAULT_FORMA_PAGO = "50% a la aceptación y 50% a la finalización" BREVO_API_KEY = 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 BREVO_EMAIL_TEMPLATE = 1 diff --git a/.env.production b/.env.production index 2b2f177..6ecdc9f 100644 --- a/.env.production +++ b/.env.production @@ -35,12 +35,6 @@ UECKO_MYSQL_DATABASE = uecko UECKO_MYSQL_USER = uecko UECKO_MYSQL_PASSWORD = u8Ax5Nw3%sjd -UECKO_DEFAULT_IVA = 2100 -UECKO_DEFAULT_CURRENCY_CODE = EUR -UECKO_DEFAULT_VALIDEZ = "30 días" -UECKO_DEFAULT_LOPD = "" -UECKO_DEFAULT_NOTAS = "" -UECKO_DEFAULT_FORMA_PAGO = "50% a la aceptación y 50% a la finalización" BREVO_API_KEY = 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 BREVO_EMAIL_TEMPLATE = 1 diff --git a/app/config/settings.py b/app/config/settings.py index fc3b950..e84bcd6 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -24,27 +24,23 @@ def load_config(): 'UECKO_MYSQL_USER': os.getenv('UECKO_MYSQL_USER'), 'UECKO_MYSQL_PASSWORD': os.getenv('UECKO_MYSQL_PASSWORD'), - 'FACTUGES_ID_EMPRESA': os.getenv('FACTUGES_ID_EMPRESA'), - 'FACTUGES_PRECIO_PUNTO': os.getenv('FACTUGES_PRECIO_PUNTO'), - 'FACTUGES_NOMBRE_TARIFA': os.getenv('FACTUGES_NOMBRE_TARIFA'), - 'FACTUGES_CONTRATO_ID_TIENDA': os.getenv('FACTUGES_CONTRATO_ID_TIENDA'), - 'FACTUGES_CONTRATO_SITUACION': os.getenv('FACTUGES_CONTRATO_SITUACION'), - 'FACTUGES_CONTRATO_ENVIADA_REVISADA': os.getenv('FACTUGES_CONTRATO_ENVIADA_REVISADA'), - 'FACTUGES_CONTRATO_TIPO_DETALLE': os.getenv('FACTUGES_CONTRATO_TIPO_DETALLE'), + 'CTE_COMPANY_ID': os.getenv('CTE_COMPANY_ID'), + 'CTE_SERIE': os.getenv('CTE_SERIE'), + 'CTE_STATUS_INVOICE': os.getenv('CTE_STATUS_INVOICE'), + 'CTE_IS_PROFORMA': os.getenv('CTE_IS_PROFORMA'), + 'CTE_STATUS_VERIFACTU': os.getenv('CTE_STATUS_VERIFACTU'), + 'CTE_LANGUAGE_CODE': os.getenv('CTE_LANGUAGE_CODE'), + 'CTE_COUNTRY_CODE': os.getenv('CTE_COUNTRY_CODE'), + 'CTE_IS_COMPANY': os.getenv('CTE_IS_COMPANY'), - 'UECKO_DEFAULT_IVA': os.getenv('UECKO_IVA', 2100), - 'UECKO_DEFAULT_CURRENCY_CODE': os.getenv('UECKO_CURRENCY_CODE', "EUR"), - 'UECKO_DEFAULT_VALIDEZ': os.getenv('UECKO_DEFAULT_VALIDEZ', ""), - 'UECKO_DEFAULT_LOPD': os.getenv('UECKO_DEFAULT_LOPD', ""), - 'UECKO_DEFAULT_NOTAS': os.getenv('UECKO_DEFAULT_NOTAS', ""), - 'UECKO_DEFAULT_FORMA_PAGO': os.getenv('UECKO_DEFAULT_FORMA_PAGO', ""), - - 'BREVO_API_KEY': os.getenv('BREVO_API_KEY'), - 'BREVO_EMAIL_TEMPLATE': os.getenv("BREVO_EMAIL_TEMPLATE"), - 'MAIL_FROM': os.getenv('MAIL_FROM'), - 'MAIL_TO': os.getenv('MAIL_TO'), 'VERIFACTU_BASE_URL': os.getenv('VERIFACTU_BASE_URL'), 'VERIFACTU_API_KEY': os.getenv('VERIFACTU_API_KEY'), 'VERIFACTU_NIFS_API_KEY': os.getenv('VERIFACTU_NIFS_API_KEY'), + + # 'BREVO_API_KEY': os.getenv('BREVO_API_KEY'), + # 'BREVO_EMAIL_TEMPLATE': os.getenv("BREVO_EMAIL_TEMPLATE"), + # 'MAIL_FROM': os.getenv('MAIL_FROM'), + # 'MAIL_TO': os.getenv('MAIL_TO'), + } diff --git a/app/db/normalizations.py b/app/db/normalizations.py new file mode 100644 index 0000000..8965fef --- /dev/null +++ b/app/db/normalizations.py @@ -0,0 +1,121 @@ +import logging +from config import load_config +import textwrap +from typing import Dict, Any +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 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']), + "invoice_date": str(fd['FECHA_FACTURA']), + "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 del registro de origen 'fd' (factura). + """ + 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()) + + # 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 { + '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), + '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')), + 'tax_amount': tax_amount + } diff --git a/app/db/sql_sentences.py b/app/db/sql_sentences.py index d15e930..957fe81 100644 --- a/app/db/sql_sentences.py +++ b/app/db/sql_sentences.py @@ -1,36 +1,37 @@ # ========================= # MYSQL (constantes) # ========================= + +SELECT_INVOICES_DELETED = ( + "SELECT ci.id " + "FROM customer_invoices as ci " + "WHERE " + "(ci.deleted_at is not null) " +) + SELECT_CUSTOMER_BY_FACTUGES = ( "SELECT customers.id FROM customers WHERE customers.factuges_id=%s" ) -INSERT_CUSTOMER = ( - "INSERT INTO customers (id, name, tin, street, city, province, postal_code, country, " - "phone_primary, phone_secondary, mobile_primary, mobile_secondary, " - "email_primary, email_secondary, website, factuges_id, company_id, is_company, " - "language_code, currency_code, status, created_at, updated_at) " - "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,1,'es','EUR','active',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" -) - -UPDATE_CUSTOMER = ( - "UPDATE customers SET name=%s, tin=%s, street=%s, city=%s, province=%s, postal_code=%s, country=%s, " - "phone_primary=%s, phone_secondary=%s, mobile_primary=%s, mobile_secondary=%s, " - "email_primary=%s, email_secondary=%s, website=%s, updated_at=CURRENT_TIMESTAMP " - "WHERE (id=%s)" -) - SELECT_PAYMENT_METHOD_BY_FACTUGES = ( "SELECT payment_methods.id FROM payment_methods WHERE payment_methods.factuges_id=%s" ) +INSERT_CUSTOMER = ( + "INSERT INTO customers (id, name, tin, street, city, province, postal_code, country, language_code, " + "phone_primary, phone_secondary, mobile_primary, mobile_secondary, " + "email_primary, email_secondary, website, factuges_id, company_id, is_company, " + "currency_code, status, created_at, updated_at) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'EUR','active',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" +) + INSERT_PAYMENT_METHOD = ( "INSERT INTO payment_methods (id, description, factuges_id, created_at, updated_at) " "VALUES (%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" ) INSERT_INVOICE = ( - "INSERT INTO customer_invoices (id, company_id, invoice_number, status, series, reference, invoice_date, operation_date, description, " + "INSERT INTO customer_invoices (id, company_id, invoice_number, status, is_proforma, series, reference, invoice_date, operation_date, description, " "subtotal_amount_value, discount_amount_value, discount_percentage_value, taxable_amount_value, taxes_amount_value, total_amount_value, " "customer_id, customer_tin, customer_name, customer_street, customer_city, customer_province, customer_postal_code, customer_country, " "payment_method_id, payment_method_description, factuges_id, " @@ -40,16 +41,22 @@ INSERT_INVOICE = ( "%s AS id, " "%s AS company_id, " "COALESCE(MAX(invoice_number + 0),0)+1 AS invoice_number, " - "%s AS status, %s AS series, %s AS reference, %s AS invoice_date, %s AS operation_date, %s AS description, " + "%s AS status, %s AS is_proforma, %s AS series, %s AS reference, %s AS invoice_date, %s AS operation_date, %s AS description, " "%s AS subtotal_amount_value, %s AS discount_amount_value, %s AS discount_percentage_value, %s AS taxable_amount_value, %s AS taxes_amount_value, %s AS total_amount_value, " "%s AS customer_id, %s AS customer_tin, %s AS customer_name, %s AS customer_street, %s AS customer_city, %s AS customer_province, %s AS customer_postal_code, %s AS customer_country, " "%s AS payment_method_id, %s AS payment_method_description, %s AS factuges_id, " "2,2,2,2,2,2, 'es','EUR', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP " "FROM customer_invoices " "WHERE company_id = %s " + "AND is_proforma = %s " "AND deleted_at is null" ) +INSERT_VERIFACTU_RECORD = ( + "INSERT INTO verifactu_records (id, invoice_id, estado, url, qr, uuid, created_at, updated_at) " + "VALUES (%s, %s, %s, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" +) + INSERT_INVOICE_BAK = ( "INSERT INTO customer_invoices (id, company_id, invoice_number, status, series, reference, invoice_date, operation_date, description, " "subtotal_amount_value, discount_amount_value, discount_percentage_value, taxable_amount_value, taxes_amount_value, total_amount_value, " @@ -60,7 +67,6 @@ INSERT_INVOICE_BAK = ( "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,2,2,2,2,2,2,'es','EUR',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" ) - INSERT_INVOICE_ITEM = ( "INSERT INTO customer_invoice_items " "(item_id, invoice_id, position, description, quantity_value, unit_amount_value, " @@ -81,17 +87,18 @@ INSERT_INVOICE_ITEM_TAX = ( "VALUES (%s,%s,%s,%s,%s,4,2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" ) -SELECT_INVOICES_DELETED = ( - "SELECT ci.id " - "FROM customer_invoices as ci " - "WHERE " - "(ci.deleted_at is not null) " +UPDATE_CUSTOMER = ( + "UPDATE customers SET name=%s, tin=%s, street=%s, city=%s, province=%s, postal_code=%s, country=%s, language_code=%s, is_company=%s, " + "phone_primary=%s, phone_secondary=%s, mobile_primary=%s, mobile_secondary=%s, " + "email_primary=%s, email_secondary=%s, website=%s, updated_at=CURRENT_TIMESTAMP " + "WHERE (id=%s)" ) # ========================= # FIREBIRD (constantes) # ========================= + SELECT_FACTUGES_FACTURAS_CLIENTE = ( f"SELECT fac.VERIFACTU, fac.ID_VERIFACTU, fac.ID || '' AS ID, fac.ID_EMPRESA || '' AS ID_EMPRESA, fac.REFERENCIA, fac.FECHA_FACTURA, fac.ID_CLIENTE || '' as ID_CLIENTE, fac.NIF_CIF, fac.NOMBRE, " f"fac.CALLE, fac.POBLACION, fac.PROVINCIA, fac.CODIGO_POSTAL, fac.FECHA_ALTA, " @@ -124,3 +131,47 @@ LIMPIAR_FACTUGES_LINK = ( "VERIFACTU = 0 " "WHERE (ID_VERIFACTU = ?)" ) + + +# ========================= +# SENTENCIAS PARA VERIFACTI +# ========================= + +# 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"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" +) + +# OPCION B SACAMOS LOS IVAS DE LOS DETALLES DE LOS ITEM +# SELECT ci.id, ci.series, ci.invoice_number, ci.invoice_date, ci.description, ci.customer_tin, ci.customer_name, ci.total_amount_value, +# ciit.tax_code, sum(ciit.taxable_amount_value), sum(ciit.taxes_amount_value) +# FROM customer_invoices as ci +# LEFT JOIN customer_invoice_items cii on (ci.id = cii.invoice_id) +# LEFT JOIN customer_invoice_item_taxes ciit on (cii.item_id = ciit.item_id) +# WHERE (ci.is_proforma = 0) AND (ci.status= 'issued') +# group by 1,2,3,4,5,6,7,8,9 + +update_verifactu_records_with_invoiceId = ("UPDATE verifactu_records " + "set estado = %s, " + "uuid = %s, " + "url = %s, " + "qr = %s, " + "updated_at = CURRENT_TIMESTAMP " + "WHERE invoice_id = %s" + ) + +update_verifactu_records_with_uuid = ("UPDATE verifactu_records " + "set estado = %s," + "operacion = %s, " + "updated_at = CURRENT_TIMESTAMP " + "WHERE uuid = %s " + ) diff --git a/app/db/sync_invoices.py b/app/db/sync_invoices.py index f73a2fd..05c2416 100644 --- a/app/db/sync_invoices.py +++ b/app/db/sync_invoices.py @@ -1,18 +1,9 @@ import logging -import textwrap -from typing import Dict, Any, Optional, Tuple +from typing import Dict, Any from uuid6 import uuid7 from config import load_config -from decimal import Decimal, ROUND_HALF_UP from . import sql_sentences as SQL -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 striprtf.striprtf import rtf_to_text - -# ========================= -# constantes -# ========================= -# Compañia RODAX -cte_company_id = '5e4dc5b3-96b9-4968-9490-14bd032fec5f' +from . import normalizations as NORMALIZA def sync_invoices(conn_factuges, conn_mysql, last_execution_date): @@ -34,7 +25,8 @@ def sync_invoices(conn_factuges, conn_mysql, last_execution_date): if ids_verifactu_deleted: sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config) else: - logging.info(f"There are customer invoices deleted") + logging.info( + f"There are NOT customer invoices deleted since the last run") except Exception as e: if cursor_mysql is not None: @@ -85,7 +77,7 @@ def sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config): try: cursor_FactuGES = conn_factuges.cursor() if ids_verifactu_deleted: - logging.info(f"Liberate factures: {ids_verifactu_deleted}") + logging.info(f"Liberate factuGES: {ids_verifactu_deleted}") cursor_FactuGES.executemany(SQL.LIMPIAR_FACTUGES_LINK, [( id_verifactu,) for id_verifactu in ids_verifactu_deleted]) else: @@ -118,13 +110,12 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config): for factura_detalle in filas: # Preparamos los campos para evitar errores - customer_fields = normalize_customer_fields(factura_detalle) - header_invoice_fields = normalize_header_invoice_fields( + customer_fields = NORMALIZA.normalize_customer_fields( factura_detalle) - details_invoice_fields = normalize_details_invoice_fields( + header_invoice_fields = NORMALIZA.normalize_header_invoice_fields( + factura_detalle) + details_invoice_fields = NORMALIZA.normalize_details_invoice_fields( factura_detalle) - logging.info( - f"FACTURAS_CLIENTE DETALLLLLLLLLLESSSSSSS: {details_invoice_fields}") factuges_id = int(factura_detalle['ID_FACTURA']) if factuges_id_anterior is None or factuges_id_anterior != factuges_id: @@ -132,7 +123,7 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config): # Comprobamos si existe el cliente del primer item de la factura customer_id = get_or_create_customer( cursorMySQL, - cte_company_id, + config['CTE_COMPANY_ID'], str(factura_detalle["ID_CLIENTE"]), customer_fields, ) @@ -147,15 +138,18 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config): # xxxxxxx = str(factura_detalle['ID_FORMA_PAGO']) según este id se debe de guardar en la factura los vencimiento asociados a la forma de pago # ---- cabecera factura - invoice_id = insert_invoice_header( - cursorMySQL, cte_company_id, customer_fields, header_invoice_fields, customer_id, pm_id, str( - factura_detalle["DES_FORMA_PAGO"]) + invoice_id = insert_invoice_header(cursorMySQL, customer_fields, header_invoice_fields, customer_id, pm_id, str( + factura_detalle["DES_FORMA_PAGO"]), config ) # ---- impuestos cabecera insert_header_taxes_if_any( cursorMySQL, invoice_id, factura_detalle['IVA'], factura_detalle['RECARGO_EQUIVALENCIA'], header_invoice_fields) + # ---- registro verifactu + insert_verifactu_record( + cursorMySQL, header_invoice_fields, invoice_id, config) + # Guardamos en Factuges el id de la customer_invoice logging.info( f"Updating FACTURAS_CLIENTE {invoice_id} {factuges_id}") @@ -187,109 +181,6 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config): cursor_FactuGES.close() -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). - """ - # >>> 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 { - "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": "es", - "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']) - """ - return { - "factuges_id": int(fd['ID_FACTURA']), - "reference": str(fd['REFERENCIA']), - "invoice_date": str(fd['FECHA_FACTURA']), - "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 del registro de origen 'fd' (factura). - """ - 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()) - - logging.info( - f"FACTURAS_CLIENTE IVAAAAAAAAAAAAAAA {str(frac)} DETALLE: {str((money_round(line_base * frac, 2)*100))}") - # 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 { - '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), - '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')), - 'tax_amount': tax_amount - } - - def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fields: Dict[str, Any]) -> str: """ Comprobamos si existe el cliente del primer item de la factura y si no lo creamos @@ -304,9 +195,9 @@ def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fiel SQL.INSERT_CUSTOMER, ( customer_id, fields["name"], fields["tin"], fields["street"], fields["city"], fields["province"], - fields["postal_code"], fields["country"], fields["phone_primary"], fields["phone_secondary"], + fields["postal_code"], fields["country"], fields["language_code"], fields["phone_primary"], fields["phone_secondary"], fields["mobile_primary"], fields["mobile_secondary"], fields["email_primary"], fields["email_secondary"], - fields["website"], factuges_customer_id, company_id, + fields["website"], factuges_customer_id, company_id, fields["is_company"] ), ) return customer_id @@ -316,7 +207,7 @@ def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fiel SQL.UPDATE_CUSTOMER, ( fields["name"], fields["tin"], fields["street"], fields["city"], fields["province"], fields["postal_code"], - fields["country"], fields["phone_primary"], fields["phone_secondary"], + fields["country"], fields["language_code"], fields["is_company"], fields["phone_primary"], fields["phone_secondary"], fields["mobile_primary"], fields["mobile_secondary"], fields["email_primary"], fields["email_secondary"], fields["website"], customer_id, ), @@ -342,19 +233,19 @@ def get_or_create_payment_method(cur, factuges_payment_id: str, description: str return pm_id -def insert_invoice_header(cur, company_id: str, cf: Dict[str, Any], hif: Dict[str, Any], customer_id: str, - payment_method_id: str, payment_method_description: str) -> str: +def insert_invoice_header(cur: str, cf: Dict[str, Any], hif: Dict[str, Any], customer_id: str, + payment_method_id: str, payment_method_description: str, config) -> str: """ Inserta cabecera y devuelve invoice_id """ invoice_id = str(uuid7()) - logging.info("Inserting invoice %s %s %s", - invoice_id, hif.get('reference'), hif.get('invoice_date')) + logging.info("Inserting invoice %s %s %s %s", + invoice_id, hif.get('reference'), hif.get('invoice_date'), config['CTE_STATUS_INVOICE']) cur.execute( SQL.INSERT_INVOICE, ( - invoice_id, company_id, 'draft', 'F25/', hif.get('reference'), hif.get( + invoice_id, hif.get('company_id'), hif.get('status'), hif.get('is_proforma'), hif.get('series'), hif.get('reference'), hif.get( 'invoice_date'), hif.get('operation_date'), hif.get('description'), hif.get('subtotal_amount_value'), hif.get('discount_amount_value'), hif.get( 'discount_percentage_val'), hif.get('taxable_amount_value'), @@ -363,12 +254,29 @@ def insert_invoice_header(cur, company_id: str, cf: Dict[str, Any], hif: Dict[st 'name'), cf.get('street'), cf.get('city'), cf.get('province'), cf.get('postal_code'), 'es', payment_method_id, payment_method_description, hif.get( - 'factuges_id'), company_id + 'factuges_id'), hif.get('company_id'), hif.get('is_proforma') ), ) return invoice_id +def insert_verifactu_record(cur: str, hif: Dict[str, Any], invoice_id: str, config) -> str: + """ + Inserta registro verifactu vacio y devuelve id + """ + id = str(uuid7()) + + logging.info("Inserting verifactu record %s %s %s", + id, hif.get('reference'), hif.get('invoice_date')) + cur.execute( + SQL.INSERT_VERIFACTU_RECORD, + ( + id, invoice_id, config['CTE_STATUS_VERIFACTU'] + ), + ) + return id + + def insert_header_taxes_if_any(cur, invoice_id: str, IVA: str, RECARGO: str, hif: Dict[str, Any]) -> None: """ Inserta impuestos de cabecera diff --git a/app/db/sync_invoices_verifactu.py b/app/db/sync_invoices_verifactu.py index 17f188c..619efd4 100644 --- a/app/db/sync_invoices_verifactu.py +++ b/app/db/sync_invoices_verifactu.py @@ -4,49 +4,18 @@ from typing import Dict, Any, Tuple, Optional, List, Iterable from config import load_config from decimal import Decimal from utils import validar_nif, estado_factura, crear_factura, TaxCatalog, unscale_to_str +from . import sql_sentences as SQL def sync_invoices_verifactu(conn_mysql, last_execution_date): config = load_config() -# OPCION A SACAMOS EL RESUMEN DE LA TAXES DE LA CABECERA -# SELECT ci.id, ci.series, ci.invoice_number, ci.invoice_date, ci.description, ci.customer_tin, ci.customer_name, ci.total_amount_value, -# cit.tax_code, sum(cit.taxable_amount_value), sum(cit.taxes_amount_value) -# FROM customer_invoices as ci -# LEFT JOIN customer_invoice_taxes cit on (ci.id = cit.invoice_id) -# WHERE (ci.is_proforma = 0) AND (ci.status= 'draft') -# group by 1,2,3,4,5,6,7,8,9 - - -# OPCION B SACAMOS LOS IVAS DE LOS DETALLES DE LOS ITEM -# SELECT ci.id, ci.series, ci.invoice_number, ci.invoice_date, ci.description, ci.customer_tin, ci.customer_name, ci.total_amount_value, -# ciit.tax_code, sum(ciit.taxable_amount_value), sum(ciit.taxes_amount_value) -# FROM customer_invoices as ci -# LEFT JOIN customer_invoice_items cii on (ci.id = cii.invoice_id) -# LEFT JOIN customer_invoice_item_taxes ciit on (cii.item_id = ciit.item_id) -# WHERE (ci.is_proforma = 0) AND (ci.status= 'issued') -# group by 1,2,3,4,5,6,7,8,9 - - # Recorrer todas las facturas emitidas para madarlas o refrescar los campos - 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"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 is null) OR (vr.estado <> 'Correcto')) " - f"group by 1,2,3,4,5,6,7,8,9,10,11 " - f"order by reference" - ) - # Crear un cursor para ejecutar consultas SQL cursor_mysql = None try: cursor_mysql = conn_mysql.cursor() - # Ejecutar la consulta de FACTURAS_CLIENTE - cursor_mysql.execute(consulta_sql_customer_invoices_issue) + # Ejecutar la consulta de customer invoices a enviar + cursor_mysql.execute(SQL.consulta_sql_customer_invoices_issue) filas = cursor_mysql.fetchall() # Obtener los nombres de las columnas @@ -64,7 +33,7 @@ def sync_invoices_verifactu(conn_mysql, last_execution_date): # Verificar si hay filas en el resultado if tuplas_seleccionadas: enviar_datos(tuplas_seleccionadas, cursor_mysql, config) - logging.info(f"Ha ido bien enviar_datos") + logging.info(f"Ok send Verifactu") else: logging.info(f"There are no rows to send") @@ -84,7 +53,7 @@ def enviar_datos(invoices_to_verifactu, cursor_mysql, config): invoice_id = None factura = None for fila in invoices_to_verifactu: - # Si los ids de factura anterior y actual no coinciden o empezamos factura nueva, miramos si ya existe una factura si es así la mandamos AEAT + # Si los ids de factura anterior y actual no coinciden, empezamos factura nueva, miramos si ya existe una factura si es así la mandamos AEAT # y creamos la cabecera de la factura siguiente, si no existe factura solo la creamos if invoice_id != str(fila['id']): @@ -132,26 +101,15 @@ def procesar_factura_verifactu( config: Dict[str, Any] ) -> bool: - insert_verifactu_records = ("INSERT INTO verifactu_records (id, invoice_id, estado, uuid, url, qr, created_at, updated_at) " - "VALUES (%s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " - ) - - update_verifactu_records = ("UPDATE verifactu_records " - "set estado = %s," - "operacion = %s, " - "updated_at = CURRENT_TIMESTAMP " - "WHERE uuid = %s " - ) - if factura != None: # Creamos registro de factura en verifactu - if factura.get('uuid') is None: + if factura.get('uuid') == '': # logging.info(f"Send to create Verifactu: {factura}") respuesta = crear_factura(factura, config) if respuesta.get("status") == 200 and respuesta.get("ok"): data = respuesta.get("data") - cursor_mysql.execute(insert_verifactu_records, (str(uuid4()), factura.get( - "id"), data.get("estado"), data.get("uuid"), data.get("url"), 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"))) logging.info( f">>> Factura {factura.get("reference")} registrada en Verifactu") return True @@ -165,7 +123,7 @@ def procesar_factura_verifactu( respuesta = estado_factura(factura.get('uuid'), config) if respuesta.get("status") == 200 and respuesta.get("ok"): data = respuesta.get("data") - cursor_mysql.execute(update_verifactu_records, (data.get( + cursor_mysql.execute(SQL.update_verifactu_records_with_uuid, (data.get( 'estado'), data.get('operacion'), factura.get('uuid'))) logging.info( f">>> Factura {factura.get("reference")} actualizado registro de Verifactu") diff --git a/app/main.py b/app/main.py index 9a7508a..713f6d3 100644 --- a/app/main.py +++ b/app/main.py @@ -60,7 +60,7 @@ def main(): # Sync Verifactu logging.info( f">>>>>>>>>> Sync facturas emitidas en FactuGES web to Verifactu") - # sync_invoices_verifactu(conn_mysql, last_execution_date_local_tz) + sync_invoices_verifactu(conn_mysql, last_execution_date_local_tz) conn_mysql.commit() conn_mysql.close() logging.info(f"FIN Sync Verifactu >>>>>>>>>>") diff --git a/app/utils/tax_catalog_helper.py b/app/utils/tax_catalog_helper.py index 80ef093..1bc5194 100644 --- a/app/utils/tax_catalog_helper.py +++ b/app/utils/tax_catalog_helper.py @@ -44,8 +44,6 @@ def tax_fraction_from_code(tax_code: str) -> Decimal: Devuelve la fracción (0.21, 0.18, ...) según el tax_code normalizado. Exenta -> 0. """ - logging.info(f"FACTURAS_CLIENTE IVAAAAAAAAAAAAAAA {tax_code}") - # Si ya tienes TaxCatalog, puedes delegarlo allí; lo dejo inline para simplicidad. mapping = { 'iva_21': Decimal('0.21'),