This commit is contained in:
David Arranz 2026-03-19 15:36:44 +01:00
parent 4e8751e119
commit a032383be3
10 changed files with 309 additions and 21 deletions

View File

@ -1,4 +1,5 @@
from .db_connection import get_factuges_connection, get_mysql_connection from .db_connection import get_factuges_connection, get_mysql_connection
from .sync_invoices_deleted_factuges import sync_invoices_deleted_factuges from .sync_invoices_deleted_factuges import sync_invoices_deleted_factuges
from .sync_invoices_factuges import sync_invoices_factuges from .sync_invoices_factuges import sync_invoices_factuges
from .sync_invoices_factuges_REST_API import sync_invoices_factuges_REST_API
from .sync_invoices_verifactu import sync_invoices_verifactu from .sync_invoices_verifactu import sync_invoices_verifactu

View File

@ -46,12 +46,11 @@ def normalize_customer_fields(fd: Dict[str, Any]) -> Dict[str, Any]:
return { return {
"is_company": config["CTE_IS_COMPANY"], "is_company": config["CTE_IS_COMPANY"],
"tin": clean_tin(str(fd.get("NIF_CIF"))), "tin": clean_tin(str(fd.get("NIF_CIF"))),
"name": str(fd.get("NOMBRE")), "name": fd.get("NOMBRE"),
"street": fd.get("CALLE"),
"street": str(fd.get("CALLE")), "city": fd.get("POBLACION"),
"city": str(fd.get("POBLACION")), "province": fd.get("PROVINCIA"),
"province": str(fd.get("PROVINCIA")), "postal_code": fd.get("CODIGO_POSTAL"),
"postal_code": str(fd.get("CODIGO_POSTAL")),
"country": config['CTE_COUNTRY_CODE'], "country": config['CTE_COUNTRY_CODE'],
"language_code": config['CTE_LANGUAGE_CODE'], "language_code": config['CTE_LANGUAGE_CODE'],
"phone_primary": clean_phone(fd.get("TELEFONO_1")), "phone_primary": clean_phone(fd.get("TELEFONO_1")),
@ -103,7 +102,7 @@ def normalize_header_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]:
"series": config['CTE_SERIE'], "series": config['CTE_SERIE'],
"factuges_id": int(fd['ID_FACTURA']), "factuges_id": int(fd['ID_FACTURA']),
"reference": str(fd['REFERENCIA']), "reference": fd['REFERENCIA'],
# Se asigna la fecha de la subida que es cuando se va a presentar a la AEAT # 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"), "invoice_date": date.today().strftime("%Y-%m-%d"),
"operation_date": str(fd['FECHA_FACTURA']), "operation_date": str(fd['FECHA_FACTURA']),
@ -175,10 +174,13 @@ def normalize_details_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]:
disc_global_pct: Optional[Decimal] = None if disc_global_raw is None else Decimal(str(disc_global_raw)) disc_global_pct: Optional[Decimal] = None if disc_global_raw is None else Decimal(str(disc_global_raw))
global_discount_percentage_value = None if ( global_discount_percentage_value = None if (
disc_global_pct is None or disc_global_pct == 0) else cents(disc_global_pct) disc_global_pct is None or disc_global_pct == 0) else cents(disc_global_pct)
global_discount_amount_value, taxable_amount_value = apply_discount_cents4(
subtotal_amount_with_dto, disc_global_pct)
total_discount_amount_value = discount_amount_value + global_discount_amount_value if (global_discount_percentage_value is None):
global_discount_amount_value, taxable_amount_value, total_discount_amount_value = None, subtotal_amount_value, None
else:
global_discount_amount_value, taxable_amount_value = apply_discount_cents4(
subtotal_amount_with_dto, disc_global_pct)
total_discount_amount_value = discount_amount_value + global_discount_amount_value
# --- total de línea (escala 4) --- # --- total de línea (escala 4) ---
total_value = cents4(total_det) total_value = cents4(total_det)

View File

@ -131,6 +131,32 @@ SELECT_FACTUGES_FACTURAS_CLIENTE = (
"ORDER BY fac.ID, facdet.POSICION" "ORDER BY fac.ID, facdet.POSICION"
) )
SELECT_FACTUGES_FACTURAS_CLIENTE_CAB = (
"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, "
"fac.CALLE, fac.POBLACION, fac.PROVINCIA, fac.CODIGO_POSTAL, fac.FECHA_ALTA, "
"fac.IMPORTE_NETO, fac.DESCUENTO, fac.IMPORTE_DESCUENTO, fac.BASE_IMPONIBLE, fac.IVA, fac.IMPORTE_IVA, fac.IMPORTE_TOTAL, "
"fac.OBSERVACIONES, 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, "
"fac.ID_CLIENTE, fac.NIF_CIF, fac.NOMBRE, fac.CALLE, fac.POBLACION, fac.PROVINCIA, fac.CODIGO_POSTAL, "
"cc.TELEFONO_1, cc.TELEFONO_2, cc.MOVIL_1, cc.MOVIL_2, cc.EMAIL_1, cc.EMAIL_2, cc.PAGINA_WEB "
"FROM FACTURAS_CLIENTE AS fac "
"LEFT JOIN CONTACTOS AS cc ON fac.ID_CLIENTE = cc.ID "
"LEFT JOIN FORMAS_PAGO AS fp ON fac.ID_FORMA_PAGO = fp.ID "
"LEFT JOIN TIPOS_IVA AS ti ON fac.ID_TIPO_IVA = ti.ID "
"WHERE "
"(fac.VERIFACTU = 1) "
"AND (fac.ID_VERIFACTU is null)"
"ORDER BY fac.ID"
)
SELECT_FACTUGES_FACTURAS_CLIENTE_DET = (
"SELECT facdet.ID || '' as ID_DET, facdet.ID_FACTURA, facdet.POSICION, facdet.TIPO_DETALLE, facdet.ID_ARTICULO, facdet.CONCEPTO, facdet.CANTIDAD, "
"facdet.IMPORTE_UNIDAD, facdet.DESCUENTO as DESCUENTO_DET, facdet.IMPORTE_TOTAL as IMPORTE_TOTAL_DET, facdet.VISIBLE, facdet.FECHA_ALTA as FECHA_ALTA_DET, facdet.FECHA_MODIFICACION as FECHA_MODIFICACION_DET "
"FROM FACTURAS_CLIENTE_DETALLES AS facdet "
"WHERE "
"(facdet.ID = ?) "
"ORDER BY facdet.POSICION"
)
UPDATE_FACTUGES_LINK = ( UPDATE_FACTUGES_LINK = (
"UPDATE FACTURAS_CLIENTE " "UPDATE FACTURAS_CLIENTE "
"SET VERIFACTU=?, " "SET VERIFACTU=?, "

View File

@ -0,0 +1,156 @@
from typing import Any, Dict
from uuid6 import uuid7
from app.config import load_config, logger
from app.utils import (
create_invoice_header,
insert_invoice_REST_API_FACTUGES,
insert_item_and_taxes,
)
from . import normalizations as NORMALIZA
from . import sql_sentences as SQL
def sync_invoices_factuges_REST_API(conn_factuges, conn_mysql, last_execution_date):
config = load_config()
# BUSCAMOS FACTURAS ENVIADAS A VERIFACTU EN FACTUGES, PARA SUBIRLAS AL NUEVO PROGRAMA DE FACTURACIÓN
# 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(SQL.SELECT_FACTUGES_FACTURAS_CLIENTE)
filas = cursor_FactuGES.fetchall()
except Exception as e:
if cursor_FactuGES is not None:
cursor_FactuGES.close()
logger.error(
f"(ERROR) Failed to fetch from database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}"
)
logger.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)
# Verificar si hay filas en el resultado
if tuplas_seleccionadas:
sync_invoices_from_FACTUGES(
conn_mysql, tuplas_seleccionadas, conn_factuges, config
)
else:
logger.info("There are NOT new FACTURAS rows since the last run.")
def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
# Insertaremos cada factura existente en las filas a la nueva estructura de tablas del programa nuevo de facturacion.
# logger.info(f"FACTURAS_CLIENTE_DETALLE rows to be processed: {len(filas)}")
cursorMySQL = None
cursor_FactuGES = None
factuges_id_anterior = None
num_fac_procesed = 0
customer_valid = True
invoice_id: str | None = None
factura_payload: dict | None = None
try:
cursorMySQL = conn_mysql.cursor()
cursor_FactuGES = conn_factuges.cursor()
# Insertar datos en la tabla 'customer_invoices'
for facturas in filas:
# Preparamos los campos para evitar errores
customer_fields = NORMALIZA.normalize_customer_fields(facturas)
header_invoice_fields = NORMALIZA.normalize_header_invoice_fields(
facturas
)
details_invoice_fields = NORMALIZA.normalize_details_invoice_fields(
facturas
)
factuges_id = int(facturas["ID"])
if factuges_id_anterior is None or factuges_id_anterior != factuges_id:
# Validamos que el cif de la factura exista en la AEAT si no es así no se hace la sincro de la factura
sync_result = int(config["CTE_SYNC_RESULT_OK"])
sync_notes = None
customer_valid = True # validar_nif_verifacti(
# customer_fields["tin"], customer_fields["name"], config
# )
if customer_valid:
# ---- cabecera factura
factura_payload = create_invoice_header(
customer_fields, header_invoice_fields, facturas["ID_FORMA_PAGO"], str(facturas["DES_FORMA_PAGO"]))
# ---- detalles factura
factura_payload["items"].append(insert_item_and_taxes(details_invoice_fields))
logger.info(f"PAYLOAD {factura_payload}")
# InsertamosREST
Resultado_REST = insert_invoice_REST_API_FACTUGES(factura_payload)
logger.info(Resultado_REST)
else:
sync_result = int(config["CTE_SYNC_RESULT_FAIL"])
sync_notes = (
f">>> Factura {header_invoice_fields['reference']} no cumple requisitos para ser mandada a Verifactu: "
f">>>>>> El NIF/NOMBRE ({customer_fields['tin']}/{customer_fields['name']}) no está registrado en la AEAT. "
f"El NIF/CIF debe estar registrado en la AEAT y el nombre debe ser suficientemente parecido al nombre registrado en la AEAT"
)
logger.info(sync_notes)
num_fac_procesed += 1
# Guardamos en Factuges el id de la customer_invoice
logger.info(
f"Updating FACTURAS_CLIENTE {sync_result} {invoice_id} {factuges_id} {sync_notes}: ")
# cursor_FactuGES.execute(
# SQL.UPDATE_FACTUGES_LINK,
# (sync_result, invoice_id, sync_notes, factuges_id),
# )
# Asignamos el id factura anterior para no volver a inserta cabecera
factuges_id_anterior = factuges_id
logger.info(f"FACTURAS_CLIENTE rows to be processed: {str(num_fac_procesed)}")
except Exception as e:
# Escribir el error en el archivo de errores
logger.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()
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())
logger.info(
"Inserting verifactu record %s %s %s",
id,
hif.get("reference"),
hif.get("invoice_date"),
)
cur.execute( # type: ignore
SQL.INSERT_VERIFACTU_RECORD,
(id, invoice_id, config["CTE_STATUS_VERIFACTU"]),
)
return id

View File

@ -9,7 +9,7 @@ from app.db import (
get_factuges_connection, get_factuges_connection,
get_mysql_connection, get_mysql_connection,
sync_invoices_deleted_factuges, sync_invoices_deleted_factuges,
sync_invoices_factuges, sync_invoices_factuges_REST_API,
) )
from app.utils import actualizar_fecha_ultima_ejecucion, obtener_fecha_ultima_ejecucion from app.utils import actualizar_fecha_ultima_ejecucion, obtener_fecha_ultima_ejecucion
@ -63,7 +63,7 @@ def main():
logger.info( logger.info(
">>>>>>>>>>> INI Sync invoices FactuGES escritorio to FactuGES web") ">>>>>>>>>>> INI Sync invoices FactuGES escritorio to FactuGES web")
sync_invoices_deleted_factuges(conn_factuges, conn_mysql, last_execution_date_local_tz) sync_invoices_deleted_factuges(conn_factuges, conn_mysql, last_execution_date_local_tz)
sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date_local_tz) sync_invoices_factuges_REST_API(conn_factuges, conn_mysql, last_execution_date_local_tz)
# Confirmar los cambios # Confirmar los cambios
conn_mysql.commit() conn_mysql.commit()

View File

@ -0,0 +1,93 @@
from typing import Any, Dict
from uuid6 import uuid7
def none_to_empty(value: Any) -> str:
return "" if value is None else value
def create_invoice_header(
cf,
hif,
payment_method_id,
payment_method_description
) -> Dict[str, Any]:
return {
"id": str(uuid7()),
"factuges_id": hif.get("factuges_id"),
"company_id": hif.get("company_id"),
"is_proforma": hif.get("is_proforma"),
"status": hif.get("status"),
"series": hif.get("series"),
"reference": hif.get("reference"),
"description": hif.get("description"),
"invoice_date": hif.get("invoice_date"),
"operation_date": hif.get("operation_date"),
"notes": none_to_empty(hif.get("notes")),
"language_code": cf.get("language_code"),
"subtotal_amount_value": none_to_empty(hif.get("subtotal_amount_value")),
"global_discount_percentage": none_to_empty(hif.get("discount_percentage_val")),
"discount_amount_value": none_to_empty(hif.get("discount_amount_value")),
"taxable_amount_value": hif.get("taxable_amount_value"),
"taxes_amount_value": hif.get("taxes_amount_value"),
"total_amount_value": hif.get("total_amount_value"),
"payment_method_id": payment_method_id,
"payment_method_description": payment_method_description,
"customer": {
"is_company": cf["is_company"],
"name": cf["name"],
"tin": cf["tin"],
"street": cf["street"],
"city": cf["city"],
"province": cf["province"],
"postal_code": cf["postal_code"],
"country": cf["country"],
"language_code": cf["language_code"],
"phone_primary": none_to_empty(cf["phone_primary"]),
"phone_secondary": none_to_empty(cf["phone_secondary"]),
"mobile_primary": none_to_empty(cf["mobile_primary"]),
"mobile_secondary": none_to_empty(cf["mobile_secondary"]),
"email_primary": none_to_empty(cf["email_primary"]),
"email_secondary": none_to_empty(cf["email_secondary"]),
"website": cf["website"],
},
"items": [],
}
def insert_item_and_taxes(fields) -> Dict[str, Any]:
# logger.info(fields)
return {
"item_id": str(uuid7()),
"position": str(fields.get("position")),
"description": fields.get("description"),
"quantity_value": str(fields.get("quantity_value")),
"unit_value": str(fields.get("unit_value")),
"subtotal_amount_value": fields.get("subtotal_amount_value"),
"item_discount_percentage_value": none_to_empty(fields.get("item_discount_percentage_value")),
"item_discount_amount_value": none_to_empty(fields.get("item_discount_amount_value")),
"global_discount_percentage_value": none_to_empty(fields.get("global_discount_percentage_value")),
"global_discount_amount_value": none_to_empty(fields.get("global_discount_amount_value")),
"total_discount_amount_value": fields.get("total_discount_amount_value"),
"taxable_amount_value": fields.get("taxable_amount_value"),
"total_value": fields.get("total_value"),
"iva_code": fields.get("iva_code"),
"iva_percentage_value": str(fields.get("iva_percentage_value")),
"iva_amount_value": fields.get("iva_amount_value"),
"rec_code": none_to_empty(fields.get("rec_code")),
"rec_percentage_value": none_to_empty(fields.get("rec_percentage_value")),
"rec_amount_value": fields.get("rec_amount_value"),
"taxes_amount_value": fields.get("taxes_amount_value"),
}
# logger.info("Inserting item tax %s code=%s base=%s tax=%s",
# item_id, fields.get('tax_code'), fields.get('total_value'), fields.get('tax_amount'))
# cur.execute(
# SQL.INSERT_INVOICE_ITEM_TAX, (str(uuid7()), item_id, fields.get('tax_code'),
# fields.get('total_value'), fields.get('tax_amount'))
# )

View File

@ -168,7 +168,7 @@ def insert_invoice_REST_API_FACTUGES(
Retorna: Retorna:
""" """
url = "http://192.168.0.104:3002/api/v1/proformas" url = "http://192.168.0.104:3002/api/v1/factuges"
timeout: int = 10 timeout: int = 10
headers = {"Content-Type": "application/json", headers = {"Content-Type": "application/json",
"Accept": "application/json"} "Accept": "application/json"}

View File

@ -4,12 +4,12 @@ STATE_PATH = ./
#LOG_PATH = ./app.log #LOG_PATH = ./app.log
#DESARROLLO ACANA #DESARROLLO ACANA
FACTUGES_HOST = 192.168.0.105 FACTUGES_HOST = 192.168.0.109
FACTUGES_PORT = 3050 FACTUGES_PORT = 3050
FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB
FACTUGES_USER = sysdba FACTUGES_USER = sysdba
FACTUGES_PASSWORD = masterkey FACTUGES_PASSWORD = masterkey
CONFIGURACION ACANA #CONFIGURACION ACANA
CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a' CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a'
VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0= VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0=

View File

@ -4,7 +4,7 @@ STATE_PATH = ./
#LOG_PATH = ./app.log #LOG_PATH = ./app.log
#DESARROLLO ACANA #DESARROLLO ACANA
FACTUGES_HOST = 192.168.0.105 FACTUGES_HOST = 192.168.0.109
FACTUGES_PORT = 3050 FACTUGES_PORT = 3050
FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB
FACTUGES_USER = sysdba FACTUGES_USER = sysdba

View File

@ -14,17 +14,27 @@ STATE_PATH = ./
#VERIFACTU_API_KEY = vf_prod_yfjonNPv2E4Fij+5J0hct0zCgUeFYT2dZzb23UZlM+Q= #VERIFACTU_API_KEY = vf_prod_yfjonNPv2E4Fij+5J0hct0zCgUeFYT2dZzb23UZlM+Q=
#CTE_SERIE = 'F25' #CTE_SERIE = 'F25'
#DESARROLLO ACANA #DESARROLLO RODAX
FACTUGES_HOST = 192.168.0.105 FACTUGES_HOST = 192.168.0.156
FACTUGES_PORT = 3050 FACTUGES_PORT = 3050
FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB FACTUGES_DATABASE = C:\Codigo\Output\Debug\Database\FACTUGES.FDB
FACTUGES_USER = sysdba FACTUGES_USER = sysdba
FACTUGES_PASSWORD = masterkey FACTUGES_PASSWORD = masterkey
#CONFIGURACION ACANA CTE_COMPANY_ID = '5e4dc5b3-96b9-4968-9490-14bd032fec5f'
CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a'
VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0= VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0=
CTE_SERIE = 'F25/' CTE_SERIE = 'F25/'
#DESARROLLO ACANA
#FACTUGES_HOST = 192.168.0.109
#FACTUGES_PORT = 3050
#FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB
#FACTUGES_USER = sysdba
#FACTUGES_PASSWORD = masterkey
#CONFIGURACION ACANA
#CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a'
#VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0=
#CTE_SERIE = 'F25/'
#DESARROLLO ALONSO Y SAL #DESARROLLO ALONSO Y SAL
#FACTUGES_HOST = 192.168.0.146 #FACTUGES_HOST = 192.168.0.146
#FACTUGES_PORT = 3050 #FACTUGES_PORT = 3050