import logging from uuid import uuid4 from config import load_config from decimal import Decimal def sync_invoices(conn_factuges, conn_mysql, last_execution_date): config = load_config() # Recorrer todas las facturas eliminadas para liberarlas en factuges # VERIFACTU = 0 and ID_VERIFACTU = NULL consulta_sql_customer_invoices_deleted = ( f"SELECT ci.id " f"FROM customer_invoices as ci " f"WHERE " f"(ci.deleted_at is not null) " ) consulta_sql_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, " f"fac.IMPORTE_NETO, fac.DESCUENTO, fac.IMPORTE_DESCUENTO, fac.BASE_IMPONIBLE, fac.IVA, fac.IMPORTE_IVA, fac.IMPORTE_TOTAL, " f"fac.ID_FORMA_PAGO, fp.DESCRIPCION as DES_FORMA_PAGO, fac.ID_TIPO_IVA, ti.REFERENCIA as DES_TIPO_IVA, fac.RECARGO_EQUIVALENCIA, fac.RE, fac.IMPORTE_RE, " f"fac.ID_CLIENTE, fac.NIF_CIF, fac.NOMBRE, fac.CALLE, fac.POBLACION, fac.PROVINCIA, fac.CODIGO_POSTAL, " f"facdet.ID || '' as ID_DET, facdet.ID_FACTURA, facdet.POSICION, facdet.TIPO_DETALLE, facdet.ID_ARTICULO, facdet.CONCEPTO, facdet.CANTIDAD, " f"facdet.IMPORTE_UNIDAD, facdet.DESCUENTO as DESCUENTO_DET, facdet.IMPORTE_TOTAL, facdet.VISIBLE, facdet.FECHA_ALTA as FECHA_ALTA_DET, facdet.FECHA_MODIFICACION as FECHA_MODIFICACION_DET " f"FROM FACTURAS_CLIENTE AS fac " f"LEFT JOIN FORMAS_PAGO AS fp ON fac.ID_FORMA_PAGO = fp.ID " f"LEFT JOIN TIPOS_IVA AS ti ON fac.ID_TIPO_IVA = ti.ID " f"LEFT JOIN FACTURAS_CLIENTE_DETALLES as facdet ON fac.ID = facdet.ID_FACTURA " f"WHERE " f"(fac.VERIFACTU > 0) " f"AND (fac.ID_VERIFACTU is null)" f"ORDER BY (fac.ID)" ) # 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_deleted) filas = cursor_mysql.fetchall() cursor_mysql.close() # Crear un conjunto con los IDs [0] de los customer_inovices que debo liberar en FactuGES ids_verifactu_deleted = {str(fila[0]) for fila in filas} logging.info(f"Customer invoices rows to be deleted: { len(ids_verifactu_deleted)}") # Verificar si hay filas en el resultado if ids_verifactu_deleted: eliminar_datos(conn_factuges, ids_verifactu_deleted, config) else: logging.info(f"There are no rows to deleted") except Exception as e: if cursor_mysql is not None: cursor_mysql.close() logging.error(f"(ERROR) Failed to fetch from database:{ config['UECKO_MYSQL_DATABASE']} - using user:{config['UECKO_MYSQL_USER']}") logging.error(e) raise e # Crear un cursor para ejecutar consultas SQL cursor_FactuGES = None try: cursor_FactuGES = conn_factuges.cursor() # Ejecutar la consulta de FACTURAS_CLIENTE cursor_FactuGES.execute(consulta_sql_FACTURAS_CLIENTE) filas = cursor_FactuGES.fetchall() except Exception as e: if cursor_FactuGES is not None: cursor_FactuGES.close() logging.error(f"(ERROR) Failed to fetch from database:{ config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}") logging.error(e) raise e # Obtener los nombres de las columnas columnas = [desc[0] for desc in cursor_FactuGES.description] cursor_FactuGES.close() # Convertir las filas en diccionarios con nombres de columnas como claves tuplas_seleccionadas = [] for fila in filas: tupla = dict(zip(columnas, fila)) tuplas_seleccionadas.append(tupla) logging.info( f"FACTURAS_CLIENTE_DETALLE rows to be processed: {len(tuplas_seleccionadas)}") # Verificar si hay filas en el resultado if tuplas_seleccionadas: insertar_datos(conn_mysql, tuplas_seleccionadas, conn_factuges, config) else: logging.info( "There are no new FACTURAS rows since the last run.") # Verificamos que en customer_invoice de mysql no se ha eliminado ninguna factura, # si se ha eliminado alguna factura, procedemos quitar la asociación en factuges para que se pueda modificar # en un futuro se modificará solo en el programa nuevo y tendrá que sincronizarse con factuges los importes totales de la factura # ya pasamos de los detalles ya que no se van a ver las facturas en FactuGES, pero la relación que tengan las facturas con otros módulos # deben verse, ejemplo contratos-facturas # try: # cursor_FactuGES.execute(consulta_sql_FACTURAS_CLIENTE) # filas = cursor_FactuGES.fetchall() # cursor_FactuGES.close() # Crear un conjunto con los IDs [0] de los artículos en FactuGES para una búsqueda rápida # ids_factuges = {str(fila[0]) for fila in filas} # logging.info(f"customer_invoice rows to be processed: { # len(ids_factuges)}") # Verificar si hay filas en el resultado # if ids_factuges: # eliminar_datos(conn_mysql, ids_factuges, config) # else: # logging.info(f"There are no rows in the { # config['FACTUGES_NOMBRE_TARIFA']}.") # except Exception as e: # if cursor_FactuGES is not None: # cursor_FactuGES.close() # logging.error(f"(ERROR) Failed to fetch from database:{ # config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}") # logging.error(e) # raise e def eliminar_datos(conn_factuges, ids_verifactu_deleted, config): # Recorrer todos los articulos del catálogo web para ver si estan en filas, si no están se eliminan update_facturas_cliente_query = ( f"UPDATE FACTURAS_CLIENTE " f"SET ID_VERIFACTU = NULL, " f"VERIFACTU = 0 " f"WHERE (ID_VERIFACTU = ?)" ) cursor_FactuGES = None try: cursor_FactuGES = conn_factuges.cursor() if ids_verifactu_deleted: logging.info(f"Liberate factures: {ids_verifactu_deleted}") cursor_FactuGES.executemany(update_facturas_cliente_query, [( id_verifactu,) for id_verifactu in ids_verifactu_deleted]) else: logging.info("No articles to delete.") except Exception as e: # Escribir el error en el archivo de errores logging.error(str(e)) raise e # Re-lanzar la excepción para detener el procesamiento finally: # Cerrar la conexión if cursor_FactuGES is not None: cursor_FactuGES.close() def insertar_datos(conn_mysql, filas, conn_factuges, config): cte_company_id = '5e4dc5b3-96b9-4968-9490-14bd032fec5f' insert_customer_query = ( "INSERT INTO customers (id, name, tin, street, city, province, postal_code, country, factuges_id, company_id, " # "email, phone, fax, website, legal_record, default_tax," "is_company, language_code, currency_code, status, created_at, updated_at ) " "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 1, 'es', 'EUR', 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " ) insert_customer_invoices_query = ( "INSERT INTO customer_invoices (id, company_id, status, series, invoice_number, invoice_date, operation_date, " "subtotal_amount_value, discount_amount_value, discount_percentage_value, taxable_amount_value, taxes_amount_value, total_amount_value, " # "payment_method_id, payment_method_description, " "customer_id, customer_tin, customer_name, customer_street, customer_city, customer_province, customer_postal_code, customer_country, " "subtotal_amount_scale, discount_amount_scale, discount_percentage_scale, taxable_amount_scale, taxes_amount_scale, total_amount_scale, " "language_code, currency_code, created_at, updated_at) " "VALUES (%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_customer_invoices_taxes_query = ( "INSERT INTO customer_invoice_taxes (tax_id, invoice_id, tax_code, taxable_amount_value, taxes_amount_value, taxable_amount_scale, taxes_amount_scale, " "created_at, updated_at) " "VALUES (%s, %s, %s, %s, %s, 2, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ) insert_customer_invoice_items_query = ( "INSERT INTO customer_invoice_items (item_id, invoice_id, position, description, quantity_value, unit_amount_value, discount_percentage_value, discount_amount_value, total_amount_value, " "quantity_scale, unit_amount_scale, discount_amount_scale, total_amount_scale, discount_percentage_scale, created_at, updated_at) " "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, 2, 4, 2, 4, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ) insert_customer_invoice_item_taxes_query = ( "INSERT INTO customer_invoice_item_taxes (tax_id, item_id, tax_code, taxable_amount_value, taxes_amount_value, taxable_amount_scale, taxes_amount_scale, " "created_at, updated_at) " "VALUES (%s, %s, %s, %s, %s, 4, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ) update_FACTURAS_CLIENTE_query = ( "UPDATE FACTURAS_CLIENTE set ID_VERIFACTU = ? WHERE ID = ?") select_customer_query = ( "SELECT customers.id " "FROM customers " "WHERE customers.factuges_id = %s" ) cursorMySQL = None cursor_FactuGES = None factuges_id_anterior = None id_customer_invoice = None num_fac_procesed = 0 try: cursorMySQL = conn_mysql.cursor() cursor_FactuGES = conn_factuges.cursor() # Insertar datos en la tabla 'customer_invoices' for factura_detalle in filas: factuges_id = int(factura_detalle['ID_FACTURA']) invoice_status = str('draft') invoice_series = str('A') invoice_number = str(factura_detalle['REFERENCIA']) invoice_date = str(factura_detalle['FECHA_FACTURA']) operation_date = str(factura_detalle['FECHA_FACTURA']) # siempre tendrán 2 decimales subtotal_amount_value = (factura_detalle['IMPORTE_NETO'] or 0)*100 discount_amount_value = ( factura_detalle['IMPORTE_DESCUENTO'] or 0)*100 discount_percentage_value = ( factura_detalle['DESCUENTO'] or 0)*100 if (factura_detalle['DESCUENTO']) is not None else None taxable_amount_value = (factura_detalle['BASE_IMPONIBLE'] or 0)*100 tax_code = str(factura_detalle['DES_TIPO_IVA']) tax_amount_value = (factura_detalle['IMPORTE_IVA'] or 0)*100 total_amount_value = (factura_detalle['IMPORTE_TOTAL'] or 0)*100 payment_method_id = str(factura_detalle['ID_FORMA_PAGO']) payment_method_description = str(factura_detalle['DES_FORMA_PAGO']) customer_id = str(uuid4()) factuges_customer_id = str(factura_detalle['ID_CLIENTE']) customer_tin = str(factura_detalle['NIF_CIF']) customer_name = str(factura_detalle['NOMBRE']) customer_street = str(factura_detalle['CALLE']) customer_city = str(factura_detalle['POBLACION']) customer_province = str(factura_detalle['PROVINCIA']) customer_postal_code = str(factura_detalle['CODIGO_POSTAL']) customer_country = 'es' item_position = int(factura_detalle['POSICION']) item_description = str(factura_detalle['CONCEPTO']) item_quantity_value = (factura_detalle['CANTIDAD'] or 0)*100 item_unit_amount_value = ( factura_detalle['IMPORTE_UNIDAD'] or 0)*10000 item_discount_percentage_value = ( factura_detalle['DESCUENTO'] or 0)*100 item_discount_amount = None # item_discount_amount = ( # (factura_detalle['IMPORTE_UNIDAD'] or 0)*((factura_detalle['DESCUENTO'] or 0)/100))*100 item_total_amount = (factura_detalle['IMPORTE_TOTAL'] or 0)*100 # campos pendiente de revisar en un futuro # xxxxxxx = str(factura_detalle['ID_EMPRESA']) # 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 # xxxxxxx = str(factura_detalle['OBSERVACIONES']) # RE, IMPORTE_RE >> en el caso que este relleno debo trasladarlo a los detalles de la factura # Comprobamos si existe el cliente del primer item de la factura if factuges_id_anterior is None or factuges_id_anterior != factuges_id: cursorMySQL.execute(select_customer_query, (factuges_customer_id, )) row = cursorMySQL.fetchone() is_new = (row is None) or (row[0] is None) if is_new: logging.info( f"Inserting customer {factuges_customer_id} {customer_tin} {customer_name}") cursorMySQL.execute(insert_customer_query, (customer_id, customer_name, customer_tin, customer_street, customer_city, customer_province, customer_postal_code, customer_country, factuges_customer_id, cte_company_id)) else: # Si ya exite ponemos el id del customer correspondiente customer_id = str(row[0]) logging.info( f"Updating customer {factuges_customer_id} {customer_id}") # cursorMySQL.execute(update_customer_query, .....) # Insertamos cabecera de la factura # Generar un ID único para la tabla customer_invoices id_customer_invoice = str(uuid4()) logging.info( f"Inserting customer_invoice {id_customer_invoice} {invoice_number} {invoice_date}") cursorMySQL.execute(insert_customer_invoices_query, (id_customer_invoice, cte_company_id, invoice_status, invoice_series, invoice_number, invoice_date, operation_date, subtotal_amount_value, discount_amount_value, discount_percentage_value, taxable_amount_value, tax_amount_value, total_amount_value, # payment_method_id, payment_method_description, customer_id, customer_tin, customer_name, customer_street, customer_city, customer_province, customer_postal_code, customer_country)) # Insertamos el IVA cursorMySQL.execute(insert_customer_invoices_taxes_query, (str(uuid4()), id_customer_invoice, tax_code, taxable_amount_value, tax_amount_value)) # Insertamos el RE si viene if (factura_detalle['RECARGO_EQUIVALENCIA'] > 0): tax_code = (factura_detalle['RE'])*100 taxable_amount_value = (factura_detalle['RE'])*100 tax_amount_value = (factura_detalle['IMPORTE_RE'])*100 cursorMySQL.execute(insert_customer_invoices_taxes_query, (str(uuid4()), id_customer_invoice, tax_code, taxable_amount_value, tax_amount_value)) # Guardamos en Factuges el id de la customer_invoice logging.info( f"Updating FACTURAS_CLIENTE {id_customer_invoice} {factuges_id}") cursor_FactuGES.execute( update_FACTURAS_CLIENTE_query, (id_customer_invoice, factuges_id)) num_fac_procesed += 1 # Insertamos detalles y taxes correspondientes siempre # Generar un ID único para la tabla customer_invoice_items item_id = str(uuid4()) logging.info( f"Inserting customer_invoice_items {id_customer_invoice} {item_position} {item_quantity_value}") cursorMySQL.execute(insert_customer_invoice_items_query, (item_id, id_customer_invoice, item_position, item_description, item_quantity_value, item_unit_amount_value, item_discount_percentage_value, item_discount_amount, item_total_amount)) if tax_code == 'IVA21': tax_amount_value = ( (factura_detalle['IMPORTE_TOTAL'] or 0)*Decimal(0.21))*100 elif tax_code == 'IVA18': tax_amount_value = ( (factura_detalle['IMPORTE_TOTAL'] or 0)*Decimal(0.18))*100 elif tax_code == 'IVA16': tax_amount_value = ( (factura_detalle['IMPORTE_TOTAL'] or 0)*Decimal(0.16))*100 elif tax_code == 'IVA10': tax_amount_value = ( (factura_detalle['IMPORTE_TOTAL'] or 0)*Decimal(0.10))*100 else: tax_amount_value = item_total_amount logging.info( f"Inserting customer_invoice_item_taxes {item_id} {item_position} {tax_code} {tax_amount_value}") cursorMySQL.execute(insert_customer_invoice_item_taxes_query, (str(uuid4()), item_id, tax_code, item_total_amount, tax_amount_value)) # Asignamos el id factura anterior para no volver a inserta cabecera factuges_id_anterior = factuges_id logging.info( f"FACTURAS_CLIENTE rows to be processed: {str(num_fac_procesed)}") except Exception as e: # Escribir el error en el archivo de errores logging.error(str(e)) raise e # Re-lanzar la excepción para detener el procesamiento finally: # Cerrar la conexión if cursorMySQL is not None: cursorMySQL.close() if cursor_FactuGES is not None: cursor_FactuGES.close()